mirror of
https://github.com/sigp/lighthouse.git
synced 2026-05-30 20:57:10 +00:00
In-memory tree states (#5533)
* Consensus changes
* EF tests
* lcli
* common and watch
* account manager
* cargo
* fork choice
* promise cache
* beacon chain
* interop genesis
* http api
* lighthouse
* op pool
* beacon chain misc
* parallel state cache
* store
* fix issues in store
* IT COMPILES
* Remove some unnecessary module qualification
* Revert Arced pubkey optimization (#5536)
* Merge remote-tracking branch 'origin/unstable' into tree-states-memory
* Fix caching, rebasing and some tests
* Remove unused deps
* Merge remote-tracking branch 'origin/unstable' into tree-states-memory
* Small cleanups
* Revert shuffling cache/promise cache changes
* Fix state advance bugs
* Fix shuffling tests
* Remove some resolved FIXMEs
* Remove StateProcessingStrategy
* Optimise withdrawals calculation
* Don't reorg if state cache is missed
* Remove inconsistent state func
* Fix beta compiler
* Rebase early, rebase often
* Fix state caching behaviour
* Update to milhouse release
* Fix on-disk consensus context format
* Merge remote-tracking branch 'origin/unstable' into tree-states-memory
* Squashed commit of the following:
commit 3a16649023
Author: Michael Sproul <michael@sigmaprime.io>
Date: Thu Apr 18 14:26:09 2024 +1000
Fix on-disk consensus context format
* Keep indexed attestations, thanks Sean
* Merge branch 'on-disk-consensus-context' into tree-states-memory
* Merge branch 'unstable' into tree-states-memory
* Address half of Sean's review
* More simplifications from Sean's review
* Cache state after get_advanced_hot_state
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
//! Space-efficient storage for `BeaconState` vector fields.
|
||||
//!
|
||||
//! This module provides logic for splitting the `FixedVector` fields of a `BeaconState` into
|
||||
//! 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
|
||||
@@ -60,12 +60,13 @@ fn genesis_value_key() -> [u8; 8] {
|
||||
/// 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 `FixedVector<T, N>`.
|
||||
/// 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: Decode + Encode + Default + Clone + PartialEq + std::fmt::Debug;
|
||||
type Value: Default + std::fmt::Debug + milhouse::Value;
|
||||
// Decode + Encode + Default + Clone + PartialEq + std::fmt::Debug
|
||||
|
||||
/// The length of this field: the `N` from `FixedVector<T, N>`.
|
||||
/// 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.
|
||||
@@ -273,10 +274,10 @@ pub trait Field<E: EthSpec>: Copy {
|
||||
}
|
||||
}
|
||||
|
||||
/// Marker trait for fixed-length fields (`FixedVector<T, N>`).
|
||||
/// Marker trait for fixed-length fields (`Vector<T, N>`).
|
||||
pub trait FixedLengthField<E: EthSpec>: Field<E> {}
|
||||
|
||||
/// Marker trait for variable-length fields (`VariableList<T, N>`).
|
||||
/// 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.
|
||||
@@ -331,7 +332,7 @@ field!(
|
||||
activation_slot: Some(Slot::new(0)),
|
||||
deactivation_slot: None
|
||||
},
|
||||
|state: &BeaconState<_>, index, _| safe_modulo_index(state.block_roots(), index)
|
||||
|state: &BeaconState<_>, index, _| safe_modulo_vector_index(state.block_roots(), index)
|
||||
);
|
||||
|
||||
field!(
|
||||
@@ -345,7 +346,7 @@ field!(
|
||||
activation_slot: Some(Slot::new(0)),
|
||||
deactivation_slot: None,
|
||||
},
|
||||
|state: &BeaconState<_>, index, _| safe_modulo_index(state.state_roots(), index)
|
||||
|state: &BeaconState<_>, index, _| safe_modulo_vector_index(state.state_roots(), index)
|
||||
);
|
||||
|
||||
field!(
|
||||
@@ -361,7 +362,7 @@ field!(
|
||||
.capella_fork_epoch
|
||||
.map(|fork_epoch| fork_epoch.start_slot(E::slots_per_epoch())),
|
||||
},
|
||||
|state: &BeaconState<_>, index, _| safe_modulo_index(state.historical_roots(), index)
|
||||
|state: &BeaconState<_>, index, _| safe_modulo_list_index(state.historical_roots(), index)
|
||||
);
|
||||
|
||||
field!(
|
||||
@@ -371,7 +372,7 @@ field!(
|
||||
E::EpochsPerHistoricalVector,
|
||||
DBColumn::BeaconRandaoMixes,
|
||||
|_| OncePerEpoch { lag: 1 },
|
||||
|state: &BeaconState<_>, index, _| safe_modulo_index(state.randao_mixes(), index)
|
||||
|state: &BeaconState<_>, index, _| safe_modulo_vector_index(state.randao_mixes(), index)
|
||||
);
|
||||
|
||||
field!(
|
||||
@@ -387,7 +388,7 @@ field!(
|
||||
.map(|fork_epoch| fork_epoch.start_slot(E::slots_per_epoch())),
|
||||
deactivation_slot: None,
|
||||
},
|
||||
|state: &BeaconState<_>, index, _| safe_modulo_index(
|
||||
|state: &BeaconState<_>, index, _| safe_modulo_list_index(
|
||||
state
|
||||
.historical_summaries()
|
||||
.map_err(|_| ChunkError::InvalidFork)?,
|
||||
@@ -565,7 +566,7 @@ pub fn load_vector_from_db<F: FixedLengthField<E>, E: EthSpec, S: KeyValueStore<
|
||||
store: &S,
|
||||
slot: Slot,
|
||||
spec: &ChainSpec,
|
||||
) -> Result<FixedVector<F::Value, F::Length>, Error> {
|
||||
) -> 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);
|
||||
@@ -589,7 +590,7 @@ pub fn load_vector_from_db<F: FixedLengthField<E>, E: EthSpec, S: KeyValueStore<
|
||||
default,
|
||||
)?;
|
||||
|
||||
Ok(result.into())
|
||||
Ok(Vector::new(result).map_err(ChunkError::Milhouse)?)
|
||||
}
|
||||
|
||||
/// The historical roots are stored in vector chunks, despite not actually being a vector.
|
||||
@@ -597,7 +598,7 @@ pub fn load_variable_list_from_db<F: VariableLengthField<E>, E: EthSpec, S: KeyV
|
||||
store: &S,
|
||||
slot: Slot,
|
||||
spec: &ChainSpec,
|
||||
) -> Result<VariableList<F::Value, F::Length>, Error> {
|
||||
) -> 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;
|
||||
@@ -617,15 +618,35 @@ pub fn load_variable_list_from_db<F: VariableLengthField<E>, E: EthSpec, S: KeyV
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result.into())
|
||||
Ok(List::new(result).map_err(ChunkError::Milhouse)?)
|
||||
}
|
||||
|
||||
/// Index into a field of the state, avoiding out of bounds and division by 0.
|
||||
fn safe_modulo_index<T: Copy>(values: &[T], index: u64) -> Result<T, ChunkError> {
|
||||
/// 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 {
|
||||
Ok(values[index as usize % values.len()])
|
||||
values
|
||||
.get(index as usize % values.len())
|
||||
.copied()
|
||||
.ok_or(ChunkError::IndexOutOfBounds { index })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -712,6 +733,10 @@ where
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum ChunkError {
|
||||
ZeroLengthVector,
|
||||
ZeroLengthList,
|
||||
IndexOutOfBounds {
|
||||
index: u64,
|
||||
},
|
||||
InvalidSize {
|
||||
chunk_index: usize,
|
||||
expected: usize,
|
||||
@@ -744,6 +769,13 @@ pub enum ChunkError {
|
||||
length: usize,
|
||||
},
|
||||
InvalidFork,
|
||||
Milhouse(milhouse::Error),
|
||||
}
|
||||
|
||||
impl From<milhouse::Error> for ChunkError {
|
||||
fn from(e: milhouse::Error) -> ChunkError {
|
||||
Self::Milhouse(e)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -9,6 +9,7 @@ use types::{EthSpec, MinimalEthSpec};
|
||||
pub const PREV_DEFAULT_SLOTS_PER_RESTORE_POINT: u64 = 2048;
|
||||
pub const DEFAULT_SLOTS_PER_RESTORE_POINT: u64 = 8192;
|
||||
pub const DEFAULT_BLOCK_CACHE_SIZE: NonZeroUsize = new_non_zero_usize(5);
|
||||
pub const DEFAULT_STATE_CACHE_SIZE: NonZeroUsize = new_non_zero_usize(128);
|
||||
pub const DEFAULT_HISTORIC_STATE_CACHE_SIZE: NonZeroUsize = new_non_zero_usize(1);
|
||||
pub const DEFAULT_EPOCHS_PER_BLOB_PRUNE: u64 = 1;
|
||||
pub const DEFAULT_BLOB_PUNE_MARGIN_EPOCHS: u64 = 0;
|
||||
@@ -22,6 +23,8 @@ pub struct StoreConfig {
|
||||
pub slots_per_restore_point_set_explicitly: bool,
|
||||
/// Maximum number of blocks to store in the in-memory block cache.
|
||||
pub block_cache_size: NonZeroUsize,
|
||||
/// Maximum number of states to store in the in-memory state cache.
|
||||
pub state_cache_size: NonZeroUsize,
|
||||
/// Maximum number of states from freezer database to store in the in-memory state cache.
|
||||
pub historic_state_cache_size: NonZeroUsize,
|
||||
/// Whether to compact the database on initialization.
|
||||
@@ -57,6 +60,7 @@ impl Default for StoreConfig {
|
||||
slots_per_restore_point: MinimalEthSpec::slots_per_historical_root() as u64,
|
||||
slots_per_restore_point_set_explicitly: false,
|
||||
block_cache_size: DEFAULT_BLOCK_CACHE_SIZE,
|
||||
state_cache_size: DEFAULT_STATE_CACHE_SIZE,
|
||||
historic_state_cache_size: DEFAULT_HISTORIC_STATE_CACHE_SIZE,
|
||||
compact_on_init: false,
|
||||
compact_on_prune: true,
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::config::StoreConfigError;
|
||||
use crate::hot_cold_store::HotColdDBError;
|
||||
use ssz::DecodeError;
|
||||
use state_processing::BlockReplayError;
|
||||
use types::{BeaconStateError, Hash256, InconsistentFork, Slot};
|
||||
use types::{BeaconStateError, EpochCacheError, Hash256, InconsistentFork, Slot};
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
@@ -49,6 +49,14 @@ pub enum Error {
|
||||
InvalidBytes,
|
||||
UnableToDowngrade,
|
||||
InconsistentFork(InconsistentFork),
|
||||
CacheBuildError(EpochCacheError),
|
||||
RandaoMixOutOfBounds,
|
||||
FinalizedStateDecreasingSlot,
|
||||
FinalizedStateUnaligned,
|
||||
StateForCacheHasPendingUpdates {
|
||||
state_root: Hash256,
|
||||
slot: Slot,
|
||||
},
|
||||
}
|
||||
|
||||
pub trait HandleUnavailable<T> {
|
||||
@@ -113,6 +121,12 @@ impl From<InconsistentFork> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EpochCacheError> for Error {
|
||||
fn from(e: EpochCacheError) -> Error {
|
||||
Error::CacheBuildError(e)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DBError {
|
||||
pub message: String,
|
||||
|
||||
@@ -17,6 +17,7 @@ use crate::metadata::{
|
||||
PRUNING_CHECKPOINT_KEY, SCHEMA_VERSION_KEY, SPLIT_KEY, STATE_UPPER_LIMIT_NO_RETAIN,
|
||||
};
|
||||
use crate::metrics;
|
||||
use crate::state_cache::{PutStateOutcome, StateCache};
|
||||
use crate::{
|
||||
get_key_for_col, ChunkWriter, DBColumn, DatabaseBlock, Error, ItemStore, KeyValueStoreOp,
|
||||
PartialBeaconState, StoreItem, StoreOp,
|
||||
@@ -30,7 +31,8 @@ use slog::{debug, error, info, trace, warn, Logger};
|
||||
use ssz::{Decode, Encode};
|
||||
use ssz_derive::{Decode, Encode};
|
||||
use state_processing::{
|
||||
BlockProcessingError, BlockReplayer, SlotProcessingError, StateProcessingStrategy,
|
||||
block_replayer::PreSlotHook, AllCaches, BlockProcessingError, BlockReplayer,
|
||||
SlotProcessingError,
|
||||
};
|
||||
use std::cmp::min;
|
||||
use std::marker::PhantomData;
|
||||
@@ -66,12 +68,16 @@ pub struct HotColdDB<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> {
|
||||
pub hot_db: Hot,
|
||||
/// LRU cache of deserialized blocks and blobs. Updated whenever a block or blob is loaded.
|
||||
block_cache: Mutex<BlockCache<E>>,
|
||||
/// Cache of beacon states.
|
||||
///
|
||||
/// LOCK ORDERING: this lock must always be locked *after* the `split` if both are required.
|
||||
state_cache: Mutex<StateCache<E>>,
|
||||
/// LRU cache of replayed states.
|
||||
state_cache: Mutex<LruCache<Slot, BeaconState<E>>>,
|
||||
historic_state_cache: Mutex<LruCache<Slot, BeaconState<E>>>,
|
||||
/// Chain spec.
|
||||
pub(crate) spec: ChainSpec,
|
||||
/// Logger.
|
||||
pub(crate) log: Logger,
|
||||
pub log: Logger,
|
||||
/// Mere vessel for E.
|
||||
_phantom: PhantomData<E>,
|
||||
}
|
||||
@@ -178,7 +184,8 @@ impl<E: EthSpec> HotColdDB<E, MemoryStore<E>, MemoryStore<E>> {
|
||||
blobs_db: MemoryStore::open(),
|
||||
hot_db: MemoryStore::open(),
|
||||
block_cache: Mutex::new(BlockCache::new(config.block_cache_size)),
|
||||
state_cache: Mutex::new(LruCache::new(config.historic_state_cache_size)),
|
||||
state_cache: Mutex::new(StateCache::new(config.state_cache_size)),
|
||||
historic_state_cache: Mutex::new(LruCache::new(config.historic_state_cache_size)),
|
||||
config,
|
||||
spec,
|
||||
log,
|
||||
@@ -192,8 +199,6 @@ impl<E: EthSpec> HotColdDB<E, MemoryStore<E>, MemoryStore<E>> {
|
||||
impl<E: EthSpec> HotColdDB<E, LevelDB<E>, LevelDB<E>> {
|
||||
/// Open a new or existing database, with the given paths to the hot and cold DBs.
|
||||
///
|
||||
/// The `slots_per_restore_point` parameter must be a divisor of `SLOTS_PER_HISTORICAL_ROOT`.
|
||||
///
|
||||
/// The `migrate_schema` function is passed in so that the parent `BeaconChain` can provide
|
||||
/// context and access `BeaconChain`-level code without creating a circular dependency.
|
||||
pub fn open(
|
||||
@@ -215,7 +220,8 @@ impl<E: EthSpec> HotColdDB<E, LevelDB<E>, LevelDB<E>> {
|
||||
blobs_db: LevelDB::open(blobs_db_path)?,
|
||||
hot_db: LevelDB::open(hot_path)?,
|
||||
block_cache: Mutex::new(BlockCache::new(config.block_cache_size)),
|
||||
state_cache: Mutex::new(LruCache::new(config.historic_state_cache_size)),
|
||||
state_cache: Mutex::new(StateCache::new(config.state_cache_size)),
|
||||
historic_state_cache: Mutex::new(LruCache::new(config.historic_state_cache_size)),
|
||||
config,
|
||||
spec,
|
||||
log,
|
||||
@@ -352,6 +358,21 @@ impl<E: EthSpec> HotColdDB<E, LevelDB<E>, LevelDB<E>> {
|
||||
}
|
||||
|
||||
impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold> {
|
||||
pub fn update_finalized_state(
|
||||
&self,
|
||||
state_root: Hash256,
|
||||
block_root: Hash256,
|
||||
state: BeaconState<E>,
|
||||
) -> Result<(), Error> {
|
||||
self.state_cache
|
||||
.lock()
|
||||
.update_finalized_state(state_root, block_root, state)
|
||||
}
|
||||
|
||||
pub fn state_cache_len(&self) -> usize {
|
||||
self.state_cache.lock().len()
|
||||
}
|
||||
|
||||
/// Store a block and update the LRU cache.
|
||||
pub fn put_block(
|
||||
&self,
|
||||
@@ -615,11 +636,26 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold>
|
||||
|
||||
/// Store a state in the store.
|
||||
pub fn put_state(&self, state_root: &Hash256, state: &BeaconState<E>) -> Result<(), Error> {
|
||||
self.put_state_possibly_temporary(state_root, state, false)
|
||||
}
|
||||
|
||||
/// Store a state in the store.
|
||||
///
|
||||
/// The `temporary` flag indicates whether this state should be considered canonical.
|
||||
pub fn put_state_possibly_temporary(
|
||||
&self,
|
||||
state_root: &Hash256,
|
||||
state: &BeaconState<E>,
|
||||
temporary: bool,
|
||||
) -> Result<(), Error> {
|
||||
let mut ops: Vec<KeyValueStoreOp> = Vec::new();
|
||||
if state.slot() < self.get_split_slot() {
|
||||
self.store_cold_state(state_root, state, &mut ops)?;
|
||||
self.cold_db.do_atomically(ops)
|
||||
} else {
|
||||
if temporary {
|
||||
ops.push(TemporaryFlag.as_kv_store_op(*state_root));
|
||||
}
|
||||
self.store_hot_state(state_root, state, &mut ops)?;
|
||||
self.hot_db.do_atomically(ops)
|
||||
}
|
||||
@@ -648,45 +684,16 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold>
|
||||
// chain. This way we avoid returning a state that doesn't match `state_root`.
|
||||
self.load_cold_state(state_root)
|
||||
} else {
|
||||
self.load_hot_state(state_root, StateProcessingStrategy::Accurate)
|
||||
self.get_hot_state(state_root)
|
||||
}
|
||||
} else {
|
||||
match self.load_hot_state(state_root, StateProcessingStrategy::Accurate)? {
|
||||
match self.get_hot_state(state_root)? {
|
||||
Some(state) => Ok(Some(state)),
|
||||
None => self.load_cold_state(state_root),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch a state from the store, but don't compute all of the values when replaying blocks
|
||||
/// upon that state (e.g., state roots). Additionally, only states from the hot store are
|
||||
/// returned.
|
||||
///
|
||||
/// See `Self::get_advanced_hot_state` for information about `max_slot`.
|
||||
///
|
||||
/// ## Warning
|
||||
///
|
||||
/// The returned state **is not a valid beacon state**, it can only be used for obtaining
|
||||
/// shuffling to process attestations. At least the following components of the state will be
|
||||
/// broken/invalid:
|
||||
///
|
||||
/// - `state.state_roots`
|
||||
/// - `state.block_roots`
|
||||
pub fn get_inconsistent_state_for_attestation_verification_only(
|
||||
&self,
|
||||
block_root: &Hash256,
|
||||
max_slot: Slot,
|
||||
state_root: Hash256,
|
||||
) -> Result<Option<(Hash256, BeaconState<E>)>, Error> {
|
||||
metrics::inc_counter(&metrics::BEACON_STATE_GET_COUNT);
|
||||
self.get_advanced_hot_state_with_strategy(
|
||||
*block_root,
|
||||
max_slot,
|
||||
state_root,
|
||||
StateProcessingStrategy::Inconsistent,
|
||||
)
|
||||
}
|
||||
|
||||
/// Get a state with `latest_block_root == block_root` advanced through to at most `max_slot`.
|
||||
///
|
||||
/// The `state_root` argument is used to look up the block's un-advanced state in case an
|
||||
@@ -697,35 +704,29 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold>
|
||||
/// - `result_state_root == state.canonical_root()`
|
||||
/// - `state.slot() <= max_slot`
|
||||
/// - `state.get_latest_block_root(result_state_root) == block_root`
|
||||
///
|
||||
/// Presently this is only used to avoid loading the un-advanced split state, but in future will
|
||||
/// be expanded to return states from an in-memory cache.
|
||||
pub fn get_advanced_hot_state(
|
||||
&self,
|
||||
block_root: Hash256,
|
||||
max_slot: Slot,
|
||||
state_root: Hash256,
|
||||
) -> Result<Option<(Hash256, BeaconState<E>)>, Error> {
|
||||
self.get_advanced_hot_state_with_strategy(
|
||||
block_root,
|
||||
max_slot,
|
||||
state_root,
|
||||
StateProcessingStrategy::Accurate,
|
||||
)
|
||||
}
|
||||
if let Some(cached) = self.get_advanced_hot_state_from_cache(block_root, max_slot) {
|
||||
return Ok(Some(cached));
|
||||
}
|
||||
|
||||
/// Same as `get_advanced_hot_state` but taking a `StateProcessingStrategy`.
|
||||
pub fn get_advanced_hot_state_with_strategy(
|
||||
&self,
|
||||
block_root: Hash256,
|
||||
max_slot: Slot,
|
||||
state_root: Hash256,
|
||||
state_processing_strategy: StateProcessingStrategy,
|
||||
) -> Result<Option<(Hash256, BeaconState<E>)>, Error> {
|
||||
// Hold a read lock on the split point so it can't move while we're trying to load the
|
||||
// state.
|
||||
let split = self.split.read_recursive();
|
||||
|
||||
if state_root != split.state_root {
|
||||
warn!(
|
||||
self.log,
|
||||
"State cache missed";
|
||||
"state_root" => ?state_root,
|
||||
"block_root" => ?block_root,
|
||||
);
|
||||
}
|
||||
|
||||
// Sanity check max-slot against the split slot.
|
||||
if max_slot < split.slot {
|
||||
return Err(HotColdDBError::FinalizedStateNotInHotDatabase {
|
||||
@@ -741,11 +742,40 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold>
|
||||
} else {
|
||||
state_root
|
||||
};
|
||||
let state = self
|
||||
.load_hot_state(&state_root, state_processing_strategy)?
|
||||
.map(|state| (state_root, state));
|
||||
let mut opt_state = self
|
||||
.load_hot_state(&state_root)?
|
||||
.map(|(state, _block_root)| (state_root, state));
|
||||
|
||||
if let Some((state_root, state)) = opt_state.as_mut() {
|
||||
state.update_tree_hash_cache()?;
|
||||
state.build_all_caches(&self.spec)?;
|
||||
self.state_cache
|
||||
.lock()
|
||||
.put_state(*state_root, block_root, state)?;
|
||||
debug!(
|
||||
self.log,
|
||||
"Cached state";
|
||||
"state_root" => ?state_root,
|
||||
"slot" => state.slot(),
|
||||
);
|
||||
}
|
||||
drop(split);
|
||||
Ok(state)
|
||||
Ok(opt_state)
|
||||
}
|
||||
|
||||
/// Same as `get_advanced_hot_state` but will return `None` if no compatible state is cached.
|
||||
///
|
||||
/// If this function returns `Some(state)` then that `state` will always have
|
||||
/// `latest_block_header` matching `block_root` but may not be advanced all the way through to
|
||||
/// `max_slot`.
|
||||
pub fn get_advanced_hot_state_from_cache(
|
||||
&self,
|
||||
block_root: Hash256,
|
||||
max_slot: Slot,
|
||||
) -> Option<(Hash256, BeaconState<E>)> {
|
||||
self.state_cache
|
||||
.lock()
|
||||
.get_by_block_root(block_root, max_slot)
|
||||
}
|
||||
|
||||
/// Delete a state, ensuring it is removed from the LRU cache, as well as from on-disk.
|
||||
@@ -755,17 +785,10 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold>
|
||||
/// (which are frozen, and won't be deleted), or valid descendents of the finalized checkpoint
|
||||
/// (which will be deleted by this function but shouldn't be).
|
||||
pub fn delete_state(&self, state_root: &Hash256, slot: Slot) -> Result<(), Error> {
|
||||
// Delete the state summary.
|
||||
self.hot_db
|
||||
.key_delete(DBColumn::BeaconStateSummary.into(), state_root.as_bytes())?;
|
||||
|
||||
// Delete the full state if it lies on an epoch boundary.
|
||||
if slot % E::slots_per_epoch() == 0 {
|
||||
self.hot_db
|
||||
.key_delete(DBColumn::BeaconState.into(), state_root.as_bytes())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
self.do_atomically_with_block_and_blobs_cache(vec![StoreOp::DeleteState(
|
||||
*state_root,
|
||||
Some(slot),
|
||||
)])
|
||||
}
|
||||
|
||||
pub fn forwards_block_roots_iterator(
|
||||
@@ -833,17 +856,9 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold>
|
||||
}) = self.load_hot_state_summary(state_root)?
|
||||
{
|
||||
// NOTE: minor inefficiency here because we load an unnecessary hot state summary
|
||||
//
|
||||
// `StateProcessingStrategy` should be irrelevant here since we never replay blocks for an epoch
|
||||
// boundary state in the hot DB.
|
||||
let state = self
|
||||
.load_hot_state(
|
||||
&epoch_boundary_state_root,
|
||||
StateProcessingStrategy::Accurate,
|
||||
)?
|
||||
.ok_or(HotColdDBError::MissingEpochBoundaryState(
|
||||
epoch_boundary_state_root,
|
||||
))?;
|
||||
let (state, _) = self.load_hot_state(&epoch_boundary_state_root)?.ok_or(
|
||||
HotColdDBError::MissingEpochBoundaryState(epoch_boundary_state_root),
|
||||
)?;
|
||||
Ok(Some(state))
|
||||
} else {
|
||||
// Try the cold DB
|
||||
@@ -1029,12 +1044,15 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold>
|
||||
|
||||
StoreOp::DeleteBlock(block_root) => {
|
||||
guard.delete_block(&block_root);
|
||||
self.state_cache.lock().delete_block_states(&block_root);
|
||||
}
|
||||
|
||||
StoreOp::DeleteState(state_root, _) => {
|
||||
self.state_cache.lock().delete_state(&state_root)
|
||||
}
|
||||
|
||||
StoreOp::DeleteBlobs(_) => (),
|
||||
|
||||
StoreOp::DeleteState(_, _) => (),
|
||||
|
||||
StoreOp::DeleteExecutionPayload(_) => (),
|
||||
|
||||
StoreOp::KeyValueOp(_) => (),
|
||||
@@ -1070,6 +1088,26 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold>
|
||||
state: &BeaconState<E>,
|
||||
ops: &mut Vec<KeyValueStoreOp>,
|
||||
) -> Result<(), Error> {
|
||||
// Put the state in the cache.
|
||||
let block_root = state.get_latest_block_root(*state_root);
|
||||
|
||||
// Avoid storing states in the database if they already exist in the state cache.
|
||||
// The exception to this is the finalized state, which must exist in the cache before it
|
||||
// is stored on disk.
|
||||
if let PutStateOutcome::Duplicate =
|
||||
self.state_cache
|
||||
.lock()
|
||||
.put_state(*state_root, block_root, state)?
|
||||
{
|
||||
debug!(
|
||||
self.log,
|
||||
"Skipping storage of cached state";
|
||||
"slot" => state.slot(),
|
||||
"state_root" => ?state_root
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// On the epoch boundary, store the full state.
|
||||
if state.slot() % E::slots_per_epoch() == 0 {
|
||||
trace!(
|
||||
@@ -1091,14 +1129,51 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold>
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get a post-finalization state from the database or store.
|
||||
pub fn get_hot_state(&self, state_root: &Hash256) -> Result<Option<BeaconState<E>>, Error> {
|
||||
if let Some(state) = self.state_cache.lock().get_by_state_root(*state_root) {
|
||||
return Ok(Some(state));
|
||||
}
|
||||
|
||||
if *state_root != self.get_split_info().state_root {
|
||||
// Do not warn on start up when loading the split state.
|
||||
warn!(
|
||||
self.log,
|
||||
"State cache missed";
|
||||
"state_root" => ?state_root,
|
||||
);
|
||||
}
|
||||
|
||||
let state_from_disk = self.load_hot_state(state_root)?;
|
||||
|
||||
if let Some((mut state, block_root)) = state_from_disk {
|
||||
state.update_tree_hash_cache()?;
|
||||
state.build_all_caches(&self.spec)?;
|
||||
self.state_cache
|
||||
.lock()
|
||||
.put_state(*state_root, block_root, &state)?;
|
||||
debug!(
|
||||
self.log,
|
||||
"Cached state";
|
||||
"state_root" => ?state_root,
|
||||
"slot" => state.slot(),
|
||||
);
|
||||
Ok(Some(state))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Load a post-finalization state from the hot database.
|
||||
///
|
||||
/// Will replay blocks from the nearest epoch boundary.
|
||||
///
|
||||
/// Return the `(state, latest_block_root)` where `latest_block_root` is the root of the last
|
||||
/// block applied to `state`.
|
||||
pub fn load_hot_state(
|
||||
&self,
|
||||
state_root: &Hash256,
|
||||
state_processing_strategy: StateProcessingStrategy,
|
||||
) -> Result<Option<BeaconState<E>>, Error> {
|
||||
) -> Result<Option<(BeaconState<E>, Hash256)>, Error> {
|
||||
metrics::inc_counter(&metrics::BEACON_STATE_HOT_GET_COUNT);
|
||||
|
||||
// If the state is marked as temporary, do not return it. It will become visible
|
||||
@@ -1113,16 +1188,47 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold>
|
||||
epoch_boundary_state_root,
|
||||
}) = self.load_hot_state_summary(state_root)?
|
||||
{
|
||||
let boundary_state =
|
||||
let mut boundary_state =
|
||||
get_full_state(&self.hot_db, &epoch_boundary_state_root, &self.spec)?.ok_or(
|
||||
HotColdDBError::MissingEpochBoundaryState(epoch_boundary_state_root),
|
||||
)?;
|
||||
|
||||
// Immediately rebase the state from disk on the finalized state so that we can reuse
|
||||
// parts of the tree for state root calculation in `replay_blocks`.
|
||||
self.state_cache
|
||||
.lock()
|
||||
.rebase_on_finalized(&mut boundary_state, &self.spec)?;
|
||||
|
||||
// Optimization to avoid even *thinking* about replaying blocks if we're already
|
||||
// on an epoch boundary.
|
||||
let state = if slot % E::slots_per_epoch() == 0 {
|
||||
let mut state = if slot % E::slots_per_epoch() == 0 {
|
||||
boundary_state
|
||||
} else {
|
||||
// Cache ALL intermediate states that are reached during block replay. We may want
|
||||
// to restrict this in future to only cache epoch boundary states. At worst we will
|
||||
// cache up to 32 states for each state loaded, which should not flush out the cache
|
||||
// entirely.
|
||||
let state_cache_hook = |state_root, state: &mut BeaconState<E>| {
|
||||
// Ensure all caches are built before attempting to cache.
|
||||
state.update_tree_hash_cache()?;
|
||||
state.build_all_caches(&self.spec)?;
|
||||
|
||||
let latest_block_root = state.get_latest_block_root(state_root);
|
||||
let state_slot = state.slot();
|
||||
if let PutStateOutcome::New =
|
||||
self.state_cache
|
||||
.lock()
|
||||
.put_state(state_root, latest_block_root, state)?
|
||||
{
|
||||
debug!(
|
||||
self.log,
|
||||
"Cached ancestor state";
|
||||
"state_root" => ?state_root,
|
||||
"slot" => state_slot,
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
let blocks =
|
||||
self.load_blocks_to_replay(boundary_state.slot(), slot, latest_block_root)?;
|
||||
self.replay_blocks(
|
||||
@@ -1130,11 +1236,12 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold>
|
||||
blocks,
|
||||
slot,
|
||||
no_state_root_iter(),
|
||||
state_processing_strategy,
|
||||
Some(Box::new(state_cache_hook)),
|
||||
)?
|
||||
};
|
||||
state.apply_pending_mutations()?;
|
||||
|
||||
Ok(Some(state))
|
||||
Ok(Some((state, latest_block_root)))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
@@ -1233,7 +1340,9 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold>
|
||||
partial_state.load_randao_mixes(&self.cold_db, &self.spec)?;
|
||||
partial_state.load_historical_summaries(&self.cold_db, &self.spec)?;
|
||||
|
||||
partial_state.try_into()
|
||||
let mut state: BeaconState<E> = partial_state.try_into()?;
|
||||
state.apply_pending_mutations()?;
|
||||
Ok(state)
|
||||
}
|
||||
|
||||
/// Load a restore point state by its `restore_point_index`.
|
||||
@@ -1247,7 +1356,7 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold>
|
||||
|
||||
/// Load a frozen state that lies between restore points.
|
||||
fn load_cold_intermediate_state(&self, slot: Slot) -> Result<BeaconState<E>, Error> {
|
||||
if let Some(state) = self.state_cache.lock().get(&slot) {
|
||||
if let Some(state) = self.historic_state_cache.lock().get(&slot) {
|
||||
return Ok(state.clone());
|
||||
}
|
||||
|
||||
@@ -1261,7 +1370,7 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold>
|
||||
let mut low_state: Option<BeaconState<E>> = None;
|
||||
|
||||
// Try to get a more recent state from the cache to avoid massive blocks replay.
|
||||
for (s, state) in self.state_cache.lock().iter() {
|
||||
for (s, state) in self.historic_state_cache.lock().iter() {
|
||||
if s.as_u64() / self.config.slots_per_restore_point == low_restore_point_idx
|
||||
&& *s < slot
|
||||
&& low_slot < *s
|
||||
@@ -1299,16 +1408,11 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold>
|
||||
&self.spec,
|
||||
)?;
|
||||
|
||||
let state = self.replay_blocks(
|
||||
low_state,
|
||||
blocks,
|
||||
slot,
|
||||
Some(state_root_iter),
|
||||
StateProcessingStrategy::Accurate,
|
||||
)?;
|
||||
let mut state = self.replay_blocks(low_state, blocks, slot, Some(state_root_iter), None)?;
|
||||
state.apply_pending_mutations()?;
|
||||
|
||||
// If state is not error, put it in the cache.
|
||||
self.state_cache.lock().put(slot, state.clone());
|
||||
self.historic_state_cache.lock().put(slot, state.clone());
|
||||
|
||||
Ok(state)
|
||||
}
|
||||
@@ -1390,16 +1494,15 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold>
|
||||
///
|
||||
/// Will skip slots as necessary. The returned state is not guaranteed
|
||||
/// to have any caches built, beyond those immediately required by block processing.
|
||||
fn replay_blocks(
|
||||
pub fn replay_blocks(
|
||||
&self,
|
||||
state: BeaconState<E>,
|
||||
blocks: Vec<SignedBeaconBlock<E, BlindedPayload<E>>>,
|
||||
target_slot: Slot,
|
||||
state_root_iter: Option<impl Iterator<Item = Result<(Hash256, Slot), Error>>>,
|
||||
state_processing_strategy: StateProcessingStrategy,
|
||||
pre_slot_hook: Option<PreSlotHook<E, Error>>,
|
||||
) -> Result<BeaconState<E>, Error> {
|
||||
let mut block_replayer = BlockReplayer::new(state, &self.spec)
|
||||
.state_processing_strategy(state_processing_strategy)
|
||||
.no_signature_verification()
|
||||
.minimal_block_root_verification();
|
||||
|
||||
@@ -1408,17 +1511,20 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold>
|
||||
block_replayer = block_replayer.state_root_iter(state_root_iter);
|
||||
}
|
||||
|
||||
if let Some(pre_slot_hook) = pre_slot_hook {
|
||||
block_replayer = block_replayer.pre_slot_hook(pre_slot_hook);
|
||||
}
|
||||
|
||||
block_replayer
|
||||
.apply_blocks(blocks, Some(target_slot))
|
||||
.map(|block_replayer| {
|
||||
if have_state_root_iterator && block_replayer.state_root_miss() {
|
||||
warn!(
|
||||
self.log,
|
||||
"State root iterator miss";
|
||||
"State root cache miss during block replay";
|
||||
"slot" => target_slot,
|
||||
);
|
||||
}
|
||||
|
||||
block_replayer.into_state()
|
||||
})
|
||||
}
|
||||
@@ -2213,7 +2319,7 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold>
|
||||
}
|
||||
|
||||
/// This function fills in missing block roots between last restore point slot and split
|
||||
/// slot, if any.
|
||||
/// slot, if any.
|
||||
pub fn heal_freezer_block_roots_at_split(&self) -> Result<(), Error> {
|
||||
let split = self.get_split_info();
|
||||
let last_restore_point_slot = (split.slot - 1) / self.config.slots_per_restore_point
|
||||
@@ -2528,15 +2634,22 @@ pub fn migrate_database<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>>(
|
||||
};
|
||||
store.hot_db.put_sync(&SPLIT_KEY, &split)?;
|
||||
|
||||
// Split point is now persisted in the hot database on disk. The in-memory split point
|
||||
// hasn't been modified elsewhere since we keep a write lock on it. It's safe to update
|
||||
// Split point is now persisted in the hot database on disk. The in-memory split point
|
||||
// hasn't been modified elsewhere since we keep a write lock on it. It's safe to update
|
||||
// the in-memory split point now.
|
||||
*split_guard = split;
|
||||
}
|
||||
|
||||
// Delete the states from the hot database if we got this far.
|
||||
// Delete the blocks and states from the hot database if we got this far.
|
||||
store.do_atomically_with_block_and_blobs_cache(hot_db_ops)?;
|
||||
|
||||
// Update the cache's view of the finalized state.
|
||||
store.update_finalized_state(
|
||||
finalized_state_root,
|
||||
finalized_block_root,
|
||||
finalized_state.clone(),
|
||||
)?;
|
||||
|
||||
debug!(
|
||||
store.log,
|
||||
"Freezer migration complete";
|
||||
|
||||
@@ -46,14 +46,14 @@ pub fn get_full_state<KV: KeyValueStore<E>, E: EthSpec>(
|
||||
#[derive(Encode)]
|
||||
pub struct StorageContainer<E: EthSpec> {
|
||||
state: BeaconState<E>,
|
||||
committee_caches: Vec<CommitteeCache>,
|
||||
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_with(CloneConfig::none()),
|
||||
state: state.clone(),
|
||||
committee_caches: state.committee_caches().to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -412,15 +412,16 @@ mod test {
|
||||
let mut hashes = (0..).map(Hash256::from_low_u64_be);
|
||||
let roots_a = state_a.block_roots_mut();
|
||||
for i in 0..roots_a.len() {
|
||||
roots_a[i] = hashes.next().unwrap()
|
||||
*roots_a.get_mut(i).unwrap() = hashes.next().unwrap();
|
||||
}
|
||||
let roots_b = state_b.block_roots_mut();
|
||||
for i in 0..roots_b.len() {
|
||||
roots_b[i] = hashes.next().unwrap()
|
||||
*roots_b.get_mut(i).unwrap() = hashes.next().unwrap();
|
||||
}
|
||||
|
||||
let state_a_root = hashes.next().unwrap();
|
||||
state_b.state_roots_mut()[0] = state_a_root;
|
||||
*state_b.state_roots_mut().get_mut(0).unwrap() = state_a_root;
|
||||
state_a.apply_pending_mutations().unwrap();
|
||||
store.put_state(&state_a_root, &state_a).unwrap();
|
||||
|
||||
let iter = BlockRootsIterator::new(&store, &state_b);
|
||||
@@ -472,6 +473,9 @@ mod test {
|
||||
let state_a_root = Hash256::from_low_u64_be(slots_per_historical_root as u64);
|
||||
let state_b_root = Hash256::from_low_u64_be(slots_per_historical_root as u64 * 2);
|
||||
|
||||
state_a.apply_pending_mutations().unwrap();
|
||||
state_b.apply_pending_mutations().unwrap();
|
||||
|
||||
store.put_state(&state_a_root, &state_a).unwrap();
|
||||
store.put_state(&state_b_root, &state_b).unwrap();
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ pub mod metadata;
|
||||
pub mod metrics;
|
||||
mod partial_beacon_state;
|
||||
pub mod reconstruct;
|
||||
pub mod state_cache;
|
||||
|
||||
pub mod iter;
|
||||
|
||||
|
||||
@@ -34,42 +34,42 @@ where
|
||||
pub latest_block_header: BeaconBlockHeader,
|
||||
|
||||
#[ssz(skip_serializing, skip_deserializing)]
|
||||
pub block_roots: Option<FixedVector<Hash256, E::SlotsPerHistoricalRoot>>,
|
||||
pub block_roots: Option<Vector<Hash256, E::SlotsPerHistoricalRoot>>,
|
||||
#[ssz(skip_serializing, skip_deserializing)]
|
||||
pub state_roots: Option<FixedVector<Hash256, E::SlotsPerHistoricalRoot>>,
|
||||
pub state_roots: Option<Vector<Hash256, E::SlotsPerHistoricalRoot>>,
|
||||
|
||||
#[ssz(skip_serializing, skip_deserializing)]
|
||||
pub historical_roots: Option<VariableList<Hash256, E::HistoricalRootsLimit>>,
|
||||
pub historical_roots: Option<List<Hash256, E::HistoricalRootsLimit>>,
|
||||
|
||||
// Ethereum 1.0 chain data
|
||||
pub eth1_data: Eth1Data,
|
||||
pub eth1_data_votes: VariableList<Eth1Data, E::SlotsPerEth1VotingPeriod>,
|
||||
pub eth1_data_votes: List<Eth1Data, E::SlotsPerEth1VotingPeriod>,
|
||||
pub eth1_deposit_index: u64,
|
||||
|
||||
// Registry
|
||||
pub validators: VariableList<Validator, E::ValidatorRegistryLimit>,
|
||||
pub balances: VariableList<u64, E::ValidatorRegistryLimit>,
|
||||
pub validators: List<Validator, E::ValidatorRegistryLimit>,
|
||||
pub balances: List<u64, E::ValidatorRegistryLimit>,
|
||||
|
||||
// Shuffling
|
||||
/// Randao value from the current slot, for patching into the per-epoch randao vector.
|
||||
pub latest_randao_value: Hash256,
|
||||
#[ssz(skip_serializing, skip_deserializing)]
|
||||
pub randao_mixes: Option<FixedVector<Hash256, E::EpochsPerHistoricalVector>>,
|
||||
pub randao_mixes: Option<Vector<Hash256, E::EpochsPerHistoricalVector>>,
|
||||
|
||||
// Slashings
|
||||
slashings: FixedVector<u64, E::EpochsPerSlashingsVector>,
|
||||
slashings: Vector<u64, E::EpochsPerSlashingsVector>,
|
||||
|
||||
// Attestations (genesis fork only)
|
||||
#[superstruct(only(Base))]
|
||||
pub previous_epoch_attestations: VariableList<PendingAttestation<E>, E::MaxPendingAttestations>,
|
||||
pub previous_epoch_attestations: List<PendingAttestation<E>, E::MaxPendingAttestations>,
|
||||
#[superstruct(only(Base))]
|
||||
pub current_epoch_attestations: VariableList<PendingAttestation<E>, E::MaxPendingAttestations>,
|
||||
pub current_epoch_attestations: List<PendingAttestation<E>, E::MaxPendingAttestations>,
|
||||
|
||||
// Participation (Altair and later)
|
||||
#[superstruct(only(Altair, Merge, Capella, Deneb, Electra))]
|
||||
pub previous_epoch_participation: VariableList<ParticipationFlags, E::ValidatorRegistryLimit>,
|
||||
pub previous_epoch_participation: List<ParticipationFlags, E::ValidatorRegistryLimit>,
|
||||
#[superstruct(only(Altair, Merge, Capella, Deneb, Electra))]
|
||||
pub current_epoch_participation: VariableList<ParticipationFlags, E::ValidatorRegistryLimit>,
|
||||
pub current_epoch_participation: List<ParticipationFlags, E::ValidatorRegistryLimit>,
|
||||
|
||||
// Finality
|
||||
pub justification_bits: BitVector<E::JustificationBitsLength>,
|
||||
@@ -79,7 +79,7 @@ where
|
||||
|
||||
// Inactivity
|
||||
#[superstruct(only(Altair, Merge, Capella, Deneb, Electra))]
|
||||
pub inactivity_scores: VariableList<u64, E::ValidatorRegistryLimit>,
|
||||
pub inactivity_scores: List<u64, E::ValidatorRegistryLimit>,
|
||||
|
||||
// Light-client sync committees
|
||||
#[superstruct(only(Altair, Merge, Capella, Deneb, Electra))]
|
||||
@@ -117,7 +117,7 @@ where
|
||||
|
||||
#[ssz(skip_serializing, skip_deserializing)]
|
||||
#[superstruct(only(Capella, Deneb, Electra))]
|
||||
pub historical_summaries: Option<VariableList<HistoricalSummary, E::HistoricalRootsLimit>>,
|
||||
pub historical_summaries: Option<List<HistoricalSummary, E::HistoricalRootsLimit>>,
|
||||
}
|
||||
|
||||
/// Implement the conversion function from BeaconState -> PartialBeaconState.
|
||||
@@ -369,7 +369,9 @@ impl<E: EthSpec> PartialBeaconState<E> {
|
||||
// Patch the value for the current slot into the index for the current epoch
|
||||
let current_epoch = self.slot().epoch(E::slots_per_epoch());
|
||||
let len = randao_mixes.len();
|
||||
randao_mixes[current_epoch.as_usize() % len] = *self.latest_randao_value();
|
||||
*randao_mixes
|
||||
.get_mut(current_epoch.as_usize() % len)
|
||||
.ok_or(Error::RandaoMixOutOfBounds)? = *self.latest_randao_value();
|
||||
|
||||
*self.randao_mixes_mut() = Some(randao_mixes)
|
||||
}
|
||||
@@ -422,7 +424,6 @@ macro_rules! impl_try_into_beacon_state {
|
||||
exit_cache: <_>::default(),
|
||||
slashings_cache: <_>::default(),
|
||||
epoch_cache: <_>::default(),
|
||||
tree_hash_cache: <_>::default(),
|
||||
|
||||
// Variant-specific fields
|
||||
$(
|
||||
|
||||
@@ -5,7 +5,7 @@ use itertools::{process_results, Itertools};
|
||||
use slog::info;
|
||||
use state_processing::{
|
||||
per_block_processing, per_slot_processing, BlockSignatureStrategy, ConsensusContext,
|
||||
StateProcessingStrategy, VerifyBlockRoot,
|
||||
VerifyBlockRoot,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use types::{EthSpec, Hash256};
|
||||
@@ -94,7 +94,6 @@ where
|
||||
&mut state,
|
||||
&block,
|
||||
BlockSignatureStrategy::NoVerification,
|
||||
StateProcessingStrategy::Accurate,
|
||||
VerifyBlockRoot::True,
|
||||
&mut ctxt,
|
||||
&self.spec,
|
||||
|
||||
303
beacon_node/store/src/state_cache.rs
Normal file
303
beacon_node/store/src/state_cache.rs
Normal file
@@ -0,0 +1,303 @@
|
||||
use crate::Error;
|
||||
use lru::LruCache;
|
||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
use std::num::NonZeroUsize;
|
||||
use types::{BeaconState, ChainSpec, Epoch, EthSpec, Hash256, Slot};
|
||||
|
||||
/// Fraction of the LRU cache to leave intact during culling.
|
||||
const CULL_EXEMPT_NUMERATOR: usize = 1;
|
||||
const CULL_EXEMPT_DENOMINATOR: usize = 10;
|
||||
|
||||
/// States that are less than or equal to this many epochs old *could* become finalized and will not
|
||||
/// be culled from the cache.
|
||||
const EPOCH_FINALIZATION_LIMIT: u64 = 4;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FinalizedState<E: EthSpec> {
|
||||
state_root: Hash256,
|
||||
state: BeaconState<E>,
|
||||
}
|
||||
|
||||
/// Map from block_root -> slot -> state_root.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct BlockMap {
|
||||
blocks: HashMap<Hash256, SlotMap>,
|
||||
}
|
||||
|
||||
/// Map from slot -> state_root.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct SlotMap {
|
||||
slots: BTreeMap<Slot, Hash256>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct StateCache<E: EthSpec> {
|
||||
finalized_state: Option<FinalizedState<E>>,
|
||||
states: LruCache<Hash256, BeaconState<E>>,
|
||||
block_map: BlockMap,
|
||||
max_epoch: Epoch,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum PutStateOutcome {
|
||||
Finalized,
|
||||
Duplicate,
|
||||
New,
|
||||
}
|
||||
|
||||
#[allow(clippy::len_without_is_empty)]
|
||||
impl<E: EthSpec> StateCache<E> {
|
||||
pub fn new(capacity: NonZeroUsize) -> Self {
|
||||
StateCache {
|
||||
finalized_state: None,
|
||||
states: LruCache::new(capacity),
|
||||
block_map: BlockMap::default(),
|
||||
max_epoch: Epoch::new(0),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.states.len()
|
||||
}
|
||||
|
||||
pub fn capacity(&self) -> usize {
|
||||
self.states.cap().get()
|
||||
}
|
||||
|
||||
pub fn update_finalized_state(
|
||||
&mut self,
|
||||
state_root: Hash256,
|
||||
block_root: Hash256,
|
||||
state: BeaconState<E>,
|
||||
) -> Result<(), Error> {
|
||||
if state.slot() % E::slots_per_epoch() != 0 {
|
||||
return Err(Error::FinalizedStateUnaligned);
|
||||
}
|
||||
|
||||
if self
|
||||
.finalized_state
|
||||
.as_ref()
|
||||
.map_or(false, |finalized_state| {
|
||||
state.slot() < finalized_state.state.slot()
|
||||
})
|
||||
{
|
||||
return Err(Error::FinalizedStateDecreasingSlot);
|
||||
}
|
||||
|
||||
// Add to block map.
|
||||
self.block_map.insert(block_root, state.slot(), state_root);
|
||||
|
||||
// Prune block map.
|
||||
let state_roots_to_prune = self.block_map.prune(state.slot());
|
||||
|
||||
// Delete states.
|
||||
for state_root in state_roots_to_prune {
|
||||
self.states.pop(&state_root);
|
||||
}
|
||||
|
||||
// Update finalized state.
|
||||
self.finalized_state = Some(FinalizedState { state_root, state });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Rebase the given state on the finalized state in order to reduce its memory consumption.
|
||||
///
|
||||
/// This function should only be called on states that are likely not to already share tree
|
||||
/// nodes with the finalized state, e.g. states loaded from disk.
|
||||
///
|
||||
/// If the finalized state is not initialized this function is a no-op.
|
||||
pub fn rebase_on_finalized(
|
||||
&self,
|
||||
state: &mut BeaconState<E>,
|
||||
spec: &ChainSpec,
|
||||
) -> Result<(), Error> {
|
||||
if let Some(finalized_state) = &self.finalized_state {
|
||||
state.rebase_on(&finalized_state.state, spec)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Return a status indicating whether the state already existed in the cache.
|
||||
pub fn put_state(
|
||||
&mut self,
|
||||
state_root: Hash256,
|
||||
block_root: Hash256,
|
||||
state: &BeaconState<E>,
|
||||
) -> Result<PutStateOutcome, Error> {
|
||||
if self
|
||||
.finalized_state
|
||||
.as_ref()
|
||||
.map_or(false, |finalized_state| {
|
||||
finalized_state.state_root == state_root
|
||||
})
|
||||
{
|
||||
return Ok(PutStateOutcome::Finalized);
|
||||
}
|
||||
|
||||
if self.states.peek(&state_root).is_some() {
|
||||
return Ok(PutStateOutcome::Duplicate);
|
||||
}
|
||||
|
||||
// Refuse states with pending mutations: we want cached states to be as small as possible
|
||||
// i.e. stored entirely as a binary merkle tree with no updates overlaid.
|
||||
if state.has_pending_mutations() {
|
||||
return Err(Error::StateForCacheHasPendingUpdates {
|
||||
state_root,
|
||||
slot: state.slot(),
|
||||
});
|
||||
}
|
||||
|
||||
// Update the cache's idea of the max epoch.
|
||||
self.max_epoch = std::cmp::max(state.current_epoch(), self.max_epoch);
|
||||
|
||||
// If the cache is full, use the custom cull routine to make room.
|
||||
if let Some(over_capacity) = self.len().checked_sub(self.capacity()) {
|
||||
self.cull(over_capacity + 1);
|
||||
}
|
||||
|
||||
// Insert the full state into the cache.
|
||||
self.states.put(state_root, state.clone());
|
||||
|
||||
// Record the connection from block root and slot to this state.
|
||||
let slot = state.slot();
|
||||
self.block_map.insert(block_root, slot, state_root);
|
||||
|
||||
Ok(PutStateOutcome::New)
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
self.states.get(&state_root).cloned()
|
||||
}
|
||||
|
||||
pub fn get_by_block_root(
|
||||
&mut self,
|
||||
block_root: Hash256,
|
||||
slot: Slot,
|
||||
) -> Option<(Hash256, BeaconState<E>)> {
|
||||
let slot_map = self.block_map.blocks.get(&block_root)?;
|
||||
|
||||
// Find the state at `slot`, or failing that the most recent ancestor.
|
||||
let state_root = slot_map
|
||||
.slots
|
||||
.iter()
|
||||
.rev()
|
||||
.find_map(|(ancestor_slot, state_root)| {
|
||||
(*ancestor_slot <= slot).then_some(*state_root)
|
||||
})?;
|
||||
|
||||
let state = self.get_by_state_root(state_root)?;
|
||||
Some((state_root, state))
|
||||
}
|
||||
|
||||
pub fn delete_state(&mut self, state_root: &Hash256) {
|
||||
self.states.pop(state_root);
|
||||
self.block_map.delete(state_root);
|
||||
}
|
||||
|
||||
pub fn delete_block_states(&mut self, block_root: &Hash256) {
|
||||
if let Some(slot_map) = self.block_map.delete_block_states(block_root) {
|
||||
for state_root in slot_map.slots.values() {
|
||||
self.states.pop(state_root);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Cull approximately `count` states from the cache.
|
||||
///
|
||||
/// States are culled LRU, with the following extra order imposed:
|
||||
///
|
||||
/// - Advanced states.
|
||||
/// - Mid-epoch unadvanced states.
|
||||
/// - Epoch-boundary states that are too old to be finalized.
|
||||
/// - Epoch-boundary states that could be finalized.
|
||||
pub fn cull(&mut self, count: usize) {
|
||||
let cull_exempt = std::cmp::max(
|
||||
1,
|
||||
self.len() * CULL_EXEMPT_NUMERATOR / CULL_EXEMPT_DENOMINATOR,
|
||||
);
|
||||
|
||||
// Stage 1: gather states to cull.
|
||||
let mut advanced_state_roots = vec![];
|
||||
let mut mid_epoch_state_roots = vec![];
|
||||
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) {
|
||||
let is_advanced = state.slot() > state.latest_block_header().slot;
|
||||
let is_boundary = state.slot() % E::slots_per_epoch() == 0;
|
||||
let could_finalize =
|
||||
(self.max_epoch - state.current_epoch()) <= EPOCH_FINALIZATION_LIMIT;
|
||||
|
||||
if is_boundary {
|
||||
if could_finalize {
|
||||
good_boundary_state_roots.push(state_root);
|
||||
} else {
|
||||
old_boundary_state_roots.push(state_root);
|
||||
}
|
||||
} else if is_advanced {
|
||||
advanced_state_roots.push(state_root);
|
||||
} else {
|
||||
mid_epoch_state_roots.push(state_root);
|
||||
}
|
||||
|
||||
// Terminate early in the common case where we've already found enough junk to cull.
|
||||
if advanced_state_roots.len() == count {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Stage 2: delete.
|
||||
// This could probably be more efficient in how it interacts with the block map.
|
||||
for state_root in advanced_state_roots
|
||||
.iter()
|
||||
.chain(mid_epoch_state_roots.iter())
|
||||
.chain(old_boundary_state_roots.iter())
|
||||
.chain(good_boundary_state_roots.iter())
|
||||
.take(count)
|
||||
{
|
||||
self.delete_state(state_root);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BlockMap {
|
||||
fn insert(&mut self, block_root: Hash256, slot: Slot, state_root: Hash256) {
|
||||
let slot_map = self.blocks.entry(block_root).or_default();
|
||||
slot_map.slots.insert(slot, state_root);
|
||||
}
|
||||
|
||||
fn prune(&mut self, finalized_slot: Slot) -> HashSet<Hash256> {
|
||||
let mut pruned_states = HashSet::new();
|
||||
|
||||
self.blocks.retain(|_, slot_map| {
|
||||
slot_map.slots.retain(|slot, state_root| {
|
||||
let keep = *slot >= finalized_slot;
|
||||
if !keep {
|
||||
pruned_states.insert(*state_root);
|
||||
}
|
||||
keep
|
||||
});
|
||||
|
||||
!slot_map.slots.is_empty()
|
||||
});
|
||||
|
||||
pruned_states
|
||||
}
|
||||
|
||||
fn delete(&mut self, state_root_to_delete: &Hash256) {
|
||||
self.blocks.retain(|_, slot_map| {
|
||||
slot_map
|
||||
.slots
|
||||
.retain(|_, state_root| state_root != state_root_to_delete);
|
||||
!slot_map.slots.is_empty()
|
||||
});
|
||||
}
|
||||
|
||||
fn delete_block_states(&mut self, block_root: &Hash256) -> Option<SlotMap> {
|
||||
self.blocks.remove(block_root)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user