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 95fde28f5b..80f3be7565 100644 --- a/beacon_node/beacon_chain/src/beacon_fork_choice_store.rs +++ b/beacon_node/beacon_chain/src/beacon_fork_choice_store.rs @@ -172,8 +172,9 @@ where 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 + // Pre-gloas the anchor state MUST be on an epoch boundary (it should be advanced by the caller). + // Post-gloas this requirement is relaxed. + if !anchor_state.fork_name_unchecked().gloas_enabled() && !anchor_state .slot() .as_u64() .is_multiple_of(E::slots_per_epoch()) diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index 11b87351b1..5920243c50 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -42,6 +42,7 @@ use store::{Error as StoreError, HotColdDB, ItemStore, KeyValueStoreOp}; use task_executor::{ShutdownReason, TaskExecutor}; use tracing::{debug, error, info, warn}; use tree_hash::TreeHash; +use types::StatePayloadStatus; use types::data::CustodyIndex; use types::{ BeaconBlock, BeaconState, BlobSidecarList, ChainSpec, ColumnIndex, DataColumnSidecarList, @@ -433,9 +434,15 @@ where .clone() .ok_or("weak_subjectivity_state requires a store")?; - // Ensure the state is advanced to an epoch boundary. + // Pre-gloas ensure the state is advanced to an epoch boundary. + // Post-gloas checkpoint states are always pending (post-block) and cannot + // be advanced across epoch boundaries without first checking for a payload + // envelope. let slots_per_epoch = E::slots_per_epoch(); - if weak_subj_state.slot() % slots_per_epoch != 0 { + + if !weak_subj_state.fork_name_unchecked().gloas_enabled() + && weak_subj_state.slot() % slots_per_epoch != 0 + { debug!( state_slot = %weak_subj_state.slot(), block_slot = %weak_subj_block.slot(), @@ -568,7 +575,7 @@ where // Write the state, block and blobs non-atomically, it doesn't matter if they're forgotten // about on a crash restart. store - .update_finalized_state( + .set_initial_finalized_state( weak_subj_state_root, weak_subj_block_root, weak_subj_state.clone(), @@ -617,7 +624,15 @@ where .map_err(|e| format!("Failed to initialize data column info: {:?}", e))?, ); - // TODO(gloas): add check that checkpoint state is Pending + if weak_subj_state.fork_name_unchecked().gloas_enabled() + && weak_subj_state.payload_status() != StatePayloadStatus::Pending + { + return Err(format!( + "Checkpoint sync state must be Pending (post-block) for Gloas, got {:?}", + weak_subj_state.payload_status() + )); + } + let snapshot = BeaconSnapshot { beacon_block_root: weak_subj_block_root, execution_envelope: None, diff --git a/beacon_node/store/src/errors.rs b/beacon_node/store/src/errors.rs index a07cc83886..e403df483a 100644 --- a/beacon_node/store/src/errors.rs +++ b/beacon_node/store/src/errors.rs @@ -101,6 +101,7 @@ pub enum Error { from_state_slot: Slot, target_slot: Slot, }, + FinalizedStateAlreadySet, } pub trait HandleUnavailable { diff --git a/beacon_node/store/src/hot_cold_store.rs b/beacon_node/store/src/hot_cold_store.rs index 78dd69e55a..83aa3f0cc4 100644 --- a/beacon_node/store/src/hot_cold_store.rs +++ b/beacon_node/store/src/hot_cold_store.rs @@ -474,6 +474,25 @@ impl, Cold: ItemStore> HotColdDB } } + /// See [`StateCache::set_initial_finalized_state`](crate::state_cache::StateCache::set_initial_finalized_state). + pub fn set_initial_finalized_state( + &self, + state_root: Hash256, + block_root: Hash256, + state: BeaconState, + ) -> Result<(), Error> { + let start_slot = self.get_anchor_info().anchor_slot; + let pre_finalized_slots_to_retain = self + .hierarchy + .closest_layer_points(state.slot(), start_slot); + self.state_cache.lock().set_initial_finalized_state( + state_root, + block_root, + state, + &pre_finalized_slots_to_retain, + ) + } + pub fn update_finalized_state( &self, state_root: Hash256, diff --git a/beacon_node/store/src/state_cache.rs b/beacon_node/store/src/state_cache.rs index d016922ade..afe909a45c 100644 --- a/beacon_node/store/src/state_cache.rs +++ b/beacon_node/store/src/state_cache.rs @@ -124,6 +124,36 @@ impl StateCache { roots } + /// Used by checkpoint sync to initialize the finalized state in the state cache. + /// + /// Post-gloas the checkpoint state may not be epoch-aligned, e.g when the epoch boundary + /// slot is skipped. We relax the epoch-alignment requirement for the initial state only. + /// Runtime finalization updates should use [`update_finalized_state`](Self::update_finalized_state), + /// which enforces alignment. + pub fn set_initial_finalized_state( + &mut self, + state_root: Hash256, + block_root: Hash256, + state: BeaconState, + pre_finalized_slots_to_retain: &[Slot], + ) -> Result<(), Error> { + if self.finalized_state.is_some() { + return Err(Error::FinalizedStateAlreadySet); + } + + if !state.fork_name_unchecked().gloas_enabled() && state.slot() % E::slots_per_epoch() != 0 + { + return Err(Error::FinalizedStateUnaligned); + } + + self.update_finalized_state_inner( + state_root, + block_root, + state, + pre_finalized_slots_to_retain, + ) + } + pub fn update_finalized_state( &mut self, state_root: Hash256, @@ -135,6 +165,21 @@ impl StateCache { return Err(Error::FinalizedStateUnaligned); } + self.update_finalized_state_inner( + state_root, + block_root, + state, + pre_finalized_slots_to_retain, + ) + } + + fn update_finalized_state_inner( + &mut self, + state_root: Hash256, + block_root: Hash256, + state: BeaconState, + pre_finalized_slots_to_retain: &[Slot], + ) -> Result<(), Error> { if self .finalized_state .as_ref() diff --git a/consensus/fork_choice/src/fork_choice.rs b/consensus/fork_choice/src/fork_choice.rs index 92fd4c1faf..0a734748cd 100644 --- a/consensus/fork_choice/src/fork_choice.rs +++ b/consensus/fork_choice/src/fork_choice.rs @@ -396,8 +396,9 @@ where current_slot: Option, spec: &ChainSpec, ) -> Result> { - // Sanity check: the anchor must lie on an epoch boundary. - if anchor_state.slot() % E::slots_per_epoch() != 0 { + // Pre-gloas sanity check: the anchor must lie on an epoch boundary. + // Post-gloas we relax this requirement + if !anchor_state.fork_name_unchecked().gloas_enabled() && anchor_state.slot() % E::slots_per_epoch() != 0 { return Err(Error::InvalidAnchor { block_slot: anchor_block.slot(), state_slot: anchor_state.slot(),