mirror of
https://github.com/sigp/lighthouse.git
synced 2026-05-30 04:37:13 +00:00
Shrink persisted fork choice data (#7805)
Closes: - https://github.com/sigp/lighthouse/issues/7760 - [x] Remove `balances_cache` from `PersistedForkChoiceStore` (~65 MB saving on mainnet) - [x] Remove `justified_balances` from `PersistedForkChoiceStore` (~16 MB saving on mainnet) - [x] Remove `balances` from `ProtoArray`/`SszContainer`. - [x] Implement zstd compression for votes - [x] Fix bug in justified state usage - [x] Bump schema version to V28 and implement migration.
This commit is contained in:
@@ -121,8 +121,8 @@ use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use store::iter::{BlockRootsIterator, ParentRootBlockIterator, StateRootsIterator};
|
||||
use store::{
|
||||
BlobSidecarListFromRoot, DatabaseBlock, Error as DBError, HotColdDB, HotStateSummary,
|
||||
KeyValueStoreOp, StoreItem, StoreOp,
|
||||
BlobSidecarListFromRoot, DBColumn, DatabaseBlock, Error as DBError, HotColdDB, HotStateSummary,
|
||||
KeyValueStore, KeyValueStoreOp, StoreItem, StoreOp,
|
||||
};
|
||||
use task_executor::{ShutdownReason, TaskExecutor};
|
||||
use tokio_stream::Stream;
|
||||
@@ -618,12 +618,15 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
reset_payload_statuses: ResetPayloadStatuses,
|
||||
spec: &ChainSpec,
|
||||
) -> Result<Option<BeaconForkChoice<T>>, Error> {
|
||||
let Some(persisted_fork_choice) =
|
||||
store.get_item::<PersistedForkChoice>(&FORK_CHOICE_DB_KEY)?
|
||||
let Some(persisted_fork_choice_bytes) = store
|
||||
.hot_db
|
||||
.get_bytes(DBColumn::ForkChoice, FORK_CHOICE_DB_KEY.as_slice())?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let persisted_fork_choice =
|
||||
PersistedForkChoice::from_bytes(&persisted_fork_choice_bytes, store.get_config())?;
|
||||
let fc_store =
|
||||
BeaconForkChoiceStore::from_persisted(persisted_fork_choice.fork_choice_store, store)?;
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ pub enum Error {
|
||||
FailedToReadState(StoreError),
|
||||
MissingState(Hash256),
|
||||
BeaconStateError(BeaconStateError),
|
||||
UnalignedCheckpoint { block_slot: Slot, state_slot: Slot },
|
||||
Arith(ArithError),
|
||||
}
|
||||
|
||||
@@ -136,7 +137,9 @@ pub struct BeaconForkChoiceStore<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<
|
||||
finalized_checkpoint: Checkpoint,
|
||||
justified_checkpoint: Checkpoint,
|
||||
justified_balances: JustifiedBalances,
|
||||
justified_state_root: Hash256,
|
||||
unrealized_justified_checkpoint: Checkpoint,
|
||||
unrealized_justified_state_root: Hash256,
|
||||
unrealized_finalized_checkpoint: Checkpoint,
|
||||
proposer_boost_root: Hash256,
|
||||
equivocating_indices: BTreeSet<u64>,
|
||||
@@ -162,21 +165,37 @@ where
|
||||
/// It is assumed that `anchor` is already persisted in `store`.
|
||||
pub fn get_forkchoice_store(
|
||||
store: Arc<HotColdDB<E, Hot, Cold>>,
|
||||
anchor: &BeaconSnapshot<E>,
|
||||
anchor: BeaconSnapshot<E>,
|
||||
) -> Result<Self, Error> {
|
||||
let anchor_state = &anchor.beacon_state;
|
||||
let unadvanced_state_root = anchor.beacon_state_root();
|
||||
let mut anchor_state = anchor.beacon_state;
|
||||
let mut anchor_block_header = anchor_state.latest_block_header().clone();
|
||||
if anchor_block_header.state_root == Hash256::zero() {
|
||||
anchor_block_header.state_root = anchor.beacon_state_root();
|
||||
|
||||
// The anchor state MUST be on an epoch boundary (it should be advanced by the caller).
|
||||
if !anchor_state
|
||||
.slot()
|
||||
.as_u64()
|
||||
.is_multiple_of(E::slots_per_epoch())
|
||||
{
|
||||
return Err(Error::UnalignedCheckpoint {
|
||||
block_slot: anchor_block_header.slot,
|
||||
state_slot: anchor_state.slot(),
|
||||
});
|
||||
}
|
||||
let anchor_root = anchor_block_header.canonical_root();
|
||||
|
||||
// Compute the accurate block root for the checkpoint block.
|
||||
if anchor_block_header.state_root.is_zero() {
|
||||
anchor_block_header.state_root = unadvanced_state_root;
|
||||
}
|
||||
let anchor_block_root = anchor_block_header.canonical_root();
|
||||
let anchor_epoch = anchor_state.current_epoch();
|
||||
let justified_checkpoint = Checkpoint {
|
||||
epoch: anchor_epoch,
|
||||
root: anchor_root,
|
||||
root: anchor_block_root,
|
||||
};
|
||||
let finalized_checkpoint = justified_checkpoint;
|
||||
let justified_balances = JustifiedBalances::from_justified_state(anchor_state)?;
|
||||
let justified_balances = JustifiedBalances::from_justified_state(&anchor_state)?;
|
||||
let justified_state_root = anchor_state.canonical_root()?;
|
||||
|
||||
Ok(Self {
|
||||
store,
|
||||
@@ -184,8 +203,10 @@ where
|
||||
time: anchor_state.slot(),
|
||||
justified_checkpoint,
|
||||
justified_balances,
|
||||
justified_state_root,
|
||||
finalized_checkpoint,
|
||||
unrealized_justified_checkpoint: justified_checkpoint,
|
||||
unrealized_justified_state_root: justified_state_root,
|
||||
unrealized_finalized_checkpoint: finalized_checkpoint,
|
||||
proposer_boost_root: Hash256::zero(),
|
||||
equivocating_indices: BTreeSet::new(),
|
||||
@@ -197,12 +218,12 @@ where
|
||||
/// on-disk database.
|
||||
pub fn to_persisted(&self) -> PersistedForkChoiceStore {
|
||||
PersistedForkChoiceStore {
|
||||
balances_cache: self.balances_cache.clone(),
|
||||
time: self.time,
|
||||
finalized_checkpoint: self.finalized_checkpoint,
|
||||
justified_checkpoint: self.justified_checkpoint,
|
||||
justified_balances: self.justified_balances.effective_balances.clone(),
|
||||
justified_state_root: self.justified_state_root,
|
||||
unrealized_justified_checkpoint: self.unrealized_justified_checkpoint,
|
||||
unrealized_justified_state_root: self.unrealized_justified_state_root,
|
||||
unrealized_finalized_checkpoint: self.unrealized_finalized_checkpoint,
|
||||
proposer_boost_root: self.proposer_boost_root,
|
||||
equivocating_indices: self.equivocating_indices.clone(),
|
||||
@@ -210,20 +231,59 @@ where
|
||||
}
|
||||
|
||||
/// Restore `Self` from a previously-generated `PersistedForkChoiceStore`.
|
||||
pub fn from_persisted(
|
||||
persisted: PersistedForkChoiceStore,
|
||||
///
|
||||
/// DEPRECATED. Can be deleted once migrations no longer require it.
|
||||
pub fn from_persisted_v17(
|
||||
persisted: PersistedForkChoiceStoreV17,
|
||||
justified_state_root: Hash256,
|
||||
unrealized_justified_state_root: Hash256,
|
||||
store: Arc<HotColdDB<E, Hot, Cold>>,
|
||||
) -> Result<Self, Error> {
|
||||
let justified_balances =
|
||||
JustifiedBalances::from_effective_balances(persisted.justified_balances)?;
|
||||
|
||||
Ok(Self {
|
||||
store,
|
||||
balances_cache: persisted.balances_cache,
|
||||
balances_cache: <_>::default(),
|
||||
time: persisted.time,
|
||||
finalized_checkpoint: persisted.finalized_checkpoint,
|
||||
justified_checkpoint: persisted.justified_checkpoint,
|
||||
justified_balances,
|
||||
justified_state_root,
|
||||
unrealized_justified_checkpoint: persisted.unrealized_justified_checkpoint,
|
||||
unrealized_justified_state_root,
|
||||
unrealized_finalized_checkpoint: persisted.unrealized_finalized_checkpoint,
|
||||
proposer_boost_root: persisted.proposer_boost_root,
|
||||
equivocating_indices: persisted.equivocating_indices,
|
||||
_phantom: PhantomData,
|
||||
})
|
||||
}
|
||||
|
||||
/// Restore `Self` from a previously-generated `PersistedForkChoiceStore`.
|
||||
pub fn from_persisted(
|
||||
persisted: PersistedForkChoiceStore,
|
||||
store: Arc<HotColdDB<E, Hot, Cold>>,
|
||||
) -> Result<Self, Error> {
|
||||
let justified_checkpoint = persisted.justified_checkpoint;
|
||||
let justified_state_root = persisted.justified_state_root;
|
||||
|
||||
let update_cache = true;
|
||||
let justified_state = store
|
||||
.get_hot_state(&justified_state_root, update_cache)
|
||||
.map_err(Error::FailedToReadState)?
|
||||
.ok_or(Error::MissingState(justified_state_root))?;
|
||||
|
||||
let justified_balances = JustifiedBalances::from_justified_state(&justified_state)?;
|
||||
Ok(Self {
|
||||
store,
|
||||
balances_cache: <_>::default(),
|
||||
time: persisted.time,
|
||||
finalized_checkpoint: persisted.finalized_checkpoint,
|
||||
justified_checkpoint,
|
||||
justified_balances,
|
||||
justified_state_root,
|
||||
unrealized_justified_checkpoint: persisted.unrealized_justified_checkpoint,
|
||||
unrealized_justified_state_root: persisted.unrealized_justified_state_root,
|
||||
unrealized_finalized_checkpoint: persisted.unrealized_finalized_checkpoint,
|
||||
proposer_boost_root: persisted.proposer_boost_root,
|
||||
equivocating_indices: persisted.equivocating_indices,
|
||||
@@ -261,6 +321,10 @@ where
|
||||
&self.justified_checkpoint
|
||||
}
|
||||
|
||||
fn justified_state_root(&self) -> Hash256 {
|
||||
self.justified_state_root
|
||||
}
|
||||
|
||||
fn justified_balances(&self) -> &JustifiedBalances {
|
||||
&self.justified_balances
|
||||
}
|
||||
@@ -273,6 +337,10 @@ where
|
||||
&self.unrealized_justified_checkpoint
|
||||
}
|
||||
|
||||
fn unrealized_justified_state_root(&self) -> Hash256 {
|
||||
self.unrealized_justified_state_root
|
||||
}
|
||||
|
||||
fn unrealized_finalized_checkpoint(&self) -> &Checkpoint {
|
||||
&self.unrealized_finalized_checkpoint
|
||||
}
|
||||
@@ -285,8 +353,13 @@ where
|
||||
self.finalized_checkpoint = checkpoint
|
||||
}
|
||||
|
||||
fn set_justified_checkpoint(&mut self, checkpoint: Checkpoint) -> Result<(), Error> {
|
||||
fn set_justified_checkpoint(
|
||||
&mut self,
|
||||
checkpoint: Checkpoint,
|
||||
justified_state_root: Hash256,
|
||||
) -> Result<(), Error> {
|
||||
self.justified_checkpoint = checkpoint;
|
||||
self.justified_state_root = justified_state_root;
|
||||
|
||||
if let Some(balances) = self.balances_cache.get(
|
||||
self.justified_checkpoint.root,
|
||||
@@ -297,27 +370,14 @@ where
|
||||
self.justified_balances = JustifiedBalances::from_effective_balances(balances)?;
|
||||
} else {
|
||||
metrics::inc_counter(&metrics::BALANCES_CACHE_MISSES);
|
||||
let justified_block = self
|
||||
.store
|
||||
.get_blinded_block(&self.justified_checkpoint.root)
|
||||
.map_err(Error::FailedToReadBlock)?
|
||||
.ok_or(Error::MissingBlock(self.justified_checkpoint.root))?
|
||||
.deconstruct()
|
||||
.0;
|
||||
|
||||
let max_slot = self
|
||||
.justified_checkpoint
|
||||
.epoch
|
||||
.start_slot(E::slots_per_epoch());
|
||||
let (_, state) = self
|
||||
// Justified state is reasonably useful to cache, it might be finalized soon.
|
||||
let update_cache = true;
|
||||
let state = self
|
||||
.store
|
||||
.get_advanced_hot_state(
|
||||
self.justified_checkpoint.root,
|
||||
max_slot,
|
||||
justified_block.state_root(),
|
||||
)
|
||||
.get_hot_state(&self.justified_state_root, update_cache)
|
||||
.map_err(Error::FailedToReadState)?
|
||||
.ok_or_else(|| Error::MissingState(justified_block.state_root()))?;
|
||||
.ok_or_else(|| Error::MissingState(self.justified_state_root))?;
|
||||
|
||||
self.justified_balances = JustifiedBalances::from_justified_state(&state)?;
|
||||
}
|
||||
@@ -325,8 +385,9 @@ where
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_unrealized_justified_checkpoint(&mut self, checkpoint: Checkpoint) {
|
||||
fn set_unrealized_justified_checkpoint(&mut self, checkpoint: Checkpoint, state_root: Hash256) {
|
||||
self.unrealized_justified_checkpoint = checkpoint;
|
||||
self.unrealized_justified_state_root = state_root;
|
||||
}
|
||||
|
||||
fn set_unrealized_finalized_checkpoint(&mut self, checkpoint: Checkpoint) {
|
||||
@@ -346,18 +407,48 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub type PersistedForkChoiceStore = PersistedForkChoiceStoreV17;
|
||||
pub type PersistedForkChoiceStore = PersistedForkChoiceStoreV28;
|
||||
|
||||
/// A container which allows persisting the `BeaconForkChoiceStore` to the on-disk database.
|
||||
#[superstruct(variants(V17), variant_attributes(derive(Encode, Decode)), no_enum)]
|
||||
#[superstruct(
|
||||
variants(V17, V28),
|
||||
variant_attributes(derive(Encode, Decode)),
|
||||
no_enum
|
||||
)]
|
||||
pub struct PersistedForkChoiceStore {
|
||||
/// The balances cache was removed from disk storage in schema V28.
|
||||
#[superstruct(only(V17))]
|
||||
pub balances_cache: BalancesCacheV8,
|
||||
pub time: Slot,
|
||||
pub finalized_checkpoint: Checkpoint,
|
||||
pub justified_checkpoint: Checkpoint,
|
||||
/// The justified balances were removed from disk storage in schema V28.
|
||||
#[superstruct(only(V17))]
|
||||
pub justified_balances: Vec<u64>,
|
||||
/// The justified state root is stored so that it can be used to load the justified balances.
|
||||
#[superstruct(only(V28))]
|
||||
pub justified_state_root: Hash256,
|
||||
pub unrealized_justified_checkpoint: Checkpoint,
|
||||
#[superstruct(only(V28))]
|
||||
pub unrealized_justified_state_root: Hash256,
|
||||
pub unrealized_finalized_checkpoint: Checkpoint,
|
||||
pub proposer_boost_root: Hash256,
|
||||
pub equivocating_indices: BTreeSet<u64>,
|
||||
}
|
||||
|
||||
// Convert V28 to V17 by adding balances and removing justified state roots.
|
||||
impl From<(PersistedForkChoiceStoreV28, JustifiedBalances)> for PersistedForkChoiceStoreV17 {
|
||||
fn from((v28, balances): (PersistedForkChoiceStoreV28, JustifiedBalances)) -> Self {
|
||||
Self {
|
||||
balances_cache: Default::default(),
|
||||
time: v28.time,
|
||||
finalized_checkpoint: v28.finalized_checkpoint,
|
||||
justified_checkpoint: v28.justified_checkpoint,
|
||||
justified_balances: balances.effective_balances,
|
||||
unrealized_justified_checkpoint: v28.unrealized_justified_checkpoint,
|
||||
unrealized_finalized_checkpoint: v28.unrealized_finalized_checkpoint,
|
||||
proposer_boost_root: v28.proposer_boost_root,
|
||||
equivocating_indices: v28.equivocating_indices,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -394,7 +394,7 @@ where
|
||||
.map_err(|e| format!("Failed to initialize genesis data column info: {:?}", e))?,
|
||||
);
|
||||
|
||||
let fc_store = BeaconForkChoiceStore::get_forkchoice_store(store, &genesis)
|
||||
let fc_store = BeaconForkChoiceStore::get_forkchoice_store(store, genesis.clone())
|
||||
.map_err(|e| format!("Unable to initialize fork choice store: {e:?}"))?;
|
||||
let current_slot = None;
|
||||
|
||||
@@ -616,7 +616,7 @@ where
|
||||
beacon_state: weak_subj_state,
|
||||
};
|
||||
|
||||
let fc_store = BeaconForkChoiceStore::get_forkchoice_store(store, &snapshot)
|
||||
let fc_store = BeaconForkChoiceStore::get_forkchoice_store(store, snapshot.clone())
|
||||
.map_err(|e| format!("Unable to initialize fork choice store: {e:?}"))?;
|
||||
|
||||
let fork_choice = ForkChoice::from_anchor(
|
||||
@@ -887,8 +887,9 @@ where
|
||||
self.pending_io_batch.push(BeaconChain::<
|
||||
Witness<TSlotClock, E, THotStore, TColdStore>,
|
||||
>::persist_fork_choice_in_batch_standalone(
|
||||
&fork_choice
|
||||
));
|
||||
&fork_choice,
|
||||
store.get_config(),
|
||||
).map_err(|e| format!("Fork choice compression error: {e:?}"))?);
|
||||
store
|
||||
.hot_db
|
||||
.do_atomically(self.pending_io_batch)
|
||||
|
||||
@@ -53,7 +53,9 @@ use slot_clock::SlotClock;
|
||||
use state_processing::AllCaches;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use store::{KeyValueStore, KeyValueStoreOp, StoreItem, iter::StateRootsIterator};
|
||||
use store::{
|
||||
Error as StoreError, KeyValueStore, KeyValueStoreOp, StoreConfig, iter::StateRootsIterator,
|
||||
};
|
||||
use task_executor::{JoinHandle, ShutdownReason};
|
||||
use tracing::{debug, error, info, instrument, warn};
|
||||
use types::*;
|
||||
@@ -998,25 +1000,30 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
/// Persist fork choice to disk, writing immediately.
|
||||
pub fn persist_fork_choice(&self) -> Result<(), Error> {
|
||||
let _fork_choice_timer = metrics::start_timer(&metrics::PERSIST_FORK_CHOICE);
|
||||
let batch = vec![self.persist_fork_choice_in_batch()];
|
||||
let batch = vec![self.persist_fork_choice_in_batch()?];
|
||||
self.store.hot_db.do_atomically(batch)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Return a database operation for writing fork choice to disk.
|
||||
pub fn persist_fork_choice_in_batch(&self) -> KeyValueStoreOp {
|
||||
Self::persist_fork_choice_in_batch_standalone(&self.canonical_head.fork_choice_read_lock())
|
||||
pub fn persist_fork_choice_in_batch(&self) -> Result<KeyValueStoreOp, Error> {
|
||||
Self::persist_fork_choice_in_batch_standalone(
|
||||
&self.canonical_head.fork_choice_read_lock(),
|
||||
self.store.get_config(),
|
||||
)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Return a database operation for writing fork choice to disk.
|
||||
pub fn persist_fork_choice_in_batch_standalone(
|
||||
fork_choice: &BeaconForkChoice<T>,
|
||||
) -> KeyValueStoreOp {
|
||||
store_config: &StoreConfig,
|
||||
) -> Result<KeyValueStoreOp, StoreError> {
|
||||
let persisted_fork_choice = PersistedForkChoice {
|
||||
fork_choice: fork_choice.to_persisted(),
|
||||
fork_choice_store: fork_choice.fc_store().to_persisted(),
|
||||
};
|
||||
persisted_fork_choice.as_kv_store_op(FORK_CHOICE_DB_KEY)
|
||||
persisted_fork_choice.as_kv_store_op(FORK_CHOICE_DB_KEY, store_config)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -142,8 +142,9 @@ pub fn reset_fork_choice_to_finalization<E: EthSpec, Hot: ItemStore<E>, Cold: It
|
||||
beacon_state: finalized_state,
|
||||
};
|
||||
|
||||
let fc_store = BeaconForkChoiceStore::get_forkchoice_store(store.clone(), &finalized_snapshot)
|
||||
.map_err(|e| format!("Unable to reset fork choice store for revert: {e:?}"))?;
|
||||
let fc_store =
|
||||
BeaconForkChoiceStore::get_forkchoice_store(store.clone(), finalized_snapshot.clone())
|
||||
.map_err(|e| format!("Unable to reset fork choice store for revert: {e:?}"))?;
|
||||
|
||||
let mut fork_choice = ForkChoice::from_anchor(
|
||||
fc_store,
|
||||
|
||||
@@ -74,7 +74,10 @@ pub use self::chain_config::ChainConfig;
|
||||
pub use self::errors::{BeaconChainError, BlockProductionError};
|
||||
pub use self::historical_blocks::HistoricalBlockError;
|
||||
pub use attestation_verification::Error as AttestationError;
|
||||
pub use beacon_fork_choice_store::{BeaconForkChoiceStore, Error as ForkChoiceStoreError};
|
||||
pub use beacon_fork_choice_store::{
|
||||
BeaconForkChoiceStore, Error as ForkChoiceStoreError, PersistedForkChoiceStoreV17,
|
||||
PersistedForkChoiceStoreV28,
|
||||
};
|
||||
pub use block_verification::{
|
||||
BlockError, ExecutionPayloadError, ExecutionPendingBlock, GossipVerifiedBlock,
|
||||
IntoExecutionPendingBlock, IntoGossipVerifiedBlock, InvalidSignature,
|
||||
|
||||
@@ -585,6 +585,18 @@ pub static FORK_CHOICE_WRITE_LOCK_AQUIRE_TIMES: LazyLock<Result<Histogram>> = La
|
||||
exponential_buckets(1e-3, 4.0, 7),
|
||||
)
|
||||
});
|
||||
pub static FORK_CHOICE_ENCODE_TIMES: LazyLock<Result<Histogram>> = LazyLock::new(|| {
|
||||
try_create_histogram(
|
||||
"beacon_fork_choice_encode_seconds",
|
||||
"Time taken to SSZ encode the persisted fork choice data",
|
||||
)
|
||||
});
|
||||
pub static FORK_CHOICE_COMPRESS_TIMES: LazyLock<Result<Histogram>> = LazyLock::new(|| {
|
||||
try_create_histogram(
|
||||
"beacon_fork_choice_compress_seconds",
|
||||
"Time taken to compress the persisted fork choice data",
|
||||
)
|
||||
});
|
||||
pub static BALANCES_CACHE_HITS: LazyLock<Result<IntCounter>> = LazyLock::new(|| {
|
||||
try_create_int_counter(
|
||||
"beacon_balances_cache_hits_total",
|
||||
|
||||
@@ -1,16 +1,30 @@
|
||||
use crate::beacon_fork_choice_store::PersistedForkChoiceStoreV17;
|
||||
use crate::{
|
||||
beacon_fork_choice_store::{PersistedForkChoiceStoreV17, PersistedForkChoiceStoreV28},
|
||||
metrics,
|
||||
};
|
||||
use ssz::{Decode, Encode};
|
||||
use ssz_derive::{Decode, Encode};
|
||||
use store::{DBColumn, Error, StoreItem};
|
||||
use store::{DBColumn, Error, KeyValueStoreOp, StoreConfig, StoreItem};
|
||||
use superstruct::superstruct;
|
||||
use types::Hash256;
|
||||
|
||||
// If adding a new version you should update this type alias and fix the breakages.
|
||||
pub type PersistedForkChoice = PersistedForkChoiceV17;
|
||||
pub type PersistedForkChoice = PersistedForkChoiceV28;
|
||||
|
||||
#[superstruct(variants(V17), variant_attributes(derive(Encode, Decode)), no_enum)]
|
||||
#[superstruct(
|
||||
variants(V17, V28),
|
||||
variant_attributes(derive(Encode, Decode)),
|
||||
no_enum
|
||||
)]
|
||||
pub struct PersistedForkChoice {
|
||||
pub fork_choice: fork_choice::PersistedForkChoice,
|
||||
pub fork_choice_store: PersistedForkChoiceStoreV17,
|
||||
#[superstruct(only(V17))]
|
||||
pub fork_choice_v17: fork_choice::PersistedForkChoiceV17,
|
||||
#[superstruct(only(V28))]
|
||||
pub fork_choice: fork_choice::PersistedForkChoiceV28,
|
||||
#[superstruct(only(V17))]
|
||||
pub fork_choice_store_v17: PersistedForkChoiceStoreV17,
|
||||
#[superstruct(only(V28))]
|
||||
pub fork_choice_store: PersistedForkChoiceStoreV28,
|
||||
}
|
||||
|
||||
macro_rules! impl_store_item {
|
||||
@@ -32,3 +46,35 @@ macro_rules! impl_store_item {
|
||||
}
|
||||
|
||||
impl_store_item!(PersistedForkChoiceV17);
|
||||
|
||||
impl PersistedForkChoiceV28 {
|
||||
pub fn from_bytes(bytes: &[u8], store_config: &StoreConfig) -> Result<Self, Error> {
|
||||
let decompressed_bytes = store_config
|
||||
.decompress_bytes(bytes)
|
||||
.map_err(Error::Compression)?;
|
||||
Self::from_ssz_bytes(&decompressed_bytes).map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn as_bytes(&self, store_config: &StoreConfig) -> Result<Vec<u8>, Error> {
|
||||
let encode_timer = metrics::start_timer(&metrics::FORK_CHOICE_ENCODE_TIMES);
|
||||
let ssz_bytes = self.as_ssz_bytes();
|
||||
drop(encode_timer);
|
||||
|
||||
let _compress_timer = metrics::start_timer(&metrics::FORK_CHOICE_COMPRESS_TIMES);
|
||||
store_config
|
||||
.compress_bytes(&ssz_bytes)
|
||||
.map_err(Error::Compression)
|
||||
}
|
||||
|
||||
pub fn as_kv_store_op(
|
||||
&self,
|
||||
key: Hash256,
|
||||
store_config: &StoreConfig,
|
||||
) -> Result<KeyValueStoreOp, Error> {
|
||||
Ok(KeyValueStoreOp::PutKeyValue(
|
||||
DBColumn::ForkChoice,
|
||||
key.as_slice().to_vec(),
|
||||
self.as_bytes(store_config)?,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ mod migration_schema_v24;
|
||||
mod migration_schema_v25;
|
||||
mod migration_schema_v26;
|
||||
mod migration_schema_v27;
|
||||
mod migration_schema_v28;
|
||||
|
||||
use crate::beacon_chain::BeaconChainTypes;
|
||||
use std::sync::Arc;
|
||||
@@ -79,6 +80,14 @@ pub fn migrate_schema<T: BeaconChainTypes>(
|
||||
migration_schema_v27::downgrade_from_v27::<T>(db.clone())?;
|
||||
db.store_schema_version_atomically(to, vec![])
|
||||
}
|
||||
(SchemaVersion(27), SchemaVersion(28)) => {
|
||||
let ops = migration_schema_v28::upgrade_to_v28::<T>(db.clone())?;
|
||||
db.store_schema_version_atomically(to, ops)
|
||||
}
|
||||
(SchemaVersion(28), SchemaVersion(27)) => {
|
||||
let ops = migration_schema_v28::downgrade_from_v28::<T>(db.clone())?;
|
||||
db.store_schema_version_atomically(to, ops)
|
||||
}
|
||||
// Anything else is an error.
|
||||
(_, _) => Err(HotColdDBError::UnsupportedSchemaVersion {
|
||||
target_version: to,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::BeaconForkChoiceStore;
|
||||
use crate::beacon_chain::BeaconChainTypes;
|
||||
use crate::persisted_fork_choice::PersistedForkChoice;
|
||||
use crate::persisted_fork_choice::PersistedForkChoiceV17;
|
||||
use crate::schema_change::StoreError;
|
||||
use crate::test_utils::{BEACON_CHAIN_DB_KEY, FORK_CHOICE_DB_KEY, PersistedBeaconChain};
|
||||
use fork_choice::{ForkChoice, ResetPayloadStatuses};
|
||||
@@ -80,7 +80,7 @@ pub fn downgrade_from_v23<T: BeaconChainTypes>(
|
||||
};
|
||||
|
||||
// Recreate head-tracker from fork choice.
|
||||
let Some(persisted_fork_choice) = db.get_item::<PersistedForkChoice>(&FORK_CHOICE_DB_KEY)?
|
||||
let Some(persisted_fork_choice) = db.get_item::<PersistedForkChoiceV17>(&FORK_CHOICE_DB_KEY)?
|
||||
else {
|
||||
// Fork choice should exist if the database exists.
|
||||
return Err(Error::MigrationError(
|
||||
@@ -88,19 +88,30 @@ pub fn downgrade_from_v23<T: BeaconChainTypes>(
|
||||
));
|
||||
};
|
||||
|
||||
let fc_store =
|
||||
BeaconForkChoiceStore::from_persisted(persisted_fork_choice.fork_choice_store, db.clone())
|
||||
.map_err(|e| {
|
||||
Error::MigrationError(format!(
|
||||
"Error loading fork choise store from persisted: {e:?}"
|
||||
))
|
||||
})?;
|
||||
// We use dummy roots for the justified states because we can source the balances from the v17
|
||||
// persited fork choice. The justified state root isn't required to look up the justified state's
|
||||
// balances (as it would be in V28). This fork choice object with corrupt state roots SHOULD NOT
|
||||
// be written to disk.
|
||||
let dummy_justified_state_root = Hash256::repeat_byte(0x66);
|
||||
let dummy_unrealized_justified_state_root = Hash256::repeat_byte(0x77);
|
||||
|
||||
let fc_store = BeaconForkChoiceStore::from_persisted_v17(
|
||||
persisted_fork_choice.fork_choice_store_v17,
|
||||
dummy_justified_state_root,
|
||||
dummy_unrealized_justified_state_root,
|
||||
db.clone(),
|
||||
)
|
||||
.map_err(|e| {
|
||||
Error::MigrationError(format!(
|
||||
"Error loading fork choice store from persisted: {e:?}"
|
||||
))
|
||||
})?;
|
||||
|
||||
// Doesn't matter what policy we use for invalid payloads, as our head calculation just
|
||||
// considers descent from finalization.
|
||||
let reset_payload_statuses = ResetPayloadStatuses::OnlyWithInvalidPayload;
|
||||
let fork_choice = ForkChoice::from_persisted(
|
||||
persisted_fork_choice.fork_choice,
|
||||
persisted_fork_choice.fork_choice_v17.try_into()?,
|
||||
reset_payload_statuses,
|
||||
fc_store,
|
||||
&db.spec,
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
use crate::{
|
||||
BeaconChain, BeaconChainTypes, BeaconForkChoiceStore, PersistedForkChoiceStoreV17,
|
||||
beacon_chain::FORK_CHOICE_DB_KEY,
|
||||
persisted_fork_choice::{PersistedForkChoiceV17, PersistedForkChoiceV28},
|
||||
summaries_dag::{DAGStateSummary, StateSummariesDAG},
|
||||
};
|
||||
use fork_choice::{ForkChoice, ForkChoiceStore, ResetPayloadStatuses};
|
||||
use std::sync::Arc;
|
||||
use store::{Error, HotColdDB, KeyValueStoreOp, StoreItem};
|
||||
use tracing::{info, warn};
|
||||
use types::{EthSpec, Hash256};
|
||||
|
||||
/// Upgrade `PersistedForkChoice` from V17 to V28.
|
||||
pub fn upgrade_to_v28<T: BeaconChainTypes>(
|
||||
db: Arc<HotColdDB<T::EthSpec, T::HotStore, T::ColdStore>>,
|
||||
) -> Result<Vec<KeyValueStoreOp>, Error> {
|
||||
let Some(persisted_fork_choice_v17) =
|
||||
db.get_item::<PersistedForkChoiceV17>(&FORK_CHOICE_DB_KEY)?
|
||||
else {
|
||||
warn!("No fork choice found to upgrade to v28");
|
||||
return Ok(vec![]);
|
||||
};
|
||||
|
||||
// Load state DAG in order to compute justified checkpoint roots.
|
||||
let state_summaries_dag = {
|
||||
let state_summaries = db
|
||||
.load_hot_state_summaries()?
|
||||
.into_iter()
|
||||
.map(|(state_root, summary)| (state_root, summary.into()))
|
||||
.collect::<Vec<(Hash256, DAGStateSummary)>>();
|
||||
|
||||
StateSummariesDAG::new(state_summaries).map_err(|e| {
|
||||
Error::MigrationError(format!("Error loading state summaries DAG: {e:?}"))
|
||||
})?
|
||||
};
|
||||
|
||||
// Determine the justified state roots.
|
||||
let justified_checkpoint = persisted_fork_choice_v17
|
||||
.fork_choice_store_v17
|
||||
.justified_checkpoint;
|
||||
let justified_block_root = justified_checkpoint.root;
|
||||
let justified_slot = justified_checkpoint
|
||||
.epoch
|
||||
.start_slot(T::EthSpec::slots_per_epoch());
|
||||
let justified_state_root = state_summaries_dag
|
||||
.state_root_at_slot(justified_block_root, justified_slot)
|
||||
.ok_or_else(|| {
|
||||
Error::MigrationError(format!(
|
||||
"Missing state root for justified slot {justified_slot} with latest_block_root \
|
||||
{justified_block_root:?}"
|
||||
))
|
||||
})?;
|
||||
|
||||
let unrealized_justified_checkpoint = persisted_fork_choice_v17
|
||||
.fork_choice_store_v17
|
||||
.unrealized_justified_checkpoint;
|
||||
let unrealized_justified_block_root = unrealized_justified_checkpoint.root;
|
||||
let unrealized_justified_slot = unrealized_justified_checkpoint
|
||||
.epoch
|
||||
.start_slot(T::EthSpec::slots_per_epoch());
|
||||
let unrealized_justified_state_root = state_summaries_dag
|
||||
.state_root_at_slot(unrealized_justified_block_root, unrealized_justified_slot)
|
||||
.ok_or_else(|| {
|
||||
Error::MigrationError(format!(
|
||||
"Missing state root for unrealized justified slot {unrealized_justified_slot} \
|
||||
with latest_block_root {unrealized_justified_block_root:?}"
|
||||
))
|
||||
})?;
|
||||
|
||||
let fc_store = BeaconForkChoiceStore::from_persisted_v17(
|
||||
persisted_fork_choice_v17.fork_choice_store_v17,
|
||||
justified_state_root,
|
||||
unrealized_justified_state_root,
|
||||
db.clone(),
|
||||
)
|
||||
.map_err(|e| {
|
||||
Error::MigrationError(format!(
|
||||
"Error loading fork choice store from persisted: {e:?}"
|
||||
))
|
||||
})?;
|
||||
|
||||
info!(
|
||||
?justified_state_root,
|
||||
%justified_slot,
|
||||
"Added justified state root to fork choice"
|
||||
);
|
||||
|
||||
// Construct top-level ForkChoice struct using the patched fork choice store, and the converted
|
||||
// proto array.
|
||||
let reset_payload_statuses = ResetPayloadStatuses::OnlyWithInvalidPayload;
|
||||
let fork_choice = ForkChoice::from_persisted(
|
||||
persisted_fork_choice_v17.fork_choice_v17.try_into()?,
|
||||
reset_payload_statuses,
|
||||
fc_store,
|
||||
db.get_chain_spec(),
|
||||
)
|
||||
.map_err(|e| Error::MigrationError(format!("Unable to build ForkChoice: {e:?}")))?;
|
||||
|
||||
let ops = vec![BeaconChain::<T>::persist_fork_choice_in_batch_standalone(
|
||||
&fork_choice,
|
||||
db.get_config(),
|
||||
)?];
|
||||
|
||||
info!("Upgraded fork choice for DB schema v28");
|
||||
|
||||
Ok(ops)
|
||||
}
|
||||
|
||||
pub fn downgrade_from_v28<T: BeaconChainTypes>(
|
||||
db: Arc<HotColdDB<T::EthSpec, T::HotStore, T::ColdStore>>,
|
||||
) -> Result<Vec<KeyValueStoreOp>, Error> {
|
||||
let reset_payload_statuses = ResetPayloadStatuses::OnlyWithInvalidPayload;
|
||||
let Some(fork_choice) =
|
||||
BeaconChain::<T>::load_fork_choice(db.clone(), reset_payload_statuses, db.get_chain_spec())
|
||||
.map_err(|e| Error::MigrationError(format!("Unable to load fork choice: {e:?}")))?
|
||||
else {
|
||||
warn!("No fork choice to downgrade");
|
||||
return Ok(vec![]);
|
||||
};
|
||||
|
||||
// Recreate V28 persisted fork choice, then convert each field back to its V17 version.
|
||||
let persisted_fork_choice = PersistedForkChoiceV28 {
|
||||
fork_choice: fork_choice.to_persisted(),
|
||||
fork_choice_store: fork_choice.fc_store().to_persisted(),
|
||||
};
|
||||
|
||||
let justified_balances = fork_choice.fc_store().justified_balances();
|
||||
|
||||
// 1. Create `proto_array::PersistedForkChoiceV17`.
|
||||
let fork_choice_v17: fork_choice::PersistedForkChoiceV17 = (
|
||||
persisted_fork_choice.fork_choice,
|
||||
justified_balances.clone(),
|
||||
)
|
||||
.into();
|
||||
|
||||
let fork_choice_store_v17: PersistedForkChoiceStoreV17 = (
|
||||
persisted_fork_choice.fork_choice_store,
|
||||
justified_balances.clone(),
|
||||
)
|
||||
.into();
|
||||
|
||||
let persisted_fork_choice_v17 = PersistedForkChoiceV17 {
|
||||
fork_choice_v17,
|
||||
fork_choice_store_v17,
|
||||
};
|
||||
|
||||
let ops = vec![persisted_fork_choice_v17.as_kv_store_op(FORK_CHOICE_DB_KEY)];
|
||||
|
||||
info!("Downgraded fork choice for DB schema v28");
|
||||
|
||||
Ok(ops)
|
||||
}
|
||||
@@ -355,6 +355,18 @@ impl StateSummariesDAG {
|
||||
}
|
||||
Ok(descendants)
|
||||
}
|
||||
|
||||
/// Returns the root of the state at `slot` with `latest_block_root`, if it exists.
|
||||
///
|
||||
/// The `slot` must be the slot of the `latest_block_root` or a skipped slot following it. This
|
||||
/// function will not return the `state_root` of a state with a different `latest_block_root`
|
||||
/// even if it lies on the same chain.
|
||||
pub fn state_root_at_slot(&self, latest_block_root: Hash256, slot: Slot) -> Option<Hash256> {
|
||||
self.state_summaries_by_block_root
|
||||
.get(&latest_block_root)?
|
||||
.get(&slot)
|
||||
.map(|(state_root, _)| *state_root)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<HotStateSummary> for DAGStateSummary {
|
||||
|
||||
Reference in New Issue
Block a user