mirror of
https://github.com/sigp/lighthouse.git
synced 2026-05-30 04:37:13 +00:00
Resolve merge conflicts
This commit is contained in:
@@ -9,37 +9,41 @@ default = ["leveldb"]
|
||||
leveldb = ["dep:leveldb"]
|
||||
redb = ["dep:redb"]
|
||||
|
||||
[dev-dependencies]
|
||||
beacon_chain = { workspace = true }
|
||||
criterion = { workspace = true }
|
||||
rand = { workspace = true, features = ["small_rng"] }
|
||||
tempfile = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
bls = { workspace = true }
|
||||
db-key = "0.0.5"
|
||||
directory = { workspace = true }
|
||||
ethereum_ssz = { workspace = true }
|
||||
ethereum_ssz_derive = { workspace = true }
|
||||
fixed_bytes = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
leveldb = { version = "0.8.6", optional = true, default-features = false }
|
||||
logging = { workspace = true }
|
||||
lru = { workspace = true }
|
||||
metrics = { workspace = true }
|
||||
milhouse = { workspace = true }
|
||||
parking_lot = { workspace = true }
|
||||
redb = { version = "2.1.3", optional = true }
|
||||
safe_arith = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
smallvec = { workspace = true }
|
||||
ssz_types = { workspace = true }
|
||||
state_processing = { workspace = true }
|
||||
strum = { workspace = true }
|
||||
superstruct = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
typenum = { workspace = true }
|
||||
types = { workspace = true }
|
||||
xdelta3 = { workspace = true }
|
||||
zstd = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
beacon_chain = { workspace = true }
|
||||
criterion = { workspace = true }
|
||||
rand = { workspace = true, features = ["small_rng"] }
|
||||
tempfile = { workspace = true }
|
||||
|
||||
[[bench]]
|
||||
name = "hdiff"
|
||||
harness = false
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use bls::PublicKeyBytes;
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
use criterion::{Criterion, criterion_group, criterion_main};
|
||||
use rand::Rng;
|
||||
use ssz::Decode;
|
||||
use store::{
|
||||
hdiff::{HDiff, HDiffBuffer},
|
||||
StoreConfig,
|
||||
hdiff::{HDiff, HDiffBuffer},
|
||||
};
|
||||
use types::{BeaconState, Epoch, Eth1Data, EthSpec, MainnetEthSpec as E, Validator};
|
||||
|
||||
@@ -12,7 +12,7 @@ pub fn all_benches(c: &mut Criterion) {
|
||||
let spec = E::default_spec();
|
||||
let genesis_time = 0;
|
||||
let eth1_data = Eth1Data::default();
|
||||
let mut rng = rand::thread_rng();
|
||||
let mut rng = rand::rng();
|
||||
let validator_mutations = 1000;
|
||||
let validator_additions = 100;
|
||||
|
||||
@@ -27,11 +27,11 @@ pub fn all_benches(c: &mut Criterion) {
|
||||
// Change all balances
|
||||
for i in 0..n {
|
||||
let balance = target_state.balances_mut().get_mut(i).unwrap();
|
||||
*balance += rng.gen_range(1..=1_000_000);
|
||||
*balance += rng.random_range(1..=1_000_000);
|
||||
}
|
||||
// And some validator records
|
||||
for _ in 0..validator_mutations {
|
||||
let index = rng.gen_range(1..n);
|
||||
let index = rng.random_range(1..n);
|
||||
// TODO: Only change a few things, and not the pubkey
|
||||
*target_state.validators_mut().get_mut(index).unwrap() = rand_validator(&mut rng);
|
||||
}
|
||||
@@ -80,7 +80,7 @@ fn bench_against_states(
|
||||
fn rand_validator(mut rng: impl Rng) -> Validator {
|
||||
let mut pubkey = [0u8; 48];
|
||||
rng.fill_bytes(&mut pubkey);
|
||||
let withdrawal_credentials: [u8; 32] = rng.gen();
|
||||
let withdrawal_credentials: [u8; 32] = rng.random();
|
||||
|
||||
Validator {
|
||||
pubkey: PublicKeyBytes::from_ssz_bytes(&pubkey).unwrap(),
|
||||
@@ -97,7 +97,7 @@ fn rand_validator(mut rng: impl Rng) -> Validator {
|
||||
fn append_validator(state: &mut BeaconState<E>, mut rng: impl Rng) {
|
||||
state
|
||||
.balances_mut()
|
||||
.push(32_000_000_000 + rng.gen_range(1..=1_000_000_000))
|
||||
.push(32_000_000_000 + rng.random_range(1..=1_000_000_000))
|
||||
.unwrap();
|
||||
if let Ok(inactivity_scores) = state.inactivity_scores_mut() {
|
||||
inactivity_scores.push(0).unwrap();
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
use crate::chunked_vector::{chunk_key, Chunk, Field};
|
||||
use crate::{HotColdDB, ItemStore};
|
||||
use tracing::error;
|
||||
use types::{ChainSpec, EthSpec, Slot};
|
||||
|
||||
/// Iterator over the values of a `BeaconState` vector field (like `block_roots`).
|
||||
///
|
||||
/// Uses the freezer DB's separate table to load the values.
|
||||
pub struct ChunkedVectorIter<'a, F, E, Hot, Cold>
|
||||
where
|
||||
F: Field<E>,
|
||||
E: EthSpec,
|
||||
Hot: ItemStore<E>,
|
||||
Cold: ItemStore<E>,
|
||||
{
|
||||
pub(crate) store: &'a HotColdDB<E, Hot, Cold>,
|
||||
current_vindex: usize,
|
||||
pub(crate) end_vindex: usize,
|
||||
next_cindex: usize,
|
||||
current_chunk: Chunk<F::Value>,
|
||||
}
|
||||
|
||||
impl<'a, F, E, Hot, Cold> ChunkedVectorIter<'a, F, E, Hot, Cold>
|
||||
where
|
||||
F: Field<E>,
|
||||
E: EthSpec,
|
||||
Hot: ItemStore<E>,
|
||||
Cold: ItemStore<E>,
|
||||
{
|
||||
/// Create a new iterator which can yield elements from `start_vindex` up to the last
|
||||
/// index stored by the restore point at `last_restore_point_slot`.
|
||||
///
|
||||
/// The `freezer_upper_limit` slot should be the slot of a recent restore point as obtained from
|
||||
/// `Root::freezer_upper_limit`. We pass it as a parameter so that the caller can
|
||||
/// maintain a stable view of the database (see `HybridForwardsBlockRootsIterator`).
|
||||
pub fn new(
|
||||
store: &'a HotColdDB<E, Hot, Cold>,
|
||||
start_vindex: usize,
|
||||
freezer_upper_limit: Slot,
|
||||
spec: &ChainSpec,
|
||||
) -> Self {
|
||||
let (_, end_vindex) = F::start_and_end_vindex(freezer_upper_limit, spec);
|
||||
|
||||
// Set the next chunk to the one containing `start_vindex`.
|
||||
let next_cindex = start_vindex / F::chunk_size();
|
||||
// Set the current chunk to the empty chunk, it will never be read.
|
||||
let current_chunk = Chunk::default();
|
||||
|
||||
Self {
|
||||
store,
|
||||
current_vindex: start_vindex,
|
||||
end_vindex,
|
||||
next_cindex,
|
||||
current_chunk,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<F, E, Hot, Cold> Iterator for ChunkedVectorIter<'_, F, E, Hot, Cold>
|
||||
where
|
||||
F: Field<E>,
|
||||
E: EthSpec,
|
||||
Hot: ItemStore<E>,
|
||||
Cold: ItemStore<E>,
|
||||
{
|
||||
type Item = (usize, F::Value);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let chunk_size = F::chunk_size();
|
||||
|
||||
// Range exhausted, return `None` forever.
|
||||
if self.current_vindex >= self.end_vindex {
|
||||
None
|
||||
}
|
||||
// Value lies in the current chunk, return it.
|
||||
else if self.current_vindex < self.next_cindex * chunk_size {
|
||||
let vindex = self.current_vindex;
|
||||
let val = self
|
||||
.current_chunk
|
||||
.values
|
||||
.get(vindex % chunk_size)
|
||||
.cloned()
|
||||
.or_else(|| {
|
||||
error!(
|
||||
vector_index = vindex,
|
||||
"Missing chunk value in forwards iterator"
|
||||
);
|
||||
None
|
||||
})?;
|
||||
self.current_vindex += 1;
|
||||
Some((vindex, val))
|
||||
}
|
||||
// Need to load the next chunk, load it and recurse back into the in-range case.
|
||||
else {
|
||||
self.current_chunk = Chunk::load(
|
||||
&self.store.cold_db,
|
||||
F::column(),
|
||||
&chunk_key(self.next_cindex),
|
||||
)
|
||||
.map_err(|e| {
|
||||
error!(
|
||||
chunk_index = self.next_cindex,
|
||||
error = ?e,
|
||||
"Database error in forwards iterator"
|
||||
);
|
||||
e
|
||||
})
|
||||
.ok()?
|
||||
.or_else(|| {
|
||||
error!(
|
||||
chunk_index = self.next_cindex,
|
||||
"Missing chunk in forwards iterator"
|
||||
);
|
||||
None
|
||||
})?;
|
||||
self.next_cindex += 1;
|
||||
self.next()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,919 +0,0 @@
|
||||
//! Space-efficient storage for `BeaconState` vector fields.
|
||||
//!
|
||||
//! This module provides logic for splitting the `Vector` fields of a `BeaconState` into
|
||||
//! chunks, and storing those chunks in contiguous ranges in the on-disk database. The motiviation
|
||||
//! for doing this is avoiding massive duplication in every on-disk state. For example, rather than
|
||||
//! storing the whole `historical_roots` vector, which is updated once every couple of thousand
|
||||
//! slots, at every slot, we instead store all the historical values as a chunked vector on-disk,
|
||||
//! and fetch only the slice we need when reconstructing the `historical_roots` of a state.
|
||||
//!
|
||||
//! ## Terminology
|
||||
//!
|
||||
//! * **Chunk size**: the number of vector values stored per on-disk chunk.
|
||||
//! * **Vector index** (vindex): index into all the historical values, identifying a single element
|
||||
//! of the vector being stored.
|
||||
//! * **Chunk index** (cindex): index into the keyspace of the on-disk database, identifying a chunk
|
||||
//! of elements. To find the chunk index of a vector index: `cindex = vindex / chunk_size`.
|
||||
use self::UpdatePattern::*;
|
||||
use crate::*;
|
||||
use ssz::{Decode, Encode};
|
||||
use types::historical_summary::HistoricalSummary;
|
||||
|
||||
/// Description of how a `BeaconState` field is updated during state processing.
|
||||
///
|
||||
/// When storing a state, this allows us to efficiently store only those entries
|
||||
/// which are not present in the DB already.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum UpdatePattern {
|
||||
/// The value is updated once per `n` slots.
|
||||
OncePerNSlots {
|
||||
n: u64,
|
||||
/// The slot at which the field begins to accumulate values.
|
||||
///
|
||||
/// The field should not be read or written until `activation_slot` is reached, and the
|
||||
/// activation slot should act as an offset when converting slots to vector indices.
|
||||
activation_slot: Option<Slot>,
|
||||
/// The slot at which the field ceases to accumulate values.
|
||||
///
|
||||
/// If this is `None` then the field is continually updated.
|
||||
deactivation_slot: Option<Slot>,
|
||||
},
|
||||
/// The value is updated once per epoch, for the epoch `current_epoch - lag`.
|
||||
OncePerEpoch { lag: u64 },
|
||||
}
|
||||
|
||||
/// Map a chunk index to bytes that can be used to key the NoSQL database.
|
||||
///
|
||||
/// We shift chunks up by 1 to make room for a genesis chunk that is handled separately.
|
||||
pub fn chunk_key(cindex: usize) -> [u8; 8] {
|
||||
(cindex as u64 + 1).to_be_bytes()
|
||||
}
|
||||
|
||||
/// Return the database key for the genesis value.
|
||||
fn genesis_value_key() -> [u8; 8] {
|
||||
0u64.to_be_bytes()
|
||||
}
|
||||
|
||||
/// Trait for types representing fields of the `BeaconState`.
|
||||
///
|
||||
/// All of the required methods are type-level, because we do most things with fields at the
|
||||
/// type-level. We require their value-level witnesses to be `Copy` so that we can avoid the
|
||||
/// turbofish when calling functions like `store_updated_vector`.
|
||||
pub trait Field<E: EthSpec>: Copy {
|
||||
/// The type of value stored in this field: the `T` from `Vector<T, N>`.
|
||||
///
|
||||
/// The `Default` impl will be used to fill extra vector entries.
|
||||
type Value: Default + std::fmt::Debug + milhouse::Value;
|
||||
// Decode + Encode + Default + Clone + PartialEq + std::fmt::Debug
|
||||
|
||||
/// The length of this field: the `N` from `Vector<T, N>`.
|
||||
type Length: Unsigned;
|
||||
|
||||
/// The database column where the integer-indexed chunks for this field should be stored.
|
||||
///
|
||||
/// Each field's column **must** be unique.
|
||||
fn column() -> DBColumn;
|
||||
|
||||
/// Update pattern for this field, so that we can do differential updates.
|
||||
fn update_pattern(spec: &ChainSpec) -> UpdatePattern;
|
||||
|
||||
/// The number of values to store per chunk on disk.
|
||||
///
|
||||
/// Default is 128 so that we read/write 4K pages when the values are 32 bytes.
|
||||
// TODO: benchmark and optimise this parameter
|
||||
fn chunk_size() -> usize {
|
||||
128
|
||||
}
|
||||
|
||||
/// Convert a v-index (vector index) to a chunk index.
|
||||
fn chunk_index(vindex: usize) -> usize {
|
||||
vindex / Self::chunk_size()
|
||||
}
|
||||
|
||||
/// Get the value of this field at the given vector index, from the state.
|
||||
fn get_value(
|
||||
state: &BeaconState<E>,
|
||||
vindex: u64,
|
||||
spec: &ChainSpec,
|
||||
) -> Result<Self::Value, ChunkError>;
|
||||
|
||||
/// True if this is a `FixedLengthField`, false otherwise.
|
||||
fn is_fixed_length() -> bool;
|
||||
|
||||
/// Compute the start and end vector indices of the slice of history required at `current_slot`.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// If we have a field that is updated once per epoch, then the end vindex will be
|
||||
/// `current_epoch + 1`, because we want to include the value for the current epoch, and the
|
||||
/// start vindex will be `end_vindex - Self::Length`, because that's how far back we can look.
|
||||
fn start_and_end_vindex(current_slot: Slot, spec: &ChainSpec) -> (usize, usize) {
|
||||
// We take advantage of saturating subtraction on slots and epochs
|
||||
match Self::update_pattern(spec) {
|
||||
OncePerNSlots {
|
||||
n,
|
||||
activation_slot,
|
||||
deactivation_slot,
|
||||
} => {
|
||||
// Per-slot changes exclude the index for the current slot, because
|
||||
// it won't be set until the slot completes (think of `state_roots`, `block_roots`).
|
||||
// This also works for the `historical_roots` because at the `n`th slot, the 0th
|
||||
// entry of the list is created, and before that the list is empty.
|
||||
//
|
||||
// To account for the switch from historical roots to historical summaries at
|
||||
// Capella we also modify the current slot by the activation and deactivation slots.
|
||||
// The activation slot acts as an offset (subtraction) while the deactivation slot
|
||||
// acts as a clamp (min).
|
||||
let slot_with_clamp = deactivation_slot.map_or(current_slot, |deactivation_slot| {
|
||||
std::cmp::min(current_slot, deactivation_slot)
|
||||
});
|
||||
let slot_with_clamp_and_offset = if let Some(activation_slot) = activation_slot {
|
||||
slot_with_clamp - activation_slot
|
||||
} else {
|
||||
// Return (0, 0) to indicate that the field should not be read/written.
|
||||
return (0, 0);
|
||||
};
|
||||
let end_vindex = slot_with_clamp_and_offset / n;
|
||||
let start_vindex = end_vindex - Self::Length::to_u64();
|
||||
(start_vindex.as_usize(), end_vindex.as_usize())
|
||||
}
|
||||
OncePerEpoch { lag } => {
|
||||
// Per-epoch changes include the index for the current epoch, because it
|
||||
// will have been set at the most recent epoch boundary.
|
||||
let current_epoch = current_slot.epoch(E::slots_per_epoch());
|
||||
let end_epoch = current_epoch + 1 - lag;
|
||||
let start_epoch = end_epoch + lag - Self::Length::to_u64();
|
||||
(start_epoch.as_usize(), end_epoch.as_usize())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Given an `existing_chunk` stored in the DB, construct an updated chunk to replace it.
|
||||
fn get_updated_chunk(
|
||||
existing_chunk: &Chunk<Self::Value>,
|
||||
chunk_index: usize,
|
||||
start_vindex: usize,
|
||||
end_vindex: usize,
|
||||
state: &BeaconState<E>,
|
||||
spec: &ChainSpec,
|
||||
) -> Result<Chunk<Self::Value>, Error> {
|
||||
let chunk_size = Self::chunk_size();
|
||||
let mut new_chunk = Chunk::new(vec![Self::Value::default(); chunk_size]);
|
||||
|
||||
for i in 0..chunk_size {
|
||||
let vindex = chunk_index * chunk_size + i;
|
||||
if vindex >= start_vindex && vindex < end_vindex {
|
||||
let vector_value = Self::get_value(state, vindex as u64, spec)?;
|
||||
|
||||
if let Some(existing_value) = existing_chunk.values.get(i) {
|
||||
if *existing_value != vector_value && *existing_value != Self::Value::default()
|
||||
{
|
||||
return Err(ChunkError::Inconsistent {
|
||||
field: Self::column(),
|
||||
chunk_index,
|
||||
existing_value: format!("{:?}", existing_value),
|
||||
new_value: format!("{:?}", vector_value),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
}
|
||||
|
||||
new_chunk.values[i] = vector_value;
|
||||
} else {
|
||||
new_chunk.values[i] = existing_chunk.values.get(i).cloned().unwrap_or_default();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(new_chunk)
|
||||
}
|
||||
|
||||
/// Determine whether a state at `slot` possesses (or requires) the genesis value.
|
||||
fn slot_needs_genesis_value(slot: Slot, spec: &ChainSpec) -> bool {
|
||||
let (_, end_vindex) = Self::start_and_end_vindex(slot, spec);
|
||||
match Self::update_pattern(spec) {
|
||||
// If the end_vindex is less than the length of the vector, then the vector
|
||||
// has not yet been completely filled with non-genesis values, and so the genesis
|
||||
// value is still required.
|
||||
OncePerNSlots { .. } => {
|
||||
Self::is_fixed_length() && end_vindex < Self::Length::to_usize()
|
||||
}
|
||||
// If the field has lag, then it takes an extra `lag` vindices beyond the
|
||||
// `end_vindex` before the vector has been filled with non-genesis values.
|
||||
OncePerEpoch { lag } => {
|
||||
Self::is_fixed_length() && end_vindex + (lag as usize) < Self::Length::to_usize()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Load the genesis value for a fixed length field from the store.
|
||||
///
|
||||
/// This genesis value should be used to fill the initial state of the vector.
|
||||
fn load_genesis_value<S: KeyValueStore<E>>(store: &S) -> Result<Self::Value, Error> {
|
||||
let key = &genesis_value_key()[..];
|
||||
let chunk =
|
||||
Chunk::load(store, Self::column(), key)?.ok_or(ChunkError::MissingGenesisValue)?;
|
||||
chunk
|
||||
.values
|
||||
.first()
|
||||
.cloned()
|
||||
.ok_or_else(|| ChunkError::MissingGenesisValue.into())
|
||||
}
|
||||
|
||||
/// Store the given `value` as the genesis value for this field, unless stored already.
|
||||
///
|
||||
/// Check the existing value (if any) for consistency with the value we intend to store, and
|
||||
/// return an error if they are inconsistent.
|
||||
fn check_and_store_genesis_value<S: KeyValueStore<E>>(
|
||||
store: &S,
|
||||
value: Self::Value,
|
||||
ops: &mut Vec<KeyValueStoreOp>,
|
||||
) -> Result<(), Error> {
|
||||
let key = &genesis_value_key()[..];
|
||||
|
||||
if let Some(existing_chunk) = Chunk::<Self::Value>::load(store, Self::column(), key)? {
|
||||
if existing_chunk.values.len() != 1 {
|
||||
Err(ChunkError::InvalidGenesisChunk {
|
||||
field: Self::column(),
|
||||
expected_len: 1,
|
||||
observed_len: existing_chunk.values.len(),
|
||||
}
|
||||
.into())
|
||||
} else if existing_chunk.values[0] != value {
|
||||
Err(ChunkError::InconsistentGenesisValue {
|
||||
field: Self::column(),
|
||||
existing_value: format!("{:?}", existing_chunk.values[0]),
|
||||
new_value: format!("{:?}", value),
|
||||
}
|
||||
.into())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
} else {
|
||||
let chunk = Chunk::new(vec![value]);
|
||||
chunk.store(Self::column(), &genesis_value_key()[..], ops)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract the genesis value for a fixed length field from an
|
||||
///
|
||||
/// Will only return a correct value if `slot_needs_genesis_value(state.slot(), spec) == true`.
|
||||
fn extract_genesis_value(
|
||||
state: &BeaconState<E>,
|
||||
spec: &ChainSpec,
|
||||
) -> Result<Self::Value, Error> {
|
||||
let (_, end_vindex) = Self::start_and_end_vindex(state.slot(), spec);
|
||||
match Self::update_pattern(spec) {
|
||||
// Genesis value is guaranteed to exist at `end_vindex`, as it won't yet have been
|
||||
// updated
|
||||
OncePerNSlots { .. } => Ok(Self::get_value(state, end_vindex as u64, spec)?),
|
||||
// If there's lag, the value of the field at the vindex *without the lag*
|
||||
// should still be set to the genesis value.
|
||||
OncePerEpoch { lag } => Ok(Self::get_value(state, end_vindex as u64 + lag, spec)?),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Marker trait for fixed-length fields (`Vector<T, N>`).
|
||||
pub trait FixedLengthField<E: EthSpec>: Field<E> {}
|
||||
|
||||
/// Marker trait for variable-length fields (`List<T, N>`).
|
||||
pub trait VariableLengthField<E: EthSpec>: Field<E> {}
|
||||
|
||||
/// Macro to implement the `Field` trait on a new unit struct type.
|
||||
macro_rules! field {
|
||||
($struct_name:ident, $marker_trait:ident, $value_ty:ty, $length_ty:ty, $column:expr,
|
||||
$update_pattern:expr, $get_value:expr) => {
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct $struct_name;
|
||||
|
||||
impl<E> Field<E> for $struct_name
|
||||
where
|
||||
E: EthSpec,
|
||||
{
|
||||
type Value = $value_ty;
|
||||
type Length = $length_ty;
|
||||
|
||||
fn column() -> DBColumn {
|
||||
$column
|
||||
}
|
||||
|
||||
fn update_pattern(spec: &ChainSpec) -> UpdatePattern {
|
||||
let update_pattern = $update_pattern;
|
||||
update_pattern(spec)
|
||||
}
|
||||
|
||||
fn get_value(
|
||||
state: &BeaconState<E>,
|
||||
vindex: u64,
|
||||
spec: &ChainSpec,
|
||||
) -> Result<Self::Value, ChunkError> {
|
||||
let get_value = $get_value;
|
||||
get_value(state, vindex, spec)
|
||||
}
|
||||
|
||||
fn is_fixed_length() -> bool {
|
||||
stringify!($marker_trait) == "FixedLengthField"
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: EthSpec> $marker_trait<E> for $struct_name {}
|
||||
};
|
||||
}
|
||||
|
||||
field!(
|
||||
BlockRootsChunked,
|
||||
FixedLengthField,
|
||||
Hash256,
|
||||
E::SlotsPerHistoricalRoot,
|
||||
DBColumn::BeaconBlockRootsChunked,
|
||||
|_| OncePerNSlots {
|
||||
n: 1,
|
||||
activation_slot: Some(Slot::new(0)),
|
||||
deactivation_slot: None
|
||||
},
|
||||
|state: &BeaconState<_>, index, _| safe_modulo_vector_index(state.block_roots(), index)
|
||||
);
|
||||
|
||||
field!(
|
||||
StateRootsChunked,
|
||||
FixedLengthField,
|
||||
Hash256,
|
||||
E::SlotsPerHistoricalRoot,
|
||||
DBColumn::BeaconStateRootsChunked,
|
||||
|_| OncePerNSlots {
|
||||
n: 1,
|
||||
activation_slot: Some(Slot::new(0)),
|
||||
deactivation_slot: None,
|
||||
},
|
||||
|state: &BeaconState<_>, index, _| safe_modulo_vector_index(state.state_roots(), index)
|
||||
);
|
||||
|
||||
field!(
|
||||
HistoricalRoots,
|
||||
VariableLengthField,
|
||||
Hash256,
|
||||
E::HistoricalRootsLimit,
|
||||
DBColumn::BeaconHistoricalRoots,
|
||||
|spec: &ChainSpec| OncePerNSlots {
|
||||
n: E::SlotsPerHistoricalRoot::to_u64(),
|
||||
activation_slot: Some(Slot::new(0)),
|
||||
deactivation_slot: spec
|
||||
.capella_fork_epoch
|
||||
.map(|fork_epoch| fork_epoch.start_slot(E::slots_per_epoch())),
|
||||
},
|
||||
|state: &BeaconState<_>, index, _| safe_modulo_list_index(state.historical_roots(), index)
|
||||
);
|
||||
|
||||
field!(
|
||||
RandaoMixes,
|
||||
FixedLengthField,
|
||||
Hash256,
|
||||
E::EpochsPerHistoricalVector,
|
||||
DBColumn::BeaconRandaoMixes,
|
||||
|_| OncePerEpoch { lag: 1 },
|
||||
|state: &BeaconState<_>, index, _| safe_modulo_vector_index(state.randao_mixes(), index)
|
||||
);
|
||||
|
||||
field!(
|
||||
HistoricalSummaries,
|
||||
VariableLengthField,
|
||||
HistoricalSummary,
|
||||
E::HistoricalRootsLimit,
|
||||
DBColumn::BeaconHistoricalSummaries,
|
||||
|spec: &ChainSpec| OncePerNSlots {
|
||||
n: E::SlotsPerHistoricalRoot::to_u64(),
|
||||
activation_slot: spec
|
||||
.capella_fork_epoch
|
||||
.map(|fork_epoch| fork_epoch.start_slot(E::slots_per_epoch())),
|
||||
deactivation_slot: None,
|
||||
},
|
||||
|state: &BeaconState<_>, index, _| safe_modulo_list_index(
|
||||
state
|
||||
.historical_summaries()
|
||||
.map_err(|_| ChunkError::InvalidFork)?,
|
||||
index
|
||||
)
|
||||
);
|
||||
|
||||
pub fn store_updated_vector<F: Field<E>, E: EthSpec, S: KeyValueStore<E>>(
|
||||
field: F,
|
||||
store: &S,
|
||||
state: &BeaconState<E>,
|
||||
spec: &ChainSpec,
|
||||
ops: &mut Vec<KeyValueStoreOp>,
|
||||
) -> Result<(), Error> {
|
||||
let chunk_size = F::chunk_size();
|
||||
let (start_vindex, end_vindex) = F::start_and_end_vindex(state.slot(), spec);
|
||||
let start_cindex = start_vindex / chunk_size;
|
||||
let end_cindex = end_vindex / chunk_size;
|
||||
|
||||
// Store the genesis value if we have access to it, and it hasn't been stored already.
|
||||
if F::slot_needs_genesis_value(state.slot(), spec) {
|
||||
let genesis_value = F::extract_genesis_value(state, spec)?;
|
||||
F::check_and_store_genesis_value(store, genesis_value, ops)?;
|
||||
}
|
||||
|
||||
// Start by iterating backwards from the last chunk, storing new chunks in the database.
|
||||
// Stop once a chunk in the database matches what we were about to store, this indicates
|
||||
// that a previously stored state has already filled-in a portion of the indices covered.
|
||||
let full_range_checked = store_range(
|
||||
field,
|
||||
(start_cindex..=end_cindex).rev(),
|
||||
start_vindex,
|
||||
end_vindex,
|
||||
store,
|
||||
state,
|
||||
spec,
|
||||
ops,
|
||||
)?;
|
||||
|
||||
// If the previous `store_range` did not check the entire range, it may be the case that the
|
||||
// state's vector includes elements at low vector indices that are not yet stored in the
|
||||
// database, so run another `store_range` to ensure these values are also stored.
|
||||
if !full_range_checked {
|
||||
store_range(
|
||||
field,
|
||||
start_cindex..end_cindex,
|
||||
start_vindex,
|
||||
end_vindex,
|
||||
store,
|
||||
state,
|
||||
spec,
|
||||
ops,
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn store_range<F, E, S, I>(
|
||||
_: F,
|
||||
range: I,
|
||||
start_vindex: usize,
|
||||
end_vindex: usize,
|
||||
store: &S,
|
||||
state: &BeaconState<E>,
|
||||
spec: &ChainSpec,
|
||||
ops: &mut Vec<KeyValueStoreOp>,
|
||||
) -> Result<bool, Error>
|
||||
where
|
||||
F: Field<E>,
|
||||
E: EthSpec,
|
||||
S: KeyValueStore<E>,
|
||||
I: Iterator<Item = usize>,
|
||||
{
|
||||
for chunk_index in range {
|
||||
let chunk_key = &chunk_key(chunk_index)[..];
|
||||
|
||||
let existing_chunk =
|
||||
Chunk::<F::Value>::load(store, F::column(), chunk_key)?.unwrap_or_default();
|
||||
|
||||
let new_chunk = F::get_updated_chunk(
|
||||
&existing_chunk,
|
||||
chunk_index,
|
||||
start_vindex,
|
||||
end_vindex,
|
||||
state,
|
||||
spec,
|
||||
)?;
|
||||
|
||||
if new_chunk == existing_chunk {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
new_chunk.store(F::column(), chunk_key, ops)?;
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
// Chunks at the end index are included.
|
||||
// TODO: could be more efficient with a real range query (perhaps RocksDB)
|
||||
fn range_query<S: KeyValueStore<E>, E: EthSpec, T: Decode + Encode>(
|
||||
store: &S,
|
||||
column: DBColumn,
|
||||
start_index: usize,
|
||||
end_index: usize,
|
||||
) -> Result<Vec<Chunk<T>>, Error> {
|
||||
let range = start_index..=end_index;
|
||||
let len = range
|
||||
.end()
|
||||
// Add one to account for inclusive range.
|
||||
.saturating_add(1)
|
||||
.saturating_sub(*range.start());
|
||||
let mut result = Vec::with_capacity(len);
|
||||
|
||||
for chunk_index in range {
|
||||
let key = &chunk_key(chunk_index)[..];
|
||||
let chunk = Chunk::load(store, column, key)?.ok_or(ChunkError::Missing { chunk_index })?;
|
||||
result.push(chunk);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Combine chunks to form a list or vector of all values with vindex in `start_vindex..end_vindex`.
|
||||
///
|
||||
/// The `length` parameter is the length of the vec to construct, with entries set to `default` if
|
||||
/// they lie outside the vindex range.
|
||||
fn stitch<T: Default + Clone>(
|
||||
chunks: Vec<Chunk<T>>,
|
||||
start_vindex: usize,
|
||||
end_vindex: usize,
|
||||
chunk_size: usize,
|
||||
length: usize,
|
||||
default: T,
|
||||
) -> Result<Vec<T>, ChunkError> {
|
||||
if start_vindex + length < end_vindex {
|
||||
return Err(ChunkError::OversizedRange {
|
||||
start_vindex,
|
||||
end_vindex,
|
||||
length,
|
||||
});
|
||||
}
|
||||
|
||||
let start_cindex = start_vindex / chunk_size;
|
||||
let end_cindex = end_vindex / chunk_size;
|
||||
|
||||
let mut result = vec![default; length];
|
||||
|
||||
for (chunk_index, chunk) in (start_cindex..=end_cindex).zip(chunks.into_iter()) {
|
||||
// All chunks but the last chunk must be full-sized
|
||||
if chunk_index != end_cindex && chunk.values.len() != chunk_size {
|
||||
return Err(ChunkError::InvalidSize {
|
||||
chunk_index,
|
||||
expected: chunk_size,
|
||||
actual: chunk.values.len(),
|
||||
});
|
||||
}
|
||||
|
||||
// Copy the chunk entries into the result vector
|
||||
for (i, value) in chunk.values.into_iter().enumerate() {
|
||||
let vindex = chunk_index * chunk_size + i;
|
||||
|
||||
if vindex >= start_vindex && vindex < end_vindex {
|
||||
result[vindex % length] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn load_vector_from_db<F: FixedLengthField<E>, E: EthSpec, S: KeyValueStore<E>>(
|
||||
store: &S,
|
||||
slot: Slot,
|
||||
spec: &ChainSpec,
|
||||
) -> Result<Vector<F::Value, F::Length>, Error> {
|
||||
// Do a range query
|
||||
let chunk_size = F::chunk_size();
|
||||
let (start_vindex, end_vindex) = F::start_and_end_vindex(slot, spec);
|
||||
let start_cindex = start_vindex / chunk_size;
|
||||
let end_cindex = end_vindex / chunk_size;
|
||||
|
||||
let chunks = range_query(store, F::column(), start_cindex, end_cindex)?;
|
||||
|
||||
let default = if F::slot_needs_genesis_value(slot, spec) {
|
||||
F::load_genesis_value(store)?
|
||||
} else {
|
||||
F::Value::default()
|
||||
};
|
||||
|
||||
let result = stitch(
|
||||
chunks,
|
||||
start_vindex,
|
||||
end_vindex,
|
||||
chunk_size,
|
||||
F::Length::to_usize(),
|
||||
default,
|
||||
)?;
|
||||
|
||||
Ok(Vector::new(result).map_err(ChunkError::Milhouse)?)
|
||||
}
|
||||
|
||||
/// The historical roots are stored in vector chunks, despite not actually being a vector.
|
||||
pub fn load_variable_list_from_db<F: VariableLengthField<E>, E: EthSpec, S: KeyValueStore<E>>(
|
||||
store: &S,
|
||||
slot: Slot,
|
||||
spec: &ChainSpec,
|
||||
) -> Result<List<F::Value, F::Length>, Error> {
|
||||
let chunk_size = F::chunk_size();
|
||||
let (start_vindex, end_vindex) = F::start_and_end_vindex(slot, spec);
|
||||
let start_cindex = start_vindex / chunk_size;
|
||||
let end_cindex = end_vindex / chunk_size;
|
||||
|
||||
let chunks: Vec<Chunk<F::Value>> = range_query(store, F::column(), start_cindex, end_cindex)?;
|
||||
|
||||
let mut result = Vec::with_capacity(chunk_size * chunks.len());
|
||||
|
||||
for (chunk_index, chunk) in chunks.into_iter().enumerate() {
|
||||
for (i, value) in chunk.values.into_iter().enumerate() {
|
||||
let vindex = chunk_index * chunk_size + i;
|
||||
|
||||
if vindex >= start_vindex && vindex < end_vindex {
|
||||
result.push(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(List::new(result).map_err(ChunkError::Milhouse)?)
|
||||
}
|
||||
|
||||
/// Index into a `List` field of the state, avoiding out of bounds and division by 0.
|
||||
fn safe_modulo_list_index<T: milhouse::Value + Copy, N: Unsigned>(
|
||||
values: &List<T, N>,
|
||||
index: u64,
|
||||
) -> Result<T, ChunkError> {
|
||||
if values.is_empty() {
|
||||
Err(ChunkError::ZeroLengthList)
|
||||
} else {
|
||||
values
|
||||
.get(index as usize % values.len())
|
||||
.copied()
|
||||
.ok_or(ChunkError::IndexOutOfBounds { index })
|
||||
}
|
||||
}
|
||||
|
||||
fn safe_modulo_vector_index<T: milhouse::Value + Copy, N: Unsigned>(
|
||||
values: &Vector<T, N>,
|
||||
index: u64,
|
||||
) -> Result<T, ChunkError> {
|
||||
if values.is_empty() {
|
||||
Err(ChunkError::ZeroLengthVector)
|
||||
} else {
|
||||
values
|
||||
.get(index as usize % values.len())
|
||||
.copied()
|
||||
.ok_or(ChunkError::IndexOutOfBounds { index })
|
||||
}
|
||||
}
|
||||
|
||||
/// A chunk of a fixed-size vector from the `BeaconState`, stored in the database.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Chunk<T> {
|
||||
/// A vector of up-to `chunk_size` values.
|
||||
pub values: Vec<T>,
|
||||
}
|
||||
|
||||
impl<T> Default for Chunk<T>
|
||||
where
|
||||
T: Decode + Encode,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Chunk { values: vec![] }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Chunk<T>
|
||||
where
|
||||
T: Decode + Encode,
|
||||
{
|
||||
pub fn new(values: Vec<T>) -> Self {
|
||||
Chunk { values }
|
||||
}
|
||||
|
||||
pub fn load<S: KeyValueStore<E>, E: EthSpec>(
|
||||
store: &S,
|
||||
column: DBColumn,
|
||||
key: &[u8],
|
||||
) -> Result<Option<Self>, Error> {
|
||||
store
|
||||
.get_bytes(column, key)?
|
||||
.map(|bytes| Self::decode(&bytes))
|
||||
.transpose()
|
||||
}
|
||||
|
||||
pub fn store(
|
||||
&self,
|
||||
column: DBColumn,
|
||||
key: &[u8],
|
||||
ops: &mut Vec<KeyValueStoreOp>,
|
||||
) -> Result<(), Error> {
|
||||
ops.push(KeyValueStoreOp::PutKeyValue(
|
||||
column,
|
||||
key.to_vec(),
|
||||
self.encode()?,
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempt to decode a single chunk.
|
||||
pub fn decode(bytes: &[u8]) -> Result<Self, Error> {
|
||||
if !<T as Decode>::is_ssz_fixed_len() {
|
||||
return Err(Error::from(ChunkError::InvalidType));
|
||||
}
|
||||
|
||||
let value_size = <T as Decode>::ssz_fixed_len();
|
||||
|
||||
if value_size == 0 {
|
||||
return Err(Error::from(ChunkError::InvalidType));
|
||||
}
|
||||
|
||||
let values = bytes
|
||||
.chunks(value_size)
|
||||
.map(T::from_ssz_bytes)
|
||||
.collect::<Result<_, _>>()?;
|
||||
|
||||
Ok(Chunk { values })
|
||||
}
|
||||
|
||||
pub fn encoded_size(&self) -> usize {
|
||||
self.values.len() * <T as Encode>::ssz_fixed_len()
|
||||
}
|
||||
|
||||
/// Encode a single chunk as bytes.
|
||||
pub fn encode(&self) -> Result<Vec<u8>, Error> {
|
||||
if !<T as Encode>::is_ssz_fixed_len() {
|
||||
return Err(Error::from(ChunkError::InvalidType));
|
||||
}
|
||||
|
||||
Ok(self.values.iter().flat_map(T::as_ssz_bytes).collect())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum ChunkError {
|
||||
ZeroLengthVector,
|
||||
ZeroLengthList,
|
||||
IndexOutOfBounds {
|
||||
index: u64,
|
||||
},
|
||||
InvalidSize {
|
||||
chunk_index: usize,
|
||||
expected: usize,
|
||||
actual: usize,
|
||||
},
|
||||
Missing {
|
||||
chunk_index: usize,
|
||||
},
|
||||
MissingGenesisValue,
|
||||
Inconsistent {
|
||||
field: DBColumn,
|
||||
chunk_index: usize,
|
||||
existing_value: String,
|
||||
new_value: String,
|
||||
},
|
||||
InconsistentGenesisValue {
|
||||
field: DBColumn,
|
||||
existing_value: String,
|
||||
new_value: String,
|
||||
},
|
||||
InvalidGenesisChunk {
|
||||
field: DBColumn,
|
||||
expected_len: usize,
|
||||
observed_len: usize,
|
||||
},
|
||||
InvalidType,
|
||||
OversizedRange {
|
||||
start_vindex: usize,
|
||||
end_vindex: usize,
|
||||
length: usize,
|
||||
},
|
||||
InvalidFork,
|
||||
Milhouse(milhouse::Error),
|
||||
}
|
||||
|
||||
impl From<milhouse::Error> for ChunkError {
|
||||
fn from(e: milhouse::Error) -> ChunkError {
|
||||
Self::Milhouse(e)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use types::MainnetEthSpec as TestSpec;
|
||||
use types::*;
|
||||
|
||||
fn v(i: u64) -> Hash256 {
|
||||
Hash256::from_low_u64_be(i)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stitch_default() {
|
||||
let chunk_size = 4;
|
||||
|
||||
let chunks = vec![
|
||||
Chunk::new(vec![0u64, 1, 2, 3]),
|
||||
Chunk::new(vec![4, 5, 0, 0]),
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
stitch(chunks, 2, 6, chunk_size, 12, 99).unwrap(),
|
||||
vec![99, 99, 2, 3, 4, 5, 99, 99, 99, 99, 99, 99]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stitch_basic() {
|
||||
let chunk_size = 4;
|
||||
let default = v(0);
|
||||
|
||||
let chunks = vec![
|
||||
Chunk::new(vec![v(0), v(1), v(2), v(3)]),
|
||||
Chunk::new(vec![v(4), v(5), v(6), v(7)]),
|
||||
Chunk::new(vec![v(8), v(9), v(10), v(11)]),
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
stitch(chunks.clone(), 0, 12, chunk_size, 12, default).unwrap(),
|
||||
(0..12).map(v).collect::<Vec<_>>()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
stitch(chunks, 2, 10, chunk_size, 8, default).unwrap(),
|
||||
vec![v(8), v(9), v(2), v(3), v(4), v(5), v(6), v(7)]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stitch_oversized_range() {
|
||||
let chunk_size = 4;
|
||||
let default = 0;
|
||||
|
||||
let chunks = vec![Chunk::new(vec![20u64, 21, 22, 23])];
|
||||
|
||||
// Args (start_vindex, end_vindex, length)
|
||||
let args = vec![(0, 21, 20), (0, 2048, 1024), (0, 2, 1)];
|
||||
|
||||
for (start_vindex, end_vindex, length) in args {
|
||||
assert_eq!(
|
||||
stitch(
|
||||
chunks.clone(),
|
||||
start_vindex,
|
||||
end_vindex,
|
||||
chunk_size,
|
||||
length,
|
||||
default
|
||||
),
|
||||
Err(ChunkError::OversizedRange {
|
||||
start_vindex,
|
||||
end_vindex,
|
||||
length,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixed_length_fields() {
|
||||
fn test_fixed_length<F: Field<TestSpec>>(_: F, expected: bool) {
|
||||
assert_eq!(F::is_fixed_length(), expected);
|
||||
}
|
||||
test_fixed_length(BlockRootsChunked, true);
|
||||
test_fixed_length(StateRootsChunked, true);
|
||||
test_fixed_length(HistoricalRoots, false);
|
||||
test_fixed_length(RandaoMixes, true);
|
||||
}
|
||||
|
||||
fn needs_genesis_value_once_per_slot<F: Field<TestSpec>>(_: F) {
|
||||
let spec = &TestSpec::default_spec();
|
||||
let max = F::Length::to_u64();
|
||||
for i in 0..max {
|
||||
assert!(
|
||||
F::slot_needs_genesis_value(Slot::new(i), spec),
|
||||
"slot {}",
|
||||
i
|
||||
);
|
||||
}
|
||||
assert!(!F::slot_needs_genesis_value(Slot::new(max), spec));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn needs_genesis_value_block_roots() {
|
||||
needs_genesis_value_once_per_slot(BlockRootsChunked);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn needs_genesis_value_state_roots() {
|
||||
needs_genesis_value_once_per_slot(StateRootsChunked);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn needs_genesis_value_historical_roots() {
|
||||
let spec = &TestSpec::default_spec();
|
||||
assert!(
|
||||
!<HistoricalRoots as Field<TestSpec>>::slot_needs_genesis_value(Slot::new(0), spec)
|
||||
);
|
||||
}
|
||||
|
||||
fn needs_genesis_value_test_randao<F: Field<TestSpec>>(_: F) {
|
||||
let spec = &TestSpec::default_spec();
|
||||
let max = TestSpec::slots_per_epoch() * (F::Length::to_u64() - 1);
|
||||
for i in 0..max {
|
||||
assert!(
|
||||
F::slot_needs_genesis_value(Slot::new(i), spec),
|
||||
"slot {}",
|
||||
i
|
||||
);
|
||||
}
|
||||
assert!(!F::slot_needs_genesis_value(Slot::new(max), spec));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn needs_genesis_value_randao() {
|
||||
needs_genesis_value_test_randao(RandaoMixes);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
use crate::hdiff::HierarchyConfig;
|
||||
use crate::superstruct;
|
||||
use crate::{AnchorInfo, DBColumn, Error, Split, StoreItem};
|
||||
use crate::{DBColumn, Error, StoreItem};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ssz::{Decode, Encode};
|
||||
use ssz_derive::{Decode, Encode};
|
||||
use std::io::Write;
|
||||
use std::io::{Read, Write};
|
||||
use std::num::NonZeroUsize;
|
||||
use strum::{Display, EnumString, EnumVariantNames};
|
||||
use types::non_zero_usize::new_non_zero_usize;
|
||||
use strum::{Display, EnumString, VariantNames};
|
||||
use superstruct::superstruct;
|
||||
use types::EthSpec;
|
||||
use zstd::Encoder;
|
||||
use types::non_zero_usize::new_non_zero_usize;
|
||||
use zstd::{Decoder, Encoder};
|
||||
|
||||
#[cfg(all(feature = "redb", not(feature = "leveldb")))]
|
||||
pub const DEFAULT_BACKEND: DatabaseBackend = DatabaseBackend::Redb;
|
||||
@@ -19,12 +19,13 @@ pub const DEFAULT_BACKEND: DatabaseBackend = DatabaseBackend::LevelDb;
|
||||
pub const PREV_DEFAULT_SLOTS_PER_RESTORE_POINT: u64 = 2048;
|
||||
pub const DEFAULT_SLOTS_PER_RESTORE_POINT: u64 = 8192;
|
||||
pub const DEFAULT_EPOCHS_PER_STATE_DIFF: u64 = 8;
|
||||
pub const DEFAULT_BLOCK_CACHE_SIZE: NonZeroUsize = new_non_zero_usize(64);
|
||||
pub const DEFAULT_BLOCK_CACHE_SIZE: usize = 0;
|
||||
pub const DEFAULT_STATE_CACHE_SIZE: NonZeroUsize = new_non_zero_usize(128);
|
||||
pub const DEFAULT_STATE_CACHE_HEADROOM: NonZeroUsize = new_non_zero_usize(1);
|
||||
pub const DEFAULT_COMPRESSION_LEVEL: i32 = 1;
|
||||
pub const DEFAULT_HISTORIC_STATE_CACHE_SIZE: NonZeroUsize = new_non_zero_usize(1);
|
||||
pub const DEFAULT_HDIFF_BUFFER_CACHE_SIZE: NonZeroUsize = new_non_zero_usize(16);
|
||||
pub const DEFAULT_COLD_HDIFF_BUFFER_CACHE_SIZE: NonZeroUsize = new_non_zero_usize(16);
|
||||
pub const DEFAULT_HOT_HDIFF_BUFFER_CACHE_SIZE: NonZeroUsize = new_non_zero_usize(1);
|
||||
const EST_COMPRESSION_FACTOR: usize = 2;
|
||||
pub const DEFAULT_EPOCHS_PER_BLOB_PRUNE: u64 = 1;
|
||||
pub const DEFAULT_BLOB_PUNE_MARGIN_EPOCHS: u64 = 0;
|
||||
@@ -33,7 +34,7 @@ pub const DEFAULT_BLOB_PUNE_MARGIN_EPOCHS: u64 = 0;
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct StoreConfig {
|
||||
/// Maximum number of blocks to store in the in-memory block cache.
|
||||
pub block_cache_size: NonZeroUsize,
|
||||
pub block_cache_size: usize,
|
||||
/// Maximum number of states to store in the in-memory state cache.
|
||||
pub state_cache_size: NonZeroUsize,
|
||||
/// Minimum number of states to cull from the state cache upon fullness.
|
||||
@@ -42,8 +43,10 @@ pub struct StoreConfig {
|
||||
pub compression_level: i32,
|
||||
/// Maximum number of historic states to store in the in-memory historic state cache.
|
||||
pub historic_state_cache_size: NonZeroUsize,
|
||||
/// Maximum number of `HDiffBuffer`s to store in memory.
|
||||
pub hdiff_buffer_cache_size: NonZeroUsize,
|
||||
/// Maximum number of cold `HDiffBuffer`s to store in memory.
|
||||
pub cold_hdiff_buffer_cache_size: NonZeroUsize,
|
||||
/// Maximum number of hot `HDiffBuffers` to store in memory.
|
||||
pub hot_hdiff_buffer_cache_size: NonZeroUsize,
|
||||
/// Whether to compact the database on initialization.
|
||||
pub compact_on_init: bool,
|
||||
/// Whether to compact the database during database pruning.
|
||||
@@ -65,14 +68,12 @@ pub struct StoreConfig {
|
||||
|
||||
/// Variant of `StoreConfig` that gets written to disk. Contains immutable configuration params.
|
||||
#[superstruct(
|
||||
variants(V1, V22),
|
||||
variants(V22),
|
||||
variant_attributes(derive(Debug, Clone, PartialEq, Eq, Encode, Decode))
|
||||
)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct OnDiskStoreConfig {
|
||||
#[superstruct(only(V1))]
|
||||
pub slots_per_restore_point: u64,
|
||||
/// Prefix byte to future-proof versions of the `OnDiskStoreConfig` post V1
|
||||
/// Prefix byte to future-proof versions of the `OnDiskStoreConfig`.
|
||||
#[superstruct(only(V22))]
|
||||
version_byte: u8,
|
||||
#[superstruct(only(V22))]
|
||||
@@ -90,10 +91,6 @@ impl OnDiskStoreConfigV22 {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum StoreConfigError {
|
||||
MismatchedSlotsPerRestorePoint {
|
||||
config: u64,
|
||||
on_disk: u64,
|
||||
},
|
||||
InvalidCompressionLevel {
|
||||
level: i32,
|
||||
},
|
||||
@@ -112,7 +109,8 @@ impl Default for StoreConfig {
|
||||
state_cache_size: DEFAULT_STATE_CACHE_SIZE,
|
||||
state_cache_headroom: DEFAULT_STATE_CACHE_HEADROOM,
|
||||
historic_state_cache_size: DEFAULT_HISTORIC_STATE_CACHE_SIZE,
|
||||
hdiff_buffer_cache_size: DEFAULT_HDIFF_BUFFER_CACHE_SIZE,
|
||||
cold_hdiff_buffer_cache_size: DEFAULT_COLD_HDIFF_BUFFER_CACHE_SIZE,
|
||||
hot_hdiff_buffer_cache_size: DEFAULT_HOT_HDIFF_BUFFER_CACHE_SIZE,
|
||||
compression_level: DEFAULT_COMPRESSION_LEVEL,
|
||||
compact_on_init: false,
|
||||
compact_on_prune: true,
|
||||
@@ -134,21 +132,13 @@ impl StoreConfig {
|
||||
pub fn check_compatibility(
|
||||
&self,
|
||||
on_disk_config: &OnDiskStoreConfig,
|
||||
split: &Split,
|
||||
anchor: &AnchorInfo,
|
||||
) -> Result<(), StoreConfigError> {
|
||||
// Allow changing the hierarchy exponents if no historic states are stored.
|
||||
let no_historic_states_stored = anchor.no_historic_states_stored(split.slot);
|
||||
let hierarchy_config_changed =
|
||||
if let Ok(on_disk_hierarchy_config) = on_disk_config.hierarchy_config() {
|
||||
*on_disk_hierarchy_config != self.hierarchy_config
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if hierarchy_config_changed && !no_historic_states_stored {
|
||||
// We previously allowed the hierarchy exponents to change on non-archive nodes, but since
|
||||
// schema v24 and the use of hdiffs in the hot DB, changing will require a resync.
|
||||
let current_config = self.as_disk_config();
|
||||
if current_config != *on_disk_config {
|
||||
Err(StoreConfigError::IncompatibleStoreConfig {
|
||||
config: self.as_disk_config(),
|
||||
config: current_config,
|
||||
on_disk: on_disk_config.clone(),
|
||||
})
|
||||
} else {
|
||||
@@ -204,15 +194,23 @@ impl StoreConfig {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compress_bytes(&self, ssz_bytes: &[u8]) -> Result<Vec<u8>, Error> {
|
||||
/// Compress bytes using zstd and the compression level from `self`.
|
||||
pub fn compress_bytes(&self, ssz_bytes: &[u8]) -> Result<Vec<u8>, std::io::Error> {
|
||||
let mut compressed_value =
|
||||
Vec::with_capacity(self.estimate_compressed_size(ssz_bytes.len()));
|
||||
let mut encoder = Encoder::new(&mut compressed_value, self.compression_level)
|
||||
.map_err(Error::Compression)?;
|
||||
encoder.write_all(ssz_bytes).map_err(Error::Compression)?;
|
||||
encoder.finish().map_err(Error::Compression)?;
|
||||
let mut encoder = Encoder::new(&mut compressed_value, self.compression_level)?;
|
||||
encoder.write_all(ssz_bytes)?;
|
||||
encoder.finish()?;
|
||||
Ok(compressed_value)
|
||||
}
|
||||
|
||||
/// Decompress bytes compressed using zstd.
|
||||
pub fn decompress_bytes(&self, input: &[u8]) -> Result<Vec<u8>, std::io::Error> {
|
||||
let mut out = Vec::with_capacity(self.estimate_decompressed_size(input.len()));
|
||||
let mut decoder = Decoder::new(input)?;
|
||||
decoder.read_to_end(&mut out)?;
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
impl StoreItem for OnDiskStoreConfig {
|
||||
@@ -222,32 +220,21 @@ impl StoreItem for OnDiskStoreConfig {
|
||||
|
||||
fn as_store_bytes(&self) -> Vec<u8> {
|
||||
match self {
|
||||
OnDiskStoreConfig::V1(value) => value.as_ssz_bytes(),
|
||||
OnDiskStoreConfig::V22(value) => value.as_ssz_bytes(),
|
||||
}
|
||||
}
|
||||
|
||||
fn from_store_bytes(bytes: &[u8]) -> Result<Self, Error> {
|
||||
// NOTE: V22 config can never be deserialized as a V1 because the minimum length of its
|
||||
// serialization is: 1 prefix byte + 1 offset (OnDiskStoreConfigV1 container) +
|
||||
// 1 offset (HierarchyConfig container) = 9.
|
||||
if let Ok(value) = OnDiskStoreConfigV1::from_ssz_bytes(bytes) {
|
||||
return Ok(Self::V1(value));
|
||||
match bytes.first() {
|
||||
Some(22) => Ok(Self::V22(OnDiskStoreConfigV22::from_ssz_bytes(bytes)?)),
|
||||
version_byte => Err(StoreConfigError::InvalidVersionByte(version_byte.copied()).into()),
|
||||
}
|
||||
|
||||
Ok(Self::V22(OnDiskStoreConfigV22::from_ssz_bytes(bytes)?))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::{
|
||||
metadata::{ANCHOR_FOR_ARCHIVE_NODE, ANCHOR_UNINITIALIZED, STATE_UPPER_LIMIT_NO_RETAIN},
|
||||
AnchorInfo, Split,
|
||||
};
|
||||
use ssz::DecodeError;
|
||||
use types::{Hash256, Slot};
|
||||
|
||||
#[test]
|
||||
fn check_compatibility_ok() {
|
||||
@@ -257,24 +244,7 @@ mod test {
|
||||
let on_disk_config = OnDiskStoreConfig::V22(OnDiskStoreConfigV22::new(
|
||||
store_config.hierarchy_config.clone(),
|
||||
));
|
||||
let split = Split::default();
|
||||
assert!(store_config
|
||||
.check_compatibility(&on_disk_config, &split, &ANCHOR_UNINITIALIZED)
|
||||
.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_compatibility_after_migration() {
|
||||
let store_config = StoreConfig {
|
||||
..Default::default()
|
||||
};
|
||||
let on_disk_config = OnDiskStoreConfig::V1(OnDiskStoreConfigV1 {
|
||||
slots_per_restore_point: 8192,
|
||||
});
|
||||
let split = Split::default();
|
||||
assert!(store_config
|
||||
.check_compatibility(&on_disk_config, &split, &ANCHOR_UNINITIALIZED)
|
||||
.is_ok());
|
||||
assert!(store_config.check_compatibility(&on_disk_config).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -283,70 +253,11 @@ mod test {
|
||||
let on_disk_config = OnDiskStoreConfig::V22(OnDiskStoreConfigV22::new(HierarchyConfig {
|
||||
exponents: vec![5, 8, 11, 13, 16, 18, 21],
|
||||
}));
|
||||
let split = Split {
|
||||
slot: Slot::new(32),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(store_config
|
||||
.check_compatibility(&on_disk_config, &split, &ANCHOR_FOR_ARCHIVE_NODE)
|
||||
.is_err());
|
||||
assert!(store_config.check_compatibility(&on_disk_config).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_compatibility_hierarchy_config_update() {
|
||||
let store_config = StoreConfig {
|
||||
..Default::default()
|
||||
};
|
||||
let on_disk_config = OnDiskStoreConfig::V22(OnDiskStoreConfigV22::new(HierarchyConfig {
|
||||
exponents: vec![5, 8, 11, 13, 16, 18, 21],
|
||||
}));
|
||||
let split = Split::default();
|
||||
let anchor = AnchorInfo {
|
||||
anchor_slot: Slot::new(0),
|
||||
oldest_block_slot: Slot::new(0),
|
||||
oldest_block_parent: Hash256::ZERO,
|
||||
state_upper_limit: STATE_UPPER_LIMIT_NO_RETAIN,
|
||||
state_lower_limit: Slot::new(0),
|
||||
};
|
||||
assert!(store_config
|
||||
.check_compatibility(&on_disk_config, &split, &anchor)
|
||||
.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serde_on_disk_config_v0_from_v1_default() {
|
||||
let config = OnDiskStoreConfig::V22(OnDiskStoreConfigV22::new(<_>::default()));
|
||||
let config_bytes = config.as_store_bytes();
|
||||
// On a downgrade, the previous version of lighthouse will attempt to deserialize the
|
||||
// prefixed V22 as just the V1 version.
|
||||
assert_eq!(
|
||||
OnDiskStoreConfigV1::from_ssz_bytes(&config_bytes).unwrap_err(),
|
||||
DecodeError::InvalidByteLength {
|
||||
len: 16,
|
||||
expected: 8
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serde_on_disk_config_v0_from_v1_empty() {
|
||||
let config = OnDiskStoreConfig::V22(OnDiskStoreConfigV22::new(HierarchyConfig {
|
||||
exponents: vec![],
|
||||
}));
|
||||
let config_bytes = config.as_store_bytes();
|
||||
// On a downgrade, the previous version of lighthouse will attempt to deserialize the
|
||||
// prefixed V22 as just the V1 version.
|
||||
assert_eq!(
|
||||
OnDiskStoreConfigV1::from_ssz_bytes(&config_bytes).unwrap_err(),
|
||||
DecodeError::InvalidByteLength {
|
||||
len: 9,
|
||||
expected: 8
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serde_on_disk_config_v1_roundtrip() {
|
||||
fn on_disk_config_v22_roundtrip() {
|
||||
let config = OnDiskStoreConfig::V22(OnDiskStoreConfigV22::new(<_>::default()));
|
||||
let bytes = config.as_store_bytes();
|
||||
assert_eq!(bytes[0], 22);
|
||||
@@ -356,7 +267,7 @@ mod test {
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Display, EnumString, EnumVariantNames,
|
||||
Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Display, EnumString, VariantNames,
|
||||
)]
|
||||
#[strum(serialize_all = "lowercase")]
|
||||
pub enum DatabaseBackend {
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
use crate::database::leveldb_impl;
|
||||
#[cfg(feature = "redb")]
|
||||
use crate::database::redb_impl;
|
||||
use crate::{config::DatabaseBackend, KeyValueStoreOp, StoreConfig};
|
||||
use crate::{metrics, ColumnIter, ColumnKeyIter, DBColumn, Error, ItemStore, Key, KeyValueStore};
|
||||
use crate::{ColumnIter, ColumnKeyIter, DBColumn, Error, ItemStore, Key, KeyValueStore, metrics};
|
||||
use crate::{KeyValueStoreOp, StoreConfig, config::DatabaseBackend};
|
||||
use std::collections::HashSet;
|
||||
use std::path::Path;
|
||||
use types::EthSpec;
|
||||
@@ -105,15 +105,6 @@ impl<E: EthSpec> KeyValueStore<E> for BeaconNodeBackend<E> {
|
||||
}
|
||||
}
|
||||
|
||||
fn begin_rw_transaction(&self) -> parking_lot::MutexGuard<()> {
|
||||
match self {
|
||||
#[cfg(feature = "leveldb")]
|
||||
BeaconNodeBackend::LevelDb(txn) => leveldb_impl::LevelDB::begin_rw_transaction(txn),
|
||||
#[cfg(feature = "redb")]
|
||||
BeaconNodeBackend::Redb(txn) => redb_impl::Redb::begin_rw_transaction(txn),
|
||||
}
|
||||
}
|
||||
|
||||
fn compact(&self) -> Result<(), Error> {
|
||||
match self {
|
||||
#[cfg(feature = "leveldb")]
|
||||
@@ -123,7 +114,11 @@ impl<E: EthSpec> KeyValueStore<E> for BeaconNodeBackend<E> {
|
||||
}
|
||||
}
|
||||
|
||||
fn iter_column_keys_from<K: Key>(&self, _column: DBColumn, from: &[u8]) -> ColumnKeyIter<K> {
|
||||
fn iter_column_keys_from<K: Key>(
|
||||
&self,
|
||||
_column: DBColumn,
|
||||
from: &[u8],
|
||||
) -> ColumnKeyIter<'_, K> {
|
||||
match self {
|
||||
#[cfg(feature = "leveldb")]
|
||||
BeaconNodeBackend::LevelDb(txn) => {
|
||||
@@ -136,7 +131,7 @@ impl<E: EthSpec> KeyValueStore<E> for BeaconNodeBackend<E> {
|
||||
}
|
||||
}
|
||||
|
||||
fn iter_column_keys<K: Key>(&self, column: DBColumn) -> ColumnKeyIter<K> {
|
||||
fn iter_column_keys<K: Key>(&self, column: DBColumn) -> ColumnKeyIter<'_, K> {
|
||||
match self {
|
||||
#[cfg(feature = "leveldb")]
|
||||
BeaconNodeBackend::LevelDb(txn) => leveldb_impl::LevelDB::iter_column_keys(txn, column),
|
||||
@@ -145,7 +140,7 @@ impl<E: EthSpec> KeyValueStore<E> for BeaconNodeBackend<E> {
|
||||
}
|
||||
}
|
||||
|
||||
fn iter_column_from<K: Key>(&self, column: DBColumn, from: &[u8]) -> ColumnIter<K> {
|
||||
fn iter_column_from<K: Key>(&self, column: DBColumn, from: &[u8]) -> ColumnIter<'_, K> {
|
||||
match self {
|
||||
#[cfg(feature = "leveldb")]
|
||||
BeaconNodeBackend::LevelDb(txn) => {
|
||||
|
||||
@@ -1,30 +1,28 @@
|
||||
use crate::hot_cold_store::{BytesKey, HotColdDBError};
|
||||
use crate::Key;
|
||||
use crate::hot_cold_store::{BytesKey, HotColdDBError};
|
||||
use crate::{
|
||||
get_key_for_col, metrics, ColumnIter, ColumnKeyIter, DBColumn, Error, KeyValueStoreOp,
|
||||
ColumnIter, ColumnKeyIter, DBColumn, Error, KeyValueStoreOp, get_key_for_col, metrics,
|
||||
};
|
||||
use fixed_bytes::FixedBytesExtended;
|
||||
use leveldb::{
|
||||
compaction::Compaction,
|
||||
database::{
|
||||
Database,
|
||||
batch::{Batch, Writebatch},
|
||||
kv::KV,
|
||||
Database,
|
||||
},
|
||||
iterator::{Iterable, LevelDBIterator},
|
||||
options::{Options, ReadOptions},
|
||||
};
|
||||
use parking_lot::{Mutex, MutexGuard};
|
||||
use std::collections::HashSet;
|
||||
use std::marker::PhantomData;
|
||||
use std::path::Path;
|
||||
use types::{EthSpec, FixedBytesExtended, Hash256};
|
||||
use types::{EthSpec, Hash256};
|
||||
|
||||
use super::interface::WriteOptions;
|
||||
|
||||
pub struct LevelDB<E: EthSpec> {
|
||||
db: Database<BytesKey>,
|
||||
/// A mutex to synchronise sensitive read-write transactions.
|
||||
transaction_mutex: Mutex<()>,
|
||||
_phantom: PhantomData<E>,
|
||||
}
|
||||
|
||||
@@ -43,16 +41,14 @@ impl<E: EthSpec> LevelDB<E> {
|
||||
options.create_if_missing = true;
|
||||
|
||||
let db = Database::open(path, options)?;
|
||||
let transaction_mutex = Mutex::new(());
|
||||
|
||||
Ok(Self {
|
||||
db,
|
||||
transaction_mutex,
|
||||
_phantom: PhantomData,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn read_options(&self) -> ReadOptions<BytesKey> {
|
||||
pub fn read_options(&self) -> ReadOptions<'_, BytesKey> {
|
||||
ReadOptions::new()
|
||||
}
|
||||
|
||||
@@ -177,10 +173,6 @@ impl<E: EthSpec> LevelDB<E> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn begin_rw_transaction(&self) -> MutexGuard<()> {
|
||||
self.transaction_mutex.lock()
|
||||
}
|
||||
|
||||
/// Compact all values in the states and states flag columns.
|
||||
pub fn compact(&self) -> Result<(), Error> {
|
||||
let _timer = metrics::start_timer(&metrics::DISK_DB_COMPACT_TIMES);
|
||||
@@ -216,7 +208,7 @@ impl<E: EthSpec> LevelDB<E> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn iter_column_from<K: Key>(&self, column: DBColumn, from: &[u8]) -> ColumnIter<K> {
|
||||
pub fn iter_column_from<K: Key>(&self, column: DBColumn, from: &[u8]) -> ColumnIter<'_, K> {
|
||||
let start_key = BytesKey::from_vec(get_key_for_col(column, from));
|
||||
let iter = self.db.iter(self.read_options());
|
||||
iter.seek(&start_key);
|
||||
@@ -240,7 +232,11 @@ impl<E: EthSpec> LevelDB<E> {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn iter_column_keys_from<K: Key>(&self, column: DBColumn, from: &[u8]) -> ColumnKeyIter<K> {
|
||||
pub fn iter_column_keys_from<K: Key>(
|
||||
&self,
|
||||
column: DBColumn,
|
||||
from: &[u8],
|
||||
) -> ColumnKeyIter<'_, K> {
|
||||
let start_key = BytesKey::from_vec(get_key_for_col(column, from));
|
||||
|
||||
let iter = self.db.keys_iter(self.read_options());
|
||||
@@ -262,11 +258,11 @@ impl<E: EthSpec> LevelDB<E> {
|
||||
}
|
||||
|
||||
/// Iterate through all keys and values in a particular column.
|
||||
pub fn iter_column_keys<K: Key>(&self, column: DBColumn) -> ColumnKeyIter<K> {
|
||||
pub fn iter_column_keys<K: Key>(&self, column: DBColumn) -> ColumnKeyIter<'_, K> {
|
||||
self.iter_column_keys_from(column, &vec![0; column.key_size()])
|
||||
}
|
||||
|
||||
pub fn iter_column<K: Key>(&self, column: DBColumn) -> ColumnIter<K> {
|
||||
pub fn iter_column<K: Key>(&self, column: DBColumn) -> ColumnIter<'_, K> {
|
||||
self.iter_column_from(column, &vec![0; column.key_size()])
|
||||
}
|
||||
|
||||
@@ -287,7 +283,8 @@ impl<E: EthSpec> LevelDB<E> {
|
||||
) -> Result<(), Error> {
|
||||
let mut leveldb_batch = Writebatch::new();
|
||||
let iter = self.db.iter(self.read_options());
|
||||
|
||||
let start_key = BytesKey::from_vec(column.as_bytes().to_vec());
|
||||
iter.seek(&start_key);
|
||||
iter.take_while(move |(key, _)| key.matches_column(column))
|
||||
.for_each(|(key, value)| {
|
||||
if f(&value).unwrap_or(false) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{metrics, ColumnIter, ColumnKeyIter, Key};
|
||||
use crate::{ColumnIter, ColumnKeyIter, Key, metrics};
|
||||
use crate::{DBColumn, Error, KeyValueStoreOp};
|
||||
use parking_lot::{Mutex, MutexGuard, RwLock};
|
||||
use parking_lot::RwLock;
|
||||
use redb::TableDefinition;
|
||||
use std::collections::HashSet;
|
||||
use std::{borrow::BorrowMut, marker::PhantomData, path::Path};
|
||||
@@ -13,7 +13,6 @@ pub const DB_FILE_NAME: &str = "database.redb";
|
||||
|
||||
pub struct Redb<E: EthSpec> {
|
||||
db: RwLock<redb::Database>,
|
||||
transaction_mutex: Mutex<()>,
|
||||
_phantom: PhantomData<E>,
|
||||
}
|
||||
|
||||
@@ -31,7 +30,6 @@ impl<E: EthSpec> Redb<E> {
|
||||
pub fn open(path: &Path) -> Result<Self, Error> {
|
||||
let db_file = path.join(DB_FILE_NAME);
|
||||
let db = redb::Database::create(db_file)?;
|
||||
let transaction_mutex = Mutex::new(());
|
||||
|
||||
for column in DBColumn::iter() {
|
||||
Redb::<E>::create_table(&db, column.into())?;
|
||||
@@ -39,7 +37,6 @@ impl<E: EthSpec> Redb<E> {
|
||||
|
||||
Ok(Self {
|
||||
db: db.into(),
|
||||
transaction_mutex,
|
||||
_phantom: PhantomData,
|
||||
})
|
||||
}
|
||||
@@ -61,10 +58,6 @@ impl<E: EthSpec> Redb<E> {
|
||||
opts
|
||||
}
|
||||
|
||||
pub fn begin_rw_transaction(&self) -> MutexGuard<()> {
|
||||
self.transaction_mutex.lock()
|
||||
}
|
||||
|
||||
pub fn put_bytes_with_options(
|
||||
&self,
|
||||
col: DBColumn,
|
||||
@@ -211,7 +204,11 @@ impl<E: EthSpec> Redb<E> {
|
||||
mut_db.compact().map_err(Into::into).map(|_| ())
|
||||
}
|
||||
|
||||
pub fn iter_column_keys_from<K: Key>(&self, column: DBColumn, from: &[u8]) -> ColumnKeyIter<K> {
|
||||
pub fn iter_column_keys_from<K: Key>(
|
||||
&self,
|
||||
column: DBColumn,
|
||||
from: &[u8],
|
||||
) -> ColumnKeyIter<'_, K> {
|
||||
let table_definition: TableDefinition<'_, &[u8], &[u8]> =
|
||||
TableDefinition::new(column.into());
|
||||
|
||||
@@ -239,11 +236,11 @@ impl<E: EthSpec> Redb<E> {
|
||||
}
|
||||
|
||||
/// Iterate through all keys and values in a particular column.
|
||||
pub fn iter_column_keys<K: Key>(&self, column: DBColumn) -> ColumnKeyIter<K> {
|
||||
pub fn iter_column_keys<K: Key>(&self, column: DBColumn) -> ColumnKeyIter<'_, K> {
|
||||
self.iter_column_keys_from(column, &vec![0; column.key_size()])
|
||||
}
|
||||
|
||||
pub fn iter_column_from<K: Key>(&self, column: DBColumn, from: &[u8]) -> ColumnIter<K> {
|
||||
pub fn iter_column_from<K: Key>(&self, column: DBColumn, from: &[u8]) -> ColumnIter<'_, K> {
|
||||
let table_definition: TableDefinition<'_, &[u8], &[u8]> =
|
||||
TableDefinition::new(column.into());
|
||||
|
||||
@@ -276,7 +273,7 @@ impl<E: EthSpec> Redb<E> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn iter_column<K: Key>(&self, column: DBColumn) -> ColumnIter<K> {
|
||||
pub fn iter_column<K: Key>(&self, column: DBColumn) -> ColumnIter<'_, K> {
|
||||
self.iter_column_from(column, &vec![0; column.key_size()])
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
use crate::chunked_vector::ChunkError;
|
||||
use crate::config::StoreConfigError;
|
||||
use crate::hot_cold_store::HotColdDBError;
|
||||
use crate::{hdiff, DBColumn};
|
||||
use crate::hot_cold_store::{HotColdDBError, StateSummaryIteratorError};
|
||||
use crate::{DBColumn, hdiff};
|
||||
#[cfg(feature = "leveldb")]
|
||||
use leveldb::error::Error as LevelDBError;
|
||||
use ssz::DecodeError;
|
||||
use state_processing::BlockReplayError;
|
||||
use types::{milhouse, BeaconStateError, EpochCacheError, Hash256, InconsistentFork, Slot};
|
||||
use types::{BeaconStateError, EpochCacheError, Hash256, InconsistentFork, Slot};
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
SszDecodeError(DecodeError),
|
||||
VectorChunkError(ChunkError),
|
||||
BeaconStateError(BeaconStateError),
|
||||
PartialBeaconStateError,
|
||||
HotColdDBError(HotColdDBError),
|
||||
DBError {
|
||||
message: String,
|
||||
@@ -26,6 +23,9 @@ pub enum Error {
|
||||
SplitPointModified(Slot, Slot),
|
||||
ConfigError(StoreConfigError),
|
||||
MigrationError(String),
|
||||
/// The store's `anchor_info` is still the default uninitialized value when attempting a state
|
||||
/// write
|
||||
AnchorUninitialized,
|
||||
/// The store's `anchor_info` was mutated concurrently, the latest modification wasn't applied.
|
||||
AnchorInfoConcurrentMutation,
|
||||
/// The store's `blob_info` was mutated concurrently, the latest modification wasn't applied.
|
||||
@@ -47,11 +47,16 @@ pub enum Error {
|
||||
expected: Hash256,
|
||||
computed: Hash256,
|
||||
},
|
||||
MissingState(Hash256),
|
||||
MissingHotStateSummary(Hash256),
|
||||
MissingHotStateSnapshot(Hash256, Slot),
|
||||
MissingGenesisState,
|
||||
MissingSnapshot(Slot),
|
||||
LoadingHotHdiffBufferError(String, Hash256, Box<Error>),
|
||||
LoadingHotStateError(String, Hash256, Box<Error>),
|
||||
BlockReplayError(BlockReplayError),
|
||||
AddPayloadLogicError,
|
||||
InvalidKey,
|
||||
InvalidKey(String),
|
||||
InvalidBytes,
|
||||
InconsistentFork(InconsistentFork),
|
||||
#[cfg(feature = "leveldb")]
|
||||
@@ -61,6 +66,7 @@ pub enum Error {
|
||||
CacheBuildError(EpochCacheError),
|
||||
RandaoMixOutOfBounds,
|
||||
MilhouseError(milhouse::Error),
|
||||
SszTypesError(ssz_types::Error),
|
||||
Compression(std::io::Error),
|
||||
FinalizedStateDecreasingSlot,
|
||||
FinalizedStateUnaligned,
|
||||
@@ -75,6 +81,26 @@ pub enum Error {
|
||||
MissingBlock(Hash256),
|
||||
GenesisStateUnknown,
|
||||
ArithError(safe_arith::ArithError),
|
||||
MismatchedDiffBaseState {
|
||||
expected_slot: Slot,
|
||||
stored_slot: Slot,
|
||||
},
|
||||
SnapshotDiffBaseState {
|
||||
slot: Slot,
|
||||
},
|
||||
LoadAnchorInfo(Box<Error>),
|
||||
LoadSplit(Box<Error>),
|
||||
LoadBlobInfo(Box<Error>),
|
||||
LoadDataColumnInfo(Box<Error>),
|
||||
LoadConfig(Box<Error>),
|
||||
LoadHotStateSummary(Hash256, Box<Error>),
|
||||
LoadHotStateSummaryForSplit(Box<Error>),
|
||||
StateSummaryIteratorError {
|
||||
error: StateSummaryIteratorError,
|
||||
from_state_root: Hash256,
|
||||
from_state_slot: Slot,
|
||||
target_slot: Slot,
|
||||
},
|
||||
}
|
||||
|
||||
pub trait HandleUnavailable<T> {
|
||||
@@ -97,12 +123,6 @@ impl From<DecodeError> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ChunkError> for Error {
|
||||
fn from(e: ChunkError) -> Error {
|
||||
Error::VectorChunkError(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<HotColdDBError> for Error {
|
||||
fn from(e: HotColdDBError) -> Error {
|
||||
Error::HotColdDBError(e)
|
||||
@@ -133,6 +153,12 @@ impl From<milhouse::Error> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ssz_types::Error> for Error {
|
||||
fn from(e: ssz_types::Error) -> Self {
|
||||
Self::SszTypesError(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<hdiff::Error> for Error {
|
||||
fn from(e: hdiff::Error) -> Self {
|
||||
Self::Hdiff(e)
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
//! Hierarchical diff implementation.
|
||||
use crate::{metrics, DBColumn, StoreConfig, StoreItem};
|
||||
use crate::{DBColumn, StoreConfig, StoreItem, metrics};
|
||||
use bls::PublicKeyBytes;
|
||||
use itertools::Itertools;
|
||||
use milhouse::List;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ssz::{Decode, Encode};
|
||||
use ssz_derive::{Decode, Encode};
|
||||
use std::cmp::Ordering;
|
||||
use std::io::{Read, Write};
|
||||
use std::ops::RangeInclusive;
|
||||
use std::str::FromStr;
|
||||
use std::sync::LazyLock;
|
||||
use superstruct::superstruct;
|
||||
use types::historical_summary::HistoricalSummary;
|
||||
use types::{BeaconState, ChainSpec, Epoch, EthSpec, Hash256, List, Slot, Validator};
|
||||
use zstd::{Decoder, Encoder};
|
||||
use types::{BeaconState, ChainSpec, Epoch, EthSpec, Hash256, Slot, Validator};
|
||||
|
||||
static EMPTY_PUBKEY: LazyLock<PublicKeyBytes> = LazyLock::new(PublicKeyBytes::empty);
|
||||
|
||||
@@ -27,6 +26,7 @@ pub enum Error {
|
||||
Compression(std::io::Error),
|
||||
InvalidSszState(ssz::DecodeError),
|
||||
InvalidBalancesLength,
|
||||
LessThanStart(Slot, Slot),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Encode, Decode)]
|
||||
@@ -67,6 +67,10 @@ impl FromStr for HierarchyConfig {
|
||||
return Err("hierarchy-exponents must be in ascending order".to_string());
|
||||
}
|
||||
|
||||
if exponents.is_empty() {
|
||||
return Err("empty exponents".to_string());
|
||||
}
|
||||
|
||||
Ok(HierarchyConfig { exponents })
|
||||
}
|
||||
}
|
||||
@@ -390,13 +394,17 @@ impl CompressedU64Diff {
|
||||
.collect();
|
||||
|
||||
Ok(CompressedU64Diff {
|
||||
bytes: compress_bytes(&uncompressed_bytes, config)?,
|
||||
bytes: config
|
||||
.compress_bytes(&uncompressed_bytes)
|
||||
.map_err(Error::Compression)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn apply(&self, xs: &mut Vec<u64>, config: &StoreConfig) -> Result<(), Error> {
|
||||
// Decompress balances diff.
|
||||
let balances_diff_bytes = uncompress_bytes(&self.bytes, config)?;
|
||||
let balances_diff_bytes = config
|
||||
.decompress_bytes(&self.bytes)
|
||||
.map_err(Error::Compression)?;
|
||||
|
||||
for (i, diff_bytes) in balances_diff_bytes
|
||||
.chunks(u64::BITS as usize / 8)
|
||||
@@ -423,22 +431,6 @@ impl CompressedU64Diff {
|
||||
}
|
||||
}
|
||||
|
||||
fn compress_bytes(input: &[u8], config: &StoreConfig) -> Result<Vec<u8>, Error> {
|
||||
let compression_level = config.compression_level;
|
||||
let mut out = Vec::with_capacity(config.estimate_compressed_size(input.len()));
|
||||
let mut encoder = Encoder::new(&mut out, compression_level).map_err(Error::Compression)?;
|
||||
encoder.write_all(input).map_err(Error::Compression)?;
|
||||
encoder.finish().map_err(Error::Compression)?;
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn uncompress_bytes(input: &[u8], config: &StoreConfig) -> Result<Vec<u8>, Error> {
|
||||
let mut out = Vec::with_capacity(config.estimate_decompressed_size(input.len()));
|
||||
let mut decoder = Decoder::new(input).map_err(Error::Compression)?;
|
||||
decoder.read_to_end(&mut out).map_err(Error::Compression)?;
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
impl ValidatorsDiff {
|
||||
pub fn compute(
|
||||
xs: &[Validator],
|
||||
@@ -478,7 +470,9 @@ impl ValidatorsDiff {
|
||||
Hash256::ZERO
|
||||
},
|
||||
// effective_balance can increase and decrease
|
||||
effective_balance: y.effective_balance - x.effective_balance,
|
||||
effective_balance: y
|
||||
.effective_balance
|
||||
.wrapping_sub(x.effective_balance),
|
||||
// slashed can only change from false into true. In an index re-use it can
|
||||
// switch back to false, but in that case the pubkey will also change.
|
||||
slashed: y.slashed,
|
||||
@@ -527,12 +521,16 @@ impl ValidatorsDiff {
|
||||
.collect::<Vec<u8>>();
|
||||
|
||||
Ok(Self {
|
||||
bytes: compress_bytes(&uncompressed_bytes, config)?,
|
||||
bytes: config
|
||||
.compress_bytes(&uncompressed_bytes)
|
||||
.map_err(Error::Compression)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn apply(&self, xs: &mut Vec<Validator>, config: &StoreConfig) -> Result<(), Error> {
|
||||
let validator_diff_bytes = uncompress_bytes(&self.bytes, config)?;
|
||||
let validator_diff_bytes = config
|
||||
.decompress_bytes(&self.bytes)
|
||||
.map_err(Error::Compression)?;
|
||||
|
||||
for diff_bytes in
|
||||
validator_diff_bytes.chunks(<ValidatorDiffEntry as Decode>::ssz_fixed_len())
|
||||
@@ -642,10 +640,26 @@ impl HierarchyConfig {
|
||||
Err(Error::InvalidHierarchy)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn exponent_for_slot(slot: Slot) -> u32 {
|
||||
slot.as_u64().trailing_zeros()
|
||||
}
|
||||
}
|
||||
|
||||
impl HierarchyModuli {
|
||||
pub fn storage_strategy(&self, slot: Slot) -> Result<StorageStrategy, Error> {
|
||||
/// * `slot` - Slot of the storage strategy
|
||||
/// * `start_slot` - Slot before which states are not available. Initial snapshot point, which
|
||||
/// may not be aligned to the hierarchy moduli values. Given an example of
|
||||
/// exponents [5,13,21], to reconstruct state at slot 3,000,003: if start = 3,000,002
|
||||
/// layer 2 diff will point to the start snapshot instead of the layer 1 diff at
|
||||
/// 2998272.
|
||||
pub fn storage_strategy(&self, slot: Slot, start_slot: Slot) -> Result<StorageStrategy, Error> {
|
||||
match slot.cmp(&start_slot) {
|
||||
Ordering::Less => return Err(Error::LessThanStart(slot, start_slot)),
|
||||
Ordering::Equal => return Ok(StorageStrategy::Snapshot),
|
||||
Ordering::Greater => {} // continue
|
||||
}
|
||||
|
||||
// last = full snapshot interval
|
||||
let last = self.moduli.last().copied().ok_or(Error::InvalidHierarchy)?;
|
||||
// first = most frequent diff layer, need to replay blocks from this layer
|
||||
@@ -667,14 +681,22 @@ impl HierarchyModuli {
|
||||
.find_map(|(&n_big, &n_small)| {
|
||||
if slot % n_small == 0 {
|
||||
// Diff from the previous layer.
|
||||
Some(StorageStrategy::DiffFrom(slot / n_big * n_big))
|
||||
let from = slot / n_big * n_big;
|
||||
// Or from start point
|
||||
let from = std::cmp::max(from, start_slot);
|
||||
Some(StorageStrategy::DiffFrom(from))
|
||||
} else {
|
||||
// Keep trying with next layer
|
||||
None
|
||||
}
|
||||
})
|
||||
// Exhausted layers, need to replay from most frequent layer
|
||||
.unwrap_or(StorageStrategy::ReplayFrom(slot / first * first)))
|
||||
.unwrap_or_else(|| {
|
||||
let from = slot / first * first;
|
||||
// Or from start point
|
||||
let from = std::cmp::max(from, start_slot);
|
||||
StorageStrategy::ReplayFrom(from)
|
||||
}))
|
||||
}
|
||||
|
||||
/// Return the smallest slot greater than or equal to `slot` at which a full snapshot should
|
||||
@@ -703,6 +725,26 @@ impl HierarchyModuli {
|
||||
|second_layer_moduli| Ok(slot % *second_layer_moduli == 0),
|
||||
)
|
||||
}
|
||||
|
||||
/// For each layer, returns the closest diff less than or equal to `slot`.
|
||||
pub fn closest_layer_points(&self, slot: Slot, start_slot: Slot) -> Vec<Slot> {
|
||||
let mut layers = self
|
||||
.moduli
|
||||
.iter()
|
||||
.map(|&n| {
|
||||
let from = slot / n * n;
|
||||
// Or from start point
|
||||
std::cmp::max(from, start_slot)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Remove duplication caused by the capping at `start_slot` (multiple
|
||||
// layers may have the same slot equal to `start_slot`), or shared multiples (a slot that is
|
||||
// a multiple of 2**n will also be a multiple of 2**m for all m < n).
|
||||
layers.dedup();
|
||||
|
||||
layers
|
||||
}
|
||||
}
|
||||
|
||||
impl StorageStrategy {
|
||||
@@ -732,45 +774,69 @@ impl StorageStrategy {
|
||||
}
|
||||
.map(Slot::from)
|
||||
}
|
||||
|
||||
/// Returns the slot that storage_strategy points to.
|
||||
pub fn diff_base_slot(&self) -> Option<Slot> {
|
||||
match self {
|
||||
Self::ReplayFrom(from) => Some(*from),
|
||||
Self::DiffFrom(from) => Some(*from),
|
||||
Self::Snapshot => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_replay_from(&self) -> bool {
|
||||
matches!(self, Self::ReplayFrom(_))
|
||||
}
|
||||
|
||||
pub fn is_diff_from(&self) -> bool {
|
||||
matches!(self, Self::DiffFrom(_))
|
||||
}
|
||||
|
||||
pub fn is_snapshot(&self) -> bool {
|
||||
matches!(self, Self::Snapshot)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rand::{rngs::SmallRng, thread_rng, Rng, SeedableRng};
|
||||
use rand::{Rng, SeedableRng, rng, rngs::SmallRng};
|
||||
|
||||
#[test]
|
||||
fn default_storage_strategy() {
|
||||
let config = HierarchyConfig::default();
|
||||
config.validate().unwrap();
|
||||
let sslot = Slot::new(0);
|
||||
|
||||
let moduli = config.to_moduli().unwrap();
|
||||
|
||||
// Full snapshots at multiples of 2^21.
|
||||
let snapshot_freq = Slot::new(1 << 21);
|
||||
assert_eq!(
|
||||
moduli.storage_strategy(Slot::new(0)).unwrap(),
|
||||
moduli.storage_strategy(Slot::new(0), sslot).unwrap(),
|
||||
StorageStrategy::Snapshot
|
||||
);
|
||||
assert_eq!(
|
||||
moduli.storage_strategy(snapshot_freq).unwrap(),
|
||||
moduli.storage_strategy(snapshot_freq, sslot).unwrap(),
|
||||
StorageStrategy::Snapshot
|
||||
);
|
||||
assert_eq!(
|
||||
moduli.storage_strategy(snapshot_freq * 3).unwrap(),
|
||||
moduli.storage_strategy(snapshot_freq * 3, sslot).unwrap(),
|
||||
StorageStrategy::Snapshot
|
||||
);
|
||||
|
||||
// Diffs should be from the previous layer (the snapshot in this case), and not the previous diff in the same layer.
|
||||
let first_layer = Slot::new(1 << 18);
|
||||
assert_eq!(
|
||||
moduli.storage_strategy(first_layer * 2).unwrap(),
|
||||
moduli.storage_strategy(first_layer * 2, sslot).unwrap(),
|
||||
StorageStrategy::DiffFrom(Slot::new(0))
|
||||
);
|
||||
|
||||
let replay_strategy_slot = first_layer + 1;
|
||||
assert_eq!(
|
||||
moduli.storage_strategy(replay_strategy_slot).unwrap(),
|
||||
moduli
|
||||
.storage_strategy(replay_strategy_slot, sslot)
|
||||
.unwrap(),
|
||||
StorageStrategy::ReplayFrom(first_layer)
|
||||
);
|
||||
}
|
||||
@@ -839,7 +905,7 @@ mod tests {
|
||||
fn compressed_validators_diff() {
|
||||
assert_eq!(<ValidatorDiffEntry as Decode>::ssz_fixed_len(), 129);
|
||||
|
||||
let mut rng = thread_rng();
|
||||
let mut rng = rng();
|
||||
let config = &StoreConfig::default();
|
||||
let xs = (0..10)
|
||||
.map(|_| rand_validator(&mut rng))
|
||||
@@ -857,7 +923,7 @@ mod tests {
|
||||
fn rand_validator(mut rng: impl Rng) -> Validator {
|
||||
let mut pubkey = [0u8; 48];
|
||||
rng.fill_bytes(&mut pubkey);
|
||||
let withdrawal_credentials: [u8; 32] = rng.gen();
|
||||
let withdrawal_credentials: [u8; 32] = rng.random();
|
||||
|
||||
Validator {
|
||||
pubkey: PublicKeyBytes::from_ssz_bytes(&pubkey).unwrap(),
|
||||
@@ -940,4 +1006,93 @@ mod tests {
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// Test that the diffs and snapshots required for storage of split states are retained in the
|
||||
// hot DB as the split slot advances, if we begin from an initial configuration where this
|
||||
// invariant holds.
|
||||
fn test_slots_retained_invariant(hierarchy: HierarchyModuli, start_slot: u64, epoch_jump: u64) {
|
||||
let start_slot = Slot::new(start_slot);
|
||||
let mut finalized_slot = start_slot;
|
||||
|
||||
// Initially we have just one snapshot stored at the `start_slot`. This is what checkpoint
|
||||
// sync sets up (or the V24 migration).
|
||||
let mut retained_slots = vec![finalized_slot];
|
||||
|
||||
// Iterate until we've reached two snapshots in the future.
|
||||
let stop_at = hierarchy
|
||||
.next_snapshot_slot(hierarchy.next_snapshot_slot(start_slot).unwrap() + 1)
|
||||
.unwrap();
|
||||
|
||||
while finalized_slot <= stop_at {
|
||||
// Jump multiple epocsh at a time because inter-epoch states are not interesting and
|
||||
// would take too long to iterate over.
|
||||
let new_finalized_slot = finalized_slot + 32 * epoch_jump;
|
||||
|
||||
let new_retained_slots = hierarchy.closest_layer_points(new_finalized_slot, start_slot);
|
||||
|
||||
for slot in &new_retained_slots {
|
||||
// All new retained slots must either be already stored prior to the old finalized
|
||||
// slot, OR newer than the finalized slot (i.e. stored in the hot DB as part of
|
||||
// regular state storage).
|
||||
assert!(retained_slots.contains(slot) || *slot >= finalized_slot);
|
||||
}
|
||||
|
||||
retained_slots = new_retained_slots;
|
||||
finalized_slot = new_finalized_slot;
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slots_retained_invariant() {
|
||||
let cases = [
|
||||
// Default hierarchy with a start_slot between the 2^13 and 2^16 layers.
|
||||
(
|
||||
HierarchyConfig::default().to_moduli().unwrap(),
|
||||
2 * (1 << 14) - 5 * 32,
|
||||
1,
|
||||
),
|
||||
// Default hierarchy with a start_slot between the 2^13 and 2^16 layers, with 8 epochs
|
||||
// finalizing at a time (should not make any difference).
|
||||
(
|
||||
HierarchyConfig::default().to_moduli().unwrap(),
|
||||
2 * (1 << 14) - 5 * 32,
|
||||
8,
|
||||
),
|
||||
// Very dense hierarchy config.
|
||||
(
|
||||
HierarchyConfig::from_str("5,7")
|
||||
.unwrap()
|
||||
.to_moduli()
|
||||
.unwrap(),
|
||||
32,
|
||||
1,
|
||||
),
|
||||
// Very dense hierarchy config that skips a whole snapshot on its first finalization.
|
||||
(
|
||||
HierarchyConfig::from_str("5,7")
|
||||
.unwrap()
|
||||
.to_moduli()
|
||||
.unwrap(),
|
||||
32,
|
||||
1 << 7,
|
||||
),
|
||||
];
|
||||
|
||||
for (hierarchy, start_slot, epoch_jump) in cases {
|
||||
test_slots_retained_invariant(hierarchy, start_slot, epoch_jump);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn closest_layer_points_unique() {
|
||||
let hierarchy = HierarchyConfig::default().to_moduli().unwrap();
|
||||
|
||||
let start_slot = Slot::new(0);
|
||||
let end_slot = hierarchy.next_snapshot_slot(Slot::new(1)).unwrap();
|
||||
|
||||
for slot in (0..end_slot.as_u64()).map(Slot::new) {
|
||||
let closest_layer_points = hierarchy.closest_layer_points(slot, start_slot);
|
||||
assert!(closest_layer_points.is_sorted_by(|a, b| a > b));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,11 +34,17 @@ impl<E: EthSpec> HistoricStateCache<E> {
|
||||
|
||||
pub fn get_hdiff_buffer(&mut self, slot: Slot) -> Option<HDiffBuffer> {
|
||||
if let Some(buffer_ref) = self.hdiff_buffers.get(&slot) {
|
||||
let _timer = metrics::start_timer(&metrics::BEACON_HDIFF_BUFFER_CLONE_TIMES);
|
||||
let _timer = metrics::start_timer_vec(
|
||||
&metrics::BEACON_HDIFF_BUFFER_CLONE_TIME,
|
||||
metrics::COLD_METRIC,
|
||||
);
|
||||
Some(buffer_ref.clone())
|
||||
} else if let Some(state) = self.states.get(&slot) {
|
||||
let buffer = HDiffBuffer::from_state(state.clone());
|
||||
let _timer = metrics::start_timer(&metrics::BEACON_HDIFF_BUFFER_CLONE_TIMES);
|
||||
let _timer = metrics::start_timer_vec(
|
||||
&metrics::BEACON_HDIFF_BUFFER_CLONE_TIME,
|
||||
metrics::COLD_METRIC,
|
||||
);
|
||||
let cloned = buffer.clone();
|
||||
drop(_timer);
|
||||
self.hdiff_buffers.put(slot, cloned);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,2 +1 @@
|
||||
pub mod beacon_state;
|
||||
pub mod execution_payload;
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
use crate::*;
|
||||
use ssz::{DecodeError, Encode};
|
||||
use ssz_derive::Encode;
|
||||
|
||||
pub fn store_full_state<E: EthSpec>(
|
||||
state_root: &Hash256,
|
||||
state: &BeaconState<E>,
|
||||
ops: &mut Vec<KeyValueStoreOp>,
|
||||
) -> Result<(), Error> {
|
||||
let bytes = {
|
||||
let _overhead_timer = metrics::start_timer(&metrics::BEACON_STATE_WRITE_OVERHEAD_TIMES);
|
||||
StorageContainer::new(state).as_ssz_bytes()
|
||||
};
|
||||
metrics::inc_counter_by(&metrics::BEACON_STATE_WRITE_BYTES, bytes.len() as u64);
|
||||
metrics::inc_counter(&metrics::BEACON_STATE_WRITE_COUNT);
|
||||
ops.push(KeyValueStoreOp::PutKeyValue(
|
||||
DBColumn::BeaconState,
|
||||
state_root.as_slice().to_vec(),
|
||||
bytes,
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_full_state<KV: KeyValueStore<E>, E: EthSpec>(
|
||||
db: &KV,
|
||||
state_root: &Hash256,
|
||||
spec: &ChainSpec,
|
||||
) -> Result<Option<BeaconState<E>>, Error> {
|
||||
let total_timer = metrics::start_timer(&metrics::BEACON_STATE_READ_TIMES);
|
||||
|
||||
match db.get_bytes(DBColumn::BeaconState, state_root.as_slice())? {
|
||||
Some(bytes) => {
|
||||
let overhead_timer = metrics::start_timer(&metrics::BEACON_STATE_READ_OVERHEAD_TIMES);
|
||||
let container = StorageContainer::from_ssz_bytes(&bytes, spec)?;
|
||||
|
||||
metrics::stop_timer(overhead_timer);
|
||||
metrics::stop_timer(total_timer);
|
||||
metrics::inc_counter(&metrics::BEACON_STATE_READ_COUNT);
|
||||
metrics::inc_counter_by(&metrics::BEACON_STATE_READ_BYTES, bytes.len() as u64);
|
||||
|
||||
Ok(Some(container.try_into()?))
|
||||
}
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// A container for storing `BeaconState` components.
|
||||
// TODO: would be more space efficient with the caches stored separately and referenced by hash
|
||||
#[derive(Encode)]
|
||||
pub struct StorageContainer<E: EthSpec> {
|
||||
state: BeaconState<E>,
|
||||
committee_caches: Vec<Arc<CommitteeCache>>,
|
||||
}
|
||||
|
||||
impl<E: EthSpec> StorageContainer<E> {
|
||||
/// Create a new instance for storing a `BeaconState`.
|
||||
pub fn new(state: &BeaconState<E>) -> Self {
|
||||
Self {
|
||||
state: state.clone(),
|
||||
committee_caches: state.committee_caches().to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_ssz_bytes(bytes: &[u8], spec: &ChainSpec) -> Result<Self, ssz::DecodeError> {
|
||||
// We need to use the slot-switching `from_ssz_bytes` of `BeaconState`, which doesn't
|
||||
// compose with the other SSZ utils, so we duplicate some parts of `ssz_derive` here.
|
||||
let mut builder = ssz::SszDecoderBuilder::new(bytes);
|
||||
|
||||
builder.register_anonymous_variable_length_item()?;
|
||||
builder.register_type::<Vec<CommitteeCache>>()?;
|
||||
|
||||
let mut decoder = builder.build()?;
|
||||
|
||||
let state = decoder.decode_next_with(|bytes| BeaconState::from_ssz_bytes(bytes, spec))?;
|
||||
let committee_caches = decoder.decode_next()?;
|
||||
|
||||
Ok(Self {
|
||||
state,
|
||||
committee_caches,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: EthSpec> TryInto<BeaconState<E>> for StorageContainer<E> {
|
||||
type Error = Error;
|
||||
|
||||
fn try_into(mut self) -> Result<BeaconState<E>, Error> {
|
||||
let mut state = self.state;
|
||||
|
||||
for i in (0..CACHED_EPOCHS).rev() {
|
||||
if i >= self.committee_caches.len() {
|
||||
return Err(Error::SszDecodeError(DecodeError::BytesInvalid(
|
||||
"Insufficient committees for BeaconState".to_string(),
|
||||
)));
|
||||
};
|
||||
|
||||
state.committee_caches_mut()[i] = self.committee_caches.remove(i);
|
||||
}
|
||||
|
||||
Ok(state)
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ use ssz::{Decode, Encode};
|
||||
use types::{
|
||||
EthSpec, ExecutionPayload, ExecutionPayloadBellatrix, ExecutionPayloadCapella,
|
||||
ExecutionPayloadDeneb, ExecutionPayloadEip7805, ExecutionPayloadElectra, ExecutionPayloadFulu,
|
||||
ExecutionPayloadGloas,
|
||||
};
|
||||
|
||||
macro_rules! impl_store_item {
|
||||
@@ -26,8 +27,9 @@ impl_store_item!(ExecutionPayloadBellatrix);
|
||||
impl_store_item!(ExecutionPayloadCapella);
|
||||
impl_store_item!(ExecutionPayloadDeneb);
|
||||
impl_store_item!(ExecutionPayloadElectra);
|
||||
impl_store_item!(ExecutionPayloadEip7805);
|
||||
impl_store_item!(ExecutionPayloadFulu);
|
||||
impl_store_item!(ExecutionPayloadEip7805);
|
||||
impl_store_item!(ExecutionPayloadGloas);
|
||||
|
||||
/// This fork-agnostic implementation should be only used for writing.
|
||||
///
|
||||
@@ -43,28 +45,32 @@ impl<E: EthSpec> StoreItem for ExecutionPayload<E> {
|
||||
}
|
||||
|
||||
fn from_store_bytes(bytes: &[u8]) -> Result<Self, Error> {
|
||||
ExecutionPayloadFulu::from_ssz_bytes(bytes)
|
||||
.map(Self::Fulu)
|
||||
.or_else(|_| {
|
||||
ExecutionPayloadEip7805::from_ssz_bytes(bytes)
|
||||
.map(Self::Eip7805)
|
||||
.or_else(|_| {
|
||||
ExecutionPayloadElectra::from_ssz_bytes(bytes)
|
||||
.map(Self::Electra)
|
||||
.or_else(|_| {
|
||||
ExecutionPayloadDeneb::from_ssz_bytes(bytes)
|
||||
.map(Self::Deneb)
|
||||
.or_else(|_| {
|
||||
ExecutionPayloadCapella::from_ssz_bytes(bytes)
|
||||
.map(Self::Capella)
|
||||
.or_else(|_| {
|
||||
ExecutionPayloadBellatrix::from_ssz_bytes(bytes)
|
||||
.map(Self::Bellatrix)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
if let Ok(payload) = ExecutionPayloadGloas::from_ssz_bytes(bytes) {
|
||||
return Ok(Self::Gloas(payload));
|
||||
}
|
||||
|
||||
if let Ok(payload) = ExecutionPayloadEip7805::from_ssz_bytes(bytes) {
|
||||
return Ok(Self::Eip7805(payload));
|
||||
}
|
||||
|
||||
if let Ok(payload) = ExecutionPayloadFulu::from_ssz_bytes(bytes) {
|
||||
return Ok(Self::Fulu(payload));
|
||||
}
|
||||
|
||||
if let Ok(payload) = ExecutionPayloadElectra::from_ssz_bytes(bytes) {
|
||||
return Ok(Self::Electra(payload));
|
||||
}
|
||||
|
||||
if let Ok(payload) = ExecutionPayloadDeneb::from_ssz_bytes(bytes) {
|
||||
return Ok(Self::Deneb(payload));
|
||||
}
|
||||
|
||||
if let Ok(payload) = ExecutionPayloadCapella::from_ssz_bytes(bytes) {
|
||||
return Ok(Self::Capella(payload));
|
||||
}
|
||||
|
||||
ExecutionPayloadBellatrix::from_ssz_bytes(bytes)
|
||||
.map(Self::Bellatrix)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ use crate::errors::HandleUnavailable;
|
||||
use crate::{Error, HotColdDB, ItemStore};
|
||||
use std::borrow::Cow;
|
||||
use std::marker::PhantomData;
|
||||
use typenum::Unsigned;
|
||||
use types::{
|
||||
typenum::Unsigned, BeaconState, BeaconStateError, BlindedPayload, EthSpec, Hash256,
|
||||
SignedBeaconBlock, Slot,
|
||||
BeaconState, BeaconStateError, BlindedPayload, EthSpec, Hash256, SignedBeaconBlock, Slot,
|
||||
};
|
||||
|
||||
/// Implemented for types that have ancestors (e.g., blocks, states) that may be iterated over.
|
||||
@@ -384,11 +384,11 @@ fn slot_of_prev_restore_point<E: EthSpec>(current_slot: Slot) -> Slot {
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::StoreConfig as Config;
|
||||
use crate::{MemoryStore, StoreConfig as Config};
|
||||
use beacon_chain::test_utils::BeaconChainHarness;
|
||||
use beacon_chain::types::{ChainSpec, MainnetEthSpec};
|
||||
use beacon_chain::types::MainnetEthSpec;
|
||||
use fixed_bytes::FixedBytesExtended;
|
||||
use std::sync::Arc;
|
||||
use types::FixedBytesExtended;
|
||||
|
||||
fn get_state<E: EthSpec>() -> BeaconState<E> {
|
||||
let harness = BeaconChainHarness::builder(E::default())
|
||||
@@ -400,10 +400,31 @@ mod test {
|
||||
harness.get_current_state()
|
||||
}
|
||||
|
||||
fn get_store<E: EthSpec>() -> HotColdDB<E, MemoryStore<E>, MemoryStore<E>> {
|
||||
let store =
|
||||
HotColdDB::open_ephemeral(Config::default(), Arc::new(E::default_spec())).unwrap();
|
||||
// Init achor info so anchor slot is set. Use a random block as it is only used for the
|
||||
// parent_root
|
||||
let _ = store
|
||||
.init_anchor_info(Hash256::ZERO, Slot::new(0), Slot::new(0), false)
|
||||
.unwrap();
|
||||
// Write a state with state root 0 which is the base `put_state` below tries to diff from
|
||||
{
|
||||
let harness = BeaconChainHarness::builder(E::default())
|
||||
.default_spec()
|
||||
.deterministic_keypairs(1)
|
||||
.fresh_ephemeral_store()
|
||||
.build();
|
||||
let genesis_state = harness.get_current_state();
|
||||
store.put_state(&Hash256::ZERO, &genesis_state).unwrap();
|
||||
}
|
||||
store
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_root_iter() {
|
||||
let store =
|
||||
HotColdDB::open_ephemeral(Config::default(), Arc::new(ChainSpec::minimal())).unwrap();
|
||||
let store = get_store::<MainnetEthSpec>();
|
||||
|
||||
let slots_per_historical_root = MainnetEthSpec::slots_per_historical_root();
|
||||
|
||||
let mut state_a: BeaconState<MainnetEthSpec> = get_state();
|
||||
@@ -449,8 +470,8 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn state_root_iter() {
|
||||
let store =
|
||||
HotColdDB::open_ephemeral(Config::default(), Arc::new(ChainSpec::minimal())).unwrap();
|
||||
let store = get_store::<MainnetEthSpec>();
|
||||
|
||||
let slots_per_historical_root = MainnetEthSpec::slots_per_historical_root();
|
||||
|
||||
let mut state_a: BeaconState<MainnetEthSpec> = get_state();
|
||||
|
||||
@@ -8,8 +8,6 @@
|
||||
//! Provides a simple API for storing/retrieving all types that sometimes needs type-hints. See
|
||||
//! tests for implementation examples.
|
||||
pub mod blob_sidecar_list_from_root;
|
||||
pub mod chunked_iter;
|
||||
pub mod chunked_vector;
|
||||
pub mod config;
|
||||
pub mod consensus_context;
|
||||
pub mod errors;
|
||||
@@ -21,7 +19,6 @@ mod impls;
|
||||
mod memory_store;
|
||||
pub mod metadata;
|
||||
pub mod metrics;
|
||||
pub mod partial_beacon_state;
|
||||
pub mod reconstruct;
|
||||
pub mod state_cache;
|
||||
|
||||
@@ -35,10 +32,8 @@ pub use self::hot_cold_store::{HotColdDB, HotStateSummary, Split};
|
||||
pub use self::memory_store::MemoryStore;
|
||||
pub use crate::metadata::BlobInfo;
|
||||
pub use errors::Error;
|
||||
pub use impls::beacon_state::StorageContainer as BeaconStateStorageContainer;
|
||||
pub use metadata::AnchorInfo;
|
||||
pub use metrics::scrape_for_metrics;
|
||||
use parking_lot::MutexGuard;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
use strum::{EnumIter, EnumString, IntoStaticStr};
|
||||
@@ -76,12 +71,6 @@ pub trait KeyValueStore<E: EthSpec>: Sync + Send + Sized + 'static {
|
||||
/// Execute either all of the operations in `batch` or none at all, returning an error.
|
||||
fn do_atomically(&self, batch: Vec<KeyValueStoreOp>) -> Result<(), Error>;
|
||||
|
||||
/// Return a mutex guard that can be used to synchronize sensitive transactions.
|
||||
///
|
||||
/// This doesn't prevent other threads writing to the DB unless they also use
|
||||
/// this method. In future we may implement a safer mandatory locking scheme.
|
||||
fn begin_rw_transaction(&self) -> MutexGuard<()>;
|
||||
|
||||
/// Compact a single column in the database, freeing space used by deleted items.
|
||||
fn compact_column(&self, column: DBColumn) -> Result<(), Error>;
|
||||
|
||||
@@ -91,7 +80,7 @@ pub trait KeyValueStore<E: EthSpec>: Sync + Send + Sized + 'static {
|
||||
// i.e. entries being created and deleted.
|
||||
for column in [
|
||||
DBColumn::BeaconState,
|
||||
DBColumn::BeaconStateSummary,
|
||||
DBColumn::BeaconStateHotSummary,
|
||||
DBColumn::BeaconBlock,
|
||||
] {
|
||||
self.compact_column(column)?;
|
||||
@@ -100,17 +89,17 @@ pub trait KeyValueStore<E: EthSpec>: Sync + Send + Sized + 'static {
|
||||
}
|
||||
|
||||
/// Iterate through all keys and values in a particular column.
|
||||
fn iter_column<K: Key>(&self, column: DBColumn) -> ColumnIter<K> {
|
||||
fn iter_column<K: Key>(&self, column: DBColumn) -> ColumnIter<'_, K> {
|
||||
self.iter_column_from(column, &vec![0; column.key_size()])
|
||||
}
|
||||
|
||||
/// Iterate through all keys and values in a column from a given starting point that fulfill the given predicate.
|
||||
fn iter_column_from<K: Key>(&self, column: DBColumn, from: &[u8]) -> ColumnIter<K>;
|
||||
fn iter_column_from<K: Key>(&self, column: DBColumn, from: &[u8]) -> ColumnIter<'_, K>;
|
||||
|
||||
fn iter_column_keys<K: Key>(&self, column: DBColumn) -> ColumnKeyIter<K>;
|
||||
fn iter_column_keys<K: Key>(&self, column: DBColumn) -> ColumnKeyIter<'_, K>;
|
||||
|
||||
/// Iterate through all keys in a particular column.
|
||||
fn iter_column_keys_from<K: Key>(&self, column: DBColumn, from: &[u8]) -> ColumnKeyIter<K>;
|
||||
fn iter_column_keys_from<K: Key>(&self, column: DBColumn, from: &[u8]) -> ColumnKeyIter<'_, K>;
|
||||
|
||||
fn delete_batch(&self, column: DBColumn, ops: HashSet<&[u8]>) -> Result<(), Error>;
|
||||
|
||||
@@ -130,7 +119,10 @@ impl Key for Hash256 {
|
||||
if key.len() == 32 {
|
||||
Ok(Hash256::from_slice(key))
|
||||
} else {
|
||||
Err(Error::InvalidKey)
|
||||
Err(Error::InvalidKey(format!(
|
||||
"Hash256 key unexpected len {}",
|
||||
key.len()
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -162,7 +154,10 @@ pub fn get_data_column_key(block_root: &Hash256, column_index: &ColumnIndex) ->
|
||||
|
||||
pub fn parse_data_column_key(data: Vec<u8>) -> Result<(Hash256, ColumnIndex), Error> {
|
||||
if data.len() != DBColumn::BeaconDataColumn.key_size() {
|
||||
return Err(Error::InvalidKey);
|
||||
return Err(Error::InvalidKey(format!(
|
||||
"Unexpected BeaconDataColumn key len {}",
|
||||
data.len()
|
||||
)));
|
||||
}
|
||||
// split_at panics if 32 < 40 which will never happen after the length check above
|
||||
let (block_root_bytes, column_index_bytes) = data.split_at(32);
|
||||
@@ -171,7 +166,7 @@ pub fn parse_data_column_key(data: Vec<u8>) -> Result<(Hash256, ColumnIndex), Er
|
||||
let column_index = ColumnIndex::from_le_bytes(
|
||||
column_index_bytes
|
||||
.try_into()
|
||||
.map_err(|_| Error::InvalidKey)?,
|
||||
.map_err(|e| Error::InvalidKey(format!("Invalid ColumnIndex {e:?}")))?,
|
||||
);
|
||||
Ok((block_root, column_index))
|
||||
}
|
||||
@@ -266,21 +261,43 @@ pub enum DBColumn {
|
||||
BeaconBlob,
|
||||
#[strum(serialize = "bdc")]
|
||||
BeaconDataColumn,
|
||||
#[strum(serialize = "bdi")]
|
||||
BeaconDataColumnCustodyInfo,
|
||||
/// For full `BeaconState`s in the hot database (finalized or fork-boundary states).
|
||||
///
|
||||
/// DEPRECATED.
|
||||
#[strum(serialize = "ste")]
|
||||
BeaconState,
|
||||
/// For compact `BeaconStateDiff`'s in the hot DB.
|
||||
///
|
||||
/// hsd = Hot State Diff.
|
||||
#[strum(serialize = "hsd")]
|
||||
BeaconStateHotDiff,
|
||||
/// For beacon state snapshots in the hot DB.
|
||||
///
|
||||
/// hsn = Hot Snapshot.
|
||||
#[strum(serialize = "hsn")]
|
||||
BeaconStateHotSnapshot,
|
||||
/// For beacon state snapshots in the freezer DB.
|
||||
#[strum(serialize = "bsn")]
|
||||
BeaconStateSnapshot,
|
||||
/// For compact `BeaconStateDiff`s in the freezer DB.
|
||||
#[strum(serialize = "bsd")]
|
||||
BeaconStateDiff,
|
||||
/// Mapping from state root to `HotStateSummary` in the hot DB.
|
||||
/// DEPRECATED
|
||||
///
|
||||
/// Mapping from state root to `HotStateSummaryV22` in the hot DB.
|
||||
///
|
||||
/// Previously this column also served a role in the freezer DB, mapping state roots to
|
||||
/// `ColdStateSummary`. However that role is now filled by `BeaconColdStateSummary`.
|
||||
#[strum(serialize = "bss")]
|
||||
BeaconStateSummary,
|
||||
/// Mapping from state root to `HotStateSummaryV23` in the hot DB.
|
||||
///
|
||||
/// This column is populated after DB schema version 23 superseding `BeaconStateSummary`. The
|
||||
/// new column is necessary to have a safe migration without data loss.
|
||||
#[strum(serialize = "bs3")]
|
||||
BeaconStateHotSummary,
|
||||
/// Mapping from state root to `ColdStateSummary` in the cold DB.
|
||||
#[strum(serialize = "bcs")]
|
||||
BeaconColdStateSummary,
|
||||
@@ -298,6 +315,7 @@ pub enum DBColumn {
|
||||
BeaconChain,
|
||||
#[strum(serialize = "opo")]
|
||||
OpPool,
|
||||
/// DEPRECATED.
|
||||
#[strum(serialize = "etc")]
|
||||
Eth1Cache,
|
||||
#[strum(serialize = "frk")]
|
||||
@@ -339,6 +357,8 @@ pub enum DBColumn {
|
||||
BeaconRandaoMixes,
|
||||
#[strum(serialize = "dht")]
|
||||
DhtEnrs,
|
||||
#[strum(serialize = "cus")]
|
||||
CustodyContext,
|
||||
/// DEPRECATED. For Optimistically Imported Merge Transition Blocks
|
||||
#[strum(serialize = "otb")]
|
||||
OptimisticTransitionBlock,
|
||||
@@ -387,6 +407,9 @@ impl DBColumn {
|
||||
| Self::BeaconState
|
||||
| Self::BeaconBlob
|
||||
| Self::BeaconStateSummary
|
||||
| Self::BeaconStateHotDiff
|
||||
| Self::BeaconStateHotSnapshot
|
||||
| Self::BeaconStateHotSummary
|
||||
| Self::BeaconColdStateSummary
|
||||
| Self::BeaconStateTemporary
|
||||
| Self::ExecPayload
|
||||
@@ -397,8 +420,10 @@ impl DBColumn {
|
||||
| Self::PubkeyCache
|
||||
| Self::BeaconRestorePoint
|
||||
| Self::DhtEnrs
|
||||
| Self::CustodyContext
|
||||
| Self::OptimisticTransitionBlock => 32,
|
||||
Self::BeaconBlockRoots
|
||||
| Self::BeaconDataColumnCustodyInfo
|
||||
| Self::BeaconBlockRootsChunked
|
||||
| Self::BeaconStateRoots
|
||||
| Self::BeaconStateRootsChunked
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::{
|
||||
errors::Error as DBError, get_key_for_col, hot_cold_store::BytesKey, ColumnIter, ColumnKeyIter,
|
||||
DBColumn, Error, ItemStore, Key, KeyValueStore, KeyValueStoreOp,
|
||||
ColumnIter, ColumnKeyIter, DBColumn, Error, ItemStore, Key, KeyValueStore, KeyValueStoreOp,
|
||||
errors::Error as DBError, get_key_for_col, hot_cold_store::BytesKey,
|
||||
};
|
||||
use parking_lot::{Mutex, MutexGuard, RwLock};
|
||||
use parking_lot::RwLock;
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
use std::marker::PhantomData;
|
||||
use types::*;
|
||||
@@ -12,7 +12,6 @@ type DBMap = BTreeMap<BytesKey, Vec<u8>>;
|
||||
/// A thread-safe `BTreeMap` wrapper.
|
||||
pub struct MemoryStore<E: EthSpec> {
|
||||
db: RwLock<DBMap>,
|
||||
transaction_mutex: Mutex<()>,
|
||||
_phantom: PhantomData<E>,
|
||||
}
|
||||
|
||||
@@ -21,7 +20,6 @@ impl<E: EthSpec> MemoryStore<E> {
|
||||
pub fn open() -> Self {
|
||||
Self {
|
||||
db: RwLock::new(BTreeMap::new()),
|
||||
transaction_mutex: Mutex::new(()),
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
@@ -82,7 +80,7 @@ impl<E: EthSpec> KeyValueStore<E> for MemoryStore<E> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn iter_column_from<K: Key>(&self, column: DBColumn, from: &[u8]) -> ColumnIter<K> {
|
||||
fn iter_column_from<K: Key>(&self, column: DBColumn, from: &[u8]) -> ColumnIter<'_, K> {
|
||||
// We use this awkward pattern because we can't lock the `self.db` field *and* maintain a
|
||||
// reference to the lock guard across calls to `.next()`. This would be require a
|
||||
// struct with a field (the iterator) which references another field (the lock guard).
|
||||
@@ -103,19 +101,15 @@ impl<E: EthSpec> KeyValueStore<E> for MemoryStore<E> {
|
||||
}))
|
||||
}
|
||||
|
||||
fn iter_column_keys<K: Key>(&self, column: DBColumn) -> ColumnKeyIter<K> {
|
||||
fn iter_column_keys<K: Key>(&self, column: DBColumn) -> ColumnKeyIter<'_, K> {
|
||||
Box::new(self.iter_column(column).map(|res| res.map(|(k, _)| k)))
|
||||
}
|
||||
|
||||
fn begin_rw_transaction(&self) -> MutexGuard<()> {
|
||||
self.transaction_mutex.lock()
|
||||
}
|
||||
|
||||
fn compact_column(&self, _column: DBColumn) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn iter_column_keys_from<K: Key>(&self, column: DBColumn, from: &[u8]) -> ColumnKeyIter<K> {
|
||||
fn iter_column_keys_from<K: Key>(&self, column: DBColumn, from: &[u8]) -> ColumnKeyIter<'_, K> {
|
||||
// We use this awkward pattern because we can't lock the `self.db` field *and* maintain a
|
||||
// reference to the lock guard across calls to `.next()`. This would be require a
|
||||
// struct with a field (the iterator) which references another field (the lock guard).
|
||||
|
||||
@@ -2,9 +2,9 @@ use crate::{DBColumn, Error, StoreItem};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ssz::{Decode, Encode};
|
||||
use ssz_derive::{Decode, Encode};
|
||||
use types::{Checkpoint, Hash256, Slot};
|
||||
use types::{Hash256, Slot};
|
||||
|
||||
pub const CURRENT_SCHEMA_VERSION: SchemaVersion = SchemaVersion(23);
|
||||
pub const CURRENT_SCHEMA_VERSION: SchemaVersion = SchemaVersion(28);
|
||||
|
||||
// All the keys that get stored under the `BeaconMeta` column.
|
||||
//
|
||||
@@ -12,24 +12,17 @@ pub const CURRENT_SCHEMA_VERSION: SchemaVersion = SchemaVersion(23);
|
||||
pub const SCHEMA_VERSION_KEY: Hash256 = Hash256::repeat_byte(0);
|
||||
pub const CONFIG_KEY: Hash256 = Hash256::repeat_byte(1);
|
||||
pub const SPLIT_KEY: Hash256 = Hash256::repeat_byte(2);
|
||||
pub const PRUNING_CHECKPOINT_KEY: Hash256 = Hash256::repeat_byte(3);
|
||||
// DEPRECATED
|
||||
// pub const PRUNING_CHECKPOINT_KEY: Hash256 = Hash256::repeat_byte(3);
|
||||
pub const COMPACTION_TIMESTAMP_KEY: Hash256 = Hash256::repeat_byte(4);
|
||||
pub const ANCHOR_INFO_KEY: Hash256 = Hash256::repeat_byte(5);
|
||||
pub const BLOB_INFO_KEY: Hash256 = Hash256::repeat_byte(6);
|
||||
pub const DATA_COLUMN_INFO_KEY: Hash256 = Hash256::repeat_byte(7);
|
||||
pub const DATA_COLUMN_CUSTODY_INFO_KEY: Hash256 = Hash256::repeat_byte(8);
|
||||
|
||||
/// State upper limit value used to indicate that a node is not storing historic states.
|
||||
pub const STATE_UPPER_LIMIT_NO_RETAIN: Slot = Slot::new(u64::MAX);
|
||||
|
||||
/// The `AnchorInfo` encoding full availability of all historic blocks & states.
|
||||
pub const ANCHOR_FOR_ARCHIVE_NODE: AnchorInfo = AnchorInfo {
|
||||
anchor_slot: Slot::new(0),
|
||||
oldest_block_slot: Slot::new(0),
|
||||
oldest_block_parent: Hash256::ZERO,
|
||||
state_upper_limit: Slot::new(0),
|
||||
state_lower_limit: Slot::new(0),
|
||||
};
|
||||
|
||||
/// The `AnchorInfo` encoding an uninitialized anchor.
|
||||
///
|
||||
/// This value should never exist except on initial start-up prior to the anchor being initialised
|
||||
@@ -65,30 +58,6 @@ impl StoreItem for SchemaVersion {
|
||||
}
|
||||
}
|
||||
|
||||
/// The checkpoint used for pruning the database.
|
||||
///
|
||||
/// Updated whenever pruning is successful.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct PruningCheckpoint {
|
||||
pub checkpoint: Checkpoint,
|
||||
}
|
||||
|
||||
impl StoreItem for PruningCheckpoint {
|
||||
fn db_column() -> DBColumn {
|
||||
DBColumn::BeaconMeta
|
||||
}
|
||||
|
||||
fn as_store_bytes(&self) -> Vec<u8> {
|
||||
self.checkpoint.as_ssz_bytes()
|
||||
}
|
||||
|
||||
fn from_store_bytes(bytes: &[u8]) -> Result<Self, Error> {
|
||||
Ok(PruningCheckpoint {
|
||||
checkpoint: Checkpoint::from_ssz_bytes(bytes)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// The last time the database was compacted.
|
||||
pub struct CompactionTimestamp(pub u64);
|
||||
|
||||
@@ -111,7 +80,8 @@ impl StoreItem for CompactionTimestamp {
|
||||
pub struct AnchorInfo {
|
||||
/// The slot at which the anchor state is present and which we cannot revert. Values on start:
|
||||
/// - Genesis start: 0
|
||||
/// - Checkpoint sync: Slot of the finalized checkpoint block
|
||||
/// - Checkpoint sync: Slot of the finalized state advanced to the checkpoint epoch
|
||||
/// - Existing DB prior to v23: Finalized state slot at the migration moment
|
||||
///
|
||||
/// Immutable
|
||||
pub anchor_slot: Slot,
|
||||
@@ -175,6 +145,21 @@ impl AnchorInfo {
|
||||
pub fn full_state_pruning_enabled(&self) -> bool {
|
||||
self.state_lower_limit == 0 && self.state_upper_limit == STATE_UPPER_LIMIT_NO_RETAIN
|
||||
}
|
||||
|
||||
/// Compute the correct `AnchorInfo` for an archive node created from the current node.
|
||||
///
|
||||
/// This method ensures that the `anchor_slot` which is used for the hot database's diff grid is
|
||||
/// preserved.
|
||||
pub fn as_archive_anchor(&self) -> Self {
|
||||
Self {
|
||||
// Anchor slot MUST be the same. It is immutable.
|
||||
anchor_slot: self.anchor_slot,
|
||||
oldest_block_slot: Slot::new(0),
|
||||
oldest_block_parent: Hash256::ZERO,
|
||||
state_upper_limit: Slot::new(0),
|
||||
state_lower_limit: Slot::new(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StoreItem for AnchorInfo {
|
||||
@@ -220,6 +205,30 @@ impl StoreItem for BlobInfo {
|
||||
}
|
||||
}
|
||||
|
||||
/// Database parameter relevant to data column custody sync. There is only at most a single
|
||||
/// `DataColumnCustodyInfo` stored in the db. `earliest_data_column_slot` is updated when cgc
|
||||
/// count changes and is updated incrementally during data column custody backfill. Once custody backfill
|
||||
/// is complete `earliest_data_column_slot` is set to `None`.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Encode, Decode, Serialize, Deserialize, Default)]
|
||||
pub struct DataColumnCustodyInfo {
|
||||
/// The earliest slot for which data columns are available.
|
||||
pub earliest_data_column_slot: Option<Slot>,
|
||||
}
|
||||
|
||||
impl StoreItem for DataColumnCustodyInfo {
|
||||
fn db_column() -> DBColumn {
|
||||
DBColumn::BeaconDataColumnCustodyInfo
|
||||
}
|
||||
|
||||
fn as_store_bytes(&self) -> Vec<u8> {
|
||||
self.as_ssz_bytes()
|
||||
}
|
||||
|
||||
fn from_store_bytes(bytes: &[u8]) -> Result<Self, Error> {
|
||||
Ok(DataColumnCustodyInfo::from_ssz_bytes(bytes)?)
|
||||
}
|
||||
}
|
||||
|
||||
/// Database parameters relevant to data column sync.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Encode, Decode, Serialize, Deserialize, Default)]
|
||||
pub struct DataColumnInfo {
|
||||
|
||||
@@ -4,6 +4,10 @@ use directory::size_of_dir;
|
||||
use std::path::Path;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
// Labels used for histogram timer vecs that are tracked per DB (hot and cold).
|
||||
pub const HOT_METRIC: &[&str] = &["hot"];
|
||||
pub const COLD_METRIC: &[&str] = &["cold"];
|
||||
|
||||
/*
|
||||
* General
|
||||
*/
|
||||
@@ -64,7 +68,7 @@ pub static DISK_DB_WRITE_COUNT: LazyLock<Result<IntCounterVec>> = LazyLock::new(
|
||||
pub static DISK_DB_READ_TIMES: LazyLock<Result<Histogram>> = LazyLock::new(|| {
|
||||
try_create_histogram(
|
||||
"store_disk_db_read_seconds",
|
||||
"Time taken to write bytes to store.",
|
||||
"Time taken to read bytes from store.",
|
||||
)
|
||||
});
|
||||
pub static DISK_DB_WRITE_TIMES: LazyLock<Result<Histogram>> = LazyLock::new(|| {
|
||||
@@ -142,71 +146,84 @@ pub static BEACON_STATE_HOT_GET_COUNT: LazyLock<Result<IntCounter>> = LazyLock::
|
||||
"Total number of hot beacon states requested from the store (cache or DB)",
|
||||
)
|
||||
});
|
||||
pub static BEACON_STATE_READ_TIMES: LazyLock<Result<Histogram>> = LazyLock::new(|| {
|
||||
try_create_histogram(
|
||||
"store_beacon_state_read_seconds",
|
||||
"Total time required to read a BeaconState from the database",
|
||||
)
|
||||
});
|
||||
pub static BEACON_STATE_READ_OVERHEAD_TIMES: LazyLock<Result<Histogram>> = LazyLock::new(|| {
|
||||
try_create_histogram(
|
||||
"store_beacon_state_read_overhead_seconds",
|
||||
"Overhead on reading a beacon state from the DB (e.g., decoding)",
|
||||
)
|
||||
});
|
||||
pub static BEACON_STATE_READ_COUNT: LazyLock<Result<IntCounter>> = LazyLock::new(|| {
|
||||
try_create_int_counter(
|
||||
"store_beacon_state_read_total",
|
||||
"Total number of beacon state reads from the DB",
|
||||
)
|
||||
});
|
||||
pub static BEACON_STATE_READ_BYTES: LazyLock<Result<IntCounter>> = LazyLock::new(|| {
|
||||
try_create_int_counter(
|
||||
"store_beacon_state_read_bytes_total",
|
||||
"Total number of beacon state bytes read from the DB",
|
||||
)
|
||||
});
|
||||
pub static BEACON_STATE_WRITE_OVERHEAD_TIMES: LazyLock<Result<Histogram>> = LazyLock::new(|| {
|
||||
try_create_histogram(
|
||||
"store_beacon_state_write_overhead_seconds",
|
||||
"Overhead on writing a beacon state to the DB (e.g., encoding)",
|
||||
)
|
||||
});
|
||||
pub static BEACON_STATE_WRITE_COUNT: LazyLock<Result<IntCounter>> = LazyLock::new(|| {
|
||||
try_create_int_counter(
|
||||
"store_beacon_state_write_total",
|
||||
"Total number of beacon state writes the DB",
|
||||
)
|
||||
});
|
||||
pub static BEACON_STATE_WRITE_BYTES: LazyLock<Result<IntCounter>> = LazyLock::new(|| {
|
||||
try_create_int_counter(
|
||||
"store_beacon_state_write_bytes_total",
|
||||
"Total number of beacon state bytes written to the DB",
|
||||
)
|
||||
});
|
||||
pub static BEACON_HDIFF_READ_TIMES: LazyLock<Result<Histogram>> = LazyLock::new(|| {
|
||||
try_create_histogram(
|
||||
|
||||
/*
|
||||
* HDiffs
|
||||
*/
|
||||
pub static BEACON_HDIFF_READ_TIME: LazyLock<Result<HistogramVec>> = LazyLock::new(|| {
|
||||
try_create_histogram_vec(
|
||||
"store_hdiff_read_seconds",
|
||||
"Time required to read the hierarchical diff bytes from the database",
|
||||
"Time taken to read hdiff bytes from disk",
|
||||
&["db"],
|
||||
)
|
||||
});
|
||||
pub static BEACON_HDIFF_DECODE_TIMES: LazyLock<Result<Histogram>> = LazyLock::new(|| {
|
||||
try_create_histogram(
|
||||
pub static BEACON_HDIFF_DECODE_TIME: LazyLock<Result<HistogramVec>> = LazyLock::new(|| {
|
||||
try_create_histogram_vec(
|
||||
"store_hdiff_decode_seconds",
|
||||
"Time required to decode hierarchical diff bytes",
|
||||
"Time taken to decode hdiff bytes",
|
||||
&["db"],
|
||||
)
|
||||
});
|
||||
pub static BEACON_HDIFF_BUFFER_CLONE_TIMES: LazyLock<Result<Histogram>> = LazyLock::new(|| {
|
||||
try_create_histogram(
|
||||
pub static BEACON_HDIFF_APPLY_TIME: LazyLock<Result<HistogramVec>> = LazyLock::new(|| {
|
||||
try_create_histogram_vec(
|
||||
"store_hdiff_apply_seconds",
|
||||
"Time taken to apply an hdiff to a buffer",
|
||||
&["db"],
|
||||
)
|
||||
});
|
||||
pub static BEACON_HDIFF_COMPUTE_TIME: LazyLock<Result<HistogramVec>> = LazyLock::new(|| {
|
||||
try_create_histogram_vec(
|
||||
"store_hdiff_compute_seconds",
|
||||
"Time taken to compute an hdiff for a state",
|
||||
&["db"],
|
||||
)
|
||||
});
|
||||
pub static BEACON_HDIFF_BUFFER_LOAD_TIME: LazyLock<Result<HistogramVec>> = LazyLock::new(|| {
|
||||
try_create_histogram_vec(
|
||||
"store_hdiff_buffer_load_seconds",
|
||||
"Time taken to load an hdiff buffer for a state",
|
||||
&["db"],
|
||||
)
|
||||
});
|
||||
pub static BEACON_HDIFF_BUFFER_CLONE_TIME: LazyLock<Result<HistogramVec>> = LazyLock::new(|| {
|
||||
try_create_histogram_vec(
|
||||
"store_hdiff_buffer_clone_seconds",
|
||||
"Time required to clone hierarchical diff buffer bytes",
|
||||
"Time taken to clone an hdiff buffer from a cache",
|
||||
&["db"],
|
||||
)
|
||||
});
|
||||
pub static BEACON_HDIFF_BUFFER_LOAD_BEFORE_STORE_TIME: LazyLock<Result<HistogramVec>> =
|
||||
LazyLock::new(|| {
|
||||
try_create_histogram_vec(
|
||||
"store_hdiff_buffer_load_before_store_seconds",
|
||||
"Time taken to load the hdiff buffer required for the storage of a new state",
|
||||
&["db"],
|
||||
)
|
||||
});
|
||||
// This metric is not split hot/cold because it is recorded in a place where that info is not known.
|
||||
pub static BEACON_HDIFF_BUFFER_APPLY_RESIZES: LazyLock<Result<Histogram>> = LazyLock::new(|| {
|
||||
try_create_histogram_with_buckets(
|
||||
"store_hdiff_buffer_apply_resizes",
|
||||
"Number of times during diff application that the output buffer had to be resized before decoding succeeded",
|
||||
Ok(vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0])
|
||||
Ok(vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0]),
|
||||
)
|
||||
});
|
||||
// This metric is not split hot/cold because both databases use the same hierarchy config anyway
|
||||
// and that's all that affects diff sizes.
|
||||
pub static BEACON_HDIFF_SIZES: LazyLock<Result<HistogramVec>> = LazyLock::new(|| {
|
||||
try_create_histogram_vec_with_buckets(
|
||||
"store_hdiff_sizes",
|
||||
"Size of hdiffs in bytes by layer (exponent)",
|
||||
Ok(vec![
|
||||
500_000.0,
|
||||
2_000_000.0,
|
||||
5_000_000.0,
|
||||
10_000_000.0,
|
||||
15_000_000.0,
|
||||
20_000_000.0,
|
||||
50_000_000.0,
|
||||
]),
|
||||
&["exponent"],
|
||||
)
|
||||
});
|
||||
/*
|
||||
@@ -259,17 +276,20 @@ pub static STORE_BEACON_HISTORIC_STATE_CACHE_SIZE: LazyLock<Result<IntGauge>> =
|
||||
"Current count of states in the historic state cache",
|
||||
)
|
||||
});
|
||||
pub static STORE_BEACON_HDIFF_BUFFER_CACHE_SIZE: LazyLock<Result<IntGauge>> = LazyLock::new(|| {
|
||||
try_create_int_gauge(
|
||||
"store_beacon_hdiff_buffer_cache_size",
|
||||
"Current count of hdiff buffers in the historic state cache",
|
||||
)
|
||||
});
|
||||
pub static STORE_BEACON_HDIFF_BUFFER_CACHE_BYTE_SIZE: LazyLock<Result<IntGauge>> =
|
||||
pub static STORE_BEACON_HDIFF_BUFFER_CACHE_SIZE: LazyLock<Result<IntGaugeVec>> =
|
||||
LazyLock::new(|| {
|
||||
try_create_int_gauge(
|
||||
try_create_int_gauge_vec(
|
||||
"store_beacon_hdiff_buffer_cache_size",
|
||||
"Current count of hdiff buffers cached in memory",
|
||||
&["db"],
|
||||
)
|
||||
});
|
||||
pub static STORE_BEACON_HDIFF_BUFFER_CACHE_BYTE_SIZE: LazyLock<Result<IntGaugeVec>> =
|
||||
LazyLock::new(|| {
|
||||
try_create_int_gauge_vec(
|
||||
"store_beacon_hdiff_buffer_cache_byte_size",
|
||||
"Memory consumed by hdiff buffers in the historic state cache",
|
||||
"Memory consumed by hdiff buffers cached in memory",
|
||||
&["db"],
|
||||
)
|
||||
});
|
||||
pub static STORE_BEACON_STATE_FREEZER_COMPRESS_TIME: LazyLock<Result<Histogram>> =
|
||||
@@ -286,33 +306,6 @@ pub static STORE_BEACON_STATE_FREEZER_DECOMPRESS_TIME: LazyLock<Result<Histogram
|
||||
"Time taken to decompress a state snapshot for the freezer DB",
|
||||
)
|
||||
});
|
||||
pub static STORE_BEACON_HDIFF_BUFFER_APPLY_TIME: LazyLock<Result<Histogram>> =
|
||||
LazyLock::new(|| {
|
||||
try_create_histogram(
|
||||
"store_beacon_hdiff_buffer_apply_seconds",
|
||||
"Time taken to apply hdiff buffer to a state buffer",
|
||||
)
|
||||
});
|
||||
pub static STORE_BEACON_HDIFF_BUFFER_COMPUTE_TIME: LazyLock<Result<Histogram>> =
|
||||
LazyLock::new(|| {
|
||||
try_create_histogram(
|
||||
"store_beacon_hdiff_buffer_compute_seconds",
|
||||
"Time taken to compute hdiff buffer to a state buffer",
|
||||
)
|
||||
});
|
||||
pub static STORE_BEACON_HDIFF_BUFFER_LOAD_TIME: LazyLock<Result<Histogram>> = LazyLock::new(|| {
|
||||
try_create_histogram(
|
||||
"store_beacon_hdiff_buffer_load_seconds",
|
||||
"Time taken to load an hdiff buffer",
|
||||
)
|
||||
});
|
||||
pub static STORE_BEACON_HDIFF_BUFFER_LOAD_FOR_STORE_TIME: LazyLock<Result<Histogram>> =
|
||||
LazyLock::new(|| {
|
||||
try_create_histogram(
|
||||
"store_beacon_hdiff_buffer_load_for_store_seconds",
|
||||
"Time taken to load an hdiff buffer to store another hdiff",
|
||||
)
|
||||
});
|
||||
pub static STORE_BEACON_HISTORIC_STATE_CACHE_HIT: LazyLock<Result<IntCounter>> =
|
||||
LazyLock::new(|| {
|
||||
try_create_int_counter(
|
||||
@@ -327,18 +320,20 @@ pub static STORE_BEACON_HISTORIC_STATE_CACHE_MISS: LazyLock<Result<IntCounter>>
|
||||
"Total count of historic state cache misses for full states",
|
||||
)
|
||||
});
|
||||
pub static STORE_BEACON_HDIFF_BUFFER_CACHE_HIT: LazyLock<Result<IntCounter>> =
|
||||
pub static STORE_BEACON_HDIFF_BUFFER_CACHE_HIT: LazyLock<Result<IntCounterVec>> =
|
||||
LazyLock::new(|| {
|
||||
try_create_int_counter(
|
||||
try_create_int_counter_vec(
|
||||
"store_beacon_hdiff_buffer_cache_hit_total",
|
||||
"Total count of hdiff buffer cache hits",
|
||||
&["db"],
|
||||
)
|
||||
});
|
||||
pub static STORE_BEACON_HDIFF_BUFFER_CACHE_MISS: LazyLock<Result<IntCounter>> =
|
||||
pub static STORE_BEACON_HDIFF_BUFFER_CACHE_MISS: LazyLock<Result<IntCounterVec>> =
|
||||
LazyLock::new(|| {
|
||||
try_create_int_counter(
|
||||
try_create_int_counter_vec(
|
||||
"store_beacon_hdiff_buffer_cache_miss_total",
|
||||
"Total count of hdiff buffer cache miss",
|
||||
&["db"],
|
||||
)
|
||||
});
|
||||
pub static STORE_BEACON_HDIFF_BUFFER_INTO_STATE_TIME: LazyLock<Result<Histogram>> =
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
//! Implementation of historic state reconstruction (given complete block history).
|
||||
use crate::hot_cold_store::{HotColdDB, HotColdDBError};
|
||||
use crate::metadata::ANCHOR_FOR_ARCHIVE_NODE;
|
||||
use crate::metrics;
|
||||
use crate::{Error, ItemStore};
|
||||
use itertools::{process_results, Itertools};
|
||||
use itertools::{Itertools, process_results};
|
||||
use state_processing::{
|
||||
per_block_processing, per_slot_processing, BlockSignatureStrategy, ConsensusContext,
|
||||
VerifyBlockRoot,
|
||||
BlockSignatureStrategy, ConsensusContext, VerifyBlockRoot, per_block_processing,
|
||||
per_slot_processing,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use tracing::{debug, info};
|
||||
@@ -48,6 +47,12 @@ where
|
||||
let lower_limit_slot = anchor.state_lower_limit;
|
||||
let upper_limit_slot = std::cmp::min(split.slot, anchor.state_upper_limit);
|
||||
|
||||
// If the split is at 0 we can't reconstruct historic states.
|
||||
if split.slot == 0 {
|
||||
debug!("No state reconstruction possible");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// If `num_blocks` is not specified iterate all blocks. Add 1 so that we end on an epoch
|
||||
// boundary when `num_blocks` is a multiple of an epoch boundary. We want to be *inclusive*
|
||||
// of the state at slot `lower_limit_slot + num_blocks`.
|
||||
@@ -145,10 +150,8 @@ where
|
||||
});
|
||||
}
|
||||
|
||||
self.compare_and_set_anchor_info_with_write(
|
||||
old_anchor,
|
||||
ANCHOR_FOR_ARCHIVE_NODE,
|
||||
)?;
|
||||
let new_anchor = old_anchor.as_archive_anchor();
|
||||
self.compare_and_set_anchor_info_with_write(old_anchor, new_anchor)?;
|
||||
|
||||
return Ok(());
|
||||
} else {
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
use crate::Error;
|
||||
use crate::hdiff::HDiffBuffer;
|
||||
use crate::{
|
||||
Error,
|
||||
metrics::{self, HOT_METRIC},
|
||||
};
|
||||
use lru::LruCache;
|
||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
use std::num::NonZeroUsize;
|
||||
use tracing::instrument;
|
||||
use types::{BeaconState, ChainSpec, Epoch, EthSpec, Hash256, Slot};
|
||||
|
||||
/// Fraction of the LRU cache to leave intact during culling.
|
||||
@@ -37,26 +42,53 @@ pub struct StateCache<E: EthSpec> {
|
||||
// the state_root
|
||||
states: LruCache<Hash256, (Hash256, BeaconState<E>)>,
|
||||
block_map: BlockMap,
|
||||
hdiff_buffers: HotHDiffBufferCache,
|
||||
max_epoch: Epoch,
|
||||
head_block_root: Hash256,
|
||||
headroom: NonZeroUsize,
|
||||
}
|
||||
|
||||
/// Cache of hdiff buffers for hot states.
|
||||
///
|
||||
/// This cache only keeps buffers prior to the finalized state, which are required by the
|
||||
/// hierarchical state diff scheme to construct newer unfinalized states.
|
||||
///
|
||||
/// The cache always retains the hdiff buffer for the most recent snapshot so that even if the
|
||||
/// cache capacity is 1, this snapshot never needs to be loaded from disk.
|
||||
#[derive(Debug)]
|
||||
pub struct HotHDiffBufferCache {
|
||||
/// Cache of HDiffBuffers for states *prior* to the `finalized_state`.
|
||||
///
|
||||
/// Maps state_root -> (slot, buffer).
|
||||
hdiff_buffers: LruCache<Hash256, (Slot, HDiffBuffer)>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum PutStateOutcome {
|
||||
/// State is prior to the cache's finalized state (lower slot) and was cached as an HDiffBuffer.
|
||||
PreFinalizedHDiffBuffer,
|
||||
/// State is equal to the cache's finalized state and was not inserted.
|
||||
Finalized,
|
||||
/// State was already present in the cache.
|
||||
Duplicate,
|
||||
/// Includes deleted states as a result of this insertion
|
||||
/// State is new to the cache and was inserted.
|
||||
///
|
||||
/// Includes deleted states as a result of this insertion.
|
||||
New(Vec<Hash256>),
|
||||
}
|
||||
|
||||
#[allow(clippy::len_without_is_empty)]
|
||||
impl<E: EthSpec> StateCache<E> {
|
||||
pub fn new(capacity: NonZeroUsize, headroom: NonZeroUsize) -> Self {
|
||||
pub fn new(
|
||||
state_capacity: NonZeroUsize,
|
||||
headroom: NonZeroUsize,
|
||||
hdiff_capacity: NonZeroUsize,
|
||||
) -> Self {
|
||||
StateCache {
|
||||
finalized_state: None,
|
||||
states: LruCache::new(capacity),
|
||||
states: LruCache::new(state_capacity),
|
||||
block_map: BlockMap::default(),
|
||||
hdiff_buffers: HotHDiffBufferCache::new(hdiff_capacity),
|
||||
max_epoch: Epoch::new(0),
|
||||
head_block_root: Hash256::ZERO,
|
||||
headroom,
|
||||
@@ -71,11 +103,20 @@ impl<E: EthSpec> StateCache<E> {
|
||||
self.states.cap().get()
|
||||
}
|
||||
|
||||
pub fn num_hdiff_buffers(&self) -> usize {
|
||||
self.hdiff_buffers.len()
|
||||
}
|
||||
|
||||
pub fn hdiff_buffer_mem_usage(&self) -> usize {
|
||||
self.hdiff_buffers.mem_usage()
|
||||
}
|
||||
|
||||
pub fn update_finalized_state(
|
||||
&mut self,
|
||||
state_root: Hash256,
|
||||
block_root: Hash256,
|
||||
state: BeaconState<E>,
|
||||
pre_finalized_slots_to_retain: &[Slot],
|
||||
) -> Result<(), Error> {
|
||||
if state.slot() % E::slots_per_epoch() != 0 {
|
||||
return Err(Error::FinalizedStateUnaligned);
|
||||
@@ -95,9 +136,31 @@ impl<E: EthSpec> StateCache<E> {
|
||||
// Prune block map.
|
||||
let state_roots_to_prune = self.block_map.prune(state.slot());
|
||||
|
||||
// Prune HDiffBuffers that are no longer required by the hdiff grid of the finalized state.
|
||||
// We need to do this prior to copying in any new hdiff buffers, because the cache
|
||||
// preferences older slots.
|
||||
// NOTE: This isn't perfect as it prunes by slot: there could be multiple buffers
|
||||
// at some slots in the case of long forks without finality.
|
||||
let new_hdiff_cache = HotHDiffBufferCache::new(self.hdiff_buffers.cap());
|
||||
let old_hdiff_cache = std::mem::replace(&mut self.hdiff_buffers, new_hdiff_cache);
|
||||
for (state_root, (slot, buffer)) in old_hdiff_cache.hdiff_buffers {
|
||||
if pre_finalized_slots_to_retain.contains(&slot) {
|
||||
self.hdiff_buffers.put(state_root, slot, buffer);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete states.
|
||||
for state_root in state_roots_to_prune {
|
||||
self.states.pop(&state_root);
|
||||
if let Some((_, state)) = self.states.pop(&state_root) {
|
||||
// Add the hdiff buffer for this state to the hdiff cache if it is now part of
|
||||
// the pre-finalized grid. The `put` method will take care of keeping the most
|
||||
// useful buffers.
|
||||
let slot = state.slot();
|
||||
if pre_finalized_slots_to_retain.contains(&slot) {
|
||||
let hdiff_buffer = HDiffBuffer::from_state(state);
|
||||
self.hdiff_buffers.put(state_root, slot, hdiff_buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update finalized state.
|
||||
@@ -123,9 +186,15 @@ impl<E: EthSpec> StateCache<E> {
|
||||
state: &mut BeaconState<E>,
|
||||
spec: &ChainSpec,
|
||||
) -> Result<(), Error> {
|
||||
if let Some(finalized_state) = &self.finalized_state {
|
||||
// Do not attempt to rebase states prior to the finalized state. This method might be called
|
||||
// with states on the hdiff grid prior to finalization, as part of the reconstruction of
|
||||
// some later unfinalized state.
|
||||
if let Some(finalized_state) = &self.finalized_state
|
||||
&& state.slot() >= finalized_state.state.slot()
|
||||
{
|
||||
state.rebase_on(&finalized_state.state, spec)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -136,12 +205,19 @@ impl<E: EthSpec> StateCache<E> {
|
||||
block_root: Hash256,
|
||||
state: &BeaconState<E>,
|
||||
) -> Result<PutStateOutcome, Error> {
|
||||
if self
|
||||
.finalized_state
|
||||
.as_ref()
|
||||
.is_some_and(|finalized_state| finalized_state.state_root == state_root)
|
||||
{
|
||||
return Ok(PutStateOutcome::Finalized);
|
||||
if let Some(ref finalized_state) = self.finalized_state {
|
||||
if finalized_state.state_root == state_root {
|
||||
return Ok(PutStateOutcome::Finalized);
|
||||
} else if state.slot() <= finalized_state.state.slot() {
|
||||
// We assume any state being inserted into the cache is grid-aligned (it is the
|
||||
// caller's responsibility to not feed us garbage) as we don't want to thread the
|
||||
// hierarchy config through here. So any state received is converted to an
|
||||
// HDiffBuffer and saved.
|
||||
let hdiff_buffer = HDiffBuffer::from_state(state.clone());
|
||||
self.hdiff_buffers
|
||||
.put(state_root, state.slot(), hdiff_buffer);
|
||||
return Ok(PutStateOutcome::PreFinalizedHDiffBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
if self.states.peek(&state_root).is_some() {
|
||||
@@ -184,14 +260,46 @@ impl<E: EthSpec> StateCache<E> {
|
||||
}
|
||||
|
||||
pub fn get_by_state_root(&mut self, state_root: Hash256) -> Option<BeaconState<E>> {
|
||||
if let Some(ref finalized_state) = self.finalized_state {
|
||||
if state_root == finalized_state.state_root {
|
||||
return Some(finalized_state.state.clone());
|
||||
}
|
||||
if let Some(ref finalized_state) = self.finalized_state
|
||||
&& state_root == finalized_state.state_root
|
||||
{
|
||||
return Some(finalized_state.state.clone());
|
||||
}
|
||||
self.states.get(&state_root).map(|(_, state)| state.clone())
|
||||
}
|
||||
|
||||
pub fn put_hdiff_buffer(&mut self, state_root: Hash256, slot: Slot, buffer: &HDiffBuffer) {
|
||||
// Only accept HDiffBuffers prior to finalization. Later states should be stored as proper
|
||||
// states, not HDiffBuffers.
|
||||
if let Some(finalized_state) = &self.finalized_state
|
||||
&& slot >= finalized_state.state.slot()
|
||||
{
|
||||
return;
|
||||
}
|
||||
self.hdiff_buffers.put(state_root, slot, buffer.clone());
|
||||
}
|
||||
|
||||
pub fn get_hdiff_buffer_by_state_root(&mut self, state_root: Hash256) -> Option<HDiffBuffer> {
|
||||
if let Some(buffer) = self.hdiff_buffers.get(&state_root) {
|
||||
metrics::inc_counter_vec(&metrics::STORE_BEACON_HDIFF_BUFFER_CACHE_HIT, HOT_METRIC);
|
||||
let timer =
|
||||
metrics::start_timer_vec(&metrics::BEACON_HDIFF_BUFFER_CLONE_TIME, HOT_METRIC);
|
||||
let result = Some(buffer.clone());
|
||||
drop(timer);
|
||||
return result;
|
||||
}
|
||||
if let Some(buffer) = self
|
||||
.get_by_state_root(state_root)
|
||||
.map(HDiffBuffer::from_state)
|
||||
{
|
||||
metrics::inc_counter_vec(&metrics::STORE_BEACON_HDIFF_BUFFER_CACHE_HIT, HOT_METRIC);
|
||||
return Some(buffer);
|
||||
}
|
||||
metrics::inc_counter_vec(&metrics::STORE_BEACON_HDIFF_BUFFER_CACHE_MISS, HOT_METRIC);
|
||||
None
|
||||
}
|
||||
|
||||
#[instrument(skip_all, fields(?block_root, %slot), level = "debug")]
|
||||
pub fn get_by_block_root(
|
||||
&mut self,
|
||||
block_root: Hash256,
|
||||
@@ -245,7 +353,9 @@ impl<E: EthSpec> StateCache<E> {
|
||||
let mut old_boundary_state_roots = vec![];
|
||||
let mut good_boundary_state_roots = vec![];
|
||||
|
||||
for (&state_root, (_, state)) in self.states.iter().skip(cull_exempt) {
|
||||
// Skip the `cull_exempt` most-recently used, then reverse the iterator to start at
|
||||
// least-recently used states.
|
||||
for (&state_root, (_, state)) in self.states.iter().skip(cull_exempt).rev() {
|
||||
let is_advanced = state.slot() > state.latest_block_header().slot;
|
||||
let is_boundary = state.slot() % E::slots_per_epoch() == 0;
|
||||
let could_finalize =
|
||||
@@ -325,3 +435,80 @@ impl BlockMap {
|
||||
self.blocks.remove(block_root)
|
||||
}
|
||||
}
|
||||
|
||||
impl HotHDiffBufferCache {
|
||||
pub fn new(capacity: NonZeroUsize) -> Self {
|
||||
Self {
|
||||
hdiff_buffers: LruCache::new(capacity),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&mut self, state_root: &Hash256) -> Option<HDiffBuffer> {
|
||||
self.hdiff_buffers
|
||||
.get(state_root)
|
||||
.map(|(_, buffer)| buffer.clone())
|
||||
}
|
||||
|
||||
/// Put a value in the cache, making room for it if necessary.
|
||||
///
|
||||
/// If the value was inserted then `true` is returned.
|
||||
pub fn put(&mut self, state_root: Hash256, slot: Slot, buffer: HDiffBuffer) -> bool {
|
||||
// If the cache is not full, simply insert the value.
|
||||
if self.hdiff_buffers.len() != self.hdiff_buffers.cap().get() {
|
||||
self.hdiff_buffers.put(state_root, (slot, buffer));
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the cache is full, it has room for this new entry if:
|
||||
//
|
||||
// - The capacity is greater than 1: we can retain the snapshot and the new entry, or
|
||||
// - The capacity is 1 and the slot of the new entry is older than the min_slot in the
|
||||
// cache. This is a simplified way of retaining the snapshot in the cache. We don't need
|
||||
// to worry about inserting/retaining states older than the snapshot because these are
|
||||
// pruned on finalization and never reinserted.
|
||||
let Some(min_slot) = self.hdiff_buffers.iter().map(|(_, (slot, _))| *slot).min() else {
|
||||
// Unreachable: cache is full so should have >0 entries.
|
||||
return false;
|
||||
};
|
||||
|
||||
if self.hdiff_buffers.cap().get() > 1 || slot < min_slot {
|
||||
// Remove LRU value. Cache is now at size `cap - 1`.
|
||||
let Some((removed_state_root, (removed_slot, removed_buffer))) =
|
||||
self.hdiff_buffers.pop_lru()
|
||||
else {
|
||||
// Unreachable: cache is full so should have at least one entry to pop.
|
||||
return false;
|
||||
};
|
||||
|
||||
// Insert new value. Cache size is now at size `cap`.
|
||||
self.hdiff_buffers.put(state_root, (slot, buffer));
|
||||
|
||||
// If the removed value had the min slot and we didn't intend to replace it (cap=1)
|
||||
// then we reinsert it.
|
||||
if removed_slot == min_slot && slot >= min_slot {
|
||||
self.hdiff_buffers
|
||||
.put(removed_state_root, (removed_slot, removed_buffer));
|
||||
}
|
||||
true
|
||||
} else {
|
||||
// No room.
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cap(&self) -> NonZeroUsize {
|
||||
self.hdiff_buffers.cap()
|
||||
}
|
||||
|
||||
#[allow(clippy::len_without_is_empty)]
|
||||
pub fn len(&self) -> usize {
|
||||
self.hdiff_buffers.len()
|
||||
}
|
||||
|
||||
pub fn mem_usage(&self) -> usize {
|
||||
self.hdiff_buffers
|
||||
.iter()
|
||||
.map(|(_, (_, buffer))| buffer.size())
|
||||
.sum()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user