From 1b908599d869dda7419ff742674c7204eb4e0258 Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Fri, 17 Oct 2025 02:35:33 +0300 Subject: [PATCH 01/30] Force consumers to decide what finality checkpoints they want to read --- .../src/attestation_verification.rs | 1 + beacon_node/beacon_chain/src/beacon_chain.rs | 35 +++- .../src/beacon_fork_choice_store.rs | 77 +++++-- .../beacon_chain/src/blob_verification.rs | 6 +- .../beacon_chain/src/block_verification.rs | 17 +- beacon_node/beacon_chain/src/builder.rs | 2 +- .../beacon_chain/src/canonical_head.rs | 188 ++++++++++-------- .../src/data_availability_checker.rs | 6 +- .../src/data_column_verification.rs | 6 +- .../beacon_chain/src/persisted_fork_choice.rs | 16 +- .../src/schema_change/migration_schema_v28.rs | 4 +- beacon_node/beacon_chain/src/test_utils.rs | 2 + beacon_node/client/src/notifier.rs | 9 +- beacon_node/http_api/src/block_id.rs | 17 +- beacon_node/http_api/src/state_id.rs | 28 +-- .../network_beacon_processor/rpc_methods.rs | 2 + beacon_node/network/src/status.rs | 2 +- consensus/fork_choice/src/fork_choice.rs | 176 +++++++--------- .../fork_choice/src/fork_choice_store.rs | 5 +- consensus/fork_choice/src/lib.rs | 7 +- consensus/fork_choice/tests/tests.rs | 16 -- testing/ef_tests/src/cases/fork_choice.rs | 15 +- 22 files changed, 328 insertions(+), 309 deletions(-) diff --git a/beacon_node/beacon_chain/src/attestation_verification.rs b/beacon_node/beacon_chain/src/attestation_verification.rs index 470664d4429..cece90b9389 100644 --- a/beacon_node/beacon_chain/src/attestation_verification.rs +++ b/beacon_node/beacon_chain/src/attestation_verification.rs @@ -1409,6 +1409,7 @@ fn verify_attestation_is_finalized_checkpoint_or_descendant let attestation_block_root = attestation_data.beacon_block_root; let finalized_slot = fork_choice .finalized_checkpoint() + .local() .epoch .start_slot(T::EthSpec::slots_per_epoch()); let split = chain.store.get_split_info(); diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 85ccb96f693..06c6bf7d497 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -549,10 +549,12 @@ impl BeaconChain { block_root: &Hash256, block_slot: Slot, ) -> Result { + // Used by the API: finalized if prior to the network finality not the local view let finalized_slot = self .canonical_head .cached_head() .finalized_checkpoint() + .on_chain() .epoch .start_slot(T::EthSpec::slots_per_epoch()); let is_canonical = self @@ -579,10 +581,12 @@ impl BeaconChain { state_root: &Hash256, state_slot: Slot, ) -> Result { + // Used by the API: finalized if prior to the network finality not the local view let finalized_slot = self .canonical_head .cached_head() .finalized_checkpoint() + .on_chain() .epoch .start_slot(T::EthSpec::slots_per_epoch()); let slot_is_finalized = state_slot <= finalized_slot; @@ -595,6 +599,17 @@ impl BeaconChain { }) } + pub fn irreversible_slot(&self) -> Slot { + let local_finalized_slot = self + .head() + .finalized_checkpoint() + .local() + .epoch + .start_slot(T::EthSpec::slots_per_epoch()); + let split = self.store.get_split_info(); + std::cmp::max(local_finalized_slot, split.slot) + } + /// Return a database operation for writing the `PersistedBeaconChain` to disk. /// /// These days the `PersistedBeaconChain` is only used to store the genesis block root, so it @@ -4088,8 +4103,10 @@ impl BeaconChain { // We are doing this to ensure that we detect changes in finalization. It's possible // that fork choice has already been updated to the finalized checkpoint in the block // we're importing. - let current_head_finalized_checkpoint = - self.canonical_head.cached_head().finalized_checkpoint(); + let current_head_finalized_checkpoint = self + .canonical_head + .cached_head() + .finalied_checkpoint_from_head_state(); // Compare the existing finalized checkpoint with the incoming block's finalized checkpoint. let new_finalized_checkpoint = state.finalized_checkpoint(); @@ -5875,16 +5892,16 @@ impl BeaconChain { let justified_block = self .spawn_blocking_handle( move || { - chain - .canonical_head - .fork_choice_read_lock() - .get_justified_block() + let fork_choice = chain.canonical_head.fork_choice_read_lock(); + fork_choice.get_block(&fork_choice.justified_checkpoint().on_chain().root) }, "invalid_payload_fork_choice_get_justified", ) - .await??; + .await?; - if justified_block.execution_status.is_invalid() { + if let Some(justified_block) = justified_block + && justified_block.execution_status.is_invalid() + { crit!( msg = "ensure you are not connected to a malicious network. This error is not \ recoverable, please reach out to the lighthouse developers for assistance.", @@ -6952,7 +6969,7 @@ impl BeaconChain { // Check if finalization is advancing. let current_epoch = current_slot.epoch(T::EthSpec::slots_per_epoch()); let epochs_since_finalization = - current_epoch.saturating_sub(cached_head.finalized_checkpoint().epoch); + current_epoch.saturating_sub(cached_head.finalized_checkpoint().on_chain().epoch); let finalization_check = epochs_since_finalization.as_usize() <= self.config.builder_fallback_epochs_since_finalization; diff --git a/beacon_node/beacon_chain/src/beacon_fork_choice_store.rs b/beacon_node/beacon_chain/src/beacon_fork_choice_store.rs index 440388661c2..012fde54610 100644 --- a/beacon_node/beacon_chain/src/beacon_fork_choice_store.rs +++ b/beacon_node/beacon_chain/src/beacon_fork_choice_store.rs @@ -6,7 +6,7 @@ use crate::{BeaconSnapshot, metrics}; use derivative::Derivative; -use fork_choice::ForkChoiceStore; +use fork_choice::{ForkChoiceCheckpoint, ForkChoiceStore}; use proto_array::JustifiedBalances; use safe_arith::ArithError; use ssz_derive::{Decode, Encode}; @@ -136,6 +136,7 @@ pub struct BeaconForkChoiceStore, Cold: ItemStore< time: Slot, finalized_checkpoint: Checkpoint, justified_checkpoint: Checkpoint, + local_irreversible_checkpoint: Checkpoint, justified_balances: JustifiedBalances, justified_state_root: Hash256, unrealized_justified_checkpoint: Checkpoint, @@ -189,25 +190,25 @@ where } let anchor_block_root = anchor_block_header.canonical_root(); let anchor_epoch = anchor_state.current_epoch(); - let justified_checkpoint = Checkpoint { + let anchor_block_checkpoint = Checkpoint { epoch: anchor_epoch, root: anchor_block_root, }; - let finalized_checkpoint = justified_checkpoint; let justified_balances = JustifiedBalances::from_justified_state(&anchor_state)?; - let justified_state_root = anchor_state.canonical_root()?; + let anchor_state_root = anchor_state.canonical_root()?; Ok(Self { store, balances_cache: <_>::default(), time: anchor_state.slot(), - justified_checkpoint, + justified_checkpoint: anchor_state.current_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, + justified_state_root: anchor_state_root, + finalized_checkpoint: anchor_state.finalized_checkpoint(), + local_irreversible_checkpoint: anchor_block_checkpoint, + unrealized_justified_checkpoint: anchor_block_checkpoint, + unrealized_justified_state_root: anchor_state_root, + unrealized_finalized_checkpoint: anchor_block_checkpoint, proposer_boost_root: Hash256::zero(), equivocating_indices: BTreeSet::new(), _phantom: PhantomData, @@ -222,6 +223,7 @@ where finalized_checkpoint: self.finalized_checkpoint, justified_checkpoint: self.justified_checkpoint, justified_state_root: self.justified_state_root, + local_irreversible_checkpoint: self.local_irreversible_checkpoint, unrealized_justified_checkpoint: self.unrealized_justified_checkpoint, unrealized_justified_state_root: self.unrealized_justified_state_root, unrealized_finalized_checkpoint: self.unrealized_finalized_checkpoint, @@ -250,6 +252,7 @@ where justified_checkpoint: persisted.justified_checkpoint, justified_balances, justified_state_root, + local_irreversible_checkpoint: persisted.finalized_checkpoint, unrealized_justified_checkpoint: persisted.unrealized_justified_checkpoint, unrealized_justified_state_root, unrealized_finalized_checkpoint: persisted.unrealized_finalized_checkpoint, @@ -282,6 +285,7 @@ where justified_checkpoint, justified_balances, justified_state_root, + local_irreversible_checkpoint: persisted.local_irreversible_checkpoint, unrealized_justified_checkpoint: persisted.unrealized_justified_checkpoint, unrealized_justified_state_root: persisted.unrealized_justified_state_root, unrealized_finalized_checkpoint: persisted.unrealized_finalized_checkpoint, @@ -317,8 +321,15 @@ where self.balances_cache.process_state(block_root, state) } - fn justified_checkpoint(&self) -> &Checkpoint { - &self.justified_checkpoint + fn justified_checkpoint(&self) -> ForkChoiceCheckpoint { + if self.local_irreversible_checkpoint.epoch > self.justified_checkpoint.epoch { + ForkChoiceCheckpoint::Local { + local: self.local_irreversible_checkpoint, + on_chain: self.justified_checkpoint, + } + } else { + ForkChoiceCheckpoint::OnChain(self.justified_checkpoint) + } } fn justified_state_root(&self) -> Hash256 { @@ -329,8 +340,15 @@ where &self.justified_balances } - fn finalized_checkpoint(&self) -> &Checkpoint { - &self.finalized_checkpoint + fn finalized_checkpoint(&self) -> ForkChoiceCheckpoint { + if self.local_irreversible_checkpoint.epoch > self.finalized_checkpoint.epoch { + ForkChoiceCheckpoint::Local { + local: self.local_irreversible_checkpoint, + on_chain: self.finalized_checkpoint, + } + } else { + ForkChoiceCheckpoint::OnChain(self.finalized_checkpoint) + } } fn unrealized_justified_checkpoint(&self) -> &Checkpoint { @@ -361,10 +379,7 @@ where self.justified_checkpoint = checkpoint; self.justified_state_root = justified_state_root; - if let Some(balances) = self.balances_cache.get( - self.justified_checkpoint.root, - self.justified_checkpoint.epoch, - ) { + if let Some(balances) = self.balances_cache.get(checkpoint.root, checkpoint.epoch) { // NOTE: could avoid this re-calculation by introducing a `PersistedCacheItem`. metrics::inc_counter(&metrics::BALANCES_CACHE_HITS); self.justified_balances = JustifiedBalances::from_effective_balances(balances)?; @@ -407,11 +422,11 @@ where } } -pub type PersistedForkChoiceStore = PersistedForkChoiceStoreV28; +pub type PersistedForkChoiceStore = PersistedForkChoiceStoreV29; /// A container which allows persisting the `BeaconForkChoiceStore` to the on-disk database. #[superstruct( - variants(V17, V28), + variants(V17, V28, V29), variant_attributes(derive(Encode, Decode)), no_enum )] @@ -422,14 +437,16 @@ pub struct PersistedForkChoiceStore { pub time: Slot, pub finalized_checkpoint: Checkpoint, pub justified_checkpoint: Checkpoint, + #[superstruct(only(V29))] + pub local_irreversible_checkpoint: Checkpoint, /// The justified balances were removed from disk storage in schema V28. #[superstruct(only(V17))] pub justified_balances: Vec, /// The justified state root is stored so that it can be used to load the justified balances. - #[superstruct(only(V28))] + #[superstruct(only(V28, V29))] pub justified_state_root: Hash256, pub unrealized_justified_checkpoint: Checkpoint, - #[superstruct(only(V28))] + #[superstruct(only(V28, V29))] pub unrealized_justified_state_root: Hash256, pub unrealized_finalized_checkpoint: Checkpoint, pub proposer_boost_root: Hash256, @@ -452,3 +469,19 @@ impl From<(PersistedForkChoiceStoreV28, JustifiedBalances)> for PersistedForkCho } } } + +impl From for PersistedForkChoiceStoreV28 { + fn from(fcs: PersistedForkChoiceStoreV29) -> Self { + Self { + time: fcs.time, + finalized_checkpoint: fcs.finalized_checkpoint, + justified_checkpoint: fcs.justified_checkpoint, + justified_state_root: fcs.justified_state_root, + unrealized_justified_checkpoint: fcs.unrealized_justified_checkpoint, + unrealized_finalized_checkpoint: fcs.unrealized_finalized_checkpoint, + unrealized_justified_state_root: fcs.unrealized_justified_state_root, + proposer_boost_root: fcs.proposer_boost_root, + equivocating_indices: fcs.equivocating_indices, + } + } +} diff --git a/beacon_node/beacon_chain/src/blob_verification.rs b/beacon_node/beacon_chain/src/blob_verification.rs index 53f2eff0ca3..623271c7157 100644 --- a/beacon_node/beacon_chain/src/blob_verification.rs +++ b/beacon_node/beacon_chain/src/blob_verification.rs @@ -434,11 +434,7 @@ pub fn validate_blob_sidecar_for_gossip( block_root: Hash256, chain: &BeaconChain, ) -> Result<(), BlockError> { - // The finalized checkpoint is being read from fork choice, rather than the cached head. - // - // Fork choice has the most up-to-date view of finalization and there's no point importing a - // block which conflicts with the fork-choice view of finalization. - let finalized_slot = chain - .canonical_head - .cached_head() - .finalized_checkpoint() - .epoch - .start_slot(T::EthSpec::slots_per_epoch()); + // Reject blocks that conflict with the local node's irreversible slot. Could be the finalized + // slot, or a more recent slot that the user marked as irreversible. + let finalized_slot = chain.irreversible_slot(); if block.slot() <= finalized_slot { chain.pre_finalization_block_rejected(block_root); @@ -1722,8 +1715,10 @@ pub fn check_block_is_finalized_checkpoint_or_descendant< // descended from that split block. It's important not to try checking `is_descendant` if // finality is ahead of the split and the split block has been pruned, as `is_descendant` will // return `false` in this case. - let finalized_slot = fork_choice + let finalized_slot = chain + .head() .finalized_checkpoint() + .local() .epoch .start_slot(T::EthSpec::slots_per_epoch()); let split = chain.store.get_split_info(); diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index 5564c7916fa..3aa8843de82 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -818,7 +818,7 @@ where // consistent. // // This is a sanity check to detect database corruption. - let fc_finalized = fork_choice.finalized_checkpoint(); + let fc_finalized = fork_choice.finalized_checkpoint().on_chain(); let head_finalized = head_snapshot.beacon_state.finalized_checkpoint(); if fc_finalized.epoch < head_finalized.epoch { return Err(format!( diff --git a/beacon_node/beacon_chain/src/canonical_head.rs b/beacon_node/beacon_chain/src/canonical_head.rs index 7dd4c88c513..c8d8f7c3fe0 100644 --- a/beacon_node/beacon_chain/src/canonical_head.rs +++ b/beacon_node/beacon_chain/src/canonical_head.rs @@ -43,8 +43,8 @@ use crate::{ }; use eth2::types::{EventKind, SseChainReorg, SseFinalizedCheckpoint, SseHead, SseLateHead}; use fork_choice::{ - ExecutionStatus, ForkChoiceStore, ForkChoiceView, ForkchoiceUpdateParameters, ProtoBlock, - ResetPayloadStatuses, + ExecutionStatus, ForkChoiceCheckpoint, ForkChoiceStore, ForkChoiceView, + ForkchoiceUpdateParameters, ProtoBlock, ResetPayloadStatuses, }; use itertools::process_results; use lighthouse_tracing::SPAN_RECOMPUTE_HEAD; @@ -102,12 +102,12 @@ pub struct CachedHead { /// /// This value may be distinct to the `self.snapshot.beacon_state.justified_checkpoint`. /// This value should be used over the beacon state value in practically all circumstances. - justified_checkpoint: Checkpoint, + justified_checkpoint: ForkChoiceCheckpoint, /// The finalized checkpoint as per `self.fork_choice`. /// /// This value may be distinct to the `self.snapshot.beacon_state.finalized_checkpoint`. /// This value should be used over the beacon state value in practically all circumstances. - finalized_checkpoint: Checkpoint, + finalized_checkpoint: ForkChoiceCheckpoint, /// The `execution_payload.block_hash` of the block at the head of the chain. Set to `None` /// before Bellatrix. head_hash: Option, @@ -205,7 +205,7 @@ impl CachedHead { /// This is *not* the finalized checkpoint of the `head_snapshot.beacon_state`, rather it is the /// best finalized checkpoint that has been observed by `self.fork_choice`. It is possible that /// the `head_snapshot.beacon_state` finalized value is earlier than the one returned here. - pub fn finalized_checkpoint(&self) -> Checkpoint { + pub fn finalized_checkpoint(&self) -> ForkChoiceCheckpoint { self.finalized_checkpoint } @@ -217,10 +217,14 @@ impl CachedHead { /// it is the justified checkpoint in the view of `self.fork_choice`. It is possible that the /// `head_snapshot.beacon_state` justified value is different to, but not conflicting with, the /// one returned here. - pub fn justified_checkpoint(&self) -> Checkpoint { + pub fn justified_checkpoint(&self) -> ForkChoiceCheckpoint { self.justified_checkpoint } + pub fn finalied_checkpoint_from_head_state(&self) -> Checkpoint { + self.snapshot.beacon_state.finalized_checkpoint() + } + /// Returns the cached values of `ForkChoice::forkchoice_update_parameters`. /// /// Useful for supplying to the execution layer. @@ -263,15 +267,18 @@ impl CanonicalHead { fork_choice: BeaconForkChoice, snapshot: Arc>, ) -> Self { - let fork_choice_view = fork_choice.cached_fork_choice_view(); - let forkchoice_update_params = fork_choice.get_forkchoice_update_parameters(); + let head_block_root = snapshot.beacon_block_root; + let justified_checkpoint = fork_choice.justified_checkpoint(); + let finalized_checkpoint = fork_choice.finalized_checkpoint(); + let cached_head = CachedHead { snapshot, - justified_checkpoint: fork_choice_view.justified_checkpoint, - finalized_checkpoint: fork_choice_view.finalized_checkpoint, - head_hash: forkchoice_update_params.head_hash, - justified_hash: forkchoice_update_params.justified_hash, - finalized_hash: forkchoice_update_params.finalized_hash, + justified_checkpoint, + finalized_checkpoint, + // TODO: Do we need to cache this nodes? + head_hash: fork_choice.execution_hash(head_block_root), + justified_hash: fork_choice.execution_hash(justified_checkpoint.on_chain().root), + finalized_hash: fork_choice.execution_hash(finalized_checkpoint.on_chain().root), }; Self { @@ -296,15 +303,14 @@ impl CanonicalHead { store: &BeaconStore, spec: &ChainSpec, ) -> Result<(), Error> { - let fork_choice = + let mut fork_choice = >::load_fork_choice(store.clone(), reset_payload_statuses, spec)? .ok_or(Error::MissingPersistedForkChoice)?; - let fork_choice_view = fork_choice.cached_fork_choice_view(); - let beacon_block_root = fork_choice_view.head_block_root; + let current_slot = fork_choice.fc_store().get_current_slot(); + let beacon_block_root = fork_choice.get_head(current_slot, spec)?; let beacon_block = store .get_full_block(&beacon_block_root)? .ok_or(Error::MissingBeaconBlock(beacon_block_root))?; - let current_slot = fork_choice.fc_store().get_current_slot(); let (_, beacon_state) = store .get_advanced_hot_state(beacon_block_root, current_slot, beacon_block.state_root())? .ok_or(Error::MissingBeaconState(beacon_block.state_root()))?; @@ -315,14 +321,15 @@ impl CanonicalHead { beacon_state, }; - let forkchoice_update_params = fork_choice.get_forkchoice_update_parameters(); + let justified_checkpoint = fork_choice.justified_checkpoint(); + let finalized_checkpoint = fork_choice.finalized_checkpoint(); let cached_head = CachedHead { snapshot: Arc::new(snapshot), - justified_checkpoint: fork_choice_view.justified_checkpoint, - finalized_checkpoint: fork_choice_view.finalized_checkpoint, - head_hash: forkchoice_update_params.head_hash, - justified_hash: forkchoice_update_params.justified_hash, - finalized_hash: forkchoice_update_params.finalized_hash, + justified_checkpoint, + finalized_checkpoint, + head_hash: fork_choice.execution_hash(beacon_block_root), + justified_hash: fork_choice.execution_hash(justified_checkpoint.on_chain().root), + finalized_hash: fork_choice.execution_hash(finalized_checkpoint.on_chain().root), }; *fork_choice_write_lock = fork_choice; @@ -597,19 +604,23 @@ impl BeaconChain { let mut fork_choice_write_lock = self.canonical_head.fork_choice_write_lock(); // Recompute the current head via the fork choice algorithm. - fork_choice_write_lock.get_head(current_slot, &self.spec)?; + // TODO: Just with get_head, is it possible for finality to advance? + let new_head_block_root = fork_choice_write_lock.get_head(current_slot, &self.spec)?; // Downgrade the fork choice write-lock to a read lock, without allowing access to any // other writers. let fork_choice_read_lock = RwLockWriteGuard::downgrade(fork_choice_write_lock); // Read the current head value from the fork choice algorithm. - let new_view = fork_choice_read_lock.cached_fork_choice_view(); + let new_view = ForkChoiceView { + head_block_root: new_head_block_root, + justified_checkpoint: fork_choice_read_lock.justified_checkpoint(), + finalized_checkpoint: fork_choice_read_lock.finalized_checkpoint(), + }; // Check to ensure that the finalized block hasn't been marked as invalid. If it has, // shut down Lighthouse. - let finalized_proto_block = fork_choice_read_lock.get_finalized_block()?; - check_finalized_payload_validity(self, &finalized_proto_block)?; + check_finalized_payload_validity(self, &fork_choice_read_lock.local_irreversible_block()?)?; // Sanity check the finalized checkpoint. // @@ -654,8 +665,14 @@ impl BeaconChain { // Get the parameters to update the execution layer since either the head or some finality // parameters have changed. - let new_forkchoice_update_parameters = - fork_choice_read_lock.get_forkchoice_update_parameters(); + let new_forkchoice_update_parameters = ForkchoiceUpdateParameters { + head_root: new_view.head_block_root, + head_hash: fork_choice_read_lock.execution_hash(new_view.head_block_root), + justified_hash: fork_choice_read_lock + .execution_hash(new_view.justified_checkpoint.on_chain().root), + finalized_hash: fork_choice_read_lock + .execution_hash(new_view.finalized_checkpoint.on_chain().root), + }; perform_debug_logging::(&old_view, &new_view, &fork_choice_read_lock); @@ -763,8 +780,7 @@ impl BeaconChain { // The `after_finalization` function will take a write-lock on `fork_choice`, therefore it // is a dead-lock risk to hold any other lock on fork choice at this point. if new_view.finalized_checkpoint != old_view.finalized_checkpoint - && let Err(e) = - self.after_finalization(&new_cached_head, new_view, finalized_proto_block) + && let Err(e) = self.after_finalization(&new_cached_head, new_view) { crit!( error = ?e, @@ -929,50 +945,56 @@ impl BeaconChain { self: &Arc, new_cached_head: &CachedHead, new_view: ForkChoiceView, - finalized_proto_block: ProtoBlock, ) -> Result<(), Error> { let _timer = metrics::start_timer(&metrics::FORK_CHOICE_AFTER_FINALIZATION_TIMES); let new_snapshot = &new_cached_head.snapshot; - let finalized_block_is_optimistic = finalized_proto_block - .execution_status - .is_optimistic_or_invalid(); - self.observed_block_producers.write().prune( - new_view - .finalized_checkpoint - .epoch - .start_slot(T::EthSpec::slots_per_epoch()), - ); + let on_chain_finalized_checkpoint = new_view.finalized_checkpoint.on_chain(); + let new_local_finalized_checkpoint = new_view.finalized_checkpoint.local(); + let new_local_finalized_slot = new_local_finalized_checkpoint + .epoch + .start_slot(T::EthSpec::slots_per_epoch()); - self.observed_blob_sidecars.write().prune( - new_view - .finalized_checkpoint - .epoch - .start_slot(T::EthSpec::slots_per_epoch()), - ); + // observed_block_producers uses the finalized slot to reject blocks prior to that slot. It + // acts as a local irreversible slot, so use the fork-choice's local to match gossip + // verification. + self.observed_block_producers + .write() + .prune(new_local_finalized_slot); - self.observed_slashable.write().prune( - new_view - .finalized_checkpoint - .epoch - .start_slot(T::EthSpec::slots_per_epoch()), - ); + // Same as observed_block_producers + self.observed_blob_sidecars + .write() + .prune(new_local_finalized_slot); + self.observed_slashable + .write() + .prune(new_local_finalized_slot); + + // TODO: Should this use the local finality of the network? self.attester_cache - .prune_below(new_view.finalized_checkpoint.epoch); + .prune_below(new_local_finalized_checkpoint.epoch); if let Some(event_handler) = self.event_handler.as_ref() && event_handler.has_finalized_subscribers() { - event_handler.register(EventKind::FinalizedCheckpoint(SseFinalizedCheckpoint { - epoch: new_view.finalized_checkpoint.epoch, - block: new_view.finalized_checkpoint.root, - // Provide the state root of the latest finalized block, rather than the - // specific state root at the first slot of the finalized epoch (which - // might be a skip slot). - state: finalized_proto_block.state_root, - execution_optimistic: finalized_block_is_optimistic, - })); + if let Some(on_chain_finalized_block) = self + .canonical_head + .fork_choice_read_lock() + .get_block(&on_chain_finalized_checkpoint.root) + { + event_handler.register(EventKind::FinalizedCheckpoint(SseFinalizedCheckpoint { + epoch: on_chain_finalized_checkpoint.epoch, + block: on_chain_finalized_checkpoint.root, + // Provide the state root of the latest finalized block, rather than the + // specific state root at the first slot of the finalized epoch (which + // might be a skip slot). + state: on_chain_finalized_block.state_root, + execution_optimistic: on_chain_finalized_block + .execution_status + .is_optimistic_or_invalid(), + })); + } } // The store migration task and op pool pruning require the *state at the first slot of the @@ -981,15 +1003,14 @@ impl BeaconChain { // // Use the `StateRootsIterator` directly rather than `BeaconChain::state_root_at_slot` // to ensure we use the same state that we just set as the head. - let new_finalized_slot = new_view - .finalized_checkpoint + let new_local_finalized_slot = new_local_finalized_checkpoint .epoch .start_slot(T::EthSpec::slots_per_epoch()); - let new_finalized_state_root = process_results( + let new_local_finalized_state_root = process_results( StateRootsIterator::new(&self.store, &new_snapshot.beacon_state), |mut iter| { iter.find_map(|(state_root, slot)| { - if slot == new_finalized_slot { + if slot == new_local_finalized_slot { Some(state_root) } else { None @@ -997,13 +1018,13 @@ impl BeaconChain { }) }, )? - .ok_or(Error::MissingFinalizedStateRoot(new_finalized_slot))?; + .ok_or(Error::MissingFinalizedStateRoot(new_local_finalized_slot))?; let update_cache = true; let new_finalized_state = self .store - .get_hot_state(&new_finalized_state_root, update_cache)? - .ok_or(Error::MissingBeaconState(new_finalized_state_root))?; + .get_hot_state(&new_local_finalized_state_root, update_cache)? + .ok_or(Error::MissingBeaconState(new_local_finalized_state_root))?; self.op_pool.prune_all( &new_snapshot.beacon_block, @@ -1017,8 +1038,8 @@ impl BeaconChain { // state from the state_cache near instantly anyway. We could experiment with sending the // state over a channel in future, but it's probably no quicker. self.store_migrator.process_finalization( - new_finalized_state_root.into(), - new_view.finalized_checkpoint, + new_local_finalized_state_root.into(), + new_local_finalized_checkpoint, )?; // Prune blobs in the background. @@ -1104,16 +1125,17 @@ fn check_against_finality_reversion( old_view: &ForkChoiceView, new_view: &ForkChoiceView, ) -> Result<(), Error> { - let finalization_equal = new_view.finalized_checkpoint == old_view.finalized_checkpoint; - let finalization_advanced = - new_view.finalized_checkpoint.epoch > old_view.finalized_checkpoint.epoch; + let new_checkpoint = new_view.finalized_checkpoint.on_chain(); + let old_checkpoint = old_view.finalized_checkpoint.on_chain(); + let finalization_equal = new_checkpoint == old_checkpoint; + let finalization_advanced = new_checkpoint.epoch > old_checkpoint.epoch; if finalization_equal || finalization_advanced { Ok(()) } else { Err(Error::RevertedFinalizedEpoch { - old: old_view.finalized_checkpoint, - new: new_view.finalized_checkpoint, + old: old_checkpoint, + new: new_checkpoint, }) } } @@ -1136,19 +1158,15 @@ fn perform_debug_logging( } if new_view.justified_checkpoint != old_view.justified_checkpoint { debug!( - new_root = ?new_view.justified_checkpoint.root, - new_epoch = %new_view.justified_checkpoint.epoch, - old_root = ?old_view.justified_checkpoint.root, - old_epoch = %old_view.justified_checkpoint.epoch, + new = %new_view.justified_checkpoint, + old = %old_view.justified_checkpoint, "Fork choice justified" ) } if new_view.finalized_checkpoint != old_view.finalized_checkpoint { debug!( - new_root = ?new_view.finalized_checkpoint.root, - new_epoch = %new_view.finalized_checkpoint.epoch, - old_root = ?old_view.finalized_checkpoint.root, - old_epoch = %old_view.finalized_checkpoint.epoch, + new = %new_view.finalized_checkpoint, + old = %old_view.finalized_checkpoint, "Fork choice finalized" ) } diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index 43b7d8f7ea3..50bed287bc4 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -714,11 +714,7 @@ async fn availability_cache_maintenance_service( continue; } - let finalized_epoch = chain - .canonical_head - .fork_choice_read_lock() - .finalized_checkpoint() - .epoch; + let finalized_epoch = chain.head().finalized_checkpoint().on_chain().epoch; let Some(min_epochs_for_blobs) = chain .spec diff --git a/beacon_node/beacon_chain/src/data_column_verification.rs b/beacon_node/beacon_chain/src/data_column_verification.rs index 01e79c49aa6..a5b9381dae8 100644 --- a/beacon_node/beacon_chain/src/data_column_verification.rs +++ b/beacon_node/beacon_chain/src/data_column_verification.rs @@ -736,11 +736,7 @@ fn verify_slot_greater_than_latest_finalized_slot( chain: &BeaconChain, column_slot: Slot, ) -> Result<(), GossipDataColumnError> { - let latest_finalized_slot = chain - .head() - .finalized_checkpoint() - .epoch - .start_slot(T::EthSpec::slots_per_epoch()); + let latest_finalized_slot = chain.irreversible_slot(); if column_slot <= latest_finalized_slot { return Err(GossipDataColumnError::PastFinalizedSlot { column_slot, diff --git a/beacon_node/beacon_chain/src/persisted_fork_choice.rs b/beacon_node/beacon_chain/src/persisted_fork_choice.rs index d8fcc0901bf..cace30dd5fd 100644 --- a/beacon_node/beacon_chain/src/persisted_fork_choice.rs +++ b/beacon_node/beacon_chain/src/persisted_fork_choice.rs @@ -1,5 +1,7 @@ use crate::{ - beacon_fork_choice_store::{PersistedForkChoiceStoreV17, PersistedForkChoiceStoreV28}, + beacon_fork_choice_store::{ + PersistedForkChoiceStoreV17, PersistedForkChoiceStoreV28, PersistedForkChoiceStoreV29, + }, metrics, }; use ssz::{Decode, Encode}; @@ -9,22 +11,24 @@ use superstruct::superstruct; use types::Hash256; // If adding a new version you should update this type alias and fix the breakages. -pub type PersistedForkChoice = PersistedForkChoiceV28; +pub type PersistedForkChoice = PersistedForkChoiceV29; #[superstruct( - variants(V17, V28), + variants(V17, V28, V29), variant_attributes(derive(Encode, Decode)), no_enum )] pub struct PersistedForkChoice { #[superstruct(only(V17))] pub fork_choice_v17: fork_choice::PersistedForkChoiceV17, - #[superstruct(only(V28))] + #[superstruct(only(V28, V29))] pub fork_choice: fork_choice::PersistedForkChoiceV28, #[superstruct(only(V17))] pub fork_choice_store_v17: PersistedForkChoiceStoreV17, #[superstruct(only(V28))] - pub fork_choice_store: PersistedForkChoiceStoreV28, + pub fork_choice_store_v28: PersistedForkChoiceStoreV28, + #[superstruct(only(V29))] + pub fork_choice_store: PersistedForkChoiceStoreV29, } macro_rules! impl_store_item { @@ -47,7 +51,7 @@ macro_rules! impl_store_item { impl_store_item!(PersistedForkChoiceV17); -impl PersistedForkChoiceV28 { +impl PersistedForkChoiceV29 { pub fn from_bytes(bytes: &[u8], store_config: &StoreConfig) -> Result { let decompressed_bytes = store_config .decompress_bytes(bytes) diff --git a/beacon_node/beacon_chain/src/schema_change/migration_schema_v28.rs b/beacon_node/beacon_chain/src/schema_change/migration_schema_v28.rs index 5885eaabc00..f37df5e0f71 100644 --- a/beacon_node/beacon_chain/src/schema_change/migration_schema_v28.rs +++ b/beacon_node/beacon_chain/src/schema_change/migration_schema_v28.rs @@ -121,7 +121,7 @@ pub fn downgrade_from_v28( // 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(), + fork_choice_store_v28: fork_choice.fc_store().to_persisted().into(), }; let justified_balances = fork_choice.fc_store().justified_balances(); @@ -134,7 +134,7 @@ pub fn downgrade_from_v28( .into(); let fork_choice_store_v17: PersistedForkChoiceStoreV17 = ( - persisted_fork_choice.fork_choice_store, + persisted_fork_choice.fork_choice_store_v28, justified_balances.clone(), ) .into(); diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index 1d575501563..7316f2b58bd 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -829,6 +829,7 @@ where .canonical_head .cached_head() .finalized_checkpoint() + .local() } pub fn justified_checkpoint(&self) -> Checkpoint { @@ -836,6 +837,7 @@ where .canonical_head .cached_head() .justified_checkpoint() + .local() } pub fn get_current_slot(&self) -> Slot { diff --git a/beacon_node/client/src/notifier.rs b/beacon_node/client/src/notifier.rs index c83cdad7e01..504fa4bccaa 100644 --- a/beacon_node/client/src/notifier.rs +++ b/beacon_node/client/src/notifier.rs @@ -179,8 +179,7 @@ pub fn spawn_notifier( debug!( peers = peer_count_pretty(connected_peer_count), - finalized_root = %finalized_checkpoint.root, - finalized_epoch = %finalized_checkpoint.epoch, + %finalized_checkpoint, head_block = %head_root, %head_slot, %current_slot, @@ -298,8 +297,7 @@ pub fn spawn_notifier( info!( peers = peer_count_pretty(connected_peer_count), exec_hash = block_hash, - finalized_root = %finalized_checkpoint.root, - finalized_epoch = %finalized_checkpoint.epoch, + %finalized_checkpoint, epoch = %current_epoch, block = block_info, slot = %current_slot, @@ -309,8 +307,7 @@ pub fn spawn_notifier( metrics::set_gauge(&metrics::IS_SYNCED, 0); info!( peers = peer_count_pretty(connected_peer_count), - finalized_root = %finalized_checkpoint.root, - finalized_epoch = %finalized_checkpoint.epoch, + %finalized_checkpoint, %head_slot, %current_slot, "Searching for peers" diff --git a/beacon_node/http_api/src/block_id.rs b/beacon_node/http_api/src/block_id.rs index 778067c32bb..7c9fb139ff6 100644 --- a/beacon_node/http_api/src/block_id.rs +++ b/beacon_node/http_api/src/block_id.rs @@ -57,15 +57,23 @@ impl BlockId { } CoreBlockId::Genesis => Ok((chain.genesis_block_root, false, true)), CoreBlockId::Finalized => { - let finalized_checkpoint = - chain.canonical_head.cached_head().finalized_checkpoint(); + // Return the network's finalized checkpoint not the node's local irreversible view. + let finalized_checkpoint = chain + .canonical_head + .cached_head() + .finalized_checkpoint() + .on_chain(); let (_slot, execution_optimistic) = checkpoint_slot_and_execution_optimistic(chain, finalized_checkpoint)?; Ok((finalized_checkpoint.root, execution_optimistic, true)) } CoreBlockId::Justified => { - let justified_checkpoint = - chain.canonical_head.cached_head().justified_checkpoint(); + // Return the network's justified checkpoint not the node's local irreversible view. + let justified_checkpoint = chain + .canonical_head + .cached_head() + .justified_checkpoint() + .on_chain(); let (_slot, execution_optimistic) = checkpoint_slot_and_execution_optimistic(chain, justified_checkpoint)?; Ok((justified_checkpoint.root, execution_optimistic, false)) @@ -90,6 +98,7 @@ impl BlockId { .canonical_head .cached_head() .finalized_checkpoint() + .on_chain() .epoch .start_slot(T::EthSpec::slots_per_epoch()); Ok((root, execution_optimistic, finalized)) diff --git a/beacon_node/http_api/src/state_id.rs b/beacon_node/http_api/src/state_id.rs index 13fb9b2c585..1579b6d2cb6 100644 --- a/beacon_node/http_api/src/state_id.rs +++ b/beacon_node/http_api/src/state_id.rs @@ -39,15 +39,15 @@ impl StateId { } CoreStateId::Genesis => return Ok((chain.genesis_state_root, false, true)), CoreStateId::Finalized => { - let finalized_checkpoint = - chain.canonical_head.cached_head().finalized_checkpoint(); + // Return the network's finalized checkpoint not the node's local irreversible view. + let finalized_checkpoint = chain.head().finalized_checkpoint().on_chain(); let (slot, execution_optimistic) = checkpoint_slot_and_execution_optimistic(chain, finalized_checkpoint)?; (slot, execution_optimistic, true) } CoreStateId::Justified => { - let justified_checkpoint = - chain.canonical_head.cached_head().justified_checkpoint(); + // Return the network's justified checkpoint not the node's local irreversible view. + let justified_checkpoint = chain.head().justified_checkpoint().on_chain(); let (slot, execution_optimistic) = checkpoint_slot_and_execution_optimistic(chain, justified_checkpoint)?; (slot, execution_optimistic, false) @@ -59,9 +59,9 @@ impl StateId { .map_err(warp_utils::reject::unhandled_error)?, *slot <= chain - .canonical_head - .cached_head() + .head() .finalized_checkpoint() + .on_chain() .epoch .start_slot(T::EthSpec::slots_per_epoch()), ), @@ -104,10 +104,9 @@ impl StateId { .map_err(warp_utils::reject::unhandled_error)? { let fork_choice = chain.canonical_head.fork_choice_read_lock(); - let finalized_root = fork_choice - .cached_fork_choice_view() - .finalized_checkpoint - .root; + // Retrieve the status of the oldest irreversible block in fork-choice. The + // network finalized checkpoint may not be available. + let finalized_root = fork_choice.finalized_checkpoint().local().root; let execution_optimistic = fork_choice .is_optimistic_or_invalid_block_no_fallback(&finalized_root) .map_err(BeaconChainError::ForkChoiceError) @@ -262,12 +261,15 @@ pub fn checkpoint_slot_and_execution_optimistic( ) -> Result<(Slot, ExecutionOptimistic), warp::reject::Rejection> { let slot = checkpoint.epoch.start_slot(T::EthSpec::slots_per_epoch()); let fork_choice = chain.canonical_head.fork_choice_read_lock(); - let finalized_checkpoint = fork_choice.cached_fork_choice_view().finalized_checkpoint; + // Use the local irreversible checkpoint of fork-choice. We need to default to a known block in + // fork-choice and oldest block in fork-choice might be more recent that the network's finalized + // checkpoint. + let local_finalized_checkpoint = fork_choice.finalized_checkpoint().local(); // If the checkpoint is pre-finalization, just use the optimistic status of the finalized // block. - let root = if checkpoint.epoch < finalized_checkpoint.epoch { - &finalized_checkpoint.root + let root = if checkpoint.epoch < local_finalized_checkpoint.epoch { + &local_finalized_checkpoint.root } else { &checkpoint.root }; diff --git a/beacon_node/network/src/network_beacon_processor/rpc_methods.rs b/beacon_node/network/src/network_beacon_processor/rpc_methods.rs index 58e02ffe007..d5308ba4b85 100644 --- a/beacon_node/network/src/network_beacon_processor/rpc_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/rpc_methods.rs @@ -843,11 +843,13 @@ impl NetworkBeaconProcessor { req_type: &str, ) -> Result, (RpcErrorResponse, &'static str)> { let start_time = std::time::Instant::now(); + // We care about blocks being in fork-choice, use its view of finality let finalized_slot = self .chain .canonical_head .cached_head() .finalized_checkpoint() + .local() .epoch .start_slot(T::EthSpec::slots_per_epoch()); diff --git a/beacon_node/network/src/status.rs b/beacon_node/network/src/status.rs index ebf5c1829e5..ee6e678df5d 100644 --- a/beacon_node/network/src/status.rs +++ b/beacon_node/network/src/status.rs @@ -20,7 +20,7 @@ impl ToStatusMessage for BeaconChain { pub(crate) fn status_message(beacon_chain: &BeaconChain) -> StatusMessage { let fork_digest = beacon_chain.enr_fork_id().fork_digest; let cached_head = beacon_chain.canonical_head.cached_head(); - let mut finalized_checkpoint = cached_head.finalized_checkpoint(); + let mut finalized_checkpoint = cached_head.finalized_checkpoint().on_chain(); // Alias the genesis checkpoint root to `0x00`. let spec = &beacon_chain.spec; diff --git a/consensus/fork_choice/src/fork_choice.rs b/consensus/fork_choice/src/fork_choice.rs index fe1f5fba9e4..ae9eef3b60d 100644 --- a/consensus/fork_choice/src/fork_choice.rs +++ b/consensus/fork_choice/src/fork_choice.rs @@ -61,12 +61,8 @@ pub enum Error { block_root: Hash256, payload_verification_status: PayloadVerificationStatus, }, - MissingJustifiedBlock { - justified_checkpoint: Checkpoint, - }, - MissingFinalizedBlock { - finalized_checkpoint: Checkpoint, - }, + MissingJustifiedBlock(Hash256), + MissingFinalizedBlock(Hash256), WrongSlotForGetProposerHead { current_slot: Slot, fc_store_slot: Slot, @@ -294,11 +290,49 @@ pub struct ForkchoiceUpdateParameters { pub finalized_hash: Option, } +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum ForkChoiceCheckpoint { + Local { + local: Checkpoint, + on_chain: Checkpoint, + }, + OnChain(Checkpoint), +} + +impl ForkChoiceCheckpoint { + pub fn on_chain(&self) -> Checkpoint { + match self { + Self::Local { on_chain, .. } => *on_chain, + Self::OnChain(cp) => *cp, + } + } + + pub fn local(&self) -> Checkpoint { + match self { + Self::Local { local, .. } => *local, + Self::OnChain(cp) => *cp, + } + } +} + +impl std::fmt::Display for ForkChoiceCheckpoint { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Local { local, on_chain } => write!( + f, + "{}/{}/local/{}/{}", + on_chain.root, on_chain.epoch, local.root, local.epoch, + ), + Self::OnChain(cp) => write!(f, "{}/{}", cp.root, cp.epoch), + } + } +} + #[derive(Clone, Copy, Debug, PartialEq)] pub struct ForkChoiceView { pub head_block_root: Hash256, - pub justified_checkpoint: Checkpoint, - pub finalized_checkpoint: Checkpoint, + pub justified_checkpoint: ForkChoiceCheckpoint, + pub finalized_checkpoint: ForkChoiceCheckpoint, } /// Provides an implementation of "Ethereum 2.0 Phase 0 -- Beacon Chain Fork Choice": @@ -318,8 +352,6 @@ pub struct ForkChoice { proto_array: ProtoArrayForkChoice, /// Attestations that arrived at the current slot and must be queued for later processing. queued_attestations: Vec, - /// Stores a cache of the values required to be sent to the execution layer. - forkchoice_update_parameters: ForkchoiceUpdateParameters, _phantom: PhantomData, } @@ -389,8 +421,8 @@ where current_slot, finalized_block_slot, finalized_block_state_root, - *fc_store.justified_checkpoint(), - *fc_store.finalized_checkpoint(), + fc_store.justified_checkpoint().local(), + fc_store.finalized_checkpoint().local(), current_epoch_shuffling_id, next_epoch_shuffling_id, execution_status, @@ -400,14 +432,6 @@ where fc_store, proto_array, queued_attestations: vec![], - // This will be updated during the next call to `Self::get_head`. - forkchoice_update_parameters: ForkchoiceUpdateParameters { - head_hash: None, - justified_hash: None, - finalized_hash: None, - // This will be updated during the next call to `Self::get_head`. - head_root: Hash256::zero(), - }, _phantom: PhantomData, }; @@ -417,14 +441,6 @@ where Ok(fork_choice) } - /// Returns cached information that can be used to issue a `forkchoiceUpdated` message to an - /// execution engine. - /// - /// These values are updated each time `Self::get_head` is called. - pub fn get_forkchoice_update_parameters(&self) -> ForkchoiceUpdateParameters { - self.forkchoice_update_parameters - } - /// Returns the block root of an ancestor of `block_root` at the given `slot`. (Note: `slot` refers /// to the block that is *returned*, not the one that is supplied.) /// @@ -488,8 +504,8 @@ where let store = &mut self.fc_store; let head_root = self.proto_array.find_head::( - *store.justified_checkpoint(), - *store.finalized_checkpoint(), + store.justified_checkpoint().local(), + store.finalized_checkpoint().local(), store.justified_balances(), store.proposer_boost_root(), store.equivocating_indices(), @@ -497,25 +513,6 @@ where spec, )?; - // Cache some values for the next forkchoiceUpdate call to the execution layer. - let head_hash = self - .get_block(&head_root) - .and_then(|b| b.execution_status.block_hash()); - let justified_root = self.justified_checkpoint().root; - let finalized_root = self.finalized_checkpoint().root; - let justified_hash = self - .get_block(&justified_root) - .and_then(|b| b.execution_status.block_hash()); - let finalized_hash = self - .get_block(&finalized_root) - .and_then(|b| b.execution_status.block_hash()); - self.forkchoice_update_parameters = ForkchoiceUpdateParameters { - head_root, - head_hash, - justified_hash, - finalized_hash, - }; - Ok(head_root) } @@ -591,24 +588,12 @@ where .map_err(ProposerHeadError::convert_inner_error) } - /// Return information about: - /// - /// - The LMD head of the chain. - /// - The FFG checkpoints. - /// - /// The information is "cached" since the last call to `Self::get_head`. - /// - /// ## Notes - /// - /// The finalized/justified checkpoints are determined from the fork choice store. Therefore, - /// it's possible that the state corresponding to `get_state(get_block(head_block_root))` will - /// have *differing* finalized and justified information. - pub fn cached_fork_choice_view(&self) -> ForkChoiceView { - ForkChoiceView { - head_block_root: self.forkchoice_update_parameters.head_root, - justified_checkpoint: self.justified_checkpoint(), - finalized_checkpoint: self.finalized_checkpoint(), - } + /// Returns the ExecutionBlockHash of a block. Return None if `block_root` is not known or if + /// the exeucution status of the block is `Irrelevant` + pub fn execution_hash(&self, block_root: Hash256) -> Option { + self.proto_array + .get_block(&block_root) + .and_then(|block| block.execution_status.block_hash()) } /// See `ProtoArrayForkChoice::process_execution_payload_validation` for documentation. @@ -700,7 +685,7 @@ where // Check that block is later than the finalized epoch slot (optimization to reduce calls to // get_ancestor). let finalized_slot = - compute_start_slot_at_epoch::(self.fc_store.finalized_checkpoint().epoch); + compute_start_slot_at_epoch::(self.fc_store.finalized_checkpoint().local().epoch); if block.slot() <= finalized_slot { return Err(Error::InvalidBlock(InvalidBlock::FinalizedSlot { finalized_slot, @@ -718,7 +703,7 @@ where // // https://github.com/ethereum/eth2.0-specs/pull/1884 let block_ancestor = self.get_ancestor(block.parent_root(), finalized_slot)?; - let finalized_root = self.fc_store.finalized_checkpoint().root; + let finalized_root = self.fc_store.finalized_checkpoint().local().root; if block_ancestor != Some(finalized_root) { return Err(Error::InvalidBlock(InvalidBlock::NotFinalizedDescendant { finalized_root, @@ -921,7 +906,7 @@ where justified_state_root_producer: impl FnOnce() -> Result>, ) -> Result<(), Error> { // Update justified checkpoint. - if justified_checkpoint.epoch > self.fc_store.justified_checkpoint().epoch { + if justified_checkpoint.epoch > self.fc_store.justified_checkpoint().local().epoch { let justified_state_root = justified_state_root_producer()?; self.fc_store .set_justified_checkpoint(justified_checkpoint, justified_state_root) @@ -929,7 +914,7 @@ where } // Update finalized checkpoint. - if finalized_checkpoint.epoch > self.fc_store.finalized_checkpoint().epoch { + if finalized_checkpoint.epoch > self.fc_store.finalized_checkpoint().local().epoch { self.fc_store.set_finalized_checkpoint(finalized_checkpoint); } @@ -1262,29 +1247,6 @@ where self.proto_array.get_weight(block_root) } - /// Returns the `ProtoBlock` for the justified checkpoint. - /// - /// ## Notes - /// - /// This does *not* return the "best justified checkpoint". It returns the justified checkpoint - /// that is used for computing balances. - pub fn get_justified_block(&self) -> Result> { - let justified_checkpoint = self.justified_checkpoint(); - self.get_block(&justified_checkpoint.root) - .ok_or(Error::MissingJustifiedBlock { - justified_checkpoint, - }) - } - - /// Returns the `ProtoBlock` for the finalized checkpoint. - pub fn get_finalized_block(&self) -> Result> { - let finalized_checkpoint = self.finalized_checkpoint(); - self.get_block(&finalized_checkpoint.root) - .ok_or(Error::MissingFinalizedBlock { - finalized_checkpoint, - }) - } - /// Return `true` if `block_root` is equal to the finalized checkpoint, or a known descendant of it. pub fn is_finalized_checkpoint_or_descendant(&self, block_root: Hash256) -> bool { self.proto_array @@ -1313,7 +1275,7 @@ where Ok(status.is_optimistic_or_invalid()) } else { Ok(self - .get_finalized_block()? + .local_irreversible_block()? .execution_status .is_optimistic_or_invalid()) } @@ -1335,14 +1297,21 @@ where } } + /// Returns the local irreversible block, which may be ahead of the network's finalized block + pub fn local_irreversible_block(&self) -> Result> { + let local_irreversible_root = self.finalized_checkpoint().local().root; + self.get_block(&local_irreversible_root) + .ok_or(Error::MissingFinalizedBlock(local_irreversible_root)) + } + /// Return the current finalized checkpoint. - pub fn finalized_checkpoint(&self) -> Checkpoint { - *self.fc_store.finalized_checkpoint() + pub fn finalized_checkpoint(&self) -> ForkChoiceCheckpoint { + self.fc_store.finalized_checkpoint() } /// Return the justified checkpoint. - pub fn justified_checkpoint(&self) -> Checkpoint { - *self.fc_store.justified_checkpoint() + pub fn justified_checkpoint(&self) -> ForkChoiceCheckpoint { + self.fc_store.justified_checkpoint() } pub fn unrealized_justified_checkpoint(&self) -> Checkpoint { @@ -1393,7 +1362,7 @@ where /// Prunes the underlying fork choice DAG. pub fn prune(&mut self) -> Result<(), Error> { - let finalized_root = self.fc_store.finalized_checkpoint().root; + let finalized_root = self.fc_store.finalized_checkpoint().local().root; self.proto_array .maybe_prune(finalized_root) @@ -1471,13 +1440,6 @@ where proto_array, queued_attestations: persisted.queued_attestations, // Will be updated in the following call to `Self::get_head`. - forkchoice_update_parameters: ForkchoiceUpdateParameters { - head_hash: None, - justified_hash: None, - finalized_hash: None, - // Will be updated in the following call to `Self::get_head`. - head_root: Hash256::zero(), - }, _phantom: PhantomData, }; diff --git a/consensus/fork_choice/src/fork_choice_store.rs b/consensus/fork_choice/src/fork_choice_store.rs index caa0ae9be24..af4addbc937 100644 --- a/consensus/fork_choice/src/fork_choice_store.rs +++ b/consensus/fork_choice/src/fork_choice_store.rs @@ -1,3 +1,4 @@ +use crate::ForkChoiceCheckpoint; use proto_array::JustifiedBalances; use std::collections::BTreeSet; use std::fmt::Debug; @@ -42,7 +43,7 @@ pub trait ForkChoiceStore: Sized { ) -> Result<(), Self::Error>; /// Returns the `justified_checkpoint`. - fn justified_checkpoint(&self) -> &Checkpoint; + fn justified_checkpoint(&self) -> ForkChoiceCheckpoint; /// Returns the state root of the justified checkpoint. fn justified_state_root(&self) -> Hash256; @@ -51,7 +52,7 @@ pub trait ForkChoiceStore: Sized { fn justified_balances(&self) -> &JustifiedBalances; /// Returns the `finalized_checkpoint`. - fn finalized_checkpoint(&self) -> &Checkpoint; + fn finalized_checkpoint(&self) -> ForkChoiceCheckpoint; /// Returns the `unrealized_justified_checkpoint`. fn unrealized_justified_checkpoint(&self) -> &Checkpoint; diff --git a/consensus/fork_choice/src/lib.rs b/consensus/fork_choice/src/lib.rs index afe06dee1bc..4593050069e 100644 --- a/consensus/fork_choice/src/lib.rs +++ b/consensus/fork_choice/src/lib.rs @@ -3,9 +3,10 @@ mod fork_choice_store; mod metrics; pub use crate::fork_choice::{ - AttestationFromBlock, Error, ForkChoice, ForkChoiceView, ForkchoiceUpdateParameters, - InvalidAttestation, InvalidBlock, PayloadVerificationStatus, PersistedForkChoice, - PersistedForkChoiceV17, PersistedForkChoiceV28, QueuedAttestation, ResetPayloadStatuses, + AttestationFromBlock, Error, ForkChoice, ForkChoiceCheckpoint, ForkChoiceView, + ForkchoiceUpdateParameters, InvalidAttestation, InvalidBlock, PayloadVerificationStatus, + PersistedForkChoice, PersistedForkChoiceV17, PersistedForkChoiceV28, QueuedAttestation, + ResetPayloadStatuses, }; pub use fork_choice_store::ForkChoiceStore; pub use proto_array::{ diff --git a/consensus/fork_choice/tests/tests.rs b/consensus/fork_choice/tests/tests.rs index 25c3f03d3b9..6fe29454236 100644 --- a/consensus/fork_choice/tests/tests.rs +++ b/consensus/fork_choice/tests/tests.rs @@ -516,22 +516,6 @@ impl ForkChoiceTest { } } -#[test] -fn justified_and_finalized_blocks() { - let tester = ForkChoiceTest::new(); - let fork_choice = tester.harness.chain.canonical_head.fork_choice_read_lock(); - - let justified_checkpoint = fork_choice.justified_checkpoint(); - assert_eq!(justified_checkpoint.epoch, 0); - assert!(justified_checkpoint.root != Hash256::zero()); - assert!(fork_choice.get_justified_block().is_ok()); - - let finalized_checkpoint = fork_choice.finalized_checkpoint(); - assert_eq!(finalized_checkpoint.epoch, 0); - assert!(finalized_checkpoint.root != Hash256::zero()); - assert!(fork_choice.get_finalized_block().is_ok()); -} - /// - The new justified checkpoint descends from the current. Near genesis. #[tokio::test] async fn justified_checkpoint_updates_with_descendent_first_justification() { diff --git a/testing/ef_tests/src/cases/fork_choice.rs b/testing/ef_tests/src/cases/fork_choice.rs index 1380e44acdd..97408708f77 100644 --- a/testing/ef_tests/src/cases/fork_choice.rs +++ b/testing/ef_tests/src/cases/fork_choice.rs @@ -788,13 +788,14 @@ impl Tester { } pub fn check_justified_checkpoint(&self, expected_checkpoint: Checkpoint) -> Result<(), Error> { - let head_checkpoint = self.find_head()?.justified_checkpoint(); + let head_checkpoint = self.find_head()?.justified_checkpoint().local(); let fc_checkpoint = self .harness .chain .canonical_head .fork_choice_read_lock() - .justified_checkpoint(); + .justified_checkpoint() + .local(); assert_checkpoints_eq("justified_checkpoint", head_checkpoint, fc_checkpoint); @@ -805,13 +806,14 @@ impl Tester { &self, expected_checkpoint_root: Hash256, ) -> Result<(), Error> { - let head_checkpoint = self.find_head()?.justified_checkpoint(); + let head_checkpoint = self.find_head()?.justified_checkpoint().local(); let fc_checkpoint = self .harness .chain .canonical_head .fork_choice_read_lock() - .justified_checkpoint(); + .justified_checkpoint() + .local(); assert_checkpoints_eq("justified_checkpoint_root", head_checkpoint, fc_checkpoint); @@ -823,13 +825,14 @@ impl Tester { } pub fn check_finalized_checkpoint(&self, expected_checkpoint: Checkpoint) -> Result<(), Error> { - let head_checkpoint = self.find_head()?.finalized_checkpoint(); + let head_checkpoint = self.find_head()?.finalized_checkpoint().local(); let fc_checkpoint = self .harness .chain .canonical_head .fork_choice_read_lock() - .finalized_checkpoint(); + .finalized_checkpoint() + .local(); assert_checkpoints_eq("finalized_checkpoint", head_checkpoint, fc_checkpoint); From a2c5b4bc9e5f13c5a260ae08a3b20acff90277d1 Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Mon, 20 Oct 2025 03:13:10 +0300 Subject: [PATCH 02/30] Update sync to handle local finality --- .../network_beacon_processor/rpc_methods.rs | 6 +- beacon_node/network/src/sync/manager.rs | 50 ++++------ .../network/src/sync/peer_sync_info.rs | 93 ++++++++++++++++--- .../src/sync/range_sync/chain_collection.rs | 58 ++++++------ .../network/src/sync/range_sync/range.rs | 63 ++++++------- 5 files changed, 154 insertions(+), 116 deletions(-) diff --git a/beacon_node/network/src/network_beacon_processor/rpc_methods.rs b/beacon_node/network/src/network_beacon_processor/rpc_methods.rs index d5308ba4b85..3e39c1d2ffe 100644 --- a/beacon_node/network/src/network_beacon_processor/rpc_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/rpc_methods.rs @@ -108,11 +108,7 @@ impl NetworkBeaconProcessor { } else { // Remote finalized epoch is less than ours. let remote_finalized_slot = start_slot(*remote.finalized_epoch()); - if remote_finalized_slot < self.chain.store.get_oldest_block_slot() { - // Peer's finalized checkpoint is older than anything in our DB. We are unlikely - // to be able to help them sync. - Some("Old finality out of range".to_string()) - } else if remote_finalized_slot < self.chain.store.get_split_slot() { + if remote_finalized_slot < self.chain.store.get_split_slot() { // Peer's finalized slot is in range for a quick block root check in our freezer DB. // If that block root check fails, reject them as they're on a different finalized // chain. diff --git a/beacon_node/network/src/sync/manager.rs b/beacon_node/network/src/sync/manager.rs index d7ba0280542..09b11f36b0e 100644 --- a/beacon_node/network/src/sync/manager.rs +++ b/beacon_node/network/src/sync/manager.rs @@ -42,11 +42,11 @@ use super::peer_sync_info::{PeerSyncType, remote_sync_type}; use super::range_sync::{EPOCHS_PER_BATCH, RangeSync, RangeSyncType}; use crate::network_beacon_processor::{ChainSegmentProcessId, NetworkBeaconProcessor}; use crate::service::NetworkMessage; -use crate::status::ToStatusMessage; use crate::sync::block_lookups::{ BlobRequestState, BlockComponent, BlockRequestState, CustodyRequestState, DownloadResult, }; use crate::sync::network_context::PeerGroup; +use crate::sync::peer_sync_info::{LocalSyncInfo, PeerSyncTypeAdvanced}; use beacon_chain::block_verification_types::AsBlock; use beacon_chain::validator_monitor::timestamp_now; use beacon_chain::{ @@ -358,16 +358,7 @@ impl SyncManager { /// If the peer is within the `SLOT_IMPORT_TOLERANCE`, then it's head is sufficiently close to /// ours that we consider it fully sync'd with respect to our current chain. fn add_peer(&mut self, peer_id: PeerId, remote: SyncInfo) { - // ensure the beacon chain still exists - let status = self.chain.status_message(); - let local = SyncInfo { - head_slot: *status.head_slot(), - head_root: *status.head_root(), - finalized_epoch: *status.finalized_epoch(), - finalized_root: *status.finalized_root(), - earliest_available_slot: status.earliest_available_slot().ok().cloned(), - }; - + let local = LocalSyncInfo::new(&self.chain); let sync_type = remote_sync_type(&local, &remote, &self.chain); // update the state of the peer. @@ -375,9 +366,9 @@ impl SyncManager { if is_still_connected { match sync_type { PeerSyncType::Behind => {} // Do nothing - PeerSyncType::Advanced => { + PeerSyncType::Advanced(advanced_type) => { self.range_sync - .add_peer(&mut self.network, local, peer_id, remote); + .add_peer(&mut self.network, local, peer_id, advanced_type); } PeerSyncType::FullySynced => { // Sync considers this peer close enough to the head to not trigger range sync. @@ -412,15 +403,7 @@ impl SyncManager { head_root: Hash256, head_slot: Option, ) { - let status = self.chain.status_message(); - let local = SyncInfo { - head_slot: *status.head_slot(), - head_root: *status.head_root(), - finalized_epoch: *status.finalized_epoch(), - finalized_root: *status.finalized_root(), - earliest_available_slot: status.earliest_available_slot().ok().cloned(), - }; - + let local = LocalSyncInfo::new(&self.chain); let head_slot = head_slot.unwrap_or_else(|| { debug!( local_head_slot = %local.head_slot, @@ -430,18 +413,17 @@ impl SyncManager { local.head_slot }); - let remote = SyncInfo { - head_slot, - head_root, - // Set finalized to same as local to trigger Head sync - finalized_epoch: local.finalized_epoch, - finalized_root: local.finalized_root, - earliest_available_slot: local.earliest_available_slot, - }; - for peer_id in peers { - self.range_sync - .add_peer(&mut self.network, local.clone(), *peer_id, remote.clone()); + self.range_sync.add_peer( + &mut self.network, + local.clone(), + *peer_id, + PeerSyncTypeAdvanced::Head { + target_root: head_root, + target_slot: head_slot, + start_epoch: local.local_irreversible_epoch, + }, + ); } } @@ -516,7 +498,7 @@ impl SyncManager { fn update_peer_sync_state( &mut self, peer_id: &PeerId, - local_sync_info: &SyncInfo, + local_sync_info: &LocalSyncInfo, remote_sync_info: &SyncInfo, sync_type: &PeerSyncType, ) -> bool { diff --git a/beacon_node/network/src/sync/peer_sync_info.rs b/beacon_node/network/src/sync/peer_sync_info.rs index 5ea1533d350..0c764708bc5 100644 --- a/beacon_node/network/src/sync/peer_sync_info.rs +++ b/beacon_node/network/src/sync/peer_sync_info.rs @@ -1,14 +1,16 @@ use super::manager::SLOT_IMPORT_TOLERANCE; +use crate::status::ToStatusMessage; use beacon_chain::{BeaconChain, BeaconChainTypes}; use lighthouse_network::{SyncInfo, SyncStatus as PeerSyncStatus}; use std::cmp::Ordering; +use types::{Epoch, EthSpec, Hash256, Slot}; /// The type of peer relative to our current state. pub enum PeerSyncType { /// The peer is on our chain and is fully synced with respect to our chain. FullySynced, /// The peer has a greater knowledge of the chain than us that warrants a full sync. - Advanced, + Advanced(PeerSyncTypeAdvanced), /// A peer is behind in the sync and not useful to us for downloading blocks. Behind, } @@ -18,13 +20,45 @@ impl PeerSyncType { match self { PeerSyncType::FullySynced => PeerSyncStatus::Synced { info: info.clone() }, PeerSyncType::Behind => PeerSyncStatus::Behind { info: info.clone() }, - PeerSyncType::Advanced => PeerSyncStatus::Advanced { info: info.clone() }, + PeerSyncType::Advanced(_) => PeerSyncStatus::Advanced { info: info.clone() }, + } + } +} + +pub enum PeerSyncTypeAdvanced { + Finalized { + target_slot: Slot, + target_root: Hash256, + start_epoch: Epoch, + }, + Head { + target_slot: Slot, + target_root: Hash256, + start_epoch: Epoch, + }, +} + +#[derive(Clone)] +pub(crate) struct LocalSyncInfo { + pub head_slot: Slot, + pub finalized_epoch: Epoch, + pub local_irreversible_epoch: Epoch, +} + +impl LocalSyncInfo { + pub fn new(chain: &BeaconChain) -> Self { + let status = chain.status_message(); + let local_finalized_checkpoint = chain.head().finalized_checkpoint().local(); + Self { + head_slot: *status.head_slot(), + finalized_epoch: *status.finalized_epoch(), + local_irreversible_epoch: local_finalized_checkpoint.epoch, } } } pub fn remote_sync_type( - local: &SyncInfo, + local: &LocalSyncInfo, remote: &SyncInfo, chain: &BeaconChain, ) -> PeerSyncType { @@ -33,6 +67,10 @@ pub fn remote_sync_type( let near_range_start = local.head_slot.saturating_sub(SLOT_IMPORT_TOLERANCE); let near_range_end = local.head_slot.saturating_add(SLOT_IMPORT_TOLERANCE); + // With the remote peer's status message let's figure out if there are enough blocks to discover + // that we trigger sync from them. We don't want to sync any blocks from epochs prior to the + // local irreversible epoch. Our finalized epoch may be less than the local irreversible epoch. + match remote.finalized_epoch.cmp(&local.finalized_epoch) { Ordering::Less => { // The node has a lower finalized epoch, their chain is not useful to us. There are two @@ -63,7 +101,11 @@ pub fn remote_sync_type( { // This peer has a head ahead enough of ours and we have no knowledge of their best // block. - PeerSyncType::Advanced + PeerSyncType::Advanced(PeerSyncTypeAdvanced::Head { + target_root: remote.head_root, + target_slot: remote.head_slot, + start_epoch: local.local_irreversible_epoch, + }) } else { // This peer is either in the tolerance range, or ahead us with an already rejected // block. @@ -71,16 +113,43 @@ pub fn remote_sync_type( } } Ordering::Greater => { - if (local.finalized_epoch + 1 == remote.finalized_epoch - && near_range_start <= remote.head_slot - && remote.head_slot <= near_range_end) - || chain.block_is_known_to_fork_choice(&remote.head_root) - { - // This peer is near enough to us to be considered synced, or - // we have already synced up to this peer's head + if chain.block_is_known_to_fork_choice(&remote.head_root) { + // We have already synced up to this peer's head PeerSyncType::FullySynced } else { - PeerSyncType::Advanced + let finality_advanced = remote.finalized_epoch > local.finalized_epoch + 1; + let head_advanced = remote.head_slot > near_range_end; + let finality_ahead_local_irreversible = + remote.finalized_epoch > local.local_irreversible_epoch; + + if finality_advanced { + if finality_ahead_local_irreversible { + PeerSyncType::Advanced(PeerSyncTypeAdvanced::Finalized { + target_root: remote.finalized_root, + target_slot: remote + .finalized_epoch + .start_slot(T::EthSpec::slots_per_epoch()), + start_epoch: local.local_irreversible_epoch, + }) + } else if head_advanced { + PeerSyncType::Advanced(PeerSyncTypeAdvanced::Head { + target_root: remote.head_root, + target_slot: remote.head_slot, + start_epoch: local.local_irreversible_epoch, + }) + } else { + PeerSyncType::FullySynced + } + } else if head_advanced { + PeerSyncType::Advanced(PeerSyncTypeAdvanced::Head { + target_root: remote.head_root, + target_slot: remote.head_slot, + start_epoch: local.local_irreversible_epoch, + }) + } else { + // This peer is near enough to us to be considered synced + PeerSyncType::FullySynced + } } } } diff --git a/beacon_node/network/src/sync/range_sync/chain_collection.rs b/beacon_node/network/src/sync/range_sync/chain_collection.rs index 1d57ee6c3dc..a234e3bb6b9 100644 --- a/beacon_node/network/src/sync/range_sync/chain_collection.rs +++ b/beacon_node/network/src/sync/range_sync/chain_collection.rs @@ -7,14 +7,14 @@ use super::chain::{ChainId, ProcessingResult, RemoveChain, SyncingChain}; use super::sync_type::RangeSyncType; use crate::metrics; use crate::sync::network_context::SyncNetworkContext; +use crate::sync::peer_sync_info::LocalSyncInfo; +use crate::sync::range_sync::range::AwaitingHeadPeers; use beacon_chain::{BeaconChain, BeaconChainTypes}; use fnv::FnvHashMap; use lighthouse_network::PeerId; -use lighthouse_network::SyncInfo; use lighthouse_network::service::api_types::Id; use logging::crit; use smallvec::SmallVec; -use std::collections::HashMap; use std::collections::hash_map::Entry; use std::sync::Arc; use tracing::{debug, error}; @@ -193,24 +193,18 @@ impl ChainCollection { pub fn update( &mut self, network: &mut SyncNetworkContext, - local: &SyncInfo, - awaiting_head_peers: &mut HashMap, + local: &LocalSyncInfo, + awaiting_head_peers: &mut AwaitingHeadPeers, ) { // Remove any outdated finalized/head chains self.purge_outdated_chains(local, awaiting_head_peers); - let local_head_epoch = local.head_slot.epoch(T::EthSpec::slots_per_epoch()); // Choose the best finalized chain if one needs to be selected. - self.update_finalized_chains(network, local.finalized_epoch, local_head_epoch); + self.update_finalized_chains(network, local); if !matches!(self.state, RangeSyncState::Finalized(_)) { // Handle head syncing chains if there are no finalized chains left. - self.update_head_chains( - network, - local.finalized_epoch, - local_head_epoch, - awaiting_head_peers, - ); + self.update_head_chains(network, local, awaiting_head_peers); } } @@ -253,8 +247,7 @@ impl ChainCollection { fn update_finalized_chains( &mut self, network: &mut SyncNetworkContext, - local_epoch: Epoch, - local_head_epoch: Epoch, + local: &LocalSyncInfo, ) { // Find the chain with most peers and check if it is already syncing if let Some((mut new_id, max_peers)) = self @@ -303,8 +296,11 @@ impl ChainCollection { // update the state to a new finalized state self.state = RangeSyncState::Finalized(new_id); - if let Err(remove_reason) = chain.start_syncing(network, local_epoch, local_head_epoch) - { + if let Err(remove_reason) = chain.start_syncing( + network, + local.local_irreversible_epoch, + local.head_slot.epoch(T::EthSpec::slots_per_epoch()), + ) { if remove_reason.is_critical() { crit!(chain = new_id, reason = ?remove_reason, "Chain removed while switching chains"); } else { @@ -321,17 +317,16 @@ impl ChainCollection { fn update_head_chains( &mut self, network: &mut SyncNetworkContext, - local_epoch: Epoch, - local_head_epoch: Epoch, - awaiting_head_peers: &mut HashMap, + local: &LocalSyncInfo, + awaiting_head_peers: &mut AwaitingHeadPeers, ) { // Include the awaiting head peers - for (peer_id, peer_sync_info) in awaiting_head_peers.drain() { + for (peer_id, (target_root, target_slot)) in awaiting_head_peers.drain() { debug!("including head peer"); self.add_peer_or_create_chain( - local_epoch, - peer_sync_info.head_root, - peer_sync_info.head_slot, + local.local_irreversible_epoch, + target_root, + target_slot, peer_id, RangeSyncType::Head, network, @@ -361,9 +356,11 @@ impl ChainCollection { if !chain.is_syncing() { debug!(id = chain.id(), "New head chain started syncing"); } - if let Err(remove_reason) = - chain.start_syncing(network, local_epoch, local_head_epoch) - { + if let Err(remove_reason) = chain.start_syncing( + network, + local.local_irreversible_epoch, + local.head_slot.epoch(T::EthSpec::slots_per_epoch()), + ) { self.head_chains.remove(&id); if remove_reason.is_critical() { crit!(chain = id, reason = ?remove_reason, "Chain removed while switching head chains"); @@ -396,8 +393,8 @@ impl ChainCollection { /// finalized block slot. Peers that would create outdated chains are removed too. pub fn purge_outdated_chains( &mut self, - local_info: &SyncInfo, - awaiting_head_peers: &mut HashMap, + local_info: &LocalSyncInfo, + awaiting_head_peers: &mut AwaitingHeadPeers, ) { let local_finalized_slot = local_info .finalized_epoch @@ -411,9 +408,8 @@ impl ChainCollection { }; // Retain only head peers that remain relevant - awaiting_head_peers.retain(|_peer_id, peer_sync_info| { - !is_outdated(&peer_sync_info.head_slot, &peer_sync_info.head_root) - }); + awaiting_head_peers + .retain(|_peer_id, (target_root, target_slot)| !is_outdated(target_slot, target_root)); // Remove chains that are out-dated let mut removed_chains = Vec::new(); diff --git a/beacon_node/network/src/sync/range_sync/range.rs b/beacon_node/network/src/sync/range_sync/range.rs index 465edd3697f..35470dcfa13 100644 --- a/beacon_node/network/src/sync/range_sync/range.rs +++ b/beacon_node/network/src/sync/range_sync/range.rs @@ -43,24 +43,26 @@ use super::chain::{BatchId, ChainId, RemoveChain, SyncingChain}; use super::chain_collection::{ChainCollection, SyncChainStatus}; use super::sync_type::RangeSyncType; use crate::metrics; -use crate::status::ToStatusMessage; use crate::sync::BatchProcessResult; use crate::sync::network_context::{RpcResponseError, SyncNetworkContext}; +use crate::sync::peer_sync_info::{LocalSyncInfo, PeerSyncTypeAdvanced}; use beacon_chain::block_verification_types::RpcBlock; use beacon_chain::{BeaconChain, BeaconChainTypes}; +use lighthouse_network::PeerId; use lighthouse_network::rpc::GoodbyeReason; use lighthouse_network::service::api_types::Id; -use lighthouse_network::{PeerId, SyncInfo}; use logging::crit; use lru_cache::LRUTimeCache; use std::collections::HashMap; use std::sync::Arc; use tracing::{debug, trace, warn}; -use types::{Epoch, EthSpec, Hash256}; +use types::{Epoch, EthSpec, Hash256, Slot}; /// For how long we store failed finalized chains to prevent retries. const FAILED_CHAINS_EXPIRY_SECONDS: u64 = 30; +pub(crate) type AwaitingHeadPeers = HashMap; + /// The primary object dealing with long range/batch syncing. This contains all the active and /// non-active chains that need to be processed before the syncing is considered complete. This /// holds the current state of the long range sync. @@ -69,7 +71,7 @@ pub struct RangeSync { beacon_chain: Arc>, /// Last known sync info of our useful connected peers. We use this information to create Head /// chains after all finalized chains have ended. - awaiting_head_peers: HashMap, + awaiting_head_peers: AwaitingHeadPeers, /// A collection of chains that need to be downloaded. This stores any head or finalized chains /// that need to be downloaded. chains: ChainCollection, @@ -109,29 +111,28 @@ where pub fn add_peer( &mut self, network: &mut SyncNetworkContext, - local_info: SyncInfo, + local_info: LocalSyncInfo, peer_id: PeerId, - remote_info: SyncInfo, + advanced_type: PeerSyncTypeAdvanced, ) { // evaluate which chain to sync from // determine if we need to run a sync to the nearest finalized state or simply sync to // its current head - // convenience variable - let remote_finalized_slot = remote_info - .finalized_epoch - .start_slot(T::EthSpec::slots_per_epoch()); - // NOTE: A peer that has been re-status'd may now exist in multiple finalized chains. This // is OK since we since only one finalized chain at a time. // determine which kind of sync to perform and set up the chains - match RangeSyncType::new(self.beacon_chain.as_ref(), &local_info, &remote_info) { - RangeSyncType::Finalized => { + match advanced_type { + PeerSyncTypeAdvanced::Finalized { + target_root, + target_slot, + start_epoch, + } => { // Make sure we have not recently tried this chain - if self.failed_chains.contains(&remote_info.finalized_root) { - debug!(failed_root = ?remote_info.finalized_root, %peer_id,"Disconnecting peer that belongs to previously failed chain"); + if self.failed_chains.contains(&target_root) { + debug!(failed_root = ?target_root, %peer_id,"Disconnecting peer that belongs to previously failed chain"); network.goodbye_peer(peer_id, GoodbyeReason::IrrelevantNetwork); return; } @@ -144,15 +145,14 @@ where // to using exact epoch boundaries for batches (rather than one slot past the epoch // boundary), we need to sync finalized sync to 2 epochs + 1 slot past our peer's // finalized slot in order to finalize the chain locally. - let target_head_slot = - remote_finalized_slot + (2 * T::EthSpec::slots_per_epoch()) + 1; + let target_head_slot = target_slot + (2 * T::EthSpec::slots_per_epoch()) + 1; // Note: We keep current head chains. These can continue syncing whilst we complete // this new finalized chain. self.chains.add_peer_or_create_chain( - local_info.finalized_epoch, - remote_info.finalized_root, + start_epoch, + target_root, target_head_slot, peer_id, RangeSyncType::Finalized, @@ -162,14 +162,19 @@ where self.chains .update(network, &local_info, &mut self.awaiting_head_peers); } - RangeSyncType::Head => { + PeerSyncTypeAdvanced::Head { + target_root, + target_slot, + start_epoch, + } => { // This peer requires a head chain sync if self.chains.is_finalizing_sync() { // If there are finalized chains to sync, finish these first, before syncing head // chains. trace!(%peer_id, awaiting_head_peers = &self.awaiting_head_peers.len(),"Waiting for finalized sync to complete"); - self.awaiting_head_peers.insert(peer_id, remote_info); + self.awaiting_head_peers + .insert(peer_id, (target_root, target_slot)); return; } @@ -180,12 +185,10 @@ where // The new peer has the same finalized (earlier filters should prevent a peer with an // earlier finalized chain from reaching here). - let start_epoch = std::cmp::min(local_info.head_slot, remote_finalized_slot) - .epoch(T::EthSpec::slots_per_epoch()); self.chains.add_peer_or_create_chain( start_epoch, - remote_info.head_root, - remote_info.head_slot, + target_root, + target_slot, peer_id, RangeSyncType::Head, network, @@ -356,16 +359,8 @@ where network.status_peers(self.beacon_chain.as_ref(), chain.peers()); - let status = self.beacon_chain.status_message(); - let local = SyncInfo { - head_slot: *status.head_slot(), - head_root: *status.head_root(), - finalized_epoch: *status.finalized_epoch(), - finalized_root: *status.finalized_root(), - earliest_available_slot: status.earliest_available_slot().ok().cloned(), - }; - // update the state of the collection + let local = LocalSyncInfo::new(&self.beacon_chain); self.chains .update(network, &local, &mut self.awaiting_head_peers); } From b27e4d3a6432943dfe8e3adb6070111f23aaa326 Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Mon, 20 Oct 2025 22:16:44 +0300 Subject: [PATCH 03/30] Hook manual finalization into fork-choice --- beacon_node/beacon_chain/src/beacon_chain.rs | 39 +--------- .../src/beacon_fork_choice_store.rs | 62 ++++++++++------ .../beacon_chain/src/canonical_head.rs | 73 ++++++++++++++++++- beacon_node/beacon_chain/src/errors.rs | 5 +- beacon_node/http_api/src/lib.rs | 2 +- consensus/fork_choice/src/fork_choice.rs | 12 +++ .../fork_choice/src/fork_choice_store.rs | 6 ++ 7 files changed, 135 insertions(+), 64 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 06c6bf7d497..0cc821eb3f1 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -41,7 +41,7 @@ use crate::light_client_optimistic_update_verification::{ Error as LightClientOptimisticUpdateError, VerifiedLightClientOptimisticUpdate, }; use crate::light_client_server_cache::LightClientServerCache; -use crate::migrate::{BackgroundMigrator, ManualFinalizationNotification}; +use crate::migrate::BackgroundMigrator; use crate::naive_aggregation_pool::{ AggregatedAttestationMap, Error as NaiveAggregationError, NaiveAggregationPool, SyncContributionAggregateMap, @@ -122,8 +122,8 @@ use std::sync::Arc; use std::time::Duration; use store::iter::{BlockRootsIterator, ParentRootBlockIterator, StateRootsIterator}; use store::{ - BlobSidecarListFromRoot, DBColumn, DatabaseBlock, Error as DBError, HotColdDB, HotStateSummary, - KeyValueStore, KeyValueStoreOp, StoreItem, StoreOp, + BlobSidecarListFromRoot, DBColumn, DatabaseBlock, Error as DBError, HotColdDB, KeyValueStore, + KeyValueStoreOp, StoreItem, StoreOp, }; use task_executor::{RayonPoolType, ShutdownReason, TaskExecutor}; use tokio_stream::Stream; @@ -1681,39 +1681,6 @@ impl BeaconChain { self.store_migrator.process_manual_compaction(); } - pub fn manually_finalize_state( - &self, - state_root: Hash256, - checkpoint: Checkpoint, - ) -> Result<(), Error> { - let HotStateSummary { - slot, - latest_block_root, - .. - } = self - .store - .load_hot_state_summary(&state_root) - .map_err(BeaconChainError::DBError)? - .ok_or(BeaconChainError::MissingHotStateSummary(state_root))?; - - if slot != checkpoint.epoch.start_slot(T::EthSpec::slots_per_epoch()) - || latest_block_root != *checkpoint.root - { - return Err(BeaconChainError::InvalidCheckpoint { - state_root, - checkpoint, - }); - } - - let notif = ManualFinalizationNotification { - state_root: state_root.into(), - checkpoint, - }; - - self.store_migrator.process_manual_finalization(notif); - Ok(()) - } - /// Returns an aggregated `Attestation`, if any, that has a matching `attestation.data`. /// /// The attestation will be obtained from `self.naive_aggregation_pool`. diff --git a/beacon_node/beacon_chain/src/beacon_fork_choice_store.rs b/beacon_node/beacon_chain/src/beacon_fork_choice_store.rs index 012fde54610..d6fbdd0f8b5 100644 --- a/beacon_node/beacon_chain/src/beacon_fork_choice_store.rs +++ b/beacon_node/beacon_chain/src/beacon_fork_choice_store.rs @@ -294,6 +294,34 @@ where _phantom: PhantomData, }) } + + fn update_justified_balances( + &mut self, + checkpoint: Checkpoint, + justified_state_root: Hash256, + ) -> Result<(), Error> { + self.justified_state_root = justified_state_root; + + if let Some(balances) = self.balances_cache.get(checkpoint.root, checkpoint.epoch) { + // NOTE: could avoid this re-calculation by introducing a `PersistedCacheItem`. + metrics::inc_counter(&metrics::BALANCES_CACHE_HITS); + self.justified_balances = JustifiedBalances::from_effective_balances(balances)?; + } else { + metrics::inc_counter(&metrics::BALANCES_CACHE_MISSES); + + // Justified state is reasonably useful to cache, it might be finalized soon. + let update_cache = true; + let state = self + .store + .get_hot_state(&self.justified_state_root, update_cache) + .map_err(Error::FailedToReadState)? + .ok_or(Error::MissingState(self.justified_state_root))?; + + self.justified_balances = JustifiedBalances::from_justified_state(&state)?; + } + + Ok(()) + } } impl ForkChoiceStore for BeaconForkChoiceStore @@ -377,27 +405,7 @@ where 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(checkpoint.root, checkpoint.epoch) { - // NOTE: could avoid this re-calculation by introducing a `PersistedCacheItem`. - metrics::inc_counter(&metrics::BALANCES_CACHE_HITS); - self.justified_balances = JustifiedBalances::from_effective_balances(balances)?; - } else { - metrics::inc_counter(&metrics::BALANCES_CACHE_MISSES); - - // Justified state is reasonably useful to cache, it might be finalized soon. - let update_cache = true; - let state = self - .store - .get_hot_state(&self.justified_state_root, update_cache) - .map_err(Error::FailedToReadState)? - .ok_or(Error::MissingState(self.justified_state_root))?; - - self.justified_balances = JustifiedBalances::from_justified_state(&state)?; - } - - Ok(()) + self.update_justified_balances(checkpoint, justified_state_root) } fn set_unrealized_justified_checkpoint(&mut self, checkpoint: Checkpoint, state_root: Hash256) { @@ -409,6 +417,18 @@ where self.unrealized_finalized_checkpoint = checkpoint; } + fn set_local_irreversible_checkpoint( + &mut self, + checkpoint: Checkpoint, + state_root: Hash256, + ) -> Result<(), Error> { + self.local_irreversible_checkpoint = checkpoint; + if self.local_irreversible_checkpoint.epoch > self.justified_checkpoint.epoch { + self.update_justified_balances(checkpoint, state_root)?; + } + Ok(()) + } + fn set_proposer_boost_root(&mut self, proposer_boost_root: Hash256) { self.proposer_boost_root = proposer_boost_root; } diff --git a/beacon_node/beacon_chain/src/canonical_head.rs b/beacon_node/beacon_chain/src/canonical_head.rs index c8d8f7c3fe0..bc45d4f12fc 100644 --- a/beacon_node/beacon_chain/src/canonical_head.rs +++ b/beacon_node/beacon_chain/src/canonical_head.rs @@ -1,4 +1,4 @@ -//! This module provides all functionality for finding the canonical head, updating all necessary +//! This module prvides all functionality for finding the canonical head, updating all necessary //! components (e.g. caches) and maintaining a cached head block and state. //! //! For practically all applications, the "canonical head" can be read using @@ -55,7 +55,8 @@ use state_processing::AllCaches; use std::sync::Arc; use std::time::Duration; use store::{ - Error as StoreError, KeyValueStore, KeyValueStoreOp, StoreConfig, iter::StateRootsIterator, + Error as StoreError, HotStateSummary, KeyValueStore, KeyValueStoreOp, StoreConfig, + iter::StateRootsIterator, }; use task_executor::{JoinHandle, ShutdownReason}; use tracing::info_span; @@ -1054,6 +1055,74 @@ impl BeaconChain { Ok(()) } + pub fn manual_finalization( + self: &Arc, + checkpoint: Checkpoint, + // TODO: Should we derive the state_root from the checkpoint? + state_root: Hash256, + ) -> Result<(), Error> { + let HotStateSummary { + slot, + latest_block_root, + .. + } = self + .store + .load_hot_state_summary(&state_root) + .map_err(Error::DBError)? + .ok_or(Error::MissingHotStateSummary(state_root))?; + + if slot != checkpoint.epoch.start_slot(T::EthSpec::slots_per_epoch()) { + return Err(Error::InvalidCheckpoint(format!( + "state {state_root:?} slot {slot} not first slot of checkpoint {checkpoint:?}" + ))); + } + if latest_block_root != *checkpoint.root { + return Err(Error::InvalidCheckpoint(format!( + "state {state_root:?} not a post state of checkpoint {checkpoint:?}" + ))); + } + + // Take a clone of the current ("old") head. + let old_cached_head = self.canonical_head.cached_head(); + + let new_view = { + let mut fork_choice_write_lock = self.canonical_head.fork_choice_write_lock(); + fork_choice_write_lock.set_local_irreversible_checkpoint(checkpoint, state_root)?; + let fork_choice_read_lock = RwLockWriteGuard::downgrade(fork_choice_write_lock); + ForkChoiceView { + head_block_root: old_cached_head.head_block_root(), + justified_checkpoint: fork_choice_read_lock.justified_checkpoint(), + finalized_checkpoint: fork_choice_read_lock.finalized_checkpoint(), + } + }; + + let new_cached_head = { + let fork_choice = self.canonical_head.fork_choice_read_lock(); + let new_cached_head = CachedHead { + // The head hasn't changed, take a relatively cheap `Arc`-clone of the existing + // head. + snapshot: old_cached_head.snapshot.clone(), + justified_checkpoint: new_view.justified_checkpoint, + finalized_checkpoint: new_view.finalized_checkpoint, + head_hash: fork_choice.execution_hash(old_cached_head.head_block_root()), + justified_hash: fork_choice + .execution_hash(new_view.justified_checkpoint.on_chain().root), + finalized_hash: fork_choice + .execution_hash(new_view.finalized_checkpoint.on_chain().root), + }; + drop(fork_choice); + + let mut cached_head_write_lock = self.canonical_head.cached_head_write_lock(); + *cached_head_write_lock = new_cached_head; + // Take a clone of the cached head for later use. It is cloned whilst + // holding the write-lock to ensure we get exactly the head we just enshrined. + cached_head_write_lock.clone() + }; + + self.after_finalization(&new_cached_head, new_view)?; + Ok(()) + } + /// 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); diff --git a/beacon_node/beacon_chain/src/errors.rs b/beacon_node/beacon_chain/src/errors.rs index 7b04a36faec..eb60ec76b46 100644 --- a/beacon_node/beacon_chain/src/errors.rs +++ b/beacon_node/beacon_chain/src/errors.rs @@ -181,10 +181,7 @@ pub enum BeaconChainError { execution_block_hash: Option, }, ForkchoiceUpdate(execution_layer::Error), - InvalidCheckpoint { - state_root: Hash256, - checkpoint: Checkpoint, - }, + InvalidCheckpoint(String), InvalidSlot(Slot), HeadBlockNotFullyVerified { beacon_block_root: Hash256, diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index 7f6c97a0f85..d5c3ada4637 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -4170,7 +4170,7 @@ pub fn serve( }; chain - .manually_finalize_state(request_data.state_root, checkpoint) + .manual_finalization(checkpoint, request_data.state_root) .map(|_| api_types::GenericResponse::from(request_data)) .map_err(|e| { warp_utils::reject::custom_bad_request(format!( diff --git a/consensus/fork_choice/src/fork_choice.rs b/consensus/fork_choice/src/fork_choice.rs index ae9eef3b60d..f547eaa6751 100644 --- a/consensus/fork_choice/src/fork_choice.rs +++ b/consensus/fork_choice/src/fork_choice.rs @@ -1322,6 +1322,18 @@ where *self.fc_store.unrealized_finalized_checkpoint() } + /// TODO: Document + pub fn set_local_irreversible_checkpoint( + &mut self, + checkpoint: Checkpoint, + state_root: Hash256, + ) -> Result<(), Error> { + self.fc_store + .set_local_irreversible_checkpoint(checkpoint, state_root) + .map_err(Error::UnableToSetJustifiedCheckpoint)?; + Ok(()) + } + /// Returns the latest message for a given validator, if any. /// /// Returns `(block_root, block_slot)`. diff --git a/consensus/fork_choice/src/fork_choice_store.rs b/consensus/fork_choice/src/fork_choice_store.rs index af4addbc937..f3c86596b8d 100644 --- a/consensus/fork_choice/src/fork_choice_store.rs +++ b/consensus/fork_choice/src/fork_choice_store.rs @@ -76,6 +76,12 @@ pub trait ForkChoiceStore: Sized { state_root: Hash256, ) -> Result<(), Self::Error>; + fn set_local_irreversible_checkpoint( + &mut self, + checkpoint: Checkpoint, + state_root: Hash256, + ) -> Result<(), Self::Error>; + /// Sets the `unrealized_justified_checkpoint`. fn set_unrealized_justified_checkpoint(&mut self, checkpoint: Checkpoint, state_root: Hash256); From f91a9c28c066a4c529b53d22ebf9e388c42f5989 Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Sat, 25 Oct 2025 14:42:10 +0300 Subject: [PATCH 04/30] Set ProtoNode checkpoints to on_chain checkpoints --- .../beacon_chain/src/beacon_fork_choice_store.rs | 11 +++++++---- consensus/fork_choice/src/fork_choice.rs | 6 ++++-- .../proto_array/src/fork_choice_test_definition.rs | 4 ++++ consensus/proto_array/src/proto_array.rs | 7 +++++-- consensus/proto_array/src/proto_array_fork_choice.rs | 6 +++++- consensus/proto_array/src/ssz_container.rs | 5 +++++ 6 files changed, 30 insertions(+), 9 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_fork_choice_store.rs b/beacon_node/beacon_chain/src/beacon_fork_choice_store.rs index d6fbdd0f8b5..d1aac1ecd32 100644 --- a/beacon_node/beacon_chain/src/beacon_fork_choice_store.rs +++ b/beacon_node/beacon_chain/src/beacon_fork_choice_store.rs @@ -197,18 +197,21 @@ where let justified_balances = JustifiedBalances::from_justified_state(&anchor_state)?; let anchor_state_root = anchor_state.canonical_root()?; + let justified_checkpoint_on_chain = anchor_state.current_justified_checkpoint(); + let finalized_checkpoint_on_chain = anchor_state.finalized_checkpoint(); + Ok(Self { store, balances_cache: <_>::default(), time: anchor_state.slot(), - justified_checkpoint: anchor_state.current_justified_checkpoint(), + justified_checkpoint: justified_checkpoint_on_chain, justified_balances, justified_state_root: anchor_state_root, - finalized_checkpoint: anchor_state.finalized_checkpoint(), + finalized_checkpoint: finalized_checkpoint_on_chain, local_irreversible_checkpoint: anchor_block_checkpoint, - unrealized_justified_checkpoint: anchor_block_checkpoint, + unrealized_justified_checkpoint: justified_checkpoint_on_chain, unrealized_justified_state_root: anchor_state_root, - unrealized_finalized_checkpoint: anchor_block_checkpoint, + unrealized_finalized_checkpoint: finalized_checkpoint_on_chain, proposer_boost_root: Hash256::zero(), equivocating_indices: BTreeSet::new(), _phantom: PhantomData, diff --git a/consensus/fork_choice/src/fork_choice.rs b/consensus/fork_choice/src/fork_choice.rs index f547eaa6751..78cc60efa1b 100644 --- a/consensus/fork_choice/src/fork_choice.rs +++ b/consensus/fork_choice/src/fork_choice.rs @@ -421,7 +421,8 @@ where current_slot, finalized_block_slot, finalized_block_state_root, - fc_store.justified_checkpoint().local(), + fc_store.justified_checkpoint().on_chain(), + fc_store.finalized_checkpoint().on_chain(), fc_store.finalized_checkpoint().local(), current_epoch_shuffling_id, next_epoch_shuffling_id, @@ -504,7 +505,8 @@ where let store = &mut self.fc_store; let head_root = self.proto_array.find_head::( - store.justified_checkpoint().local(), + store.justified_checkpoint().on_chain(), + store.finalized_checkpoint().on_chain(), store.finalized_checkpoint().local(), store.justified_balances(), store.proposer_boost_root(), diff --git a/consensus/proto_array/src/fork_choice_test_definition.rs b/consensus/proto_array/src/fork_choice_test_definition.rs index 20987dff26d..1da08c3a690 100644 --- a/consensus/proto_array/src/fork_choice_test_definition.rs +++ b/consensus/proto_array/src/fork_choice_test_definition.rs @@ -85,6 +85,7 @@ impl ForkChoiceTestDefinition { Hash256::zero(), self.justified_checkpoint, self.finalized_checkpoint, + self.finalized_checkpoint, junk_shuffling_id.clone(), junk_shuffling_id, ExecutionStatus::Optimistic(ExecutionBlockHash::zero()), @@ -107,6 +108,7 @@ impl ForkChoiceTestDefinition { .find_head::( justified_checkpoint, finalized_checkpoint, + finalized_checkpoint, &justified_balances, Hash256::zero(), &equivocating_indices, @@ -138,6 +140,7 @@ impl ForkChoiceTestDefinition { .find_head::( justified_checkpoint, finalized_checkpoint, + finalized_checkpoint, &justified_balances, proposer_boost_root, &equivocating_indices, @@ -166,6 +169,7 @@ impl ForkChoiceTestDefinition { let result = fork_choice.find_head::( justified_checkpoint, finalized_checkpoint, + finalized_checkpoint, &justified_balances, Hash256::zero(), &equivocating_indices, diff --git a/consensus/proto_array/src/proto_array.rs b/consensus/proto_array/src/proto_array.rs index 18af2dfc24c..f925a321e8f 100644 --- a/consensus/proto_array/src/proto_array.rs +++ b/consensus/proto_array/src/proto_array.rs @@ -132,6 +132,7 @@ pub struct ProtoArray { pub prune_threshold: usize, pub justified_checkpoint: Checkpoint, pub finalized_checkpoint: Checkpoint, + pub local_irreversible_checkpoint: Checkpoint, pub nodes: Vec, pub indices: HashMap, pub previous_proposer_boost: ProposerBoost, @@ -157,6 +158,7 @@ impl ProtoArray { mut deltas: Vec, justified_checkpoint: Checkpoint, finalized_checkpoint: Checkpoint, + local_irreversible_checkpoint: Checkpoint, new_justified_balances: &JustifiedBalances, proposer_boost_root: Hash256, current_slot: Slot, @@ -174,6 +176,7 @@ impl ProtoArray { { self.justified_checkpoint = justified_checkpoint; self.finalized_checkpoint = finalized_checkpoint; + self.local_irreversible_checkpoint = local_irreversible_checkpoint; } // Default the proposer boost score to zero. @@ -962,9 +965,9 @@ impl ProtoArray { /// Notably, this function is checking ancestory of the finalized /// *checkpoint* not the finalized *block*. pub fn is_finalized_checkpoint_or_descendant(&self, root: Hash256) -> bool { - let finalized_root = self.finalized_checkpoint.root; + let finalized_root = self.local_irreversible_checkpoint.root; let finalized_slot = self - .finalized_checkpoint + .local_irreversible_checkpoint .epoch .start_slot(E::slots_per_epoch()); diff --git a/consensus/proto_array/src/proto_array_fork_choice.rs b/consensus/proto_array/src/proto_array_fork_choice.rs index dea853d245d..f794f57880d 100644 --- a/consensus/proto_array/src/proto_array_fork_choice.rs +++ b/consensus/proto_array/src/proto_array_fork_choice.rs @@ -418,6 +418,7 @@ impl ProtoArrayForkChoice { finalized_block_state_root: Hash256, justified_checkpoint: Checkpoint, finalized_checkpoint: Checkpoint, + local_irreversible_checkpoint: Checkpoint, current_epoch_shuffling_id: AttestationShufflingId, next_epoch_shuffling_id: AttestationShufflingId, execution_status: ExecutionStatus, @@ -426,6 +427,7 @@ impl ProtoArrayForkChoice { prune_threshold: DEFAULT_PRUNE_THRESHOLD, justified_checkpoint, finalized_checkpoint, + local_irreversible_checkpoint, nodes: Vec::with_capacity(1), indices: HashMap::with_capacity(1), previous_proposer_boost: ProposerBoost::default(), @@ -514,6 +516,7 @@ impl ProtoArrayForkChoice { &mut self, justified_checkpoint: Checkpoint, finalized_checkpoint: Checkpoint, + local_irreversible_checkpoint: Checkpoint, justified_state_balances: &JustifiedBalances, proposer_boost_root: Hash256, equivocating_indices: &BTreeSet, @@ -537,6 +540,7 @@ impl ProtoArrayForkChoice { deltas, justified_checkpoint, finalized_checkpoint, + local_irreversible_checkpoint, new_balances, proposer_boost_root, current_slot, @@ -547,7 +551,7 @@ impl ProtoArrayForkChoice { *old_balances = new_balances.clone(); self.proto_array - .find_head::(&justified_checkpoint.root, current_slot) + .find_head::(&local_irreversible_checkpoint.root, current_slot) .map_err(|e| format!("find_head failed: {:?}", e)) } diff --git a/consensus/proto_array/src/ssz_container.rs b/consensus/proto_array/src/ssz_container.rs index 0bb3f2b35d8..d34bfa6c3e2 100644 --- a/consensus/proto_array/src/ssz_container.rs +++ b/consensus/proto_array/src/ssz_container.rs @@ -28,6 +28,7 @@ pub struct SszContainer { pub prune_threshold: usize, pub justified_checkpoint: Checkpoint, pub finalized_checkpoint: Checkpoint, + pub local_irreversible_checkpoint: Checkpoint, pub nodes: Vec, pub indices: Vec<(Hash256, usize)>, pub previous_proposer_boost: ProposerBoost, @@ -42,6 +43,7 @@ impl From<&ProtoArrayForkChoice> for SszContainer { prune_threshold: proto_array.prune_threshold, justified_checkpoint: proto_array.justified_checkpoint, finalized_checkpoint: proto_array.finalized_checkpoint, + local_irreversible_checkpoint: proto_array.local_irreversible_checkpoint, nodes: proto_array.nodes.clone(), indices: proto_array.indices.iter().map(|(k, v)| (*k, *v)).collect(), previous_proposer_boost: proto_array.previous_proposer_boost, @@ -57,6 +59,7 @@ impl TryFrom<(SszContainer, JustifiedBalances)> for ProtoArrayForkChoice { prune_threshold: from.prune_threshold, justified_checkpoint: from.justified_checkpoint, finalized_checkpoint: from.finalized_checkpoint, + local_irreversible_checkpoint: from.local_irreversible_checkpoint, nodes: from.nodes, indices: from.indices.into_iter().collect::>(), previous_proposer_boost: from.previous_proposer_boost, @@ -78,6 +81,7 @@ impl From for SszContainerV28 { prune_threshold: v17.prune_threshold, justified_checkpoint: v17.justified_checkpoint, finalized_checkpoint: v17.finalized_checkpoint, + local_irreversible_checkpoint: v17.finalized_checkpoint, nodes: v17.nodes, indices: v17.indices, previous_proposer_boost: v17.previous_proposer_boost, @@ -94,6 +98,7 @@ impl From<(SszContainerV28, JustifiedBalances)> for SszContainerV17 { prune_threshold: v28.prune_threshold, justified_checkpoint: v28.justified_checkpoint, finalized_checkpoint: v28.finalized_checkpoint, + local_irreversible_checkpoint: v28.finalized_checkpoint, nodes: v28.nodes, indices: v28.indices, previous_proposer_boost: v28.previous_proposer_boost, From ff431acdc704fb46b025d4b687d2e5637b039f65 Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Sat, 25 Oct 2025 15:13:26 +0300 Subject: [PATCH 05/30] Initialize first node with correct root --- consensus/fork_choice/src/fork_choice.rs | 8 ++++---- .../proto_array/src/proto_array_fork_choice.rs | 14 ++++++++------ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/consensus/fork_choice/src/fork_choice.rs b/consensus/fork_choice/src/fork_choice.rs index 78cc60efa1b..e35cea56fb1 100644 --- a/consensus/fork_choice/src/fork_choice.rs +++ b/consensus/fork_choice/src/fork_choice.rs @@ -389,8 +389,8 @@ where }); } - let finalized_block_slot = anchor_block.slot(); - let finalized_block_state_root = anchor_block.state_root(); + let anchor_block_slot = anchor_block.slot(); + let anchor_block_state_root = anchor_block.state_root(); let current_epoch_shuffling_id = AttestationShufflingId::new(anchor_block_root, anchor_state, RelativeEpoch::Current) .map_err(Error::BeaconStateError)?; @@ -419,8 +419,8 @@ where let proto_array = ProtoArrayForkChoice::new::( current_slot, - finalized_block_slot, - finalized_block_state_root, + anchor_block_slot, + anchor_block_state_root, fc_store.justified_checkpoint().on_chain(), fc_store.finalized_checkpoint().on_chain(), fc_store.finalized_checkpoint().local(), diff --git a/consensus/proto_array/src/proto_array_fork_choice.rs b/consensus/proto_array/src/proto_array_fork_choice.rs index f794f57880d..06d7ce60c4c 100644 --- a/consensus/proto_array/src/proto_array_fork_choice.rs +++ b/consensus/proto_array/src/proto_array_fork_choice.rs @@ -414,8 +414,8 @@ impl ProtoArrayForkChoice { #[allow(clippy::too_many_arguments)] pub fn new( current_slot: Slot, - finalized_block_slot: Slot, - finalized_block_state_root: Hash256, + anchor_block_slot: Slot, + anchor_block_state_root: Hash256, justified_checkpoint: Checkpoint, finalized_checkpoint: Checkpoint, local_irreversible_checkpoint: Checkpoint, @@ -434,13 +434,13 @@ impl ProtoArrayForkChoice { }; let block = Block { - slot: finalized_block_slot, - root: finalized_checkpoint.root, + slot: anchor_block_slot, + root: local_irreversible_checkpoint.root, parent_root: None, - state_root: finalized_block_state_root, + state_root: anchor_block_state_root, // We are using the finalized_root as the target_root, since it always lies on an // epoch boundary. - target_root: finalized_checkpoint.root, + target_root: local_irreversible_checkpoint.root, current_epoch_shuffling_id, next_epoch_shuffling_id, justified_checkpoint, @@ -1105,6 +1105,7 @@ mod test_compute_deltas { state_root, genesis_checkpoint, genesis_checkpoint, + genesis_checkpoint, junk_shuffling_id.clone(), junk_shuffling_id.clone(), execution_status, @@ -1231,6 +1232,7 @@ mod test_compute_deltas { junk_state_root, genesis_checkpoint, genesis_checkpoint, + genesis_checkpoint, junk_shuffling_id.clone(), junk_shuffling_id.clone(), execution_status, From df7c477bfea4d93eaa2ae6d5ccca0a46f5d13818 Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Wed, 29 Oct 2025 13:37:36 +0300 Subject: [PATCH 06/30] Lint tests --- .../beacon_chain/src/canonical_head.rs | 28 +++++++++---------- .../beacon_chain/tests/block_verification.rs | 6 ++-- .../tests/payload_invalidation.rs | 6 ++-- beacon_node/beacon_chain/tests/store_tests.rs | 3 +- beacon_node/beacon_chain/tests/tests.rs | 2 +- beacon_node/http_api/tests/tests.rs | 2 ++ 6 files changed, 24 insertions(+), 23 deletions(-) diff --git a/beacon_node/beacon_chain/src/canonical_head.rs b/beacon_node/beacon_chain/src/canonical_head.rs index bc45d4f12fc..5b22dee5af4 100644 --- a/beacon_node/beacon_chain/src/canonical_head.rs +++ b/beacon_node/beacon_chain/src/canonical_head.rs @@ -978,24 +978,22 @@ impl BeaconChain { if let Some(event_handler) = self.event_handler.as_ref() && event_handler.has_finalized_subscribers() - { - if let Some(on_chain_finalized_block) = self + && let Some(on_chain_finalized_block) = self .canonical_head .fork_choice_read_lock() .get_block(&on_chain_finalized_checkpoint.root) - { - event_handler.register(EventKind::FinalizedCheckpoint(SseFinalizedCheckpoint { - epoch: on_chain_finalized_checkpoint.epoch, - block: on_chain_finalized_checkpoint.root, - // Provide the state root of the latest finalized block, rather than the - // specific state root at the first slot of the finalized epoch (which - // might be a skip slot). - state: on_chain_finalized_block.state_root, - execution_optimistic: on_chain_finalized_block - .execution_status - .is_optimistic_or_invalid(), - })); - } + { + event_handler.register(EventKind::FinalizedCheckpoint(SseFinalizedCheckpoint { + epoch: on_chain_finalized_checkpoint.epoch, + block: on_chain_finalized_checkpoint.root, + // Provide the state root of the latest finalized block, rather than the + // specific state root at the first slot of the finalized epoch (which + // might be a skip slot). + state: on_chain_finalized_block.state_root, + execution_optimistic: on_chain_finalized_block + .execution_status + .is_optimistic_or_invalid(), + })); } // The store migration task and op pool pruning require the *state at the first slot of the diff --git a/beacon_node/beacon_chain/tests/block_verification.rs b/beacon_node/beacon_chain/tests/block_verification.rs index 47f5be02cb4..e49a52b2df4 100644 --- a/beacon_node/beacon_chain/tests/block_verification.rs +++ b/beacon_node/beacon_chain/tests/block_verification.rs @@ -1771,7 +1771,7 @@ async fn import_duplicate_block_unrealized_justification() { // must be at epoch 1. { let fc = chain.canonical_head.fork_choice_read_lock(); - assert_eq!(fc.justified_checkpoint().epoch, 0); + assert_eq!(fc.justified_checkpoint().on_chain().epoch, 0); assert_eq!(fc.unrealized_justified_checkpoint().epoch, 1); drop(fc); } @@ -1803,7 +1803,7 @@ async fn import_duplicate_block_unrealized_justification() { // The store's global unrealized justification should update immediately and match the block. let unrealized_justification = { let fc = chain.canonical_head.fork_choice_read_lock(); - assert_eq!(fc.justified_checkpoint().epoch, 0); + assert_eq!(fc.justified_checkpoint().on_chain().epoch, 0); let unrealized_justification = fc.unrealized_justified_checkpoint(); assert_eq!(unrealized_justification.epoch, 2); // The fork choice node for the block should have unrealized justification. @@ -1826,7 +1826,7 @@ async fn import_duplicate_block_unrealized_justification() { // Unrealized justification should still be updated. let fc3 = chain.canonical_head.fork_choice_read_lock(); - assert_eq!(fc3.justified_checkpoint().epoch, 0); + assert_eq!(fc3.justified_checkpoint().on_chain().epoch, 0); assert_eq!( fc3.unrealized_justified_checkpoint(), unrealized_justification diff --git a/beacon_node/beacon_chain/tests/payload_invalidation.rs b/beacon_node/beacon_chain/tests/payload_invalidation.rs index 5bd43835e33..25357ecee6e 100644 --- a/beacon_node/beacon_chain/tests/payload_invalidation.rs +++ b/beacon_node/beacon_chain/tests/payload_invalidation.rs @@ -1144,9 +1144,9 @@ async fn payload_preparation_before_transition_block() { let forkchoice_update_params = rig .harness .chain + .head() .canonical_head - .fork_choice_read_lock() - .get_forkchoice_update_parameters(); + .forkchoice_update_parameters(); rig.harness .chain .update_execution_engine_forkchoice( @@ -1294,7 +1294,7 @@ impl InvalidHeadSetup { // Import blocks until the first time the chain finalizes. This avoids // some edge-cases around genesis. - while rig.cached_head().finalized_checkpoint().epoch == 0 { + while rig.cached_head().finalized_checkpoint().on_chain().epoch == 0 { rig.import_block(Payload::Syncing).await; } diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index 449b5dd0434..14ea611508b 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -168,7 +168,8 @@ async fn light_client_bootstrap_test() { .chain .canonical_head .cached_head() - .finalized_checkpoint(); + .finalized_checkpoint() + .on_chain(); let block_root = finalized_checkpoint.root; diff --git a/beacon_node/beacon_chain/tests/tests.rs b/beacon_node/beacon_chain/tests/tests.rs index ec0e607d00a..b33e05ce0ab 100644 --- a/beacon_node/beacon_chain/tests/tests.rs +++ b/beacon_node/beacon_chain/tests/tests.rs @@ -957,7 +957,7 @@ async fn pseudo_finalize_test_generic( // pseudo finalize harness .chain - .manually_finalize_state(head.beacon_state_root(), checkpoint) + .manual_finalization(checkpoint, head.beacon_state_root()) .unwrap(); let split = harness.chain.store.get_split_info(); diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 9c18a7c1e87..216c53a5327 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -263,6 +263,7 @@ impl ApiTester { .canonical_head .cached_head() .finalized_checkpoint() + .on_chain() .epoch, 2, "precondition: finality" @@ -272,6 +273,7 @@ impl ApiTester { .canonical_head .cached_head() .justified_checkpoint() + .on_chain() .epoch, 3, "precondition: justification" From 18215404ed975ff1a7de51dc2530695f509ae934 Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Wed, 29 Oct 2025 14:18:13 +0300 Subject: [PATCH 07/30] Fix tests --- .../beacon_chain/src/beacon_fork_choice_store.rs | 12 ++++++++++-- beacon_node/beacon_chain/src/builder.rs | 9 ++++++++- consensus/fork_choice/src/fork_choice.rs | 1 + .../proto_array/src/fork_choice_test_definition.rs | 1 + consensus/proto_array/src/proto_array_fork_choice.rs | 8 ++++---- 5 files changed, 24 insertions(+), 7 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_fork_choice_store.rs b/beacon_node/beacon_chain/src/beacon_fork_choice_store.rs index d1aac1ecd32..7dc0761ddf6 100644 --- a/beacon_node/beacon_chain/src/beacon_fork_choice_store.rs +++ b/beacon_node/beacon_chain/src/beacon_fork_choice_store.rs @@ -197,8 +197,16 @@ where let justified_balances = JustifiedBalances::from_justified_state(&anchor_state)?; let anchor_state_root = anchor_state.canonical_root()?; - let justified_checkpoint_on_chain = anchor_state.current_justified_checkpoint(); - let finalized_checkpoint_on_chain = anchor_state.finalized_checkpoint(); + let mut justified_checkpoint_on_chain = anchor_state.current_justified_checkpoint(); + let mut finalized_checkpoint_on_chain = anchor_state.finalized_checkpoint(); + + // If the network has not justified or finalized yet, use anchor checkpoint + if justified_checkpoint_on_chain.root == Hash256::ZERO { + justified_checkpoint_on_chain = anchor_block_checkpoint; + } + if finalized_checkpoint_on_chain.root == Hash256::ZERO { + finalized_checkpoint_on_chain = anchor_block_checkpoint; + } Ok(Self { store, diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index 3aa8843de82..15314597971 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -787,7 +787,14 @@ where let (_head_state_root, head_state) = store .get_advanced_hot_state(head_block_root, current_slot, head_block.state_root()) .map_err(|e| descriptive_db_error("head state", &e))? - .ok_or("Head state not found in store")?; + .ok_or_else(|| { + format!( + "Head state not found in store block_root {:?} slot {} state_root {:?}", + head_block_root, + current_slot, + head_block.state_root() + ) + })?; // If the head reverted then we need to reset fork choice using the new head's finalized // checkpoint. diff --git a/consensus/fork_choice/src/fork_choice.rs b/consensus/fork_choice/src/fork_choice.rs index e35cea56fb1..bda69e4b4ec 100644 --- a/consensus/fork_choice/src/fork_choice.rs +++ b/consensus/fork_choice/src/fork_choice.rs @@ -420,6 +420,7 @@ where let proto_array = ProtoArrayForkChoice::new::( current_slot, anchor_block_slot, + anchor_block_root, anchor_block_state_root, fc_store.justified_checkpoint().on_chain(), fc_store.finalized_checkpoint().on_chain(), diff --git a/consensus/proto_array/src/fork_choice_test_definition.rs b/consensus/proto_array/src/fork_choice_test_definition.rs index 1da08c3a690..2a8953a3915 100644 --- a/consensus/proto_array/src/fork_choice_test_definition.rs +++ b/consensus/proto_array/src/fork_choice_test_definition.rs @@ -83,6 +83,7 @@ impl ForkChoiceTestDefinition { self.finalized_block_slot, self.finalized_block_slot, Hash256::zero(), + Hash256::zero(), self.justified_checkpoint, self.finalized_checkpoint, self.finalized_checkpoint, diff --git a/consensus/proto_array/src/proto_array_fork_choice.rs b/consensus/proto_array/src/proto_array_fork_choice.rs index 06d7ce60c4c..955befa7fd5 100644 --- a/consensus/proto_array/src/proto_array_fork_choice.rs +++ b/consensus/proto_array/src/proto_array_fork_choice.rs @@ -415,6 +415,7 @@ impl ProtoArrayForkChoice { pub fn new( current_slot: Slot, anchor_block_slot: Slot, + anchor_block_root: Hash256, anchor_block_state_root: Hash256, justified_checkpoint: Checkpoint, finalized_checkpoint: Checkpoint, @@ -435,12 +436,11 @@ impl ProtoArrayForkChoice { let block = Block { slot: anchor_block_slot, - root: local_irreversible_checkpoint.root, + root: anchor_block_root, parent_root: None, state_root: anchor_block_state_root, - // We are using the finalized_root as the target_root, since it always lies on an - // epoch boundary. - target_root: local_irreversible_checkpoint.root, + // TODO: What root to use here? + target_root: anchor_block_root, current_epoch_shuffling_id, next_epoch_shuffling_id, justified_checkpoint, From 511e7053fe9da9a50a9fe303e66cd17903608f85 Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Fri, 7 Nov 2025 11:22:36 -0300 Subject: [PATCH 08/30] Fix tests --- beacon_node/beacon_chain/tests/payload_invalidation.rs | 7 +------ consensus/fork_choice/tests/tests.rs | 6 +++--- consensus/proto_array/src/proto_array_fork_choice.rs | 6 +++++- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/beacon_node/beacon_chain/tests/payload_invalidation.rs b/beacon_node/beacon_chain/tests/payload_invalidation.rs index 25357ecee6e..a55bb7cd5f9 100644 --- a/beacon_node/beacon_chain/tests/payload_invalidation.rs +++ b/beacon_node/beacon_chain/tests/payload_invalidation.rs @@ -1141,12 +1141,7 @@ async fn payload_preparation_before_transition_block() { .prepare_beacon_proposer(current_slot) .await .unwrap(); - let forkchoice_update_params = rig - .harness - .chain - .head() - .canonical_head - .forkchoice_update_parameters(); + let forkchoice_update_params = rig.harness.chain.head().forkchoice_update_parameters(); rig.harness .chain .update_execution_engine_forkchoice( diff --git a/consensus/fork_choice/tests/tests.rs b/consensus/fork_choice/tests/tests.rs index 6fe29454236..3305ef76cc0 100644 --- a/consensus/fork_choice/tests/tests.rs +++ b/consensus/fork_choice/tests/tests.rs @@ -87,7 +87,7 @@ impl ForkChoiceTest { /// Assert the epochs match. pub fn assert_finalized_epoch(self, epoch: u64) -> Self { assert_eq!( - self.get(|fc_store| fc_store.finalized_checkpoint().epoch), + self.get(|fc_store| fc_store.finalized_checkpoint().on_chain().epoch), Epoch::new(epoch), "finalized_epoch" ); @@ -97,7 +97,7 @@ impl ForkChoiceTest { /// Assert the epochs match. pub fn assert_justified_epoch(self, epoch: u64) -> Self { assert_eq!( - self.get(|fc_store| fc_store.justified_checkpoint().epoch), + self.get(|fc_store| fc_store.justified_checkpoint().on_chain().epoch), Epoch::new(epoch), "justified_epoch" ); @@ -369,7 +369,7 @@ impl ForkChoiceTest { let state_root = harness .chain .store - .get_blinded_block(&fc.fc_store().justified_checkpoint().root) + .get_blinded_block(&fc.fc_store().justified_checkpoint().local().root) .unwrap() .unwrap() .message() diff --git a/consensus/proto_array/src/proto_array_fork_choice.rs b/consensus/proto_array/src/proto_array_fork_choice.rs index 955befa7fd5..6951ea0e029 100644 --- a/consensus/proto_array/src/proto_array_fork_choice.rs +++ b/consensus/proto_array/src/proto_array_fork_choice.rs @@ -1080,9 +1080,10 @@ mod test_compute_deltas { fn finalized_descendant() { let genesis_slot = Slot::new(0); let genesis_epoch = Epoch::new(0); + let genesis_block_root = Hash256::from_low_u64_be(1); let state_root = Hash256::from_low_u64_be(0); - let finalized_root = Hash256::from_low_u64_be(1); + let finalized_root = genesis_block_root; let finalized_desc = Hash256::from_low_u64_be(2); let not_finalized_desc = Hash256::from_low_u64_be(3); let unknown = Hash256::from_low_u64_be(4); @@ -1102,6 +1103,7 @@ mod test_compute_deltas { let mut fc = ProtoArrayForkChoice::new::( genesis_slot, genesis_slot, + genesis_block_root, state_root, genesis_checkpoint, genesis_checkpoint, @@ -1220,6 +1222,7 @@ mod test_compute_deltas { let junk_shuffling_id = AttestationShufflingId::from_components(Epoch::new(0), Hash256::zero()); let execution_status = ExecutionStatus::irrelevant(); + let genesis_block_root = get_block_root(0); let genesis_checkpoint = Checkpoint { epoch: Epoch::new(0), @@ -1229,6 +1232,7 @@ mod test_compute_deltas { let mut fc = ProtoArrayForkChoice::new::( genesis_slot, genesis_slot, + genesis_block_root, junk_state_root, genesis_checkpoint, genesis_checkpoint, From 2b907a687add538fd58cc666154591127b0acd50 Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Fri, 7 Nov 2025 18:08:02 -0300 Subject: [PATCH 09/30] Fix revert fc tests --- .../src/beacon_fork_choice_store.rs | 35 +++++------------ beacon_node/beacon_chain/src/builder.rs | 2 - beacon_node/beacon_chain/src/fork_revert.rs | 5 +-- consensus/fork_choice/src/fork_choice.rs | 38 +++++++++---------- 4 files changed, 31 insertions(+), 49 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_fork_choice_store.rs b/beacon_node/beacon_chain/src/beacon_fork_choice_store.rs index 0cc6b343771..ef46d469459 100644 --- a/beacon_node/beacon_chain/src/beacon_fork_choice_store.rs +++ b/beacon_node/beacon_chain/src/beacon_fork_choice_store.rs @@ -168,9 +168,7 @@ where store: Arc>, anchor: BeaconSnapshot, ) -> Result { - 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(); // The anchor state MUST be on an epoch boundary (it should be advanced by the caller). if !anchor_state @@ -179,20 +177,15 @@ where .is_multiple_of(E::slots_per_epoch()) { return Err(Error::UnalignedCheckpoint { - block_slot: anchor_block_header.slot, + block_slot: anchor_state.latest_block_header().slot, state_slot: anchor_state.slot(), }); } - // 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 anchor_block_checkpoint = Checkpoint { epoch: anchor_epoch, - root: anchor_block_root, + root: anchor.beacon_block_root, }; let justified_balances = JustifiedBalances::from_justified_state(&anchor_state)?; let anchor_state_root = anchor_state.canonical_root()?; @@ -361,14 +354,10 @@ where } fn justified_checkpoint(&self) -> ForkChoiceCheckpoint { - if self.local_irreversible_checkpoint.epoch > self.justified_checkpoint.epoch { - ForkChoiceCheckpoint::Local { - local: self.local_irreversible_checkpoint, - on_chain: self.justified_checkpoint, - } - } else { - ForkChoiceCheckpoint::OnChain(self.justified_checkpoint) - } + ForkChoiceCheckpoint::new( + self.local_irreversible_checkpoint, + self.justified_checkpoint, + ) } fn justified_state_root(&self) -> Hash256 { @@ -380,14 +369,10 @@ where } fn finalized_checkpoint(&self) -> ForkChoiceCheckpoint { - if self.local_irreversible_checkpoint.epoch > self.finalized_checkpoint.epoch { - ForkChoiceCheckpoint::Local { - local: self.local_irreversible_checkpoint, - on_chain: self.finalized_checkpoint, - } - } else { - ForkChoiceCheckpoint::OnChain(self.finalized_checkpoint) - } + ForkChoiceCheckpoint::new( + self.local_irreversible_checkpoint, + self.finalized_checkpoint, + ) } fn unrealized_justified_checkpoint(&self) -> &Checkpoint { diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index 1cc24185fb0..24be70805f6 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -401,7 +401,6 @@ where let fork_choice = ForkChoice::from_anchor( fc_store, - genesis.beacon_block_root, &genesis.beacon_block, &genesis.beacon_state, current_slot, @@ -622,7 +621,6 @@ where let fork_choice = ForkChoice::from_anchor( fc_store, - snapshot.beacon_block_root, &snapshot.beacon_block, &snapshot.beacon_state, Some(weak_subj_slot), diff --git a/beacon_node/beacon_chain/src/fork_revert.rs b/beacon_node/beacon_chain/src/fork_revert.rs index 4db79790d38..60e1f87b6b6 100644 --- a/beacon_node/beacon_chain/src/fork_revert.rs +++ b/beacon_node/beacon_chain/src/fork_revert.rs @@ -1,5 +1,5 @@ use crate::{BeaconForkChoiceStore, BeaconSnapshot}; -use fork_choice::{ForkChoice, PayloadVerificationStatus}; +use fork_choice::{ForkChoice, ForkChoiceStore, PayloadVerificationStatus}; use itertools::process_results; use state_processing::state_advance::complete_state_advance; use state_processing::{ @@ -137,7 +137,7 @@ pub fn reset_fork_choice_to_finalization, Cold: It ) })?; let finalized_snapshot = BeaconSnapshot { - beacon_block_root: finalized_block_root, + beacon_block_root: finalized_block.canonical_root(), beacon_block: Arc::new(finalized_block), beacon_state: finalized_state, }; @@ -148,7 +148,6 @@ pub fn reset_fork_choice_to_finalization, Cold: It let mut fork_choice = ForkChoice::from_anchor( fc_store, - finalized_block_root, &finalized_snapshot.beacon_block, &finalized_snapshot.beacon_state, current_slot, diff --git a/consensus/fork_choice/src/fork_choice.rs b/consensus/fork_choice/src/fork_choice.rs index bda69e4b4ec..ff5e8ba6477 100644 --- a/consensus/fork_choice/src/fork_choice.rs +++ b/consensus/fork_choice/src/fork_choice.rs @@ -291,39 +291,39 @@ pub struct ForkchoiceUpdateParameters { } #[derive(Clone, Copy, Debug, PartialEq)] -pub enum ForkChoiceCheckpoint { - Local { - local: Checkpoint, - on_chain: Checkpoint, - }, - OnChain(Checkpoint), +pub struct ForkChoiceCheckpoint { + local: Checkpoint, + on_chain: Checkpoint, } impl ForkChoiceCheckpoint { + pub fn new(local: Checkpoint, on_chain: Checkpoint) -> Self { + Self { local, on_chain } + } + pub fn on_chain(&self) -> Checkpoint { - match self { - Self::Local { on_chain, .. } => *on_chain, - Self::OnChain(cp) => *cp, - } + self.on_chain } pub fn local(&self) -> Checkpoint { - match self { - Self::Local { local, .. } => *local, - Self::OnChain(cp) => *cp, + if self.on_chain.epoch >= self.local.epoch { + self.on_chain + } else { + self.local } } } impl std::fmt::Display for ForkChoiceCheckpoint { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Local { local, on_chain } => write!( + if self.on_chain.epoch >= self.local.epoch { + write!(f, "{}/{}", self.on_chain.root, self.on_chain.epoch) + } else { + write!( f, "{}/{}/local/{}/{}", - on_chain.root, on_chain.epoch, local.root, local.epoch, - ), - Self::OnChain(cp) => write!(f, "{}/{}", cp.root, cp.epoch), + self.on_chain.root, self.on_chain.epoch, self.local.root, self.local.epoch, + ) } } } @@ -375,7 +375,6 @@ where /// Instantiates `Self` from an anchor (genesis or another finalized checkpoint). pub fn from_anchor( fc_store: T, - anchor_block_root: Hash256, anchor_block: &SignedBeaconBlock, anchor_state: &BeaconState, current_slot: Option, @@ -389,6 +388,7 @@ where }); } + let anchor_block_root = anchor_block.canonical_root(); let anchor_block_slot = anchor_block.slot(); let anchor_block_state_root = anchor_block.state_root(); let current_epoch_shuffling_id = From 3372df20a995b38b27c611e7d789b9ba83f51a7b Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Fri, 7 Nov 2025 18:45:21 -0300 Subject: [PATCH 10/30] Fix fork-choice tests --- consensus/fork_choice/src/fork_choice.rs | 7 ++----- .../proto_array/src/fork_choice_test_definition.rs | 6 ++++-- consensus/proto_array/src/proto_array.rs | 8 +++++--- consensus/proto_array/src/proto_array_fork_choice.rs | 9 ++++++++- consensus/types/src/checkpoint.rs | 11 +++++++++++ 5 files changed, 30 insertions(+), 11 deletions(-) diff --git a/consensus/fork_choice/src/fork_choice.rs b/consensus/fork_choice/src/fork_choice.rs index ff5e8ba6477..ae856c5fcc7 100644 --- a/consensus/fork_choice/src/fork_choice.rs +++ b/consensus/fork_choice/src/fork_choice.rs @@ -306,11 +306,7 @@ impl ForkChoiceCheckpoint { } pub fn local(&self) -> Checkpoint { - if self.on_chain.epoch >= self.local.epoch { - self.on_chain - } else { - self.local - } + self.on_chain.clamp_min(self.local) } } @@ -319,6 +315,7 @@ impl std::fmt::Display for ForkChoiceCheckpoint { if self.on_chain.epoch >= self.local.epoch { write!(f, "{}/{}", self.on_chain.root, self.on_chain.epoch) } else { + // Only log local if local is ahead: only happens if explicitly set write!( f, "{}/{}/local/{}/{}", diff --git a/consensus/proto_array/src/fork_choice_test_definition.rs b/consensus/proto_array/src/fork_choice_test_definition.rs index 2a8953a3915..6d01e7a8d03 100644 --- a/consensus/proto_array/src/fork_choice_test_definition.rs +++ b/consensus/proto_array/src/fork_choice_test_definition.rs @@ -79,11 +79,13 @@ impl ForkChoiceTestDefinition { let junk_shuffling_id = AttestationShufflingId::from_components(Epoch::new(0), Hash256::zero()); + let anchor_block_root = self.justified_checkpoint.root; + let anchor_block_state_root = Hash256::zero(); let mut fork_choice = ProtoArrayForkChoice::new::( self.finalized_block_slot, self.finalized_block_slot, - Hash256::zero(), - Hash256::zero(), + anchor_block_root, + anchor_block_state_root, self.justified_checkpoint, self.finalized_checkpoint, self.finalized_checkpoint, diff --git a/consensus/proto_array/src/proto_array.rs b/consensus/proto_array/src/proto_array.rs index f925a321e8f..e0e8af0550e 100644 --- a/consensus/proto_array/src/proto_array.rs +++ b/consensus/proto_array/src/proto_array.rs @@ -965,9 +965,11 @@ impl ProtoArray { /// Notably, this function is checking ancestory of the finalized /// *checkpoint* not the finalized *block*. pub fn is_finalized_checkpoint_or_descendant(&self, root: Hash256) -> bool { - let finalized_root = self.local_irreversible_checkpoint.root; - let finalized_slot = self - .local_irreversible_checkpoint + let local_finalized_checkpoint = self + .finalized_checkpoint + .clamp_min(self.local_irreversible_checkpoint); + let finalized_root = local_finalized_checkpoint.root; + let finalized_slot = local_finalized_checkpoint .epoch .start_slot(E::slots_per_epoch()); diff --git a/consensus/proto_array/src/proto_array_fork_choice.rs b/consensus/proto_array/src/proto_array_fork_choice.rs index 6951ea0e029..7b9676af558 100644 --- a/consensus/proto_array/src/proto_array_fork_choice.rs +++ b/consensus/proto_array/src/proto_array_fork_choice.rs @@ -550,8 +550,11 @@ impl ProtoArrayForkChoice { *old_balances = new_balances.clone(); + let local_justified_checkpoint = + justified_checkpoint.clamp_min(local_irreversible_checkpoint); + self.proto_array - .find_head::(&local_irreversible_checkpoint.root, current_slot) + .find_head::(&local_justified_checkpoint.root, current_slot) .map_err(|e| format!("find_head failed: {:?}", e)) } @@ -1328,6 +1331,10 @@ mod test_compute_deltas { root: finalized_root, epoch: Epoch::new(1), }; + fc.proto_array.local_irreversible_checkpoint = Checkpoint { + root: finalized_root, + epoch: Epoch::new(1), + }; assert!( fc.proto_array diff --git a/consensus/types/src/checkpoint.rs b/consensus/types/src/checkpoint.rs index 545af59985e..c26cdbffebe 100644 --- a/consensus/types/src/checkpoint.rs +++ b/consensus/types/src/checkpoint.rs @@ -31,6 +31,17 @@ pub struct Checkpoint { pub root: Hash256, } +impl Checkpoint { + /// Returns `self` if its epoch is >= than the `other` checkpoint epoch + pub fn clamp_min(&self, other: Checkpoint) -> Checkpoint { + if self.epoch >= other.epoch { + *self + } else { + other + } + } +} + #[cfg(test)] mod tests { use super::*; From 86508ca3d996150f669342db339bc7b53459f407 Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Fri, 7 Nov 2025 19:06:34 -0300 Subject: [PATCH 11/30] lint --- beacon_node/beacon_chain/src/fork_revert.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beacon_node/beacon_chain/src/fork_revert.rs b/beacon_node/beacon_chain/src/fork_revert.rs index 60e1f87b6b6..ab11de2fb42 100644 --- a/beacon_node/beacon_chain/src/fork_revert.rs +++ b/beacon_node/beacon_chain/src/fork_revert.rs @@ -1,5 +1,5 @@ use crate::{BeaconForkChoiceStore, BeaconSnapshot}; -use fork_choice::{ForkChoice, ForkChoiceStore, PayloadVerificationStatus}; +use fork_choice::{ForkChoice, PayloadVerificationStatus}; use itertools::process_results; use state_processing::state_advance::complete_state_advance; use state_processing::{ From 3ca4f4b8363c9a8cd6c4f54989a69b55dcfb8964 Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Fri, 7 Nov 2025 20:36:54 -0300 Subject: [PATCH 12/30] Add ws non fin test --- beacon_node/beacon_chain/tests/store_tests.rs | 132 ++++++++++++++---- 1 file changed, 103 insertions(+), 29 deletions(-) diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index ebc6fc8d0b1..1eb461ea703 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -2701,23 +2701,31 @@ async fn garbage_collect_temp_states_from_failed_block_on_finalization() { #[tokio::test] async fn weak_subjectivity_sync_easy() { - let num_initial_slots = E::slots_per_epoch() * 11; + let num_initial_slots = E::slots_per_epoch() * 13; let checkpoint_slot = Slot::new(E::slots_per_epoch() * 9); let slots = (1..num_initial_slots).map(Slot::new).collect(); - weak_subjectivity_sync_test(slots, checkpoint_slot, None).await + weak_subjectivity_sync_test(WeakSubjectivitySyncTestConfig::finality( + slots, + checkpoint_slot, + )) + .await } #[tokio::test] async fn weak_subjectivity_sync_single_block_batches() { - let num_initial_slots = E::slots_per_epoch() * 11; + let num_initial_slots = E::slots_per_epoch() * 13; let checkpoint_slot = Slot::new(E::slots_per_epoch() * 9); let slots = (1..num_initial_slots).map(Slot::new).collect(); - weak_subjectivity_sync_test(slots, checkpoint_slot, Some(1)).await + weak_subjectivity_sync_test(WeakSubjectivitySyncTestConfig::finality( + slots, + checkpoint_slot, + )) + .await } #[tokio::test] async fn weak_subjectivity_sync_unaligned_advanced_checkpoint() { - let num_initial_slots = E::slots_per_epoch() * 11; + let num_initial_slots = E::slots_per_epoch() * 13; let checkpoint_slot = Slot::new(E::slots_per_epoch() * 9); let slots = (1..num_initial_slots) .map(Slot::new) @@ -2726,12 +2734,16 @@ async fn weak_subjectivity_sync_unaligned_advanced_checkpoint() { slot <= checkpoint_slot - 3 || slot > checkpoint_slot }) .collect(); - weak_subjectivity_sync_test(slots, checkpoint_slot, None).await + weak_subjectivity_sync_test(WeakSubjectivitySyncTestConfig::finality( + slots, + checkpoint_slot, + )) + .await } #[tokio::test] async fn weak_subjectivity_sync_unaligned_unadvanced_checkpoint() { - let num_initial_slots = E::slots_per_epoch() * 11; + let num_initial_slots = E::slots_per_epoch() * 13; let checkpoint_slot = Slot::new(E::slots_per_epoch() * 9 - 3); let slots = (1..num_initial_slots) .map(Slot::new) @@ -2740,7 +2752,11 @@ async fn weak_subjectivity_sync_unaligned_unadvanced_checkpoint() { slot <= checkpoint_slot || slot > checkpoint_slot + 3 }) .collect(); - weak_subjectivity_sync_test(slots, checkpoint_slot, None).await + weak_subjectivity_sync_test(WeakSubjectivitySyncTestConfig::finality( + slots, + checkpoint_slot, + )) + .await } // Regression test for https://github.com/sigp/lighthouse/issues/4817 @@ -2749,10 +2765,14 @@ async fn weak_subjectivity_sync_unaligned_unadvanced_checkpoint() { #[tokio::test] async fn weak_subjectivity_sync_skips_at_genesis() { let start_slot = 4; - let end_slot = E::slots_per_epoch() * 4; + let end_slot = E::slots_per_epoch() * 6; let slots = (start_slot..end_slot).map(Slot::new).collect(); let checkpoint_slot = Slot::new(E::slots_per_epoch() * 2); - weak_subjectivity_sync_test(slots, checkpoint_slot, None).await + weak_subjectivity_sync_test(WeakSubjectivitySyncTestConfig::finality( + slots, + checkpoint_slot, + )) + .await } // Checkpoint sync from the genesis state. @@ -2762,20 +2782,59 @@ async fn weak_subjectivity_sync_skips_at_genesis() { #[tokio::test] async fn weak_subjectivity_sync_from_genesis() { let start_slot = 1; - let end_slot = E::slots_per_epoch() * 2; + let end_slot = E::slots_per_epoch() * 4; let slots = (start_slot..end_slot).map(Slot::new).collect(); let checkpoint_slot = Slot::new(0); - weak_subjectivity_sync_test(slots, checkpoint_slot, None).await + weak_subjectivity_sync_test(WeakSubjectivitySyncTestConfig::finality( + slots, + checkpoint_slot, + )) + .await +} + +// Checkpoint sync from the genesis state. +// +// This is a regression test for a bug we had involving the storage of the genesis state in the hot +// DB. +#[tokio::test] +async fn weak_subjectivity_sync_non_finality() { + let end_slot = E::slots_per_epoch() * 8; + let checkpoint_slot = E::slots_per_epoch() * 4; + let slots_with_blocks = (1..=checkpoint_slot).map(Slot::new).collect(); + weak_subjectivity_sync_test(WeakSubjectivitySyncTestConfig { + slots_with_blocks, + slots_with_not_attested_blocks: end_slot - checkpoint_slot, + checkpoint_slot: Slot::new(checkpoint_slot), + backfill_batch_size: None, + }) + .await } -async fn weak_subjectivity_sync_test( - slots: Vec, +struct WeakSubjectivitySyncTestConfig { + slots_with_blocks: Vec, + slots_with_not_attested_blocks: u64, checkpoint_slot: Slot, backfill_batch_size: Option, -) { - // Build an initial chain on one harness, representing a synced node with full history. - let num_final_blocks = E::slots_per_epoch() * 2; +} +impl WeakSubjectivitySyncTestConfig { + fn finality(slots: Vec, checkpoint_slot: Slot) -> Self { + Self { + slots_with_blocks: slots, + slots_with_not_attested_blocks: 0, + checkpoint_slot, + backfill_batch_size: None, + } + } +} + +async fn weak_subjectivity_sync_test(config: WeakSubjectivitySyncTestConfig) { + let WeakSubjectivitySyncTestConfig { + slots_with_blocks, + slots_with_not_attested_blocks, + checkpoint_slot, + backfill_batch_size, + } = config; let temp1 = tempdir().unwrap(); let full_store = get_store(&temp1); @@ -2787,12 +2846,16 @@ async fn weak_subjectivity_sync_test( let all_validators = (0..LOW_VALIDATOR_COUNT).collect::>(); + info!( + max_slot = ?slots_with_blocks.iter().max(), + "Producing blocks with attestations" + ); let (genesis_state, genesis_state_root) = harness.get_current_state_and_root(); harness .add_attested_blocks_at_slots( genesis_state.clone(), genesis_state_root, - &slots, + &slots_with_blocks, &all_validators, ) .await; @@ -2801,7 +2864,7 @@ async fn weak_subjectivity_sync_test( .chain .block_root_at_slot(checkpoint_slot, WhenSlotSkipped::Prev) .unwrap() - .unwrap(); + .unwrap_or_else(|| panic!("Block not found {checkpoint_slot}")); let wss_state_root = harness .chain .state_root_at_slot(checkpoint_slot) @@ -2825,15 +2888,21 @@ async fn weak_subjectivity_sync_test( let wss_state_slot = wss_state.slot(); let wss_block_slot = wss_block.slot(); - // Add more blocks that advance finalization further. - harness.advance_slot(); - harness - .extend_chain( - num_final_blocks as usize, - BlockStrategy::OnCanonicalHead, - AttestationStrategy::AllValidators, - ) - .await; + if slots_with_not_attested_blocks > 0 { + info!( + count = slots_with_not_attested_blocks, + "Producing blocks without attestations" + ); + harness.advance_slot(); + harness + .extend_chain( + slots_with_not_attested_blocks as usize, + BlockStrategy::OnCanonicalHead, + // No validators attest to this blocks + AttestationStrategy::SomeValidators(vec![]), + ) + .await; + } let (shutdown_tx, _shutdown_rx) = futures::channel::mpsc::channel(1); @@ -2917,7 +2986,12 @@ async fn weak_subjectivity_sync_test( let chain_dump = harness.chain.chain_dump().unwrap(); let new_blocks = chain_dump .iter() - .filter(|snapshot| snapshot.beacon_block.slot() > checkpoint_slot); + .filter(|snapshot| snapshot.beacon_block.slot() > checkpoint_slot) + .collect::>(); + info!( + block_count = new_blocks.len(), + "Importing blocks to new node" + ); for snapshot in new_blocks { let block_root = snapshot.beacon_block_root; From c5507d082d51100606c0d00a20f08e78de526d2c Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Fri, 7 Nov 2025 20:58:59 -0300 Subject: [PATCH 13/30] Fix prune test --- .../beacon_chain/src/canonical_head.rs | 34 ++++++++++++------- beacon_node/beacon_chain/src/errors.rs | 5 ++- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/beacon_node/beacon_chain/src/canonical_head.rs b/beacon_node/beacon_chain/src/canonical_head.rs index 5b22dee5af4..55eae02eddb 100644 --- a/beacon_node/beacon_chain/src/canonical_head.rs +++ b/beacon_node/beacon_chain/src/canonical_head.rs @@ -1005,19 +1005,27 @@ impl BeaconChain { let new_local_finalized_slot = new_local_finalized_checkpoint .epoch .start_slot(T::EthSpec::slots_per_epoch()); - let new_local_finalized_state_root = process_results( - StateRootsIterator::new(&self.store, &new_snapshot.beacon_state), - |mut iter| { - iter.find_map(|(state_root, slot)| { - if slot == new_local_finalized_slot { - Some(state_root) - } else { - None - } - }) - }, - )? - .ok_or(Error::MissingFinalizedStateRoot(new_local_finalized_slot))?; + let new_local_finalized_state_root = + if new_local_finalized_slot == new_snapshot.beacon_state.slot() { + new_snapshot.beacon_block.state_root() + } else { + process_results( + StateRootsIterator::new(&self.store, &new_snapshot.beacon_state), + |mut iter| { + iter.find_map(|(state_root, slot)| { + if slot == new_local_finalized_slot { + Some(state_root) + } else { + None + } + }) + }, + )? + .ok_or(Error::MissingFinalizedStateRoot { + state_slot: new_snapshot.beacon_state.slot(), + target_slot: new_local_finalized_slot, + })? + }; let update_cache = true; let new_finalized_state = self diff --git a/beacon_node/beacon_chain/src/errors.rs b/beacon_node/beacon_chain/src/errors.rs index d4283372d41..f014429cd40 100644 --- a/beacon_node/beacon_chain/src/errors.rs +++ b/beacon_node/beacon_chain/src/errors.rs @@ -75,7 +75,10 @@ pub enum BeaconChainError { ProposerSlashingValidationError(ProposerSlashingValidationError), AttesterSlashingValidationError(AttesterSlashingValidationError), BlsExecutionChangeValidationError(BlsExecutionChangeValidationError), - MissingFinalizedStateRoot(Slot), + MissingFinalizedStateRoot { + state_slot: Slot, + target_slot: Slot, + }, SszTypesError(SszTypesError), NoProposerForSlot(Slot), CanonicalHeadLockTimeout, From a56394491cf5a7d2e7cfea0f4b48319b8c649174 Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Sat, 8 Nov 2025 11:21:16 -0300 Subject: [PATCH 14/30] Complete non fin ws test --- beacon_node/beacon_chain/tests/store_tests.rs | 168 ++++++++++++------ 1 file changed, 110 insertions(+), 58 deletions(-) diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index 1eb461ea703..1928ec168a3 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -2708,7 +2708,7 @@ async fn weak_subjectivity_sync_easy() { slots, checkpoint_slot, )) - .await + .await; } #[tokio::test] @@ -2720,7 +2720,7 @@ async fn weak_subjectivity_sync_single_block_batches() { slots, checkpoint_slot, )) - .await + .await; } #[tokio::test] @@ -2738,7 +2738,7 @@ async fn weak_subjectivity_sync_unaligned_advanced_checkpoint() { slots, checkpoint_slot, )) - .await + .await; } #[tokio::test] @@ -2756,7 +2756,7 @@ async fn weak_subjectivity_sync_unaligned_unadvanced_checkpoint() { slots, checkpoint_slot, )) - .await + .await; } // Regression test for https://github.com/sigp/lighthouse/issues/4817 @@ -2772,7 +2772,7 @@ async fn weak_subjectivity_sync_skips_at_genesis() { slots, checkpoint_slot, )) - .await + .await; } // Checkpoint sync from the genesis state. @@ -2789,7 +2789,7 @@ async fn weak_subjectivity_sync_from_genesis() { slots, checkpoint_slot, )) - .await + .await; } // Checkpoint sync from the genesis state. @@ -2798,16 +2798,46 @@ async fn weak_subjectivity_sync_from_genesis() { // DB. #[tokio::test] async fn weak_subjectivity_sync_non_finality() { - let end_slot = E::slots_per_epoch() * 8; - let checkpoint_slot = E::slots_per_epoch() * 4; + let end_slot = E::slots_per_epoch() * 13; + let checkpoint_slot = E::slots_per_epoch() * 9; let slots_with_blocks = (1..=checkpoint_slot).map(Slot::new).collect(); - weak_subjectivity_sync_test(WeakSubjectivitySyncTestConfig { + + let (harness, beacon_chain) = weak_subjectivity_sync_test(WeakSubjectivitySyncTestConfig { slots_with_blocks, slots_with_not_attested_blocks: end_slot - checkpoint_slot, - checkpoint_slot: Slot::new(checkpoint_slot), + checkpoint_slot: checkpoint_slot.into(), backfill_batch_size: None, }) - .await + .await; + + // Check that the checkpoint slot is not finalized + let finalized_slot = get_finalized_slot(&beacon_chain).as_u64(); + assert!( + checkpoint_slot > finalized_slot, + "checkpoint_slot {checkpoint_slot} finalized_slot {finalized_slot}" + ); + + let slots_to_finalize = E::slots_per_epoch() * 3; + info!( + count = slots_to_finalize, + "Producing blocks with attestations" + ); + harness.advance_slot(); + harness + .extend_chain( + slots_to_finalize as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + sync_blocks_from_harness_to_chain(&harness, &beacon_chain, end_slot.into()).await; + + // Check that the checkpoint slot is finalized + let finalized_slot = get_finalized_slot(&beacon_chain).as_u64(); + assert!( + checkpoint_slot < finalized_slot, + "checkpoint_slot {checkpoint_slot} finalized_slot {finalized_slot}" + ); } struct WeakSubjectivitySyncTestConfig { @@ -2828,7 +2858,18 @@ impl WeakSubjectivitySyncTestConfig { } } -async fn weak_subjectivity_sync_test(config: WeakSubjectivitySyncTestConfig) { +fn get_finalized_slot(chain: &BeaconChain) -> Slot { + chain + .head() + .finalized_checkpoint() + .on_chain() + .epoch + .start_slot(T::EthSpec::slots_per_epoch()) +} + +async fn weak_subjectivity_sync_test( + config: WeakSubjectivitySyncTestConfig, +) -> (TestHarness, Arc>>) { let WeakSubjectivitySyncTestConfig { slots_with_blocks, slots_with_not_attested_blocks, @@ -2982,52 +3023,7 @@ async fn weak_subjectivity_sync_test(config: WeakSubjectivitySyncTestConfig) { assert_eq!(store_wss_blobs_opt, wss_blobs_opt); } - // Apply blocks forward to reach head. - let chain_dump = harness.chain.chain_dump().unwrap(); - let new_blocks = chain_dump - .iter() - .filter(|snapshot| snapshot.beacon_block.slot() > checkpoint_slot) - .collect::>(); - info!( - block_count = new_blocks.len(), - "Importing blocks to new node" - ); - - for snapshot in new_blocks { - let block_root = snapshot.beacon_block_root; - let full_block = harness - .chain - .get_block(&snapshot.beacon_block_root) - .await - .unwrap() - .unwrap(); - - let slot = full_block.slot(); - let full_block_root = full_block.canonical_root(); - let state_root = full_block.state_root(); - - info!(block_root = ?full_block_root, ?state_root, %slot, "Importing block from chain dump"); - beacon_chain.slot_clock.set_slot(slot.as_u64()); - beacon_chain - .process_block( - full_block_root, - harness.build_rpc_block_from_store_blobs(Some(block_root), Arc::new(full_block)), - NotifyExecutionLayer::Yes, - BlockImportSource::Lookup, - || Ok(()), - ) - .await - .unwrap(); - beacon_chain.recompute_head_at_current_slot().await; - - // Check that the new block's state can be loaded correctly. - let mut state = beacon_chain - .store - .get_state(&state_root, Some(slot), CACHE_STATE_IN_TESTS) - .unwrap() - .unwrap(); - assert_eq!(state.update_tree_hash_cache().unwrap(), state_root); - } + sync_blocks_from_harness_to_chain(&harness, &beacon_chain, checkpoint_slot).await; if checkpoint_slot != 0 { // Forwards iterator from 0 should fail as we lack blocks (unless checkpoint slot is 0). @@ -3065,6 +3061,7 @@ async fn weak_subjectivity_sync_test(config: WeakSubjectivitySyncTestConfig) { assert_eq!(beacon_chain.state_root_at_slot(Slot::new(1)).unwrap(), None); // Supply blocks backwards to reach genesis. Omit the genesis block to check genesis handling. + let chain_dump = harness.chain.chain_dump().unwrap(); let historical_blocks = chain_dump[..wss_block.slot().as_usize()] .iter() .filter(|s| s.beacon_block.slot() != 0) @@ -3262,6 +3259,61 @@ async fn weak_subjectivity_sync_test(config: WeakSubjectivitySyncTestConfig) { store.clone().reconstruct_historic_states(None).unwrap(); assert_eq!(store.get_anchor_info().anchor_slot, wss_aligned_slot); assert_eq!(store.get_anchor_info().state_upper_limit, Slot::new(0)); + + (harness, beacon_chain) +} + +async fn sync_blocks_from_harness_to_chain( + harness: &TestHarness, + beacon_chain: &Arc>>, + from_slot: Slot, +) { + // Apply blocks forward to reach head. + let chain_dump = harness.chain.chain_dump().unwrap(); + let new_blocks = chain_dump + .iter() + .filter(|snapshot| snapshot.beacon_block.slot() > from_slot) + .collect::>(); + info!( + block_count = new_blocks.len(), + "Importing blocks to new node" + ); + + for snapshot in new_blocks { + let block_root = snapshot.beacon_block_root; + let full_block = harness + .chain + .get_block(&snapshot.beacon_block_root) + .await + .unwrap() + .unwrap(); + + let slot = full_block.slot(); + let full_block_root = full_block.canonical_root(); + let state_root = full_block.state_root(); + + info!(block_root = ?full_block_root, ?state_root, %slot, "Importing block from chain dump"); + beacon_chain.slot_clock.set_slot(slot.as_u64()); + beacon_chain + .process_block( + full_block_root, + harness.build_rpc_block_from_store_blobs(Some(block_root), Arc::new(full_block)), + NotifyExecutionLayer::Yes, + BlockImportSource::Lookup, + || Ok(()), + ) + .await + .unwrap(); + beacon_chain.recompute_head_at_current_slot().await; + + // Check that the new block's state can be loaded correctly. + let mut state = beacon_chain + .store + .get_state(&state_root, Some(slot), CACHE_STATE_IN_TESTS) + .unwrap() + .unwrap(); + assert_eq!(state.update_tree_hash_cache().unwrap(), state_root); + } } // This test prunes data columns from epoch 0 and then tries to re-import them via From 00a87df5fe01f504d5d745c5b106de33ad4a9983 Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Sat, 8 Nov 2025 12:31:11 -0300 Subject: [PATCH 15/30] Add CLI tests --- Cargo.lock | 1 + beacon_node/Cargo.toml | 1 + beacon_node/client/src/builder.rs | 33 +++++++++++++------------ beacon_node/client/src/config.rs | 4 +++- beacon_node/src/cli.rs | 11 +++++++++ beacon_node/src/config.rs | 12 ++++++++-- book/src/help_bn.md | 4 ++++ common/eth2/src/types.rs | 2 +- lighthouse/tests/beacon_node.rs | 40 ++++++++++++++++++++++++++++++- 9 files changed, 86 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e045c8697fb..acbb4306603 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -928,6 +928,7 @@ dependencies = [ "directory", "dirs", "environment", + "eth2", "eth2_config", "execution_layer", "genesis", diff --git a/beacon_node/Cargo.toml b/beacon_node/Cargo.toml index fd013559785..6c68935bbaf 100644 --- a/beacon_node/Cargo.toml +++ b/beacon_node/Cargo.toml @@ -27,6 +27,7 @@ directory = { workspace = true } dirs = { workspace = true } environment = { workspace = true } eth2_config = { workspace = true } +eth2 = { workspace = true } execution_layer = { workspace = true } genesis = { workspace = true } hex = { workspace = true } diff --git a/beacon_node/client/src/builder.rs b/beacon_node/client/src/builder.rs index c3c827f0aae..c5224b1b48f 100644 --- a/beacon_node/client/src/builder.rs +++ b/beacon_node/client/src/builder.rs @@ -20,10 +20,7 @@ use beacon_chain::{Kzg, LightClientProducerEvent}; use beacon_processor::{BeaconProcessor, BeaconProcessorChannels}; use beacon_processor::{BeaconProcessorConfig, BeaconProcessorQueueLengths}; use environment::RuntimeContext; -use eth2::{ - BeaconNodeHttpClient, Error as ApiError, Timeouts, - types::{BlockId, StateId}, -}; +use eth2::{BeaconNodeHttpClient, Error as ApiError, Timeouts, types::BlockId}; use execution_layer::ExecutionLayer; use execution_layer::test_utils::generate_genesis_header; use futures::channel::mpsc::Receiver; @@ -365,7 +362,7 @@ where genesis_state, )? } - ClientGenesis::CheckpointSyncUrl { url } => { + ClientGenesis::CheckpointSyncUrl { url, state_id } => { info!( remote_url = %url, "Starting checkpoint sync" @@ -381,20 +378,20 @@ where )), ); - debug!("Downloading finalized state"); + debug!("Downloading checkpoint state {state_id}"); let state = remote - .get_debug_beacon_states_ssz::(StateId::Finalized, &spec) + .get_debug_beacon_states_ssz::(state_id, &spec) .await .map_err(|e| format!("Error loading checkpoint state from remote: {:?}", e))? .ok_or_else(|| "Checkpoint state missing from remote".to_string())?; - debug!(slot = ?state.slot(), "Downloaded finalized state"); + debug!(slot = ?state.slot(), "Downloaded checkpoint state {state_id}"); - let finalized_block_slot = state.latest_block_header().slot; + let block_slot = state.latest_block_header().slot; - debug!(block_slot = ?finalized_block_slot,"Downloading finalized block"); + debug!(?block_slot, "Downloading checkpoint block {state_id}"); let block = remote - .get_beacon_blocks_ssz::(BlockId::Slot(finalized_block_slot), &spec) + .get_beacon_blocks_ssz::(BlockId::Slot(block_slot), &spec) .await .map_err(|e| match e { ApiError::InvalidSsz(e) => format!( @@ -402,21 +399,23 @@ where node for the correct network", e ), - e => format!("Error fetching finalized block from remote: {:?}", e), + e => format!("Error fetching checkpoint block remote: {:?}", e), })? - .ok_or("Finalized block missing from remote, it returned 404")?; + .ok_or("Checkpoint block from remote, it returned 404")?; let block_root = block.canonical_root(); - debug!("Downloaded finalized block"); + debug!("Downloaded checkpoint block {block_slot}"); let blobs = if block.message().body().has_blobs() { - debug!("Downloading finalized blobs"); + debug!("Downloading checkpoint blobs {block_slot} {block_root}"); if let Some(response) = remote .get_blob_sidecars::(BlockId::Root(block_root), None, &spec) .await - .map_err(|e| format!("Error fetching finalized blobs from remote: {e:?}"))? + .map_err(|e| { + format!("Error fetching checkpoint blobs from remote: {e:?}") + })? { - debug!("Downloaded finalized blobs"); + debug!("Downloaded checkpoint blobs"); Some(response.into_data()) } else { warn!( diff --git a/beacon_node/client/src/config.rs b/beacon_node/client/src/config.rs index aeaa196df86..e02a726fcab 100644 --- a/beacon_node/client/src/config.rs +++ b/beacon_node/client/src/config.rs @@ -3,6 +3,7 @@ use beacon_chain::validator_monitor::ValidatorMonitorConfig; use beacon_processor::BeaconProcessorConfig; use directory::DEFAULT_ROOT_DIR; use environment::LoggerConfig; +use eth2::types::StateId; use kzg::trusted_setup::get_trusted_setup; use network::NetworkConfig; use sensitive_url::SensitiveUrl; @@ -17,7 +18,7 @@ const DEFAULT_FREEZER_DB_DIR: &str = "freezer_db"; const DEFAULT_BLOBS_DB_DIR: &str = "blobs_db"; /// Defines how the client should initialize the `BeaconChain` and other components. -#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] pub enum ClientGenesis { /// Creates a genesis state as per the 2019 Canada interop specifications. Interop { @@ -44,6 +45,7 @@ pub enum ClientGenesis { }, CheckpointSyncUrl { url: SensitiveUrl, + state_id: StateId, }, } diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index e4c7c6ff1fe..43f158aeb56 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -1224,6 +1224,17 @@ pub fn cli_app() -> Command { .conflicts_with("checkpoint-state") .display_order(0) ) + .arg( + Arg::new("checkpoint-sync-state-id") + .long("checkpoint-sync-state-id") + .help("Set the state ID to checkpoint sync to using a beacon node HTTP endpoint. \ + Possible values: 'finalized', 'justified', , ") + .value_name("CHECKPOINT_SYNC_STATE_ID") + .action(ArgAction::Set) + .conflicts_with("checkpoint-state") + .default_value("finalized") + .display_order(0) + ) .arg( Arg::new("checkpoint-sync-url-timeout") .long("checkpoint-sync-url-timeout") diff --git a/beacon_node/src/config.rs b/beacon_node/src/config.rs index 0f169ffaad6..146eac3b060 100644 --- a/beacon_node/src/config.rs +++ b/beacon_node/src/config.rs @@ -12,6 +12,7 @@ use clap_utils::{parse_flag, parse_required}; use client::{ClientConfig, ClientGenesis}; use directory::{DEFAULT_BEACON_NODE_DIR, DEFAULT_NETWORK_DIR, DEFAULT_ROOT_DIR}; use environment::RuntimeContext; +use eth2::types::StateId; use execution_layer::DEFAULT_JWT_FILE; use http_api::TlsConfig; use lighthouse_network::{Enr, Multiaddr, NetworkConfig, PeerIdSerialized, multiaddr::Protocol}; @@ -538,8 +539,15 @@ pub fn get_config( } else if let Some(remote_bn_url) = cli_args.get_one::("checkpoint-sync-url") { let url = SensitiveUrl::parse(remote_bn_url) .map_err(|e| format!("Invalid checkpoint sync URL: {:?}", e))?; - - ClientGenesis::CheckpointSyncUrl { url } + let state_id = cli_args + .get_one::("checkpoint-sync-state-id") + .ok_or("Missing --checkpoint-sync-state-id flag")?; + + ClientGenesis::CheckpointSyncUrl { + url, + state_id: StateId::from_str(state_id) + .map_err(|e| format!("Invalid state-id {e:?}"))?, + } } else { ClientGenesis::GenesisState } diff --git a/book/src/help_bn.md b/book/src/help_bn.md index 5f3c43a7e42..2df5e317ce5 100644 --- a/book/src/help_bn.md +++ b/book/src/help_bn.md @@ -66,6 +66,10 @@ Options: Set a checkpoint state to start syncing from. Must be aligned and match --checkpoint-block. Using --checkpoint-sync-url instead is recommended. + --checkpoint-sync-state-id + Set the state ID to checkpoint sync to using a beacon node HTTP + endpoint. Possible values: 'finalized', 'justified', , [default: finalized] --checkpoint-sync-url Set the remote beacon node HTTP endpoint to use for checkpoint sync. --checkpoint-sync-url-timeout diff --git a/common/eth2/src/types.rs b/common/eth2/src/types.rs index c3f9c305e00..8b9d2552f18 100644 --- a/common/eth2/src/types.rs +++ b/common/eth2/src/types.rs @@ -110,7 +110,7 @@ impl fmt::Display for BlockId { } } -#[derive(Debug, Copy, Clone, PartialEq)] +#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)] pub enum StateId { Head, Genesis, diff --git a/lighthouse/tests/beacon_node.rs b/lighthouse/tests/beacon_node.rs index 207324ea33f..a97742edeff 100644 --- a/lighthouse/tests/beacon_node.rs +++ b/lighthouse/tests/beacon_node.rs @@ -6,14 +6,16 @@ use beacon_node::beacon_chain::chain_config::{ }; use beacon_node::beacon_chain::custody_context::NodeCustodyType; use beacon_node::{ - ClientConfig as Config, beacon_chain::graffiti_calculator::GraffitiOrigin, + ClientConfig as Config, ClientGenesis, beacon_chain::graffiti_calculator::GraffitiOrigin, beacon_chain::store::config::DatabaseBackend as BeaconNodeBackend, }; use beacon_processor::BeaconProcessorConfig; +use eth2::types::StateId; use lighthouse_network::PeerId; use network_utils::unused_port::{ unused_tcp4_port, unused_tcp6_port, unused_udp4_port, unused_udp6_port, }; +use sensitive_url::SensitiveUrl; use std::fs::File; use std::io::{Read, Write}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; @@ -211,6 +213,42 @@ fn fork_choice_before_proposal_timeout_zero() { .with_config(|config| assert_eq!(config.chain.fork_choice_before_proposal_timeout_ms, 0)); } +#[test] +fn checkpoint_sync_state_id_default() { + CommandLineTest::new() + .flag("checkpoint-sync-url", Some("http://beacon.node")) + .run_with_zero_port_and_no_genesis_sync() + .with_config(|config| { + assert_eq!( + config.genesis, + ClientGenesis::CheckpointSyncUrl { + url: SensitiveUrl::parse("http://beacon.node").unwrap(), + state_id: StateId::Finalized + } + ); + }); +} + +#[test] +fn checkpoint_sync_state_id_root() { + CommandLineTest::new() + .flag("checkpoint-sync-url", Some("http://beacon.node")) + .flag( + "checkpoint-sync-state-id", + Some("0x0000000000000000000000000000000000000000000000000000000000000000"), + ) + .run_with_zero_port_and_no_genesis_sync() + .with_config(|config| { + assert_eq!( + config.genesis, + ClientGenesis::CheckpointSyncUrl { + url: SensitiveUrl::parse("http://beacon.node").unwrap(), + state_id: StateId::Root(Hash256::ZERO) + } + ); + }); +} + #[test] fn checkpoint_sync_url_timeout_flag() { CommandLineTest::new() From dec34a4c7ded372d077fcf726e53c02911450313 Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Sat, 8 Nov 2025 12:33:13 -0300 Subject: [PATCH 16/30] Sort deps --- beacon_node/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beacon_node/Cargo.toml b/beacon_node/Cargo.toml index 6c68935bbaf..72256e76c75 100644 --- a/beacon_node/Cargo.toml +++ b/beacon_node/Cargo.toml @@ -26,8 +26,8 @@ client = { path = "client" } directory = { workspace = true } dirs = { workspace = true } environment = { workspace = true } -eth2_config = { workspace = true } eth2 = { workspace = true } +eth2_config = { workspace = true } execution_layer = { workspace = true } genesis = { workspace = true } hex = { workspace = true } From 71271cad5c8350556dc67acdd476dbead17797d4 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Wed, 12 Nov 2025 15:30:27 +1100 Subject: [PATCH 17/30] Fix tests by removing dodgy skip --- beacon_node/client/src/config.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/beacon_node/client/src/config.rs b/beacon_node/client/src/config.rs index e02a726fcab..2edcd0e1238 100644 --- a/beacon_node/client/src/config.rs +++ b/beacon_node/client/src/config.rs @@ -63,9 +63,6 @@ pub struct Config { /// Graffiti to be inserted everytime we create a block if the validator doesn't specify. pub beacon_graffiti: GraffitiOrigin, pub validator_monitor: ValidatorMonitorConfig, - #[serde(skip)] - /// The `genesis` field is not serialized or deserialized by `serde` to ensure it is defined - /// via the CLI at runtime, instead of from a configuration file saved to disk. pub genesis: ClientGenesis, pub store: store::StoreConfig, pub network: network::NetworkConfig, From 309af3be9098d95119485ddce4805a1cc87d8a8b Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Fri, 14 Nov 2025 13:41:39 -0300 Subject: [PATCH 18/30] Fix tests --- beacon_node/http_api/tests/tests.rs | 4 +-- consensus/proto_array/src/proto_array.rs | 12 +++++++ .../src/proto_array_fork_choice.rs | 35 +++++++++---------- 3 files changed, 30 insertions(+), 21 deletions(-) diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 309d054b779..f6541b03398 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -3059,11 +3059,11 @@ impl ApiTester { assert_eq!( result.justified_checkpoint, - beacon_fork_choice.justified_checkpoint() + beacon_fork_choice.justified_checkpoint().on_chain() ); assert_eq!( result.finalized_checkpoint, - beacon_fork_choice.finalized_checkpoint() + beacon_fork_choice.finalized_checkpoint().on_chain() ); let expected_fork_choice_nodes: Vec = expected_proto_array diff --git a/consensus/proto_array/src/proto_array.rs b/consensus/proto_array/src/proto_array.rs index 8fb18f225fb..2d1ec02276e 100644 --- a/consensus/proto_array/src/proto_array.rs +++ b/consensus/proto_array/src/proto_array.rs @@ -125,6 +125,18 @@ impl Default for ProposerBoost { } } +/// Represents a checkpoint with a root that **MUST** have a node in ProtoArray. To enable +/// non-finalized checkpoint sync, the fork-choice deals with checkpoints with a block root that +/// is prior to the oldest node in the ProtoArray. However, some functions will error if you pass a +/// checkpoint for an unknown root. This type makes this distinction explicit. +/// +/// Thanks to the invariant: +/// +/// > Either the on chain finalized checkpoint or the local irreversible checkpoint root have a node +/// in the ProtoArray. +/// +/// We can assure that a LocalCheckpoint created with `new` is a checkpoint whose root is in the +/// ProtoArray. #[derive(Clone, Copy)] pub struct LocalCheckpoint(Checkpoint); diff --git a/consensus/proto_array/src/proto_array_fork_choice.rs b/consensus/proto_array/src/proto_array_fork_choice.rs index cce23c4bcb8..9b4138775ed 100644 --- a/consensus/proto_array/src/proto_array_fork_choice.rs +++ b/consensus/proto_array/src/proto_array_fork_choice.rs @@ -1131,6 +1131,7 @@ mod test_compute_deltas { epoch: genesis_epoch, root: finalized_root, }; + let local_genesis_checkpoint = LocalCheckpoint::new(genesis_checkpoint, genesis_checkpoint); let junk_checkpoint = Checkpoint { epoch: Epoch::new(42), root: Hash256::repeat_byte(42), @@ -1169,7 +1170,7 @@ mod test_compute_deltas { }, genesis_slot + 1, genesis_checkpoint, - genesis_checkpoint, + local_genesis_checkpoint, ) .unwrap(); @@ -1194,7 +1195,7 @@ mod test_compute_deltas { }, genesis_slot + 1, genesis_checkpoint, - genesis_checkpoint, + local_genesis_checkpoint, ) .unwrap(); @@ -1210,22 +1211,20 @@ mod test_compute_deltas { assert!(fc.is_finalized_checkpoint_or_descendant::( finalized_root, - genesis_checkpoint + local_genesis_checkpoint )); assert!(fc.is_finalized_checkpoint_or_descendant::( finalized_desc, - genesis_checkpoint + local_genesis_checkpoint )); assert!(!fc.is_finalized_checkpoint_or_descendant::( not_finalized_desc, - genesis_checkpoint + local_genesis_checkpoint + )); + assert!(!fc.is_finalized_checkpoint_or_descendant::( + unknown, + local_genesis_checkpoint )); - assert!( - !fc.is_finalized_checkpoint_or_descendant::( - unknown, - genesis_checkpoint - ) - ); assert!(!fc.is_descendant(finalized_desc, not_finalized_desc)); assert!(fc.is_descendant(finalized_desc, finalized_desc)); @@ -1325,7 +1324,7 @@ mod test_compute_deltas { }, Slot::from(block.slot), genesis_checkpoint, - genesis_checkpoint, + LocalCheckpoint::new(genesis_checkpoint, genesis_checkpoint), ) .unwrap(); }; @@ -1384,16 +1383,14 @@ mod test_compute_deltas { root: finalized_root, epoch: Epoch::new(1), }; - fc.proto_array.local_irreversible_checkpoint = Checkpoint { - root: finalized_root, - epoch: Epoch::new(1), - }; + let local_finalized_checkpoint = + LocalCheckpoint::new(finalized_checkpoint, finalized_checkpoint); assert!( fc.proto_array .is_finalized_checkpoint_or_descendant::( finalized_root, - finalized_checkpoint + local_finalized_checkpoint, ), "the finalized checkpoint is the finalized checkpoint" ); @@ -1402,7 +1399,7 @@ mod test_compute_deltas { fc.proto_array .is_finalized_checkpoint_or_descendant::( get_block_root(canonical_slot), - finalized_checkpoint + local_finalized_checkpoint, ), "the canonical block is a descendant of the finalized checkpoint" ); @@ -1410,7 +1407,7 @@ mod test_compute_deltas { !fc.proto_array .is_finalized_checkpoint_or_descendant::( get_block_root(non_canonical_slot), - finalized_checkpoint + local_finalized_checkpoint, ), "although the non-canonical block is a descendant of the finalized block, \ it's not a descendant of the finalized checkpoint" From a9e0b19ad819de4396b80d0c6879c8a0b5b8444b Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Fri, 14 Nov 2025 14:42:44 -0300 Subject: [PATCH 19/30] Extend ws tests --- beacon_node/beacon_chain/tests/store_tests.rs | 87 +++++++++++-------- 1 file changed, 53 insertions(+), 34 deletions(-) diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index 1928ec168a3..5b06dc99ac6 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -32,6 +32,7 @@ use state_processing::{BlockReplayer, state_advance::complete_state_advance}; use std::collections::HashMap; use std::collections::HashSet; use std::convert::TryInto; +use std::pin::Pin; use std::str::FromStr; use std::sync::{Arc, LazyLock}; use std::time::Duration; @@ -2802,49 +2803,65 @@ async fn weak_subjectivity_sync_non_finality() { let checkpoint_slot = E::slots_per_epoch() * 9; let slots_with_blocks = (1..=checkpoint_slot).map(Slot::new).collect(); - let (harness, beacon_chain) = weak_subjectivity_sync_test(WeakSubjectivitySyncTestConfig { + weak_subjectivity_sync_test(WeakSubjectivitySyncTestConfig { slots_with_blocks, slots_with_not_attested_blocks: end_slot - checkpoint_slot, checkpoint_slot: checkpoint_slot.into(), backfill_batch_size: None, - }) - .await; - - // Check that the checkpoint slot is not finalized - let finalized_slot = get_finalized_slot(&beacon_chain).as_u64(); - assert!( - checkpoint_slot > finalized_slot, - "checkpoint_slot {checkpoint_slot} finalized_slot {finalized_slot}" - ); + extra_steps: Some(Arc::new(|harness, beacon_chain| { + let end_slot = E::slots_per_epoch() * 13; + let checkpoint_slot = E::slots_per_epoch() * 9; + Box::pin(async move { + // Check that the checkpoint slot is not finalized + let finalized_slot = get_finalized_slot(&beacon_chain).as_u64(); + assert!( + checkpoint_slot > finalized_slot, + "checkpoint_slot {checkpoint_slot} finalized_slot {finalized_slot}" + ); - let slots_to_finalize = E::slots_per_epoch() * 3; - info!( - count = slots_to_finalize, - "Producing blocks with attestations" - ); - harness.advance_slot(); - harness - .extend_chain( - slots_to_finalize as usize, - BlockStrategy::OnCanonicalHead, - AttestationStrategy::AllValidators, - ) - .await; - sync_blocks_from_harness_to_chain(&harness, &beacon_chain, end_slot.into()).await; + let slots_to_finalize = E::slots_per_epoch() * 3; + info!( + count = slots_to_finalize, + "Producing blocks with attestations to finalize again" + ); + harness.advance_slot(); + harness + .extend_chain( + slots_to_finalize as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + sync_blocks_from_harness_to_chain(&harness, &beacon_chain, Slot::new(end_slot)) + .await; - // Check that the checkpoint slot is finalized - let finalized_slot = get_finalized_slot(&beacon_chain).as_u64(); - assert!( - checkpoint_slot < finalized_slot, - "checkpoint_slot {checkpoint_slot} finalized_slot {finalized_slot}" - ); + // Check that the checkpoint slot is finalized + let finalized_slot = get_finalized_slot(&beacon_chain).as_u64(); + assert!( + checkpoint_slot < finalized_slot, + "checkpoint_slot {checkpoint_slot} finalized_slot {finalized_slot}" + ); + }) + })), + }) + .await; } +type WeakSubjectivitySyncTestExtraSteps = Arc< + dyn Fn( + TestHarness, + Arc>>, + ) -> Pin + Send + 'static>> + + Send + + Sync, +>; + struct WeakSubjectivitySyncTestConfig { slots_with_blocks: Vec, slots_with_not_attested_blocks: u64, checkpoint_slot: Slot, backfill_batch_size: Option, + extra_steps: Option>, } impl WeakSubjectivitySyncTestConfig { @@ -2854,6 +2871,7 @@ impl WeakSubjectivitySyncTestConfig { slots_with_not_attested_blocks: 0, checkpoint_slot, backfill_batch_size: None, + extra_steps: None, } } } @@ -2867,14 +2885,13 @@ fn get_finalized_slot(chain: &BeaconChain) -> Slot { .start_slot(T::EthSpec::slots_per_epoch()) } -async fn weak_subjectivity_sync_test( - config: WeakSubjectivitySyncTestConfig, -) -> (TestHarness, Arc>>) { +async fn weak_subjectivity_sync_test(config: WeakSubjectivitySyncTestConfig) { let WeakSubjectivitySyncTestConfig { slots_with_blocks, slots_with_not_attested_blocks, checkpoint_slot, backfill_batch_size, + extra_steps, } = config; let temp1 = tempdir().unwrap(); let full_store = get_store(&temp1); @@ -3260,7 +3277,9 @@ async fn weak_subjectivity_sync_test( assert_eq!(store.get_anchor_info().anchor_slot, wss_aligned_slot); assert_eq!(store.get_anchor_info().state_upper_limit, Slot::new(0)); - (harness, beacon_chain) + if let Some(f) = extra_steps { + f(harness, beacon_chain).await; + } } async fn sync_blocks_from_harness_to_chain( From 066c5b6a3cf849b4bddbb4d562d04c26aa440a79 Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Fri, 14 Nov 2025 14:43:52 -0300 Subject: [PATCH 20/30] lint comment --- consensus/proto_array/src/proto_array.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/consensus/proto_array/src/proto_array.rs b/consensus/proto_array/src/proto_array.rs index 2d1ec02276e..e6047d543fb 100644 --- a/consensus/proto_array/src/proto_array.rs +++ b/consensus/proto_array/src/proto_array.rs @@ -133,7 +133,7 @@ impl Default for ProposerBoost { /// Thanks to the invariant: /// /// > Either the on chain finalized checkpoint or the local irreversible checkpoint root have a node -/// in the ProtoArray. +/// > in the ProtoArray. /// /// We can assure that a LocalCheckpoint created with `new` is a checkpoint whose root is in the /// ProtoArray. From b6ffe8a783be8d1c668dd824c73ed074079fdae5 Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Fri, 14 Nov 2025 20:22:15 -0300 Subject: [PATCH 21/30] Fix pseudo migration tests --- beacon_node/beacon_chain/src/test_utils.rs | 10 ++++++++++ beacon_node/beacon_chain/tests/tests.rs | 12 +++++++----- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index 361c070ea73..425f2a6ced5 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -58,6 +58,7 @@ use store::database::interface::BeaconNodeBackend; use store::{HotColdDB, ItemStore, MemoryStore, config::StoreConfig}; use task_executor::TaskExecutor; use task_executor::{ShutdownReason, test_utils::TestRuntime}; +use tracing::info; use tree_hash::TreeHash; use types::indexed_attestation::IndexedAttestationBase; use types::payload::BlockProductionVersion; @@ -3052,6 +3053,15 @@ where sync_committee_strategy: SyncCommitteeStrategy, light_client_strategy: LightClientStrategy, ) -> Hash256 { + info!( + num_blocks, + ?block_strategy, + ?attestation_strategy, + ?sync_committee_strategy, + ?light_client_strategy, + "Harness extend_chain_with_sync" + ); + let (mut state, slots) = match block_strategy { BlockStrategy::OnCanonicalHead => { let current_slot: u64 = self.get_current_slot().into(); diff --git a/beacon_node/beacon_chain/tests/tests.rs b/beacon_node/beacon_chain/tests/tests.rs index b33e05ce0ab..365b3407f5e 100644 --- a/beacon_node/beacon_chain/tests/tests.rs +++ b/beacon_node/beacon_chain/tests/tests.rs @@ -973,11 +973,13 @@ async fn pseudo_finalize_test_generic( 0, "We pseudo finalized, but our finalized checkpoint should still be unset" ); - assert_eq!( - split.slot, - head.beacon_state.slot(), - "We pseudo finalized, our split point should be at the current head slot" - ); + if expect_true_finalization_migration { + assert_eq!( + split.slot, + head.beacon_state.slot(), + "We pseudo finalized, our split point should be at the current head slot" + ); + } // finalize the chain harness From c71fc97e5672b12e02d3276f66d9e89ce620ae97 Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Mon, 24 Nov 2025 17:46:02 -0300 Subject: [PATCH 22/30] Don't update justified balances on set irreversible checkpoint --- .../src/beacon_fork_choice_store.rs | 70 +++++++++---------- consensus/fork_choice/src/fork_choice.rs | 3 +- .../fork_choice/src/fork_choice_store.rs | 3 +- 3 files changed, 36 insertions(+), 40 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_fork_choice_store.rs b/beacon_node/beacon_chain/src/beacon_fork_choice_store.rs index ef46d469459..301ca7a0a1e 100644 --- a/beacon_node/beacon_chain/src/beacon_fork_choice_store.rs +++ b/beacon_node/beacon_chain/src/beacon_fork_choice_store.rs @@ -187,6 +187,10 @@ where epoch: anchor_epoch, root: anchor.beacon_block_root, }; + + // For post-genesis states this justified balances will not match the justified checkpoint. + // TODO(non-fin-cp-sync): Fetch the justified state from checkpointz server and get the + // justified balances from it. let justified_balances = JustifiedBalances::from_justified_state(&anchor_state)?; let anchor_state_root = anchor_state.canonical_root()?; @@ -298,34 +302,6 @@ where _phantom: PhantomData, }) } - - fn update_justified_balances( - &mut self, - checkpoint: Checkpoint, - justified_state_root: Hash256, - ) -> Result<(), Error> { - self.justified_state_root = justified_state_root; - - if let Some(balances) = self.balances_cache.get(checkpoint.root, checkpoint.epoch) { - // NOTE: could avoid this re-calculation by introducing a `PersistedCacheItem`. - metrics::inc_counter(&metrics::BALANCES_CACHE_HITS); - self.justified_balances = JustifiedBalances::from_effective_balances(balances)?; - } else { - metrics::inc_counter(&metrics::BALANCES_CACHE_MISSES); - - // Justified state is reasonably useful to cache, it might be finalized soon. - let update_cache = true; - let state = self - .store - .get_hot_state(&self.justified_state_root, update_cache) - .map_err(Error::FailedToReadState)? - .ok_or(Error::MissingState(self.justified_state_root))?; - - self.justified_balances = JustifiedBalances::from_justified_state(&state)?; - } - - Ok(()) - } } impl ForkChoiceStore for BeaconForkChoiceStore @@ -401,7 +377,30 @@ where justified_state_root: Hash256, ) -> Result<(), Error> { self.justified_checkpoint = checkpoint; - self.update_justified_balances(checkpoint, justified_state_root) + self.justified_state_root = justified_state_root; + + if let Some(balances) = self.balances_cache.get( + self.justified_checkpoint.root, + self.justified_checkpoint.epoch, + ) { + // NOTE: could avoid this re-calculation by introducing a `PersistedCacheItem`. + metrics::inc_counter(&metrics::BALANCES_CACHE_HITS); + self.justified_balances = JustifiedBalances::from_effective_balances(balances)?; + } else { + metrics::inc_counter(&metrics::BALANCES_CACHE_MISSES); + + // Justified state is reasonably useful to cache, it might be finalized soon. + let update_cache = true; + let state = self + .store + .get_hot_state(&self.justified_state_root, update_cache) + .map_err(Error::FailedToReadState)? + .ok_or(Error::MissingState(self.justified_state_root))?; + + self.justified_balances = JustifiedBalances::from_justified_state(&state)?; + } + + Ok(()) } fn set_unrealized_justified_checkpoint(&mut self, checkpoint: Checkpoint, state_root: Hash256) { @@ -413,15 +412,12 @@ where self.unrealized_finalized_checkpoint = checkpoint; } - fn set_local_irreversible_checkpoint( - &mut self, - checkpoint: Checkpoint, - state_root: Hash256, - ) -> Result<(), Error> { + fn set_local_irreversible_checkpoint(&mut self, checkpoint: Checkpoint) -> Result<(), Error> { self.local_irreversible_checkpoint = checkpoint; - if self.local_irreversible_checkpoint.epoch > self.justified_checkpoint.epoch { - self.update_justified_balances(checkpoint, state_root)?; - } + // Do not update the justified balances. They should match the network justified balances, + // such that all nodes have a consistent fork-choice view. The current balances are cached + // and stored in the fork-choice store. Even the node can't access the justified state, the + // justified balances will remain available. Ok(()) } diff --git a/consensus/fork_choice/src/fork_choice.rs b/consensus/fork_choice/src/fork_choice.rs index defd1285275..71093d745eb 100644 --- a/consensus/fork_choice/src/fork_choice.rs +++ b/consensus/fork_choice/src/fork_choice.rs @@ -1334,10 +1334,9 @@ where pub fn set_local_irreversible_checkpoint( &mut self, checkpoint: Checkpoint, - state_root: Hash256, ) -> Result<(), Error> { self.fc_store - .set_local_irreversible_checkpoint(checkpoint, state_root) + .set_local_irreversible_checkpoint(checkpoint) .map_err(Error::UnableToSetJustifiedCheckpoint)?; Ok(()) } diff --git a/consensus/fork_choice/src/fork_choice_store.rs b/consensus/fork_choice/src/fork_choice_store.rs index f3c86596b8d..bfc69a4933e 100644 --- a/consensus/fork_choice/src/fork_choice_store.rs +++ b/consensus/fork_choice/src/fork_choice_store.rs @@ -76,10 +76,11 @@ pub trait ForkChoiceStore: Sized { state_root: Hash256, ) -> Result<(), Self::Error>; + /// Sets the local irreversible checkpoint, which may be ahead of the network's justified + /// checkpoint. Only blocks descendant of this checkpoint are viable heads. fn set_local_irreversible_checkpoint( &mut self, checkpoint: Checkpoint, - state_root: Hash256, ) -> Result<(), Self::Error>; /// Sets the `unrealized_justified_checkpoint`. From da83332f399effb5f0960cc29b96f95fa468c73d Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Mon, 24 Nov 2025 17:46:31 -0300 Subject: [PATCH 23/30] Remove unnecessary state_root checks --- .../beacon_chain/src/canonical_head.rs | 33 ++----------------- beacon_node/http_api/src/lib.rs | 2 +- common/eth2/src/types.rs | 2 ++ 3 files changed, 6 insertions(+), 31 deletions(-) diff --git a/beacon_node/beacon_chain/src/canonical_head.rs b/beacon_node/beacon_chain/src/canonical_head.rs index 55eae02eddb..7be1df6fcb8 100644 --- a/beacon_node/beacon_chain/src/canonical_head.rs +++ b/beacon_node/beacon_chain/src/canonical_head.rs @@ -55,8 +55,7 @@ use state_processing::AllCaches; use std::sync::Arc; use std::time::Duration; use store::{ - Error as StoreError, HotStateSummary, KeyValueStore, KeyValueStoreOp, StoreConfig, - iter::StateRootsIterator, + Error as StoreError, KeyValueStore, KeyValueStoreOp, StoreConfig, iter::StateRootsIterator, }; use task_executor::{JoinHandle, ShutdownReason}; use tracing::info_span; @@ -1061,39 +1060,13 @@ impl BeaconChain { Ok(()) } - pub fn manual_finalization( - self: &Arc, - checkpoint: Checkpoint, - // TODO: Should we derive the state_root from the checkpoint? - state_root: Hash256, - ) -> Result<(), Error> { - let HotStateSummary { - slot, - latest_block_root, - .. - } = self - .store - .load_hot_state_summary(&state_root) - .map_err(Error::DBError)? - .ok_or(Error::MissingHotStateSummary(state_root))?; - - if slot != checkpoint.epoch.start_slot(T::EthSpec::slots_per_epoch()) { - return Err(Error::InvalidCheckpoint(format!( - "state {state_root:?} slot {slot} not first slot of checkpoint {checkpoint:?}" - ))); - } - if latest_block_root != *checkpoint.root { - return Err(Error::InvalidCheckpoint(format!( - "state {state_root:?} not a post state of checkpoint {checkpoint:?}" - ))); - } - + pub fn manual_finalization(self: &Arc, checkpoint: Checkpoint) -> Result<(), Error> { // Take a clone of the current ("old") head. let old_cached_head = self.canonical_head.cached_head(); let new_view = { let mut fork_choice_write_lock = self.canonical_head.fork_choice_write_lock(); - fork_choice_write_lock.set_local_irreversible_checkpoint(checkpoint, state_root)?; + fork_choice_write_lock.set_local_irreversible_checkpoint(checkpoint)?; let fork_choice_read_lock = RwLockWriteGuard::downgrade(fork_choice_write_lock); ForkChoiceView { head_block_root: old_cached_head.head_block_root(), diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index 7a0011b7da0..afe033c4ed8 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -4207,7 +4207,7 @@ pub fn serve( }; chain - .manual_finalization(checkpoint, request_data.state_root) + .manual_finalization(checkpoint) .map(|_| api_types::GenericResponse::from(request_data)) .map_err(|e| { warp_utils::reject::custom_bad_request(format!( diff --git a/common/eth2/src/types.rs b/common/eth2/src/types.rs index e107a9b97d8..b9d27072dc9 100644 --- a/common/eth2/src/types.rs +++ b/common/eth2/src/types.rs @@ -1477,6 +1477,8 @@ pub struct StandardLivenessResponseData { #[derive(Debug, Serialize, Deserialize)] pub struct ManualFinalizationRequestData { + /// DEPRECATED + /// TODO(non-fin-cp-sync): Should we remove? pub state_root: Hash256, pub epoch: Epoch, pub block_root: Hash256, From 1877d649f6cfcc2b93a127a6999bf1b1b42417c0 Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Mon, 24 Nov 2025 18:00:44 -0300 Subject: [PATCH 24/30] Check that irreversible checkpoint is of a known block and well formed --- .../src/beacon_fork_choice_store.rs | 3 +-- consensus/fork_choice/src/fork_choice.rs | 19 ++++++++++++++++--- .../fork_choice/src/fork_choice_store.rs | 5 +---- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_fork_choice_store.rs b/beacon_node/beacon_chain/src/beacon_fork_choice_store.rs index 301ca7a0a1e..23bac647dab 100644 --- a/beacon_node/beacon_chain/src/beacon_fork_choice_store.rs +++ b/beacon_node/beacon_chain/src/beacon_fork_choice_store.rs @@ -412,13 +412,12 @@ where self.unrealized_finalized_checkpoint = checkpoint; } - fn set_local_irreversible_checkpoint(&mut self, checkpoint: Checkpoint) -> Result<(), Error> { + fn set_local_irreversible_checkpoint(&mut self, checkpoint: Checkpoint) { self.local_irreversible_checkpoint = checkpoint; // Do not update the justified balances. They should match the network justified balances, // such that all nodes have a consistent fork-choice view. The current balances are cached // and stored in the fork-choice store. Even the node can't access the justified state, the // justified balances will remain available. - Ok(()) } fn set_proposer_boost_root(&mut self, proposer_boost_root: Hash256) { diff --git a/consensus/fork_choice/src/fork_choice.rs b/consensus/fork_choice/src/fork_choice.rs index 71093d745eb..5d52da4c32f 100644 --- a/consensus/fork_choice/src/fork_choice.rs +++ b/consensus/fork_choice/src/fork_choice.rs @@ -72,6 +72,7 @@ pub enum Error { }, UnrealizedVoteProcessing(state_processing::EpochProcessingError), ValidatorStatuses(BeaconStateError), + BadIrreversibleCheckpoint(String), } impl From for Error { @@ -1335,9 +1336,21 @@ where &mut self, checkpoint: Checkpoint, ) -> Result<(), Error> { - self.fc_store - .set_local_irreversible_checkpoint(checkpoint) - .map_err(Error::UnableToSetJustifiedCheckpoint)?; + // Irreversible checkpoint is potentially user input, sanity check it + if let Some(block) = self.proto_array.get_block(&checkpoint.root) { + if block.slot.epoch(E::slots_per_epoch()) > checkpoint.epoch { + return Err(Error::BadIrreversibleCheckpoint(format!( + "Epoch {} ahead of block slot {}", + checkpoint.epoch, block.slot + ))); + } + } else { + return Err(Error::BadIrreversibleCheckpoint(format!( + "Unknown root {:?}", + checkpoint.root + ))); + } + self.fc_store.set_local_irreversible_checkpoint(checkpoint); Ok(()) } diff --git a/consensus/fork_choice/src/fork_choice_store.rs b/consensus/fork_choice/src/fork_choice_store.rs index bfc69a4933e..51cc222869e 100644 --- a/consensus/fork_choice/src/fork_choice_store.rs +++ b/consensus/fork_choice/src/fork_choice_store.rs @@ -78,10 +78,7 @@ pub trait ForkChoiceStore: Sized { /// Sets the local irreversible checkpoint, which may be ahead of the network's justified /// checkpoint. Only blocks descendant of this checkpoint are viable heads. - fn set_local_irreversible_checkpoint( - &mut self, - checkpoint: Checkpoint, - ) -> Result<(), Self::Error>; + fn set_local_irreversible_checkpoint(&mut self, checkpoint: Checkpoint); /// Sets the `unrealized_justified_checkpoint`. fn set_unrealized_justified_checkpoint(&mut self, checkpoint: Checkpoint, state_root: Hash256); From 586f232bfcf1a64df54f5f428e8ac263beb07e26 Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Mon, 24 Nov 2025 18:21:18 -0300 Subject: [PATCH 25/30] Recompute head after set manual finalization --- .../beacon_chain/src/canonical_head.rs | 59 +++++++------------ beacon_node/http_api/src/lib.rs | 19 +++--- 2 files changed, 31 insertions(+), 47 deletions(-) diff --git a/beacon_node/beacon_chain/src/canonical_head.rs b/beacon_node/beacon_chain/src/canonical_head.rs index 7be1df6fcb8..efc9c142166 100644 --- a/beacon_node/beacon_chain/src/canonical_head.rs +++ b/beacon_node/beacon_chain/src/canonical_head.rs @@ -1060,45 +1060,28 @@ impl BeaconChain { Ok(()) } - pub fn manual_finalization(self: &Arc, checkpoint: Checkpoint) -> Result<(), Error> { - // Take a clone of the current ("old") head. - let old_cached_head = self.canonical_head.cached_head(); - - let new_view = { - let mut fork_choice_write_lock = self.canonical_head.fork_choice_write_lock(); - fork_choice_write_lock.set_local_irreversible_checkpoint(checkpoint)?; - let fork_choice_read_lock = RwLockWriteGuard::downgrade(fork_choice_write_lock); - ForkChoiceView { - head_block_root: old_cached_head.head_block_root(), - justified_checkpoint: fork_choice_read_lock.justified_checkpoint(), - finalized_checkpoint: fork_choice_read_lock.finalized_checkpoint(), - } - }; - - let new_cached_head = { - let fork_choice = self.canonical_head.fork_choice_read_lock(); - let new_cached_head = CachedHead { - // The head hasn't changed, take a relatively cheap `Arc`-clone of the existing - // head. - snapshot: old_cached_head.snapshot.clone(), - justified_checkpoint: new_view.justified_checkpoint, - finalized_checkpoint: new_view.finalized_checkpoint, - head_hash: fork_choice.execution_hash(old_cached_head.head_block_root()), - justified_hash: fork_choice - .execution_hash(new_view.justified_checkpoint.on_chain().root), - finalized_hash: fork_choice - .execution_hash(new_view.finalized_checkpoint.on_chain().root), - }; - drop(fork_choice); - - let mut cached_head_write_lock = self.canonical_head.cached_head_write_lock(); - *cached_head_write_lock = new_cached_head; - // Take a clone of the cached head for later use. It is cloned whilst - // holding the write-lock to ensure we get exactly the head we just enshrined. - cached_head_write_lock.clone() - }; + pub async fn manual_finalization( + self: &Arc, + checkpoint: Checkpoint, + ) -> Result<(), Error> { + // Set the irreversible checkpoint first. This will cause finality to advance if the + // checkpoint is greater than current finality. + let chain = self.clone(); + self.spawn_blocking_handle( + move || { + chain + .canonical_head + .fork_choice_write_lock() + .set_local_irreversible_checkpoint(checkpoint) + }, + "set_local_irreversible_checkpoint", + ) + .await??; - self.after_finalization(&new_cached_head, new_view)?; + // Allow to set an irreversible checkpoint that is not an ancestor of the current head. If + // that happens the current head will become non-viable. Re-compute head in case the + // current head is not a descendant of the irreversible checkpoint. + self.recompute_head_at_slot(self.slot()?).await; Ok(()) } diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index afe033c4ed8..112942525d0 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -4200,20 +4200,21 @@ pub fn serve( |request_data: api_types::ManualFinalizationRequestData, task_spawner: TaskSpawner, chain: Arc>| { - task_spawner.blocking_json_task(Priority::P0, move || { + task_spawner.spawn_async_with_rejection(Priority::P0, async move { let checkpoint = Checkpoint { epoch: request_data.epoch, root: request_data.block_root, }; - chain - .manual_finalization(checkpoint) - .map(|_| api_types::GenericResponse::from(request_data)) - .map_err(|e| { - warp_utils::reject::custom_bad_request(format!( - "Failed to finalize state due to error: {e:?}" - )) - }) + chain.manual_finalization(checkpoint).await.map_err(|e| { + warp_utils::reject::custom_bad_request(format!( + "Failed to finalize state due to error: {e:?}" + )) + })?; + Ok::<_, warp::reject::Rejection>( + warp::reply::json(&api_types::GenericResponse::from(request_data)) + .into_response(), + ) }) }, ); From 381c8a3a8b56ce31eb0ee354f7a5980feab3ae06 Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Mon, 24 Nov 2025 18:27:10 -0300 Subject: [PATCH 26/30] Remove irreversible slot function --- beacon_node/beacon_chain/src/beacon_chain.rs | 11 ----------- beacon_node/beacon_chain/src/blob_verification.rs | 7 ++++++- beacon_node/beacon_chain/src/block_verification.rs | 7 ++++++- .../beacon_chain/src/data_column_verification.rs | 7 ++++++- 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 1eded8426df..8554cc472b1 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -599,17 +599,6 @@ impl BeaconChain { }) } - pub fn irreversible_slot(&self) -> Slot { - let local_finalized_slot = self - .head() - .finalized_checkpoint() - .local() - .epoch - .start_slot(T::EthSpec::slots_per_epoch()); - let split = self.store.get_split_info(); - std::cmp::max(local_finalized_slot, split.slot) - } - /// Return a database operation for writing the `PersistedBeaconChain` to disk. /// /// These days the `PersistedBeaconChain` is only used to store the genesis block root, so it diff --git a/beacon_node/beacon_chain/src/blob_verification.rs b/beacon_node/beacon_chain/src/blob_verification.rs index dbd906a67f5..0a9c208ce7b 100644 --- a/beacon_node/beacon_chain/src/blob_verification.rs +++ b/beacon_node/beacon_chain/src/blob_verification.rs @@ -434,7 +434,12 @@ pub fn validate_blob_sidecar_for_gossip( ) -> Result<(), BlockError> { // Reject blocks that conflict with the local node's irreversible slot. Could be the finalized // slot, or a more recent slot that the user marked as irreversible. - let finalized_slot = chain.irreversible_slot(); + let finalized_slot = chain + .head() + .finalized_checkpoint() + .local() + .epoch + .start_slot(T::EthSpec::slots_per_epoch()); if block.slot() <= finalized_slot { chain.pre_finalization_block_rejected(block_root); diff --git a/beacon_node/beacon_chain/src/data_column_verification.rs b/beacon_node/beacon_chain/src/data_column_verification.rs index d33f1f80de5..41c0752e7bd 100644 --- a/beacon_node/beacon_chain/src/data_column_verification.rs +++ b/beacon_node/beacon_chain/src/data_column_verification.rs @@ -737,7 +737,12 @@ fn verify_slot_greater_than_latest_finalized_slot( chain: &BeaconChain, column_slot: Slot, ) -> Result<(), GossipDataColumnError> { - let latest_finalized_slot = chain.irreversible_slot(); + let latest_finalized_slot = chain + .head() + .finalized_checkpoint() + .local() + .epoch + .start_slot(T::EthSpec::slots_per_epoch()); if column_slot <= latest_finalized_slot { return Err(GossipDataColumnError::PastFinalizedSlot { column_slot, From 7c6461d4e62954ae1368ae7fa2938a0e30b518ed Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Mon, 24 Nov 2025 19:35:53 -0300 Subject: [PATCH 27/30] Compute execution_hash from store also --- beacon_node/beacon_chain/src/beacon_chain.rs | 20 ++++++ .../beacon_chain/src/canonical_head.rs | 63 ++++++++++++------- 2 files changed, 60 insertions(+), 23 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 8554cc472b1..310e542f2ee 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -1208,6 +1208,26 @@ impl BeaconChain { .map(Some) } + pub fn get_execution_hash( + &self, + block_root: &Hash256, + ) -> Result, Error> { + if let Some(execution_hash) = self + .canonical_head + .fork_choice_read_lock() + .execution_hash(*block_root) + { + return Ok(Some(execution_hash)); + } + Ok(self.store.get_blinded_block(block_root)?.and_then(|block| { + if let Ok(payload) = block.message().execution_payload() { + Some(payload.block_hash()) + } else { + None + } + })) + } + /// Returns the blobs at the given root, if any. /// /// ## Errors diff --git a/beacon_node/beacon_chain/src/canonical_head.rs b/beacon_node/beacon_chain/src/canonical_head.rs index efc9c142166..24e67be1dfe 100644 --- a/beacon_node/beacon_chain/src/canonical_head.rs +++ b/beacon_node/beacon_chain/src/canonical_head.rs @@ -267,24 +267,33 @@ impl CanonicalHead { fork_choice: BeaconForkChoice, snapshot: Arc>, ) -> Self { + let cached_head = Self::new_cached_head(&fork_choice, snapshot); + Self { + fork_choice: CanonicalHeadRwLock::new(fork_choice), + cached_head: CanonicalHeadRwLock::new(cached_head), + recompute_head_lock: Mutex::new(()), + } + } + + fn new_cached_head( + fork_choice: &BeaconForkChoice, + snapshot: Arc>, + ) -> CachedHead { let head_block_root = snapshot.beacon_block_root; let justified_checkpoint = fork_choice.justified_checkpoint(); let finalized_checkpoint = fork_choice.finalized_checkpoint(); - let cached_head = CachedHead { + CachedHead { snapshot, justified_checkpoint, finalized_checkpoint, - // TODO: Do we need to cache this nodes? + // TODO(non-fin-cp-sync): This values maybe be None. It's unclear if ELs can handle + // zero hashes. We decide to only send to the EL the "real" network on chain finalized + // and justified hashes such that the "safe" tag retains the same economic security no + // matter in what mode this beacon node is running. head_hash: fork_choice.execution_hash(head_block_root), justified_hash: fork_choice.execution_hash(justified_checkpoint.on_chain().root), finalized_hash: fork_choice.execution_hash(finalized_checkpoint.on_chain().root), - }; - - Self { - fork_choice: CanonicalHeadRwLock::new(fork_choice), - cached_head: CanonicalHeadRwLock::new(cached_head), - recompute_head_lock: Mutex::new(()), } } @@ -321,16 +330,7 @@ impl CanonicalHead { beacon_state, }; - let justified_checkpoint = fork_choice.justified_checkpoint(); - let finalized_checkpoint = fork_choice.finalized_checkpoint(); - let cached_head = CachedHead { - snapshot: Arc::new(snapshot), - justified_checkpoint, - finalized_checkpoint, - head_hash: fork_choice.execution_hash(beacon_block_root), - justified_hash: fork_choice.execution_hash(justified_checkpoint.on_chain().root), - finalized_hash: fork_choice.execution_hash(finalized_checkpoint.on_chain().root), - }; + let cached_head = Self::new_cached_head(&fork_choice, snapshot.into()); *fork_choice_write_lock = fork_choice; // Avoid interleaving the fork choice and cached head locks. @@ -667,11 +667,28 @@ impl BeaconChain { // parameters have changed. let new_forkchoice_update_parameters = ForkchoiceUpdateParameters { head_root: new_view.head_block_root, - head_hash: fork_choice_read_lock.execution_hash(new_view.head_block_root), - justified_hash: fork_choice_read_lock - .execution_hash(new_view.justified_checkpoint.on_chain().root), - finalized_hash: fork_choice_read_lock - .execution_hash(new_view.finalized_checkpoint.on_chain().root), + // Only fetch the execution hashes of head and finality checkpoints if necessary. The + // fork-choice may not include nodes for the finalized / justified blocks, so a DB read + // may be necessary. + head_hash: if new_view.head_block_root == old_view.head_block_root { + old_cached_head.head_hash + } else { + self.get_execution_hash(&new_view.head_block_root)? + }, + justified_hash: if new_view.justified_checkpoint.on_chain() + == old_view.justified_checkpoint.on_chain() + { + old_cached_head.justified_hash + } else { + self.get_execution_hash(&new_view.justified_checkpoint.on_chain().root)? + }, + finalized_hash: if new_view.finalized_checkpoint.on_chain() + == old_view.finalized_checkpoint.on_chain() + { + old_cached_head.finalized_hash + } else { + self.get_execution_hash(&new_view.finalized_checkpoint.on_chain().root)? + }, }; perform_debug_logging::(&old_view, &new_view, &fork_choice_read_lock); From ac76022f822659788cd55c6079bd481b9a1452b7 Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Tue, 25 Nov 2025 01:44:45 -0300 Subject: [PATCH 28/30] Common logic for forkchoice udpate params --- beacon_node/beacon_chain/src/builder.rs | 3 +- .../beacon_chain/src/canonical_head.rs | 146 +++++++++++------- 2 files changed, 89 insertions(+), 60 deletions(-) diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index 0833845779c..dc80553b1fe 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -919,7 +919,8 @@ where let genesis_validators_root = head_snapshot.beacon_state.genesis_validators_root(); let genesis_time = head_snapshot.beacon_state.genesis_time(); - let canonical_head = CanonicalHead::new(fork_choice, Arc::new(head_snapshot)); + let canonical_head = CanonicalHead::new(&store, fork_choice, Arc::new(head_snapshot)) + .map_err(|e| format!("Error creating canonical head: {:?}", e))?; let shuffling_cache_size = self.chain_config.shuffling_cache_size; let complete_blob_backfill = self.chain_config.complete_blob_backfill; diff --git a/beacon_node/beacon_chain/src/canonical_head.rs b/beacon_node/beacon_chain/src/canonical_head.rs index 24e67be1dfe..937b11c56ac 100644 --- a/beacon_node/beacon_chain/src/canonical_head.rs +++ b/beacon_node/beacon_chain/src/canonical_head.rs @@ -108,13 +108,10 @@ pub struct CachedHead { /// This value may be distinct to the `self.snapshot.beacon_state.finalized_checkpoint`. /// This value should be used over the beacon state value in practically all circumstances. finalized_checkpoint: ForkChoiceCheckpoint, - /// The `execution_payload.block_hash` of the block at the head of the chain. Set to `None` - /// before Bellatrix. - head_hash: Option, - /// The `execution_payload.block_hash` of the justified block. Set to `None` before Bellatrix. - justified_hash: Option, - /// The `execution_payload.block_hash` of the finalized block. Set to `None` before Bellatrix. - finalized_hash: Option, + /// The `execution_payload.block_hash` of the block at the head of the chain, the justified root + /// and the finalized root. Maybe None before Bellatrix, and if the blocks they reference are + /// unknown. + forkchoice_update_parameters: ForkchoiceUpdateParameters, } impl CachedHead { @@ -229,12 +226,7 @@ impl CachedHead { /// /// Useful for supplying to the execution layer. pub fn forkchoice_update_parameters(&self) -> ForkchoiceUpdateParameters { - ForkchoiceUpdateParameters { - head_root: self.snapshot.beacon_block_root, - head_hash: self.head_hash, - justified_hash: self.justified_hash, - finalized_hash: self.finalized_hash, - } + self.forkchoice_update_parameters } } @@ -264,37 +256,43 @@ pub struct CanonicalHead { impl CanonicalHead { /// Instantiate `Self`. pub fn new( + store: &BeaconStore, fork_choice: BeaconForkChoice, snapshot: Arc>, - ) -> Self { - let cached_head = Self::new_cached_head(&fork_choice, snapshot); - Self { + ) -> Result { + let cached_head = Self::new_cached_head(store, &fork_choice, snapshot)?; + Ok(Self { fork_choice: CanonicalHeadRwLock::new(fork_choice), cached_head: CanonicalHeadRwLock::new(cached_head), recompute_head_lock: Mutex::new(()), - } + }) } fn new_cached_head( + store: &BeaconStore, fork_choice: &BeaconForkChoice, snapshot: Arc>, - ) -> CachedHead { + ) -> Result, Error> { let head_block_root = snapshot.beacon_block_root; let justified_checkpoint = fork_choice.justified_checkpoint(); let finalized_checkpoint = fork_choice.finalized_checkpoint(); + let forkchoice_update_parameters = compute_fork_choice_update_parameters::( + fork_choice, + store, + ForkChoiceView { + head_block_root, + justified_checkpoint, + finalized_checkpoint, + }, + None, + )?; - CachedHead { + Ok(CachedHead { snapshot, justified_checkpoint, finalized_checkpoint, - // TODO(non-fin-cp-sync): This values maybe be None. It's unclear if ELs can handle - // zero hashes. We decide to only send to the EL the "real" network on chain finalized - // and justified hashes such that the "safe" tag retains the same economic security no - // matter in what mode this beacon node is running. - head_hash: fork_choice.execution_hash(head_block_root), - justified_hash: fork_choice.execution_hash(justified_checkpoint.on_chain().root), - finalized_hash: fork_choice.execution_hash(finalized_checkpoint.on_chain().root), - } + forkchoice_update_parameters, + }) } /// Load a persisted version of `BeaconForkChoice` from the `store` and restore `self` to that @@ -330,7 +328,7 @@ impl CanonicalHead { beacon_state, }; - let cached_head = Self::new_cached_head(&fork_choice, snapshot.into()); + let cached_head = Self::new_cached_head(store, &fork_choice, snapshot.into())?; *fork_choice_write_lock = fork_choice; // Avoid interleaving the fork choice and cached head locks. @@ -665,31 +663,13 @@ impl BeaconChain { // Get the parameters to update the execution layer since either the head or some finality // parameters have changed. - let new_forkchoice_update_parameters = ForkchoiceUpdateParameters { - head_root: new_view.head_block_root, - // Only fetch the execution hashes of head and finality checkpoints if necessary. The - // fork-choice may not include nodes for the finalized / justified blocks, so a DB read - // may be necessary. - head_hash: if new_view.head_block_root == old_view.head_block_root { - old_cached_head.head_hash - } else { - self.get_execution_hash(&new_view.head_block_root)? - }, - justified_hash: if new_view.justified_checkpoint.on_chain() - == old_view.justified_checkpoint.on_chain() - { - old_cached_head.justified_hash - } else { - self.get_execution_hash(&new_view.justified_checkpoint.on_chain().root)? - }, - finalized_hash: if new_view.finalized_checkpoint.on_chain() - == old_view.finalized_checkpoint.on_chain() - { - old_cached_head.finalized_hash - } else { - self.get_execution_hash(&new_view.finalized_checkpoint.on_chain().root)? - }, - }; + let new_forkchoice_update_parameters = compute_fork_choice_update_parameters::( + &fork_choice_read_lock, + &self.store, + new_view, + // Pass the old cached params to prevent DB reads + Some((old_view, old_cached_head.forkchoice_update_parameters())), + )?; perform_debug_logging::(&old_view, &new_view, &fork_choice_read_lock); @@ -731,9 +711,7 @@ impl BeaconChain { snapshot: Arc::new(new_snapshot), justified_checkpoint: new_view.justified_checkpoint, finalized_checkpoint: new_view.finalized_checkpoint, - head_hash: new_forkchoice_update_parameters.head_hash, - justified_hash: new_forkchoice_update_parameters.justified_hash, - finalized_hash: new_forkchoice_update_parameters.finalized_hash, + forkchoice_update_parameters: new_forkchoice_update_parameters, }; let new_head = { @@ -758,9 +736,7 @@ impl BeaconChain { snapshot: old_cached_head.snapshot.clone(), justified_checkpoint: new_view.justified_checkpoint, finalized_checkpoint: new_view.finalized_checkpoint, - head_hash: new_forkchoice_update_parameters.head_hash, - justified_hash: new_forkchoice_update_parameters.justified_hash, - finalized_hash: new_forkchoice_update_parameters.finalized_hash, + forkchoice_update_parameters: new_forkchoice_update_parameters, }; let mut cached_head_write_lock = self.canonical_head.cached_head_write_lock(); @@ -1132,6 +1108,58 @@ impl BeaconChain { } } +fn compute_fork_choice_update_parameters( + fork_choice: &BeaconForkChoice, + store: &BeaconStore, + new_view: ForkChoiceView, + old_view_and_params: Option<(ForkChoiceView, ForkchoiceUpdateParameters)>, +) -> Result { + // TODO(non-fin-cp-sync): This values maybe be None. It's unclear if ELs can handle + // zero hashes. We decide to only send to the EL the "real" network on chain finalized + // and justified hashes such that the "safe" tag retains the same economic security no + // matter in what mode this beacon node is running. + let get_execution_hash = |block_root: &Hash256| -> Result, Error> { + if let Some(execution_hash) = fork_choice.execution_hash(*block_root) { + return Ok(Some(execution_hash)); + } + Ok(store.get_blinded_block(block_root)?.and_then(|block| { + if let Ok(payload) = block.message().execution_payload() { + Some(payload.block_hash()) + } else { + None + } + })) + }; + + Ok(ForkchoiceUpdateParameters { + head_root: new_view.head_block_root, + // Only fetch the execution hashes of head and finality checkpoints if necessary. The + // fork-choice may not include nodes for the finalized / justified blocks, so a DB read + // may be necessary. + head_hash: if let Some((old_view, old_params)) = old_view_and_params + && new_view.head_block_root == old_view.head_block_root + { + old_params.head_hash + } else { + get_execution_hash(&new_view.head_block_root)? + }, + justified_hash: if let Some((old_view, old_params)) = old_view_and_params + && new_view.justified_checkpoint.on_chain() == old_view.justified_checkpoint.on_chain() + { + old_params.justified_hash + } else { + get_execution_hash(&new_view.justified_checkpoint.on_chain().root)? + }, + finalized_hash: if let Some((old_view, old_params)) = old_view_and_params + && new_view.finalized_checkpoint.on_chain() == old_view.finalized_checkpoint.on_chain() + { + old_params.finalized_hash + } else { + get_execution_hash(&new_view.finalized_checkpoint.on_chain().root)? + }, + }) +} + /// Check to see if the `finalized_proto_block` has an invalid execution payload. If so, shut down /// Lighthouse. /// From f3f974b9c61536de4939935a245f7fcc100d6842 Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Fri, 19 Dec 2025 18:54:19 -0300 Subject: [PATCH 29/30] Lint full --- beacon_node/beacon_chain/tests/tests.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/beacon_node/beacon_chain/tests/tests.rs b/beacon_node/beacon_chain/tests/tests.rs index 4760e93e5ff..00ec553a05e 100644 --- a/beacon_node/beacon_chain/tests/tests.rs +++ b/beacon_node/beacon_chain/tests/tests.rs @@ -956,10 +956,7 @@ async fn pseudo_finalize_test_generic( }; // pseudo finalize - harness - .chain - .manual_finalization(checkpoint, head.beacon_state_root()) - .unwrap(); + harness.chain.manual_finalization(checkpoint).await.unwrap(); let split = harness.chain.store.get_split_info(); let pseudo_finalized_slot = split.slot; From aeb3a803d2ec427ce122d43c3296c3e23245088d Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Fri, 19 Dec 2025 19:03:09 -0300 Subject: [PATCH 30/30] Improve manual finalization API --- beacon_node/src/cli.rs | 1 + common/eth2/src/types.rs | 5 ++--- consensus/fork_choice/src/fork_choice.rs | 10 ++++++++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index 43f158aeb56..e23de4ad64e 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -1232,6 +1232,7 @@ pub fn cli_app() -> Command { .value_name("CHECKPOINT_SYNC_STATE_ID") .action(ArgAction::Set) .conflicts_with("checkpoint-state") + .requires("checkpoint-sync-url") .default_value("finalized") .display_order(0) ) diff --git a/common/eth2/src/types.rs b/common/eth2/src/types.rs index 007883e10bf..a777724905a 100644 --- a/common/eth2/src/types.rs +++ b/common/eth2/src/types.rs @@ -1495,10 +1495,9 @@ pub struct StandardLivenessResponseData { #[derive(Debug, Serialize, Deserialize)] pub struct ManualFinalizationRequestData { - /// DEPRECATED - /// TODO(non-fin-cp-sync): Should we remove? - pub state_root: Hash256, + /// Irreversible checkpoint epoch pub epoch: Epoch, + /// Irreversible checkpoint root pub block_root: Hash256, } diff --git a/consensus/fork_choice/src/fork_choice.rs b/consensus/fork_choice/src/fork_choice.rs index 37ba6292c89..0cd8c6a40a9 100644 --- a/consensus/fork_choice/src/fork_choice.rs +++ b/consensus/fork_choice/src/fork_choice.rs @@ -1339,12 +1339,18 @@ where ) -> Result<(), Error> { // Irreversible checkpoint is potentially user input, sanity check it if let Some(block) = self.proto_array.get_block(&checkpoint.root) { - if block.slot.epoch(E::slots_per_epoch()) > checkpoint.epoch { + if checkpoint.epoch < block.slot.epoch(E::slots_per_epoch()) { return Err(Error::BadIrreversibleCheckpoint(format!( - "Epoch {} ahead of block slot {}", + "Epoch {} less than block slot {}", checkpoint.epoch, block.slot ))); } + if !self.is_finalized_checkpoint_or_descendant(checkpoint.root) { + return Err(Error::BadIrreversibleCheckpoint(format!( + "Block {:?} is not descendant of finalized checkpoint", + checkpoint.root + ))); + } } else { return Err(Error::BadIrreversibleCheckpoint(format!( "Unknown root {:?}",