diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 9a30553678..047610a4a7 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -941,6 +941,28 @@ impl BeaconChain { )? } + /// Returns the Pending (pre-payload) state root at the given slot in the canonical chain. + /// + /// In ePBS (Gloas+), if the canonical state at `slot` is Full (post-payload), this resolves + /// to the same-slot Pending state root. For skipped slots or pre-Gloas, returns the canonical + /// state root unchanged. + pub fn pending_state_root_at_slot(&self, request_slot: Slot) -> Result, Error> { + let Some(root) = self.state_root_at_slot(request_slot)? else { + return Ok(None); + }; + + // Pre-Gloas: all states are inherently Pending. + if !self + .spec + .fork_name_at_slot::(request_slot) + .gloas_enabled() + { + return Ok(Some(root)); + } + + Ok(Some(self.store.resolve_pending_state_root(&root)?)) + } + /// Returns the block root at the given slot, if any. Only returns roots in the canonical chain. /// /// ## Notes diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index 11b87351b1..f848b48f05 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::SignedExecutionPayloadEnvelope; use types::data::CustodyIndex; use types::{ BeaconBlock, BeaconState, BlobSidecarList, ChainSpec, ColumnIndex, DataColumnSidecarList, @@ -426,6 +427,7 @@ where mut weak_subj_state: BeaconState, weak_subj_block: SignedBeaconBlock, weak_subj_blobs: Option>, + weak_subj_payload: Option>, genesis_state: BeaconState, ) -> Result { let store = self @@ -601,6 +603,13 @@ where .map_err(|e| format!("Failed to store weak subjectivity blobs: {e:?}"))?; } } + if let Some(ref envelope) = weak_subj_payload { + store + .put_payload_envelope(&weak_subj_block_root, envelope.clone()) + .map_err(|e| { + format!("Failed to store weak subjectivity payload envelope: {e:?}") + })?; + } // Stage the database's metadata fields for atomic storage when `build` is called. // This prevents the database from restarting in an inconsistent state if the anchor @@ -617,10 +626,25 @@ where .map_err(|e| format!("Failed to initialize data column info: {:?}", e))?, ); + if self + .spec + .fork_name_at_slot::(weak_subj_slot) + .gloas_enabled() + { + let envelope = weak_subj_payload.as_ref().ok_or_else(|| { + "Gloas checkpoint sync requires an execution payload envelope".to_string() + })?; + if envelope.message.beacon_block_root != weak_subj_block_root { + return Err(format!( + "Envelope beacon_block_root {:?} does not match block root {:?}", + envelope.message.beacon_block_root, weak_subj_block_root + )); + } + } // TODO(gloas): add check that checkpoint state is Pending let snapshot = BeaconSnapshot { beacon_block_root: weak_subj_block_root, - execution_envelope: None, + execution_envelope: weak_subj_payload.map(Arc::new), beacon_block: Arc::new(weak_subj_block), beacon_state: weak_subj_state, }; diff --git a/beacon_node/client/src/builder.rs b/beacon_node/client/src/builder.rs index 865599b9bd..f4c8689b1e 100644 --- a/beacon_node/client/src/builder.rs +++ b/beacon_node/client/src/builder.rs @@ -372,6 +372,7 @@ where anchor_state, anchor_block, anchor_blobs, + None, genesis_state, )? } @@ -445,6 +446,21 @@ where None }; + let envelope = if spec + .fork_name_at_slot::(finalized_block_slot) + .gloas_enabled() + { + debug!("Downloading payload"); + remote + .get_beacon_execution_payload_envelope(BlockId::Slot(finalized_block_slot)) + .await + .map_err(|e| format!("Error fetching finalized blobs from remote: {e:?}"))? + .map(|resp| resp.into_data()) + } else { + None + }; + debug!("Downloaded finalized payload"); + let genesis_state = genesis_state(&runtime_context, &config).await?; info!( @@ -454,7 +470,7 @@ where "Loaded checkpoint block and state" ); - builder.weak_subjectivity_state(state, block, blobs, genesis_state)? + builder.weak_subjectivity_state(state, block, blobs, envelope, genesis_state)? } ClientGenesis::DepositContract => { return Err("Loading genesis from deposit contract no longer supported".to_string()); diff --git a/beacon_node/http_api/src/state_id.rs b/beacon_node/http_api/src/state_id.rs index 13fb9b2c58..9f9a01d48b 100644 --- a/beacon_node/http_api/src/state_id.rs +++ b/beacon_node/http_api/src/state_id.rs @@ -43,14 +43,32 @@ impl StateId { chain.canonical_head.cached_head().finalized_checkpoint(); let (slot, execution_optimistic) = checkpoint_slot_and_execution_optimistic(chain, finalized_checkpoint)?; - (slot, execution_optimistic, true) + let root = chain + .pending_state_root_at_slot(slot) + .map_err(warp_utils::reject::unhandled_error)? + .ok_or_else(|| { + warp_utils::reject::custom_not_found(format!( + "beacon state at slot {}", + slot + )) + })?; + return Ok((root, execution_optimistic, true)); } CoreStateId::Justified => { let justified_checkpoint = chain.canonical_head.cached_head().justified_checkpoint(); let (slot, execution_optimistic) = checkpoint_slot_and_execution_optimistic(chain, justified_checkpoint)?; - (slot, execution_optimistic, false) + let root = chain + .pending_state_root_at_slot(slot) + .map_err(warp_utils::reject::unhandled_error)? + .ok_or_else(|| { + warp_utils::reject::custom_not_found(format!( + "beacon state at slot {}", + slot + )) + })?; + return Ok((root, execution_optimistic, false)); } CoreStateId::Slot(slot) => ( *slot, diff --git a/beacon_node/network/src/sync/backfill_sync/mod.rs b/beacon_node/network/src/sync/backfill_sync/mod.rs index 0f80138d24..29beb96e5a 100644 --- a/beacon_node/network/src/sync/backfill_sync/mod.rs +++ b/beacon_node/network/src/sync/backfill_sync/mod.rs @@ -35,7 +35,7 @@ use std::marker::PhantomData; use std::sync::Arc; use strum::IntoEnumIterator; use tracing::{debug, error, info, warn}; -use types::{ColumnIndex, Epoch, EthSpec}; +use types::{ColumnIndex, Epoch, EthSpec, ForkName}; /// Blocks are downloaded in batches from peers. This constant specifies how many epochs worth of /// blocks per batch are requested _at most_. A batch may request less blocks to account for @@ -218,6 +218,14 @@ impl BackFillSync { match self.state() { BackFillState::Syncing => {} // already syncing ignore. BackFillState::Paused => { + if self + .beacon_chain + .spec + .fork_name_at_epoch(self.to_be_downloaded) + >= ForkName::Gloas + { + return Ok(SyncStart::NotSyncing); + } if self .network_globals .peers diff --git a/beacon_node/network/src/sync/network_context/requests.rs b/beacon_node/network/src/sync/network_context/requests.rs index 7ba0838ee1..8c9e1b2b34 100644 --- a/beacon_node/network/src/sync/network_context/requests.rs +++ b/beacon_node/network/src/sync/network_context/requests.rs @@ -16,10 +16,10 @@ pub use data_columns_by_range::DataColumnsByRangeRequestItems; pub use data_columns_by_root::{ DataColumnsByRootRequestItems, DataColumnsByRootSingleBlockRequest, }; +pub use payload_envelopes_by_range::PayloadEnvelopesByRangeRequestItems; pub use payload_envelopes_by_root::{ PayloadEnvelopesByRootRequestItems, PayloadEnvelopesByRootSingleRequest, }; -pub use payload_envelopes_by_range::PayloadEnvelopesByRangeRequestItems; use crate::metrics; @@ -31,8 +31,8 @@ mod blocks_by_range; mod blocks_by_root; mod data_columns_by_range; mod data_columns_by_root; -mod payload_envelopes_by_root; mod payload_envelopes_by_range; +mod payload_envelopes_by_root; #[derive(Debug, PartialEq, Eq, IntoStaticStr)] pub enum LookupVerifyError { diff --git a/beacon_node/store/src/hot_cold_store.rs b/beacon_node/store/src/hot_cold_store.rs index 78dd69e55a..9a99b56348 100644 --- a/beacon_node/store/src/hot_cold_store.rs +++ b/beacon_node/store/src/hot_cold_store.rs @@ -1951,6 +1951,66 @@ impl, Cold: ItemStore> HotColdDB } } + /// Resolve a canonical state root to the Pending (pre-payload) state root at the same slot. + /// + /// In ePBS, checkpoint states (finalized, justified) should be returned as their Pending + /// variant. This function takes a canonical state root and: + /// + /// - If the state is already Pending (or pre-Gloas), returns it unchanged. + /// - If the state is Full due to a payload applied at this slot, returns the same-slot + /// Pending state root via `previous_state_root`. + /// - If the state is at a skipped slot (inheriting Full status from a prior slot), returns + /// it unchanged — there is no distinct Pending state at a skipped slot. + pub fn resolve_pending_state_root(&self, state_root: &Hash256) -> Result { + // Fast path: split state is always Pending. + let split = self.get_split_info(); + if *state_root == split.state_root { + return Ok(split.state_root); + } + + // Try hot DB first. + if let Some(summary) = self.load_hot_state_summary(state_root)? { + // Pre-Gloas states are always Pending. + if !self + .spec + .fork_name_at_slot::(summary.slot) + .gloas_enabled() + { + return Ok(*state_root); + } + + // Genesis state is always Pending. + if summary.previous_state_root.is_zero() { + return Ok(*state_root); + } + + // Load the previous state summary. If it has the same slot, the current state is + // Full (post-payload) and the previous state is Pending (post-block). Return the + // Pending state root. + let previous_summary = self + .load_hot_state_summary(&summary.previous_state_root)? + .ok_or(Error::MissingHotStateSummary(summary.previous_state_root))?; + + if previous_summary.slot == summary.slot { + // This is a Full state at a non-skipped slot. Return the Pending state root. + return Ok(summary.previous_state_root); + } + + // Either already Pending (block at this slot) or a skipped slot — return as-is. + return Ok(*state_root); + } + + // Try cold DB. + if let Some(_slot) = self.load_cold_state_slot(state_root)? { + // Cold DB states: the non-canonical payload variant is pruned during migration. + // Return whatever is stored. In practice, finalized/justified states are almost + // always in the hot DB or at the split point. + return Ok(*state_root); + } + + Err(Error::MissingHotStateSummary(*state_root)) + } + fn load_hot_hdiff_buffer(&self, state_root: Hash256) -> Result { if let Some(buffer) = self .state_cache