Relax requirements that a checkpoint state must be epoch aligned post-gloas

This commit is contained in:
Eitan Seri- Levi
2026-04-04 00:16:09 -07:00
parent 27af0ed82c
commit 5472c300dc
6 changed files with 90 additions and 8 deletions

View File

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

View File

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

View File

@@ -101,6 +101,7 @@ pub enum Error {
from_state_slot: Slot,
target_slot: Slot,
},
FinalizedStateAlreadySet,
}
pub trait HandleUnavailable<T> {

View File

@@ -474,6 +474,25 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold>
}
}
/// 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<E>,
) -> 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,

View File

@@ -124,6 +124,36 @@ impl<E: EthSpec> StateCache<E> {
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<E>,
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<E: EthSpec> StateCache<E> {
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<E>,
pre_finalized_slots_to_retain: &[Slot],
) -> Result<(), Error> {
if self
.finalized_state
.as_ref()