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:
Michael Sproul
2025-08-18 16:03:28 +10:00
committed by GitHub
parent 08234b2823
commit 836c39efaa
26 changed files with 610 additions and 127 deletions

View File

@@ -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)?;

View File

@@ -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,
}
}
}

View File

@@ -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)

View File

@@ -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)
}
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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",

View File

@@ -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)?,
))
}
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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)
}

View File

@@ -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 {