From 180aee1fdc6ad07afa258874ed15ff6de94002c4 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Mon, 15 Jun 2026 13:21:01 +0300 Subject: [PATCH 01/10] Allow for reimporting block+envelope in certain cases --- beacon_node/beacon_chain/src/beacon_chain.rs | 14 +++++- .../beacon_chain/src/block_verification.rs | 23 ++++----- .../src/block_verification_types.rs | 11 ++++- .../payload_envelope_verification/import.rs | 8 ---- .../src/payload_envelope_verification/mod.rs | 48 ++++++++++++++++--- .../gossip_methods.rs | 1 - 6 files changed, 77 insertions(+), 28 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index a7f1a7cfcd..9de17c9ed9 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -64,6 +64,7 @@ use crate::payload_attestation_verification::VerifiedPayloadAttestationMessage; use crate::payload_bid_verification::payload_bid_cache::GossipVerifiedPayloadBidCache; #[cfg(not(test))] use crate::payload_envelope_streamer::{EnvelopeRequestSource, launch_payload_envelope_stream}; +use crate::payload_envelope_verification::check_envelope_relevancy; use crate::pending_payload_cache::PendingPayloadCache; use crate::pending_payload_cache::{ Availability as PayloadAvailability, @@ -3000,7 +3001,18 @@ impl BeaconChain { } } - match check_block_relevancy(block.as_block(), block_root, self) { + let is_envelope_relevant = if let Some(envelope) = block.as_envelope() { + // If the envelope is relevant we skip the duplicate import check in + // `check_block_relevancy` + match check_envelope_relevancy(block.as_block(), envelope, self) { + Ok(_) => true, + Err(_) => false, + } + } else { + false + }; + + match check_block_relevancy(block.as_block(), block_root, is_envelope_relevant, self) { // If the block is relevant, add it to the filtered chain segment. Ok(_) => filtered_chain_segment.push((block_root, block)), // If the block is already known, simply ignore this block. diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 6b1ac3b033..4caa934d83 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -97,11 +97,10 @@ use store::{Error as DBError, KeyValueStore}; use strum::{AsRefStr, IntoStaticStr}; use task_executor::JoinHandle; use tracing::{Instrument, Span, debug, debug_span, error, info_span, instrument}; -use types::ExecutionBlockHash; use types::{ BeaconBlockRef, BeaconState, BeaconStateError, BlobsList, ChainSpec, DataColumnSidecarList, - Epoch, EthSpec, FullPayload, Hash256, InconsistentFork, KzgProofs, RelativeEpoch, - SignedBeaconBlock, SignedBeaconBlockHeader, Slot, data::DataColumnSidecarError, + Epoch, EthSpec, ExecutionBlockHash, FullPayload, Hash256, InconsistentFork, KzgProofs, + RelativeEpoch, SignedBeaconBlock, SignedBeaconBlockHeader, Slot, data::DataColumnSidecarError, }; /// Maximum block slot number. Block with slots bigger than this constant will NOT be processed. @@ -1350,7 +1349,7 @@ impl IntoExecutionPendingBlock for RangeSyncBlock Result, BlockSlashInfo> { // Perform an early check to prevent wasting time on irrelevant blocks. let header = self.signed_block_header(); - let block_root = check_block_relevancy(self.as_block(), block_root, chain) + let block_root = check_block_relevancy(self.as_block(), block_root, false, chain) .map_err(|e| BlockSlashInfo::SignatureNotChecked(header.clone(), e))?; let (available_block, _envelope) = self.into_available_block().map_err(|e| { @@ -1394,7 +1393,7 @@ impl IntoExecutionPendingBlock for LookupBlock Result, BlockSlashInfo> { // Perform an early check to prevent wasting time on irrelevant blocks. - let block_root = check_block_relevancy(self.as_block(), block_root, chain) + let block_root = check_block_relevancy(self.as_block(), block_root, false, chain) .map_err(|e| BlockSlashInfo::SignatureNotChecked(self.signed_block_header(), e))?; let maybe_available_block = MaybeAvailableBlock::AvailabilityPending { @@ -1467,7 +1466,7 @@ impl ExecutionPendingBlock { /* * Perform cursory checks to see if the block is even worth processing. */ - check_block_relevancy(block.as_block(), block_root, chain)?; + check_block_relevancy(block.as_block(), block_root, false, chain)?; // Define a future that will verify the execution payload with an execution engine. // @@ -1851,6 +1850,7 @@ pub fn check_block_is_finalized_checkpoint_or_descendant< pub fn check_block_relevancy( signed_block: &SignedBeaconBlock, block_root: Hash256, + skip_import_check: bool, chain: &BeaconChain, ) -> Result { let block = signed_block.message(); @@ -1880,11 +1880,12 @@ pub fn check_block_relevancy( check_block_against_finalized_slot(block, block_root, chain)?; // Check if the block is already known. We know it is post-finalization, so it is - // sufficient to check the fork choice. - if chain - .canonical_head - .fork_choice_read_lock() - .contains_block(&block_root) + // sufficient to check the fork choice. This check can optionally be skipped. + if skip_import_check + && chain + .canonical_head + .fork_choice_read_lock() + .contains_block(&block_root) { return Err(BlockError::DuplicateFullyImported(block_root)); } diff --git a/beacon_node/beacon_chain/src/block_verification_types.rs b/beacon_node/beacon_chain/src/block_verification_types.rs index 18e95f58f3..8abed19814 100644 --- a/beacon_node/beacon_chain/src/block_verification_types.rs +++ b/beacon_node/beacon_chain/src/block_verification_types.rs @@ -12,7 +12,7 @@ use std::sync::Arc; use types::data::BlobIdentifier; use types::{ BeaconBlockRef, BeaconState, BlindedPayload, ChainSpec, Epoch, EthSpec, Hash256, - SignedBeaconBlock, SignedBeaconBlockHeader, Slot, + SignedBeaconBlock, SignedBeaconBlockHeader, SignedExecutionPayloadEnvelope, Slot, }; /// A wrapper around a `SignedBeaconBlock`. This varaint is constructed @@ -111,6 +111,15 @@ impl RangeSyncBlock { .filter(|columns| !columns.is_empty()), } } + + pub fn as_envelope(&self) -> Option<&SignedExecutionPayloadEnvelope> { + match self { + RangeSyncBlock::Base(_) => None, + RangeSyncBlock::Gloas { envelope, .. } => { + envelope.as_ref().map(|e| e.envelope().as_ref()) + } + } + } } impl RangeSyncBlock { diff --git a/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs b/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs index 00806f0e17..5e90667a16 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs @@ -173,14 +173,6 @@ impl BeaconChain { .map_err(BeaconChainError::TokioJoin)? .ok_or(BeaconChainError::RuntimeShutdown)??; - // TODO(gloas): optimistic sync is not supported for Gloas, maybe we could re-add it - if payload_verification_outcome - .payload_verification_status - .is_optimistic() - { - return Err(EnvelopeError::OptimisticSyncNotSupported { block_root }); - } - Ok(AvailabilityPendingExecutedEnvelope::new( signed_envelope, block_root, diff --git a/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs b/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs index a0d34949c6..6f24744e2c 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs @@ -25,12 +25,13 @@ use strum::AsRefStr; use tracing::instrument; use types::{ BeaconState, BeaconStateError, DataColumnSidecarList, EthSpec, ExecutionBlockHash, - ExecutionPayloadEnvelope, Hash256, SignedExecutionPayloadEnvelope, Slot, + ExecutionPayloadEnvelope, Hash256, SignedBeaconBlock, SignedExecutionPayloadEnvelope, Slot, }; use crate::{ - BeaconChainError, BeaconChainTypes, BeaconStore, BlockError, ExecutionPayloadError, - PayloadVerificationError, PayloadVerificationOutcome, + BeaconChain, BeaconChainError, BeaconChainTypes, BeaconStore, BlockError, + ExecutionPayloadError, PayloadVerificationError, PayloadVerificationOutcome, + payload_envelope_verification::gossip_verified_envelope::verify_envelope_consistency, }; pub mod execution_pending_envelope; @@ -166,8 +167,6 @@ pub enum EnvelopeError { EnvelopeProcessingError(EnvelopeProcessingError), /// Error verifying the execution payload ExecutionPayloadError(ExecutionPayloadError), - /// Optimistic sync is not supported for Gloas payload envelopes. - OptimisticSyncNotSupported { block_root: Hash256 }, /// The envelope's beacon block was not present in fork choice at import time. /// /// Unlike [`EnvelopeError::BlockRootUnknown`] (raised during gossip verification, where the @@ -199,7 +198,6 @@ impl EnvelopeError { | EnvelopeError::PriorToFinalization { .. } | EnvelopeError::BeaconChainError(_) | EnvelopeError::BeaconStateError(_) - | EnvelopeError::OptimisticSyncNotSupported { .. } | EnvelopeError::BlockRootNotInForkChoice(_) | EnvelopeError::InternalError(_) => false, } @@ -292,3 +290,41 @@ pub(crate) fn load_snapshot_from_state_root( beacon_block_root, }) } + +/// Performs simple, cheap checks to ensure that the envelope is relevant to be imported. +/// +/// `Ok(block_root` is returned if the envelope passes these checks and should progress with +/// verification. +/// +/// Returns an error if the envelope is not relevant or if an error occurs during a verification step. +pub fn check_envelope_relevancy( + block: &SignedBeaconBlock, + signed_envelope: &SignedExecutionPayloadEnvelope, + chain: &BeaconChain, +) -> Result { + let envelope = &signed_envelope.message; + let Ok(bid) = block.message().body().signed_execution_payload_bid() else { + return Err(EnvelopeError::InternalError( + "Block is pre-gloas".to_string(), + )); + }; + + let latest_finalized_slot = chain + .canonical_head + .cached_head() + .finalized_checkpoint() + .epoch + .start_slot(T::EthSpec::slots_per_epoch()); + + verify_envelope_consistency(envelope, block, &bid.message, latest_finalized_slot)?; + + if chain + .canonical_head + .fork_choice_read_lock() + .is_payload_received(&envelope.beacon_block_root) + { + return Ok(false); + } + + Ok(true) +} diff --git a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs index 98c143eaeb..8b9db8f04c 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -3715,7 +3715,6 @@ impl NetworkBeaconProcessor { | EnvelopeError::BeaconStateError(_) // The following variants are produced during envelope import, not gossip // verification, so they cannot be reached here. Ignore them to be safe. - | EnvelopeError::OptimisticSyncNotSupported { .. } | EnvelopeError::BlockRootNotInForkChoice(_) | EnvelopeError::InternalError(_) => { self.propagate_validation_result( From 3dd6be37b023c51838cd2f8889b7a9828813f1d7 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Mon, 15 Jun 2026 14:25:27 +0300 Subject: [PATCH 02/10] Clean up --- beacon_node/beacon_chain/src/beacon_chain.rs | 19 +++++++++---------- .../beacon_chain/src/block_verification.rs | 2 +- .../src/payload_envelope_verification/mod.rs | 6 +++--- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 9de17c9ed9..0b47e27415 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -3001,16 +3001,15 @@ impl BeaconChain { } } - let is_envelope_relevant = if let Some(envelope) = block.as_envelope() { - // If the envelope is relevant we skip the duplicate import check in - // `check_block_relevancy` - match check_envelope_relevancy(block.as_block(), envelope, self) { - Ok(_) => true, - Err(_) => false, - } - } else { - false - }; + // If the envelope is relevant we skip the duplicate import check in + // `check_block_relevancy`. A verification error or an already-received payload + // both leave the envelope irrelevant. + let is_envelope_relevant = block + .as_envelope() + .and_then(|envelope| { + check_envelope_relevancy(block.as_block(), envelope, self).ok() + }) + .unwrap_or(false); match check_block_relevancy(block.as_block(), block_root, is_envelope_relevant, self) { // If the block is relevant, add it to the filtered chain segment. diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 4caa934d83..71a0b03651 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -1881,7 +1881,7 @@ pub fn check_block_relevancy( // Check if the block is already known. We know it is post-finalization, so it is // sufficient to check the fork choice. This check can optionally be skipped. - if skip_import_check + if !skip_import_check && chain .canonical_head .fork_choice_read_lock() diff --git a/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs b/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs index 6f24744e2c..41f003a2f8 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs @@ -293,10 +293,10 @@ pub(crate) fn load_snapshot_from_state_root( /// Performs simple, cheap checks to ensure that the envelope is relevant to be imported. /// -/// `Ok(block_root` is returned if the envelope passes these checks and should progress with -/// verification. +/// Returns `Ok(true)` if the envelope passes these checks and should progress with verification, +/// or `Ok(false)` if its payload has already been received and is no longer relevant. /// -/// Returns an error if the envelope is not relevant or if an error occurs during a verification step. +/// Returns an error if a verification step fails. pub fn check_envelope_relevancy( block: &SignedBeaconBlock, signed_envelope: &SignedExecutionPayloadEnvelope, From 696ca8dcc0cc8b5c1c0a59e97219c2cede3edacf Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Mon, 15 Jun 2026 15:32:22 +0300 Subject: [PATCH 03/10] Add tests --- .../beacon_chain/tests/block_verification.rs | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/beacon_node/beacon_chain/tests/block_verification.rs b/beacon_node/beacon_chain/tests/block_verification.rs index 94d4b3b9da..597025a0c0 100644 --- a/beacon_node/beacon_chain/tests/block_verification.rs +++ b/beacon_node/beacon_chain/tests/block_verification.rs @@ -2389,6 +2389,104 @@ async fn range_sync_block_new_gloas_rejects_block_hash_mismatch() { ); } +/// Produces a Gloas block + envelope on top of the current head and imports the block (but not its +/// envelope), so the block is known to fork choice with its payload not yet received. +async fn import_gloas_block_without_envelope( + harness: &BeaconChainHarness>, +) -> ( + Arc>, + SignedExecutionPayloadEnvelope, + Hash256, +) { + harness.advance_slot(); + + let state = harness.get_current_state(); + let slot = harness.get_current_slot(); + let (block_contents, envelope, _) = harness.make_block_with_envelope(state, slot).await; + let block = block_contents.0.clone(); + let block_root = block.canonical_root(); + let envelope = envelope.expect("gloas block should have envelope"); + + harness + .process_block(slot, block_root, block_contents) + .await + .expect("block should import"); + + (block, envelope, block_root) +} + +/// A relevant envelope (payload not yet received) lets an already-imported block back through +/// `filter_chain_segment` so it can be re-processed with its envelope. +#[tokio::test] +async fn filter_chain_segment_keeps_imported_block_with_relevant_envelope() { + let spec = test_spec::(); + if !spec.fork_name_at_slot::(Slot::new(1)).gloas_enabled() { + return; + } + + let harness = BeaconChainHarness::builder(MainnetEthSpec) + .spec(spec.into()) + .keypairs(KEYPAIRS[0..VALIDATOR_COUNT].to_vec()) + .node_custody_type(NodeCustodyType::Supernode) + .fresh_ephemeral_store() + .mock_execution_layer() + .build(); + + let (block, envelope, _) = import_gloas_block_without_envelope(&harness).await; + + let available_envelope = AvailableEnvelope::new(Arc::new(envelope), vec![]); + let segment = vec![RangeSyncBlock::new_gloas(block, Some(available_envelope)).unwrap()]; + + let Ok(filtered) = harness.chain.filter_chain_segment(segment) else { + panic!("filter should succeed"); + }; + + assert_eq!( + filtered.len(), + 1, + "block with a relevant envelope should not be filtered as a duplicate" + ); +} + +/// Once the payload has been received the envelope is no longer relevant, so an already-imported +/// block is filtered out of the segment as a duplicate. +#[tokio::test] +async fn filter_chain_segment_drops_imported_block_when_payload_received() { + let spec = test_spec::(); + if !spec.fork_name_at_slot::(Slot::new(1)).gloas_enabled() { + return; + } + + let harness = BeaconChainHarness::builder(MainnetEthSpec) + .spec(spec.into()) + .keypairs(KEYPAIRS[0..VALIDATOR_COUNT].to_vec()) + .node_custody_type(NodeCustodyType::Supernode) + .fresh_ephemeral_store() + .mock_execution_layer() + .build(); + + let (block, envelope, block_root) = import_gloas_block_without_envelope(&harness).await; + + harness + .chain + .canonical_head + .fork_choice_write_lock() + .on_valid_payload_envelope_received(block_root) + .expect("payload should be marked received"); + + let available_envelope = AvailableEnvelope::new(Arc::new(envelope), vec![]); + let segment = vec![RangeSyncBlock::new_gloas(block, Some(available_envelope)).unwrap()]; + + let Ok(filtered) = harness.chain.filter_chain_segment(segment) else { + panic!("filter should succeed"); + }; + + assert!( + filtered.is_empty(), + "block whose payload was already received should be filtered as a duplicate" + ); +} + // Test that RpcBlock::new() rejects blocks when blob count doesn't match expected. #[tokio::test] async fn range_sync_block_construction_fails_with_wrong_blob_count() { From c00dff7b7d455f368065d6b351dd2fdf39ea3fb8 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Wed, 17 Jun 2026 14:21:09 +0300 Subject: [PATCH 04/10] Fix --- .../src/payload_envelope_verification/import.rs | 8 ++++++++ .../beacon_chain/src/payload_envelope_verification/mod.rs | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs b/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs index 5e90667a16..7f66f77ff8 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs @@ -222,6 +222,14 @@ impl BeaconChain { block_root: Hash256, payload_verification_status: PayloadVerificationStatus, ) -> Result { + // TODO(gloas): optimistic sync is not supported for Gloas. Proto-array only tracks + // `payload_received` as a bool, so an optimistically-imported payload would be treated as + // valid with no way to invalidate it if the EL later rejects it. Reject here (covering both + // the gossip and range-sync paths) until fork choice can track optimistic payload status. + if payload_verification_status.is_optimistic() { + return Err(EnvelopeError::OptimisticSyncNotSupported { block_root }); + } + // Everything in this initial section is on the hot path for processing the envelope. // Take an upgradable read lock on fork choice so we can check if this block has already // been imported. We don't want to repeat work importing a block that is already imported. diff --git a/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs b/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs index 41f003a2f8..870969c107 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs @@ -167,6 +167,13 @@ pub enum EnvelopeError { EnvelopeProcessingError(EnvelopeProcessingError), /// Error verifying the execution payload ExecutionPayloadError(ExecutionPayloadError), + /// Optimistic sync is not supported for Gloas payload envelopes. + /// + /// Proto-array only tracks `payload_received` as a bool, so it cannot represent an + /// optimistically-imported payload that the EL may later invalidate. Until fork choice can + /// track optimistic payload status, we reject optimistic envelopes rather than treat them as + /// valid. + OptimisticSyncNotSupported { block_root: Hash256 }, /// The envelope's beacon block was not present in fork choice at import time. /// /// Unlike [`EnvelopeError::BlockRootUnknown`] (raised during gossip verification, where the @@ -198,6 +205,7 @@ impl EnvelopeError { | EnvelopeError::PriorToFinalization { .. } | EnvelopeError::BeaconChainError(_) | EnvelopeError::BeaconStateError(_) + | EnvelopeError::OptimisticSyncNotSupported { .. } | EnvelopeError::BlockRootNotInForkChoice(_) | EnvelopeError::InternalError(_) => false, } From 7f827fbc8141bcea84e8eb85b7248c1c996bb747 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Wed, 17 Jun 2026 14:24:36 +0300 Subject: [PATCH 05/10] revert --- .../src/payload_envelope_verification/import.rs | 16 ++++++++-------- .../src/payload_envelope_verification/mod.rs | 5 ----- .../network_beacon_processor/gossip_methods.rs | 1 + 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs b/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs index 7f66f77ff8..00806f0e17 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs @@ -173,6 +173,14 @@ impl BeaconChain { .map_err(BeaconChainError::TokioJoin)? .ok_or(BeaconChainError::RuntimeShutdown)??; + // TODO(gloas): optimistic sync is not supported for Gloas, maybe we could re-add it + if payload_verification_outcome + .payload_verification_status + .is_optimistic() + { + return Err(EnvelopeError::OptimisticSyncNotSupported { block_root }); + } + Ok(AvailabilityPendingExecutedEnvelope::new( signed_envelope, block_root, @@ -222,14 +230,6 @@ impl BeaconChain { block_root: Hash256, payload_verification_status: PayloadVerificationStatus, ) -> Result { - // TODO(gloas): optimistic sync is not supported for Gloas. Proto-array only tracks - // `payload_received` as a bool, so an optimistically-imported payload would be treated as - // valid with no way to invalidate it if the EL later rejects it. Reject here (covering both - // the gossip and range-sync paths) until fork choice can track optimistic payload status. - if payload_verification_status.is_optimistic() { - return Err(EnvelopeError::OptimisticSyncNotSupported { block_root }); - } - // Everything in this initial section is on the hot path for processing the envelope. // Take an upgradable read lock on fork choice so we can check if this block has already // been imported. We don't want to repeat work importing a block that is already imported. diff --git a/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs b/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs index 870969c107..95209cad4d 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs @@ -168,11 +168,6 @@ pub enum EnvelopeError { /// Error verifying the execution payload ExecutionPayloadError(ExecutionPayloadError), /// Optimistic sync is not supported for Gloas payload envelopes. - /// - /// Proto-array only tracks `payload_received` as a bool, so it cannot represent an - /// optimistically-imported payload that the EL may later invalidate. Until fork choice can - /// track optimistic payload status, we reject optimistic envelopes rather than treat them as - /// valid. OptimisticSyncNotSupported { block_root: Hash256 }, /// The envelope's beacon block was not present in fork choice at import time. /// diff --git a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs index 8b9db8f04c..98c143eaeb 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -3715,6 +3715,7 @@ impl NetworkBeaconProcessor { | EnvelopeError::BeaconStateError(_) // The following variants are produced during envelope import, not gossip // verification, so they cannot be reached here. Ignore them to be safe. + | EnvelopeError::OptimisticSyncNotSupported { .. } | EnvelopeError::BlockRootNotInForkChoice(_) | EnvelopeError::InternalError(_) => { self.propagate_validation_result( From 94587753a2ef1319ab38d3232e9b84998cdbfa64 Mon Sep 17 00:00:00 2001 From: Pawan Dhananjay Date: Fri, 19 Jun 2026 15:43:23 -0700 Subject: [PATCH 06/10] Special cases checkpoint envelope processing --- beacon_node/beacon_chain/src/beacon_chain.rs | 62 +++++++++++++++++++- 1 file changed, 60 insertions(+), 2 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 0b47e27415..b1aa2ec71a 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -20,6 +20,7 @@ use crate::custody_context::CustodyContextSsz; use crate::data_availability_checker::{ Availability as BlockAvailability, AvailabilityCheckError, AvailableBlock, AvailableBlockData, DataColumnReconstructionResult as DataColumnReconstructionResultV1, + verify_columns_against_block, }; use crate::data_availability_checker::DataAvailabilityChecker; @@ -3029,12 +3030,27 @@ impl BeaconChain { // 2. In some non-canonical chain at a slot that has been finalized already. // // In the case of (1), there's no need to re-import and later blocks in this - // segement might be useful. + // segment might be useful. + // This changes slightly post-gloas because the finalized block can be + // imported without its corresponding envelope. If the block we are processing is + // the finalized block then we still add it to the filtered chain segment so that + // its envelope can be processed. // // In the case of (2), skipping the block is valid since we should never import it. // However, we will potentially get a `ParentUnknown` on a later block. The sync // protocol will need to ensure this is handled gracefully. - Err(BlockError::WouldRevertFinalizedSlot { .. }) => continue, + Err(BlockError::WouldRevertFinalizedSlot { .. }) => { + if is_envelope_relevant + && self + .canonical_head + .cached_head() + .finalized_checkpoint() + .root + == block_root + { + filtered_chain_segment.push((block_root, block)); + } + } // The block has a known parent that does not descend from the finalized block. // There is no need to process this block or any children. Err(BlockError::NotFinalizedDescendant { block_parent_root }) => { @@ -3119,6 +3135,48 @@ impl BeaconChain { let mut blocks = filtered_chain_segment.split_off(last_index); std::mem::swap(&mut blocks, &mut filtered_chain_segment); + // Here, we are special casing the checkpoint sync block's envelope processing. + // Post-gloas, if the first filtered block is the checkpoint block, range + // sync may still need to process its envelope so that the first post-checkpoint + // child can resolve its parent payload status. + // The block is an anchor, so there won't be a parent present in fork choice, + // so we need to avoid processing it. + if matches!(blocks.first(), Some((root, _)) if *root == self + .canonical_head + .cached_head() + .finalized_checkpoint() + .root) + { + let (block_root, block) = blocks.remove(0); + let block_slot = block.slot(); + + if let RangeSyncBlock::Gloas { + block, + envelope: Some(envelope), + } = block + { + let chain = self.clone(); + if let Err(error) = async move { + verify_columns_against_block(&chain.kzg, &block, &envelope.columns) + .map_err(BlockError::AvailabilityCheck)?; + + self.process_range_sync_envelope(envelope, block_root, block) + .await + .map_err(BlockError::from)?; + + Ok::<(), BlockError>(()) + } + .await + { + return ChainSegmentResult::Failed { + imported_blocks, + error, + }; + } + } + imported_blocks.push((block_root, block_slot)); + } + // Extract envelopes before passing blocks to signature verification. let envelopes: Vec<_> = blocks .iter() From 20c909c027a4b92202e3d242ab9e563d96228382 Mon Sep 17 00:00:00 2001 From: Pawan Dhananjay Date: Mon, 22 Jun 2026 15:44:57 -0700 Subject: [PATCH 07/10] Get it working without check_payload_relevency --- beacon_node/beacon_chain/src/beacon_chain.rs | 29 +++++++----- .../beacon_chain/src/block_verification.rs | 18 ++++---- .../src/block_verification_types.rs | 13 +----- .../src/payload_envelope_verification/mod.rs | 45 ++----------------- 4 files changed, 29 insertions(+), 76 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index b1aa2ec71a..39261e394f 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -65,7 +65,6 @@ use crate::payload_attestation_verification::VerifiedPayloadAttestationMessage; use crate::payload_bid_verification::payload_bid_cache::GossipVerifiedPayloadBidCache; #[cfg(not(test))] use crate::payload_envelope_streamer::{EnvelopeRequestSource, launch_payload_envelope_stream}; -use crate::payload_envelope_verification::check_envelope_relevancy; use crate::pending_payload_cache::PendingPayloadCache; use crate::pending_payload_cache::{ Availability as PayloadAvailability, @@ -3002,19 +3001,25 @@ impl BeaconChain { } } - // If the envelope is relevant we skip the duplicate import check in - // `check_block_relevancy`. A verification error or an already-received payload - // both leave the envelope irrelevant. - let is_envelope_relevant = block - .as_envelope() - .and_then(|envelope| { - check_envelope_relevancy(block.as_block(), envelope, self).ok() - }) - .unwrap_or(false); + // The envelope needs import only if it's a Gloas block with an envelope and + // the envelope isn't already in fork choice. + let range_sync_envelope_needs_import = matches!( + block, + RangeSyncBlock::Gloas { + envelope: Some(_), + .. + } + ) && !self + .canonical_head + .fork_choice_read_lock() + .is_payload_received(&block_root); - match check_block_relevancy(block.as_block(), block_root, is_envelope_relevant, self) { + match check_block_relevancy(block.as_block(), block_root, self) { // If the block is relevant, add it to the filtered chain segment. Ok(_) => filtered_chain_segment.push((block_root, block)), + Err(BlockError::DuplicateFullyImported(_)) if range_sync_envelope_needs_import => { + filtered_chain_segment.push((block_root, block)); + } // If the block is already known, simply ignore this block. // // Note that `check_block_relevancy` is incapable of returning @@ -3040,7 +3045,7 @@ impl BeaconChain { // However, we will potentially get a `ParentUnknown` on a later block. The sync // protocol will need to ensure this is handled gracefully. Err(BlockError::WouldRevertFinalizedSlot { .. }) => { - if is_envelope_relevant + if range_sync_envelope_needs_import && self .canonical_head .cached_head() diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 71a0b03651..368df038f0 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -1349,7 +1349,7 @@ impl IntoExecutionPendingBlock for RangeSyncBlock Result, BlockSlashInfo> { // Perform an early check to prevent wasting time on irrelevant blocks. let header = self.signed_block_header(); - let block_root = check_block_relevancy(self.as_block(), block_root, false, chain) + let block_root = check_block_relevancy(self.as_block(), block_root, chain) .map_err(|e| BlockSlashInfo::SignatureNotChecked(header.clone(), e))?; let (available_block, _envelope) = self.into_available_block().map_err(|e| { @@ -1393,7 +1393,7 @@ impl IntoExecutionPendingBlock for LookupBlock Result, BlockSlashInfo> { // Perform an early check to prevent wasting time on irrelevant blocks. - let block_root = check_block_relevancy(self.as_block(), block_root, false, chain) + let block_root = check_block_relevancy(self.as_block(), block_root, chain) .map_err(|e| BlockSlashInfo::SignatureNotChecked(self.signed_block_header(), e))?; let maybe_available_block = MaybeAvailableBlock::AvailabilityPending { @@ -1466,7 +1466,7 @@ impl ExecutionPendingBlock { /* * Perform cursory checks to see if the block is even worth processing. */ - check_block_relevancy(block.as_block(), block_root, false, chain)?; + check_block_relevancy(block.as_block(), block_root, chain)?; // Define a future that will verify the execution payload with an execution engine. // @@ -1850,7 +1850,6 @@ pub fn check_block_is_finalized_checkpoint_or_descendant< pub fn check_block_relevancy( signed_block: &SignedBeaconBlock, block_root: Hash256, - skip_import_check: bool, chain: &BeaconChain, ) -> Result { let block = signed_block.message(); @@ -1880,12 +1879,11 @@ pub fn check_block_relevancy( check_block_against_finalized_slot(block, block_root, chain)?; // Check if the block is already known. We know it is post-finalization, so it is - // sufficient to check the fork choice. This check can optionally be skipped. - if !skip_import_check - && chain - .canonical_head - .fork_choice_read_lock() - .contains_block(&block_root) + // sufficient to check the fork choice. + if chain + .canonical_head + .fork_choice_read_lock() + .contains_block(&block_root) { return Err(BlockError::DuplicateFullyImported(block_root)); } diff --git a/beacon_node/beacon_chain/src/block_verification_types.rs b/beacon_node/beacon_chain/src/block_verification_types.rs index 8abed19814..75c50e5048 100644 --- a/beacon_node/beacon_chain/src/block_verification_types.rs +++ b/beacon_node/beacon_chain/src/block_verification_types.rs @@ -12,7 +12,7 @@ use std::sync::Arc; use types::data::BlobIdentifier; use types::{ BeaconBlockRef, BeaconState, BlindedPayload, ChainSpec, Epoch, EthSpec, Hash256, - SignedBeaconBlock, SignedBeaconBlockHeader, SignedExecutionPayloadEnvelope, Slot, + SignedBeaconBlock, SignedBeaconBlockHeader, Slot, }; /// A wrapper around a `SignedBeaconBlock`. This varaint is constructed @@ -112,17 +112,6 @@ impl RangeSyncBlock { } } - pub fn as_envelope(&self) -> Option<&SignedExecutionPayloadEnvelope> { - match self { - RangeSyncBlock::Base(_) => None, - RangeSyncBlock::Gloas { envelope, .. } => { - envelope.as_ref().map(|e| e.envelope().as_ref()) - } - } - } -} - -impl RangeSyncBlock { /// Constructs a `RangeSyncBlock` from a block and availability data (pre-Gloas). pub fn new( block: Arc>, diff --git a/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs b/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs index 95209cad4d..a0d34949c6 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs @@ -25,13 +25,12 @@ use strum::AsRefStr; use tracing::instrument; use types::{ BeaconState, BeaconStateError, DataColumnSidecarList, EthSpec, ExecutionBlockHash, - ExecutionPayloadEnvelope, Hash256, SignedBeaconBlock, SignedExecutionPayloadEnvelope, Slot, + ExecutionPayloadEnvelope, Hash256, SignedExecutionPayloadEnvelope, Slot, }; use crate::{ - BeaconChain, BeaconChainError, BeaconChainTypes, BeaconStore, BlockError, - ExecutionPayloadError, PayloadVerificationError, PayloadVerificationOutcome, - payload_envelope_verification::gossip_verified_envelope::verify_envelope_consistency, + BeaconChainError, BeaconChainTypes, BeaconStore, BlockError, ExecutionPayloadError, + PayloadVerificationError, PayloadVerificationOutcome, }; pub mod execution_pending_envelope; @@ -293,41 +292,3 @@ pub(crate) fn load_snapshot_from_state_root( beacon_block_root, }) } - -/// Performs simple, cheap checks to ensure that the envelope is relevant to be imported. -/// -/// Returns `Ok(true)` if the envelope passes these checks and should progress with verification, -/// or `Ok(false)` if its payload has already been received and is no longer relevant. -/// -/// Returns an error if a verification step fails. -pub fn check_envelope_relevancy( - block: &SignedBeaconBlock, - signed_envelope: &SignedExecutionPayloadEnvelope, - chain: &BeaconChain, -) -> Result { - let envelope = &signed_envelope.message; - let Ok(bid) = block.message().body().signed_execution_payload_bid() else { - return Err(EnvelopeError::InternalError( - "Block is pre-gloas".to_string(), - )); - }; - - let latest_finalized_slot = chain - .canonical_head - .cached_head() - .finalized_checkpoint() - .epoch - .start_slot(T::EthSpec::slots_per_epoch()); - - verify_envelope_consistency(envelope, block, &bid.message, latest_finalized_slot)?; - - if chain - .canonical_head - .fork_choice_read_lock() - .is_payload_received(&envelope.beacon_block_root) - { - return Ok(false); - } - - Ok(true) -} From 8f23b587df7eaab6ff83ec39365f02738558d0ea Mon Sep 17 00:00:00 2001 From: Pawan Dhananjay Date: Mon, 22 Jun 2026 15:45:04 -0700 Subject: [PATCH 08/10] Add tests --- .../beacon_chain/tests/block_verification.rs | 77 +++++++++++++++---- 1 file changed, 61 insertions(+), 16 deletions(-) diff --git a/beacon_node/beacon_chain/tests/block_verification.rs b/beacon_node/beacon_chain/tests/block_verification.rs index 597025a0c0..fd164a0f8c 100644 --- a/beacon_node/beacon_chain/tests/block_verification.rs +++ b/beacon_node/beacon_chain/tests/block_verification.rs @@ -2415,10 +2415,10 @@ async fn import_gloas_block_without_envelope( (block, envelope, block_root) } -/// A relevant envelope (payload not yet received) lets an already-imported block back through -/// `filter_chain_segment` so it can be re-processed with its envelope. +/// Retrying a range-sync batch can provide the envelope for a block that was previously imported +/// without one. The duplicate block should be allowed through far enough to import the envelope. #[tokio::test] -async fn filter_chain_segment_keeps_imported_block_with_relevant_envelope() { +async fn process_chain_segment_imports_missing_envelope_for_duplicate_gloas_block() { let spec = test_spec::(); if !spec.fork_name_at_slot::(Slot::new(1)).gloas_enabled() { return; @@ -2432,26 +2432,66 @@ async fn filter_chain_segment_keeps_imported_block_with_relevant_envelope() { .mock_execution_layer() .build(); - let (block, envelope, _) = import_gloas_block_without_envelope(&harness).await; + let (block, envelope, block_root) = import_gloas_block_without_envelope(&harness).await; + let block_slot = block.slot(); + + assert!( + !harness + .chain + .canonical_head + .fork_choice_read_lock() + .is_payload_received(&block_root), + "payload should start missing" + ); + assert!( + harness + .chain + .store + .get_payload_envelope(&block_root) + .expect("should read envelope from store") + .is_none(), + "envelope should start missing from the store" + ); let available_envelope = AvailableEnvelope::new(Arc::new(envelope), vec![]); let segment = vec![RangeSyncBlock::new_gloas(block, Some(available_envelope)).unwrap()]; - let Ok(filtered) = harness.chain.filter_chain_segment(segment) else { - panic!("filter should succeed"); + let result = harness + .chain + .process_chain_segment(segment, NotifyExecutionLayer::Yes) + .await; + + let ChainSegmentResult::Successful { imported_blocks } = result else { + panic!("range sync should succeed"); }; assert_eq!( - filtered.len(), - 1, - "block with a relevant envelope should not be filtered as a duplicate" + imported_blocks, + vec![(block_root, block_slot)], + "the duplicate block should be reported as processed" + ); + assert!( + harness + .chain + .canonical_head + .fork_choice_read_lock() + .is_payload_received(&block_root), + "range sync should mark the payload as received" + ); + assert!( + harness + .chain + .store + .get_payload_envelope(&block_root) + .expect("should read envelope from store") + .is_some(), + "range sync should persist the envelope" ); } -/// Once the payload has been received the envelope is no longer relevant, so an already-imported -/// block is filtered out of the segment as a duplicate. +/// Once the payload has been received, retrying the same block and envelope is a no-op. #[tokio::test] -async fn filter_chain_segment_drops_imported_block_when_payload_received() { +async fn process_chain_segment_ignores_duplicate_gloas_block_when_payload_received() { let spec = test_spec::(); if !spec.fork_name_at_slot::(Slot::new(1)).gloas_enabled() { return; @@ -2477,13 +2517,18 @@ async fn filter_chain_segment_drops_imported_block_when_payload_received() { let available_envelope = AvailableEnvelope::new(Arc::new(envelope), vec![]); let segment = vec![RangeSyncBlock::new_gloas(block, Some(available_envelope)).unwrap()]; - let Ok(filtered) = harness.chain.filter_chain_segment(segment) else { - panic!("filter should succeed"); + let result = harness + .chain + .process_chain_segment(segment, NotifyExecutionLayer::Yes) + .await; + + let ChainSegmentResult::Successful { imported_blocks } = result else { + panic!("range sync should succeed"); }; assert!( - filtered.is_empty(), - "block whose payload was already received should be filtered as a duplicate" + imported_blocks.is_empty(), + "block whose payload was already received should be ignored as a duplicate" ); } From e1c1f0713700b862698a6df02ebb69f520ba22d1 Mon Sep 17 00:00:00 2001 From: Pawan Dhananjay Date: Mon, 22 Jun 2026 15:45:50 -0700 Subject: [PATCH 09/10] Cleanup signature_verify_chain_segment --- .../beacon_chain/src/block_verification.rs | 23 ++++++------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 368df038f0..1c1fdeafb3 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -621,6 +621,7 @@ pub(crate) fn process_block_slash_info( &chain.spec, )?; - let mut available_blocks = Vec::with_capacity(chain_segment.len()); - let mut envelopes = Vec::with_capacity(chain_segment.len()); let mut signature_verified_blocks = Vec::with_capacity(chain_segment.len()); for (block_root, block) in chain_segment { let consensus_context = ConsensusContext::new(block.slot()).set_current_block_root(block_root); - - let (available_block, envelope) = block.into_available_block()?; - available_blocks.push(available_block.clone()); - envelopes.push(envelope); + // This gets columns from the block for pre-gloas and from the envelope for + // post gloas. + if let Some(columns) = block.data_columns() { + verify_columns_against_block(&chain.kzg, block.as_block(), &columns)?; + } + let (available_block, _envelope) = block.into_available_block()?; signature_verified_blocks.push(SignatureVerifiedBlock { block: MaybeAvailableBlock::Available(available_block), block_root, @@ -670,16 +671,6 @@ pub fn signature_verify_chain_segment( }); } - chain - .data_availability_checker - .batch_verify_kzg_for_available_blocks(&available_blocks)?; - - for (available_block, maybe_envelope) in available_blocks.iter().zip(envelopes.iter()) { - if let Some(envelope) = maybe_envelope { - verify_columns_against_block(&chain.kzg, available_block.block(), &envelope.columns)?; - } - } - // verify signatures let pubkey_cache = get_validator_pubkey_cache(chain)?; let mut signature_verifier = get_signature_verifier(&state, &pubkey_cache, &chain.spec); From 8121189c919d2ea506f13f23ae9e40edce852299 Mon Sep 17 00:00:00 2001 From: Pawan Dhananjay Date: Mon, 22 Jun 2026 15:51:56 -0700 Subject: [PATCH 10/10] Reduce diff --- beacon_node/beacon_chain/src/block_verification_types.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/beacon_node/beacon_chain/src/block_verification_types.rs b/beacon_node/beacon_chain/src/block_verification_types.rs index 75c50e5048..18e95f58f3 100644 --- a/beacon_node/beacon_chain/src/block_verification_types.rs +++ b/beacon_node/beacon_chain/src/block_verification_types.rs @@ -111,7 +111,9 @@ impl RangeSyncBlock { .filter(|columns| !columns.is_empty()), } } +} +impl RangeSyncBlock { /// Constructs a `RangeSyncBlock` from a block and availability data (pre-Gloas). pub fn new( block: Arc>,