From d9c21f5e3301570090bf10cdff4b46c9942a704e Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Tue, 27 Jan 2026 19:32:30 -0800 Subject: [PATCH 001/118] Add da router, and initial logic --- beacon_node/beacon_chain/src/beacon_chain.rs | 143 +- .../beacon_chain/src/block_verification.rs | 2 + beacon_node/beacon_chain/src/builder.rs | 45 +- .../beacon_chain/src/custody_context.rs | 9 +- .../src/data_availability_checker.rs | 226 +-- .../src/data_availability_checker_v2.rs | 1098 ++++++++++++++ .../overflow_lru_cache.rs | 1306 +++++++++++++++++ .../state_lru_cache.rs | 138 ++ .../src/data_column_availability_cache.rs | 388 +++++ .../src/data_column_verification.rs | 19 +- .../fetch_blobs/fetch_blobs_beacon_adapter.rs | 9 +- .../beacon_chain/src/fetch_blobs/mod.rs | 2 +- .../beacon_chain/src/fetch_blobs/tests.rs | 2 +- beacon_node/beacon_chain/src/kzg_utils.rs | 2 +- beacon_node/beacon_chain/src/lib.rs | 3 + beacon_node/beacon_chain/src/metrics.rs | 3 +- .../src/payload_verification_types.rs | 74 + beacon_node/beacon_chain/src/test_utils.rs | 74 + .../lighthouse_network/src/rpc/codec.rs | 2 + .../lighthouse_network/src/rpc/methods.rs | 8 +- .../lighthouse_network/tests/rpc_tests.rs | 2 + .../gossip_methods.rs | 29 +- .../src/network_beacon_processor/mod.rs | 4 +- .../network_beacon_processor/rpc_methods.rs | 4 +- .../network_beacon_processor/sync_methods.rs | 2 + .../network/src/sync/block_lookups/common.rs | 2 +- .../sync/block_lookups/single_block_lookup.rs | 6 +- .../network/src/sync/network_context.rs | 6 +- .../requests/data_columns_by_root.rs | 1 + beacon_node/network/src/sync/tests/lookups.rs | 4 + 30 files changed, 3405 insertions(+), 208 deletions(-) create mode 100644 beacon_node/beacon_chain/src/data_availability_checker_v2.rs create mode 100644 beacon_node/beacon_chain/src/data_availability_checker_v2/overflow_lru_cache.rs create mode 100644 beacon_node/beacon_chain/src/data_availability_checker_v2/state_lru_cache.rs create mode 100644 beacon_node/beacon_chain/src/data_column_availability_cache.rs create mode 100644 beacon_node/beacon_chain/src/payload_verification_types.rs diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index e5bdda384f..2b71734f15 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -25,6 +25,10 @@ use crate::data_availability_checker::{ Availability, AvailabilityCheckError, AvailableBlock, AvailableBlockData, DataAvailabilityChecker, DataColumnReconstructionResult, }; +use crate::data_availability_checker_v2::DataAvailabilityChecker as DataAvailabilityCheckerV2; +use crate::data_column_availability_cache::{ + AvailabilityOutcome, DataAvailabilityRouter, ReconstructionOutcome, +}; use crate::data_column_verification::{GossipDataColumnError, GossipVerifiedDataColumn}; use crate::early_attester_cache::EarlyAttesterCache; use crate::errors::{BeaconChainError as Error, BlockProductionError}; @@ -476,7 +480,8 @@ pub struct BeaconChain { pub genesis_backfill_slot: Slot, /// Provides a KZG verification and temporary storage for blocks and blobs as /// they are collected and combined. - pub data_availability_checker: Arc>, + pub data_availability_checker: + Arc, DataAvailabilityCheckerV2>>, /// The KZG trusted setup used by this chain. pub kzg: Arc, /// RNG instance used by the chain. Currently used for shuffling column sidecars in block publishing. @@ -1123,10 +1128,11 @@ impl BeaconChain { &self, block_root: Hash256, indices: &[ColumnIndex], + fork_name: ForkName, ) -> Result, Error> { let all_cached_columns_opt = self .data_availability_checker - .get_data_columns(block_root) + .get_data_columns(block_root, fork_name) .or_else(|| self.early_attester_cache.get_data_columns(block_root)); if let Some(mut all_cached_columns) = all_cached_columns_opt { @@ -1286,7 +1292,11 @@ impl BeaconChain { /// chain. Used by sync to learn the status of a block and prevent repeated downloads / /// processing attempts. pub fn get_block_process_status(&self, block_root: &Hash256) -> BlockProcessStatus { - if let Some(cached_block) = self.data_availability_checker.get_cached_block(block_root) { + if let Some(cached_block) = self + .data_availability_checker + .v1() + .get_cached_block(block_root) + { return cached_block; } @@ -3060,6 +3070,7 @@ impl BeaconChain { } self.emit_sse_data_column_sidecar_events( + slot, &block_root, data_columns.iter().map(|column| column.as_data_column()), ); @@ -3136,6 +3147,7 @@ impl BeaconChain { } EngineGetBlobsOutput::CustodyColumns(columns) => { self.emit_sse_data_column_sidecar_events( + slot, &block_root, columns.iter().map(|column| column.as_data_column()), ); @@ -3155,6 +3167,7 @@ impl BeaconChain { { let imported_blobs = self .data_availability_checker + .v1() .cached_blob_indexes(block_root) .unwrap_or_default(); let new_blobs = blobs_iter.filter(|b| !imported_blobs.contains(&b.index)); @@ -3169,6 +3182,7 @@ impl BeaconChain { fn emit_sse_data_column_sidecar_events<'a, I>( self: &Arc, + slot: Slot, block_root: &Hash256, data_columns_iter: I, ) where @@ -3179,7 +3193,7 @@ impl BeaconChain { { let imported_data_columns = self .data_availability_checker - .cached_data_column_indexes(block_root) + .cached_data_column_indexes(block_root, slot) .unwrap_or_default(); let new_data_columns = data_columns_iter.filter(|b| !imported_data_columns.contains(&b.index)); @@ -3232,6 +3246,7 @@ impl BeaconChain { } self.emit_sse_data_column_sidecar_events( + slot, &block_root, custody_columns.iter().map(|column| column.as_ref()), ); @@ -3242,6 +3257,7 @@ impl BeaconChain { pub async fn reconstruct_data_columns( self: &Arc, + slot: Slot, block_root: Hash256, ) -> Result< Option<( @@ -3268,33 +3284,45 @@ impl BeaconChain { .task_executor .spawn_blocking_with_rayon_async(RayonPoolType::HighPriority, move || { let _guard = current_span.enter(); - data_availability_checker.reconstruct_data_columns(&block_root) + data_availability_checker.reconstruct_data_columns(&block_root, slot) }) .await .map_err(|_| BeaconChainError::RuntimeShutdown)??; match result { - DataColumnReconstructionResult::Success((availability, data_columns_to_publish)) => { - let Some(slot) = data_columns_to_publish.first().map(|d| d.slot()) else { - // This should be unreachable because empty result would return `RecoveredColumnsNotImported` instead of success. - return Ok(None); - }; + ReconstructionOutcome::Block(data_column_reconstruction_result) => { + match data_column_reconstruction_result { + DataColumnReconstructionResult::Success(( + availability, + data_columns_to_publish, + )) => { + let Some(slot) = data_columns_to_publish.first().map(|d| d.slot()) else { + // This should be unreachable because empty result would return `RecoveredColumnsNotImported` instead of success. + return Ok(None); + }; - self.process_availability(slot, availability, || Ok(())) - .await - .map(|availability_processing_status| { - Some((availability_processing_status, data_columns_to_publish)) - }) - } - DataColumnReconstructionResult::NotStarted(reason) - | DataColumnReconstructionResult::RecoveredColumnsNotImported(reason) => { - // We use metric here because logging this would be *very* noisy. - metrics::inc_counter_vec( - &metrics::KZG_DATA_COLUMN_RECONSTRUCTION_INCOMPLETE_TOTAL, - &[reason], - ); - Ok(None) + self.process_availability( + slot, + AvailabilityOutcome::Block(availability), + || Ok(()), + ) + .await + .map(|availability_processing_status| { + Some((availability_processing_status, data_columns_to_publish)) + }) + } + DataColumnReconstructionResult::NotStarted(reason) + | DataColumnReconstructionResult::RecoveredColumnsNotImported(reason) => { + // We use metric here because logging this would be *very* noisy. + metrics::inc_counter_vec( + &metrics::KZG_DATA_COLUMN_RECONSTRUCTION_INCOMPLETE_TOTAL, + &[reason], + ); + Ok(None) + } + } } + ReconstructionOutcome::Payload(_data_column_reconstruction_result) => todo!(), } } @@ -3343,11 +3371,9 @@ impl BeaconChain { ); } - self.data_availability_checker.put_pre_execution_block( - block_root, - unverified_block.block_cloned(), - block_source, - )?; + self.data_availability_checker + .v1() + .put_pre_execution_block(block_root, unverified_block.block_cloned(), block_source)?; // Start the Prometheus timer. let _full_timer = metrics::start_timer(&metrics::BLOCK_PROCESSING_TIMES); @@ -3382,6 +3408,7 @@ impl BeaconChain { // chain to get stuck temporarily if the block is canonical. Therefore we remove // it from the cache if execution fails. self.data_availability_checker + .v1() .remove_block_on_execution_error(&block_root); })?; @@ -3509,7 +3536,11 @@ impl BeaconChain { block: AvailabilityPendingExecutedBlock, ) -> Result { let slot = block.block.slot(); - let availability = self.data_availability_checker.put_executed_block(block)?; + let availability = AvailabilityOutcome::Block( + self.data_availability_checker + .v1() + .put_executed_block(block)?, + ); self.process_availability(slot, availability, || Ok(())) .await } @@ -3524,9 +3555,11 @@ impl BeaconChain { if let Some(slasher) = self.slasher.as_ref() { slasher.accept_block_header(blob.signed_block_header()); } - let availability = self - .data_availability_checker - .put_gossip_verified_blobs(blob.block_root(), std::iter::once(blob))?; + let availability = AvailabilityOutcome::Block( + self.data_availability_checker + .v1() + .put_gossip_verified_blobs(blob.block_root(), std::iter::once(blob))?, + ); self.process_availability(slot, availability, || Ok(())) .await @@ -3597,9 +3630,11 @@ impl BeaconChain { block_root, blobs.iter().flatten().map(Arc::as_ref), )?; - let availability = self - .data_availability_checker - .put_rpc_blobs(block_root, blobs)?; + let availability = AvailabilityOutcome::Block( + self.data_availability_checker + .v1() + .put_rpc_blobs(block_root, blobs)?, + ); self.process_availability(slot, availability, || Ok(())) .await @@ -3617,8 +3652,12 @@ impl BeaconChain { block_root, blobs.iter().map(|b| b.as_blob()), )?; - self.data_availability_checker - .put_kzg_verified_blobs(block_root, blobs)? + let availability = self + .data_availability_checker + .v1() + .put_kzg_verified_blobs(block_root, blobs)?; + + AvailabilityOutcome::Block(availability) } EngineGetBlobsOutput::CustodyColumns(data_columns) => { self.check_data_column_sidecar_header_signature_and_slashability( @@ -3626,7 +3665,7 @@ impl BeaconChain { data_columns.iter().map(|c| c.as_data_column()), )?; self.data_availability_checker - .put_kzg_verified_custody_data_columns(block_root, data_columns)? + .put_kzg_verified_custody_data_columns(block_root, slot, data_columns)? } }; @@ -3699,18 +3738,23 @@ impl BeaconChain { async fn process_availability( self: &Arc, slot: Slot, - availability: Availability, + availability: AvailabilityOutcome, publish_fn: impl FnOnce() -> Result<(), BlockError>, ) -> Result { match availability { - Availability::Available(block) => { - publish_fn()?; - // Block is fully available, import into fork choice - self.import_available_block(block).await + AvailabilityOutcome::Block(availability) => { + match availability { + Availability::Available(block) => { + publish_fn()?; + // Block is fully available, import into fork choice + self.import_available_block(block).await + } + Availability::MissingComponents(block_root) => Ok( + AvailabilityProcessingStatus::MissingComponents(slot, block_root), + ), + } } - Availability::MissingComponents(block_root) => Ok( - AvailabilityProcessingStatus::MissingComponents(slot, block_root), - ), + AvailabilityOutcome::Payload(_availability) => todo!(), } } @@ -7300,12 +7344,15 @@ impl BeaconChain { /// The epoch at which we require a data availability check in block processing. /// `None` if the `Deneb` fork is disabled. pub fn data_availability_boundary(&self) -> Option { - self.data_availability_checker.data_availability_boundary() + self.data_availability_checker + .v1() + .data_availability_boundary() } /// Returns true if epoch is within the data availability boundary pub fn da_check_required_for_epoch(&self, epoch: Epoch) -> bool { self.data_availability_checker + .v1() .da_check_required_for_epoch(epoch) } diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index df8c49f8de..6100335fbc 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -649,6 +649,7 @@ pub fn signature_verify_chain_segment( let (roots, blocks): (Vec<_>, Vec<_>) = chain_segment.into_iter().unzip(); let maybe_available_blocks = chain .data_availability_checker + .v1() .verify_kzg_for_rpc_blocks(blocks)?; // zip it back up let mut signature_verified_blocks = roots @@ -1299,6 +1300,7 @@ impl IntoExecutionPendingBlock for RpcBlock .map_err(|e| BlockSlashInfo::SignatureNotChecked(self.signed_block_header(), e))?; let maybe_available = chain .data_availability_checker + .v1() .verify_kzg_for_rpc_block(self.clone()) .map_err(|e| { BlockSlashInfo::SignatureNotChecked( diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index dc38fc1c29..f5ee72f7e7 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -6,6 +6,8 @@ use crate::beacon_chain::{ use crate::beacon_proposer_cache::BeaconProposerCache; use crate::custody_context::NodeCustodyType; use crate::data_availability_checker::DataAvailabilityChecker; +use crate::data_availability_checker_v2::DataAvailabilityChecker as DataAvailabilityCheckerV2; +use crate::data_column_availability_cache::DataAvailabilityRouter; use crate::fork_choice_signal::ForkChoiceSignalTx; use crate::fork_revert::{reset_fork_choice_to_finalization, revert_to_fork_boundary}; use crate::graffiti_calculator::{GraffitiCalculator, GraffitiOrigin}; @@ -975,6 +977,37 @@ where }; debug!(?custody_context, "Loaded persisted custody context"); + let custody_context = Arc::new(custody_context); + let da_checker_v1 = Arc::new( + DataAvailabilityChecker::new( + complete_blob_backfill, + slot_clock.clone(), + self.kzg.clone(), + store.clone(), + custody_context.clone(), + self.spec.clone(), + ) + .map_err(|e| format!("Error initializing DataAvailabilityCheckerV1: {:?}", e))?, + ); + + let da_checker_v2 = Arc::new( + DataAvailabilityCheckerV2::new( + complete_blob_backfill, + slot_clock.clone(), + self.kzg.clone(), + store.clone(), + custody_context.clone(), + self.spec.clone(), + ) + .map_err(|e| format!("Error initializing DataAvailabilityCheckerV2: {:?}", e))?, + ); + + let data_availability_checker = Arc::new(DataAvailabilityRouter::new( + da_checker_v1, + da_checker_v2, + self.spec.clone(), + )); + let beacon_chain = BeaconChain { spec: self.spec.clone(), config: self.chain_config, @@ -1043,17 +1076,7 @@ where slasher: self.slasher.clone(), validator_monitor: RwLock::new(validator_monitor), genesis_backfill_slot, - data_availability_checker: Arc::new( - DataAvailabilityChecker::new( - complete_blob_backfill, - slot_clock, - self.kzg.clone(), - store, - Arc::new(custody_context), - self.spec, - ) - .map_err(|e| format!("Error initializing DataAvailabilityChecker: {:?}", e))?, - ), + data_availability_checker, kzg: self.kzg.clone(), rng: Arc::new(Mutex::new(rng)), }; diff --git a/beacon_node/beacon_chain/src/custody_context.rs b/beacon_node/beacon_chain/src/custody_context.rs index c512ce616a..cebb256a02 100644 --- a/beacon_node/beacon_chain/src/custody_context.rs +++ b/beacon_node/beacon_chain/src/custody_context.rs @@ -7,7 +7,7 @@ use std::{ sync::atomic::{AtomicU64, Ordering}, }; use tracing::{debug, warn}; -use types::{ChainSpec, ColumnIndex, Epoch, EthSpec, Slot}; +use types::{ChainSpec, ColumnIndex, Epoch, EthSpec, SignedExecutionPayloadEnvelope, Slot}; /// A delay before making the CGC change effective to the data availability checker. pub const CUSTODY_CHANGE_DA_EFFECTIVE_DELAY_SECONDS: u64 = 30; @@ -527,6 +527,13 @@ impl CustodyContext { .write() .reset_validator_custody_requirements(effective_epoch); } + + pub fn data_columns_required_for_payload( + &self, + _payload: &SignedExecutionPayloadEnvelope, + ) -> bool { + todo!() + } } /// Indicates that the custody group count (CGC) has increased. diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index 7aec24b8e5..dc35965c0d 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -7,6 +7,7 @@ use crate::block_verification_types::{ use crate::data_availability_checker::overflow_lru_cache::{ DataAvailabilityCheckerInner, ReconstructColumnsDecision, }; +use crate::data_column_availability_cache::DataColumnCache; use crate::{ BeaconChain, BeaconChainTypes, BeaconStore, BlockProcessStatus, CustodyContext, metrics, }; @@ -142,10 +143,6 @@ impl DataAvailabilityChecker { }) } - pub fn custody_context(&self) -> &Arc> { - &self.custody_context - } - /// Checks if the block root is currently in the availability cache awaiting import because /// of missing components. /// @@ -169,30 +166,6 @@ impl DataAvailabilityChecker { }) } - /// Return the set of cached custody column indexes for `block_root`. Returns None if there is - /// no block component for `block_root`. - pub fn cached_data_column_indexes(&self, block_root: &Hash256) -> Option> { - self.availability_cache - .peek_pending_components(block_root, |components| { - components.map(|components| components.get_cached_data_columns_indices()) - }) - } - - /// Check if the exact data column is in the availability cache. - pub fn is_data_column_cached( - &self, - block_root: &Hash256, - data_column: &DataColumnSidecar, - ) -> bool { - self.availability_cache - .peek_pending_components(block_root, |components| { - components.is_some_and(|components| { - let cached_column_opt = components.get_cached_data_column(data_column.index); - cached_column_opt.is_some_and(|cached| *cached == *data_column) - }) - }) - } - /// Get a blob from the availability cache. pub fn get_blob( &self, @@ -201,14 +174,6 @@ impl DataAvailabilityChecker { self.availability_cache.peek_blob(blob_id) } - /// Get data columns for a block from the availability cache. - pub fn get_data_columns( - &self, - block_root: Hash256, - ) -> Option> { - self.availability_cache.peek_data_columns(block_root) - } - /// Put a list of blobs received via RPC into the availability cache. This performs KZG /// verification on the blobs in the list. #[instrument(skip_all, level = "trace")] @@ -236,39 +201,6 @@ impl DataAvailabilityChecker { .put_kzg_verified_blobs(block_root, verified_blobs) } - /// Put a list of custody columns received via RPC into the availability cache. This performs KZG - /// verification on the blobs in the list. - #[allow(clippy::type_complexity)] - #[instrument(skip_all, level = "trace")] - pub fn put_rpc_custody_columns( - &self, - block_root: Hash256, - slot: Slot, - custody_columns: DataColumnSidecarList, - ) -> Result, AvailabilityCheckError> { - // Attributes fault to the specific peer that sent an invalid column - let kzg_verified_columns = - KzgVerifiedDataColumn::from_batch_with_scoring(custody_columns, &self.kzg) - .map_err(AvailabilityCheckError::InvalidColumn)?; - - // Filter out columns that aren't required for custody for this slot - // This is required because `data_columns_by_root` requests the **latest** CGC that _may_ - // not be yet effective for data availability check, as CGC changes are only effecive from - // a new epoch. - let epoch = slot.epoch(T::EthSpec::slots_per_epoch()); - let sampling_columns = self - .custody_context - .sampling_columns_for_epoch(epoch, &self.spec); - let verified_custody_columns = kzg_verified_columns - .into_iter() - .filter(|col| sampling_columns.contains(&col.index())) - .map(KzgVerifiedCustodyDataColumn::from_asserted_custody) - .collect::>(); - - self.availability_cache - .put_kzg_verified_data_columns(block_root, verified_custody_columns) - } - /// Check if we've cached other blobs for this block. If it completes a set and we also /// have a block cached, return the `Availability` variant triggering block import. /// Otherwise cache the blob sidecar. @@ -297,47 +229,6 @@ impl DataAvailabilityChecker { .put_kzg_verified_blobs(block_root, blobs) } - /// Check if we've cached other data columns for this block. If it satisfies the custody requirement and we also - /// have a block cached, return the `Availability` variant triggering block import. - /// Otherwise cache the data column sidecar. - /// - /// This should only accept gossip verified data columns, so we should not have to worry about dupes. - #[instrument(skip_all, level = "trace")] - pub fn put_gossip_verified_data_columns< - O: ObservationStrategy, - I: IntoIterator>, - >( - &self, - block_root: Hash256, - slot: Slot, - data_columns: I, - ) -> Result, AvailabilityCheckError> { - let epoch = slot.epoch(T::EthSpec::slots_per_epoch()); - let sampling_columns = self - .custody_context - .sampling_columns_for_epoch(epoch, &self.spec); - let custody_columns = data_columns - .into_iter() - .filter(|col| sampling_columns.contains(&col.index())) - .map(|c| KzgVerifiedCustodyDataColumn::from_asserted_custody(c.into_inner())) - .collect::>(); - - self.availability_cache - .put_kzg_verified_data_columns(block_root, custody_columns) - } - - #[instrument(skip_all, level = "trace")] - pub fn put_kzg_verified_custody_data_columns< - I: IntoIterator>, - >( - &self, - block_root: Hash256, - custody_columns: I, - ) -> Result, AvailabilityCheckError> { - self.availability_cache - .put_kzg_verified_data_columns(block_root, custody_columns) - } - /// Check if we have all the blobs for a block. Returns `Availability` which has information /// about whether all components have been received or more are required. pub fn put_executed_block( @@ -573,9 +464,116 @@ impl DataAvailabilityChecker { block_cache_size: self.availability_cache.block_cache_size(), } } +} + +impl DataColumnCache for DataAvailabilityChecker { + type Availability = Availability; + type ReconstructionResult = DataColumnReconstructionResult; + + fn custody_context(&self) -> &Arc> { + &self.custody_context + } + + /// Get data columns for a block from the availability cache. + fn get_data_columns(&self, block_root: Hash256) -> Option> { + self.availability_cache.peek_data_columns(block_root) + } + + /// Return the set of cached custody column indices for `block_root`. Returns None if there is + /// no block component for `block_root`. + fn cached_data_column_indexes(&self, block_root: &Hash256) -> Option> { + self.availability_cache + .peek_pending_components(block_root, |components| { + components.map(|components| components.get_cached_data_columns_indices()) + }) + } + + /// Check if the exact data column is in the availability cache. + fn is_data_column_cached( + &self, + block_root: &Hash256, + data_column: &DataColumnSidecar, + ) -> bool { + self.availability_cache + .peek_pending_components(block_root, |components| { + components.is_some_and(|components| { + let cached_column_opt = components.get_cached_data_column(data_column.index); + cached_column_opt.is_some_and(|cached| *cached == *data_column) + }) + }) + } + + /// Put a list of custody columns received via RPC into the availability cache. This performs KZG + /// verification on the blobs in the list. + #[allow(clippy::type_complexity)] + #[instrument(skip_all, level = "trace")] + fn put_rpc_custody_columns( + &self, + block_root: Hash256, + slot: Slot, + custody_columns: DataColumnSidecarList, + ) -> Result, AvailabilityCheckError> { + // Attributes fault to the specific peer that sent an invalid column + let kzg_verified_columns = + KzgVerifiedDataColumn::from_batch_with_scoring(custody_columns, &self.kzg) + .map_err(AvailabilityCheckError::InvalidColumn)?; + + // Filter out columns that aren't required for custody for this slot + // This is required because `data_columns_by_root` requests the **latest** CGC that _may_ + // not be yet effective for data availability check, as CGC changes are only effecive from + // a new epoch. + let epoch = slot.epoch(T::EthSpec::slots_per_epoch()); + let sampling_columns = self + .custody_context + .sampling_columns_for_epoch(epoch, &self.spec); + let verified_custody_columns = kzg_verified_columns + .into_iter() + .filter(|col| sampling_columns.contains(&col.index())) + .map(KzgVerifiedCustodyDataColumn::from_asserted_custody) + .collect::>(); + + self.availability_cache + .put_kzg_verified_data_columns(block_root, verified_custody_columns) + } + + /// Check if we've cached other data columns for this block. If it satisfies the custody requirement and we also + /// have a block cached, return the `Availability` variant triggering block import. + /// Otherwise cache the data column sidecar. + /// + /// This should only accept gossip verified data columns, so we should not have to worry about dupes. + #[instrument(skip_all, level = "trace")] + fn put_gossip_verified_data_columns( + &self, + block_root: Hash256, + slot: Slot, + data_columns: Vec>, + ) -> Result, AvailabilityCheckError> { + let epoch = slot.epoch(T::EthSpec::slots_per_epoch()); + let sampling_columns = self + .custody_context + .sampling_columns_for_epoch(epoch, &self.spec); + let custody_columns = data_columns + .into_iter() + .filter(|col| sampling_columns.contains(&col.index())) + .map(|c| KzgVerifiedCustodyDataColumn::from_asserted_custody(c.into_inner())) + .collect::>(); + + self.availability_cache + .put_kzg_verified_data_columns(block_root, custody_columns) + } + + #[instrument(skip_all, level = "trace")] + fn put_kzg_verified_custody_data_columns( + &self, + block_root: Hash256, + custody_columns: Vec>, + ) -> Result, AvailabilityCheckError> { + self.availability_cache + .put_kzg_verified_data_columns(block_root, custody_columns) + } #[instrument(skip_all, level = "debug")] - pub fn reconstruct_data_columns( + fn reconstruct_data_columns( &self, block_root: &Hash256, ) -> Result, AvailabilityCheckError> { @@ -675,7 +673,11 @@ pub fn start_availability_cache_maintenance_service( ) { // this cache only needs to be maintained if deneb is configured if chain.spec.deneb_fork_epoch.is_some() { - let overflow_cache = chain.data_availability_checker.availability_cache.clone(); + let overflow_cache = chain + .data_availability_checker + .v1() + .availability_cache + .clone(); executor.spawn( async move { availability_cache_maintenance_service(chain, overflow_cache).await }, "availability_cache_service", diff --git a/beacon_node/beacon_chain/src/data_availability_checker_v2.rs b/beacon_node/beacon_chain/src/data_availability_checker_v2.rs new file mode 100644 index 0000000000..3378e41e25 --- /dev/null +++ b/beacon_node/beacon_chain/src/data_availability_checker_v2.rs @@ -0,0 +1,1098 @@ +use crate::data_availability_checker_v2::overflow_lru_cache::{ + DataAvailabilityCheckerInner, ReconstructColumnsDecision, +}; + +use crate::data_availability_checker::AvailabilityCheckError; +use crate::data_column_availability_cache::DataColumnCache; +use crate::payload_verification_types::{ + AvailabilityPendingExecutedPayload, AvailableExecutedPayload, PayloadProcessStatus, +}; +use crate::{BeaconChain, BeaconChainTypes, BeaconStore, CustodyContext, metrics}; +use educe::Educe; +use kzg::Kzg; +use slot_clock::SlotClock; +use std::collections::HashSet; +use std::fmt; +use std::fmt::Debug; +use std::num::NonZeroUsize; +use std::sync::Arc; +use std::time::Duration; +use task_executor::TaskExecutor; +use tracing::{debug, error, instrument}; +use types::{ + BlockImportSource, ChainSpec, ColumnIndex, DataColumnSidecar, DataColumnSidecarList, EthSpec, + Hash256, SignedBeaconBlock, SignedExecutionPayloadEnvelope, Slot, +}; + +mod overflow_lru_cache; +mod state_lru_cache; + +use crate::data_column_verification::{ + GossipVerifiedDataColumn, KzgVerifiedCustodyDataColumn, KzgVerifiedDataColumn, + verify_kzg_for_data_column_list, +}; +use crate::metrics::{ + KZG_DATA_COLUMN_RECONSTRUCTION_ATTEMPTS, KZG_DATA_COLUMN_RECONSTRUCTION_FAILURES, +}; +use crate::observed_data_sidecars::ObservationStrategy; +use types::new_non_zero_usize; + +/// The LRU Cache stores `PendingComponents`, which store payload and its associated column data: +/// +/// With `MAX_BLOBS_PER_BLOCK` = 48 for exa,ple, the maximum size of data columns +/// in `PendingComponents` is ~12.29 MB. Setting this to 32 means the maximum size of the cache is +/// approximately 0.4 GB. +/// +/// `PendingComponents` are now never removed from the cache manually are only removed via LRU +/// eviction to prevent race conditions (#7961), so we expect this cache to be full all the time. +const OVERFLOW_LRU_CAPACITY_NON_ZERO: NonZeroUsize = new_non_zero_usize(32); +const STATE_LRU_CAPACITY_NON_ZERO: NonZeroUsize = new_non_zero_usize(32); + +/// Cache to hold fully valid data that can't be imported to fork-choice yet. After the Gloas hard-fork +/// beacon blocks can be immediately imported into fork choice. The execution payload is now separated out from +/// the beacon block. The payload envelope and data columns are received separately from the network. The block +/// is now always considered "available". Availability checks are now made on the payload and it is considered +/// "fully available" when the payload and all required columns are inserted into this cache. +/// +/// Usually a payload becomes available on its slot within a second of receiving its first component +/// over gossip. However, a payload may never become available if a malicious proposer does not +/// publish its data, or there are network issues that prevent us from receiving it. If the payload +/// does not become available after some time we can safely forget about it. Consider these two +/// cases: +/// +/// - Global unavailability: If nobody has received the payload components it's likely that the +/// builder never made the payload available. So we can safely forget about the payload as it will +/// never become available. +/// - Local unavailability: Some fraction of the network has received all payload components, but not us. +/// Some of our peers will eventually attest to a descendant of that block and lookup sync will +/// fetch its components. Therefore it's not strictly necessary to hold to the partially available +/// payload for too long as we can recover from other peers. +/// +/// Even in periods of non-finality, the builder is expected to publish the payload's data +/// immediately. Because this cache only holds fully valid data, its capacity is bound to 1 block +/// per slot and fork: before inserting into this cache we check the proposer signature and correct +/// proposer. Having a capacity > 1 is an optimization to prevent sync lookup from having re-fetch +/// data during moments of unstable network conditions. +pub struct DataAvailabilityChecker { + #[allow(dead_code)] + complete_blob_backfill: bool, + availability_cache: Arc>, + #[allow(dead_code)] + slot_clock: T::SlotClock, + kzg: Arc, + custody_context: Arc>, + spec: Arc, +} + +pub type AvailabilityAndReconstructedColumns = (Availability, DataColumnSidecarList); + +#[derive(Debug)] +pub enum DataColumnReconstructionResult { + Success(AvailabilityAndReconstructedColumns), + NotStarted(&'static str), + RecoveredColumnsNotImported(&'static str), +} + +/// This type is returned after adding a payload / column to the `DataAvailabilityChecker`. +/// +/// Indicates if the payload is fully `Available` or if we need columns or payload +/// to "complete" the requirements for an `AvailablePayload`. +pub enum Availability { + MissingComponents(Hash256), + Available(Box>), +} + +impl Debug for Availability { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::MissingComponents(block_root) => { + write!(f, "MissingComponents({})", block_root) + } + Self::Available(payload) => write!(f, "Available({:?})", payload.payload.block_root), + } + } +} + +impl DataColumnCache for DataAvailabilityChecker { + type Availability = Availability; + type ReconstructionResult = DataColumnReconstructionResult; + + /// Returns the custody context used by this checker. + fn custody_context(&self) -> &Arc> { + &self.custody_context + } + + /// Returns all cached data columns for the given block root, if any. + #[instrument(skip_all, level = "trace")] + fn get_data_columns(&self, block_root: Hash256) -> Option> { + self.availability_cache.peek_data_columns(block_root) + } + + /// Returns the indices of cached data columns for the given block root. + #[instrument(skip_all, level = "trace")] + fn cached_data_column_indexes(&self, block_root: &Hash256) -> Option> { + self.availability_cache + .peek_pending_components(block_root, |components| { + components.map(|components| components.get_cached_data_columns_indices()) + }) + } + + /// Checks if a specific data column is cached for the given block root. + #[instrument(skip_all, level = "trace")] + fn is_data_column_cached( + &self, + block_root: &Hash256, + data_column: &DataColumnSidecar, + ) -> bool { + self.availability_cache + .peek_pending_components(block_root, |components| { + components.is_some_and(|components| { + let cached_column_opt = components.get_cached_data_column(data_column.index); + cached_column_opt.is_some_and(|cached| *cached == *data_column) + }) + }) + } + + /// Insert RPC custody columns and check if the block/payload becomes available. + #[instrument(skip_all, level = "trace")] + fn put_rpc_custody_columns( + &self, + block_root: Hash256, + slot: Slot, + custody_columns: DataColumnSidecarList, + ) -> Result, AvailabilityCheckError> { + // Attributes fault to the specific peer that sent an invalid column + let kzg_verified_columns = + KzgVerifiedDataColumn::from_batch_with_scoring(custody_columns, &self.kzg) + .map_err(AvailabilityCheckError::InvalidColumn)?; + + // Filter out columns that aren't required for custody for this slot + // This is required because `data_columns_by_root` requests the **latest** CGC that _may_ + // not be yet effective for data availability check, as CGC changes are only effecive from + // a new epoch. + let epoch = slot.epoch(T::EthSpec::slots_per_epoch()); + let sampling_columns = self + .custody_context + .sampling_columns_for_epoch(epoch, &self.spec); + let verified_custody_columns = kzg_verified_columns + .into_iter() + .filter(|col| sampling_columns.contains(&col.index())) + .map(KzgVerifiedCustodyDataColumn::from_asserted_custody) + .collect::>(); + + self.availability_cache + .put_kzg_verified_data_columns(block_root, verified_custody_columns) + } + + /// Check if we've cached other data columns for this payload. If it satisfies the custody requirement and we also + /// have a payload cached, return the `Availability` variant triggering payload import. + /// Otherwise cache the data column sidecar. + /// + /// This should only accept gossip verified data columns, so we should not have to worry about dupes. + #[instrument(skip_all, level = "trace")] + fn put_gossip_verified_data_columns( + &self, + block_root: Hash256, + slot: Slot, + data_columns: Vec>, + ) -> Result, AvailabilityCheckError> { + let epoch = slot.epoch(T::EthSpec::slots_per_epoch()); + let sampling_columns = self + .custody_context + .sampling_columns_for_epoch(epoch, &self.spec); + let custody_columns = data_columns + .into_iter() + .filter(|col| sampling_columns.contains(&col.index())) + .map(|c| KzgVerifiedCustodyDataColumn::from_asserted_custody(c.into_inner())) + .collect::>(); + + self.availability_cache + .put_kzg_verified_data_columns(block_root, custody_columns) + } + + #[instrument(skip_all, level = "trace")] + fn put_kzg_verified_custody_data_columns( + &self, + block_root: Hash256, + custody_columns: Vec>, + ) -> Result, AvailabilityCheckError> { + self.availability_cache + .put_kzg_verified_data_columns(block_root, custody_columns) + } + + #[instrument(skip_all, level = "debug")] + fn reconstruct_data_columns( + &self, + block_root: &Hash256, + ) -> Result, AvailabilityCheckError> { + let verified_data_columns = match self + .availability_cache + .check_and_set_reconstruction_started(block_root) + { + ReconstructColumnsDecision::Yes(verified_data_columns) => verified_data_columns, + ReconstructColumnsDecision::No(reason) => { + return Ok(DataColumnReconstructionResult::NotStarted(reason)); + } + }; + + metrics::inc_counter(&KZG_DATA_COLUMN_RECONSTRUCTION_ATTEMPTS); + let timer = metrics::start_timer(&metrics::DATA_AVAILABILITY_RECONSTRUCTION_TIME); + + let all_data_columns = KzgVerifiedCustodyDataColumn::reconstruct_columns( + &self.kzg, + &verified_data_columns, + &self.spec, + ) + .map_err(|e| { + error!( + ?block_root, + error = ?e, + "Error reconstructing data columns" + ); + self.availability_cache + .handle_reconstruction_failure(block_root); + metrics::inc_counter(&KZG_DATA_COLUMN_RECONSTRUCTION_FAILURES); + AvailabilityCheckError::ReconstructColumnsError(e) + })?; + + // Check indices from cache again to make sure we don't publish components we've already received. + let Some(existing_column_indices) = self.cached_data_column_indexes(block_root) else { + return Err(AvailabilityCheckError::Unexpected( + "block no longer exists in the data availability checker".to_string(), + )); + }; + + let Some(slot) = all_data_columns.first().map(|d| d.as_data_column().slot()) else { + return Ok(DataColumnReconstructionResult::RecoveredColumnsNotImported( + "No new columns to import and publish", + )); + }; + + let columns_to_sample = self + .custody_context() + .sampling_columns_for_epoch(slot.epoch(T::EthSpec::slots_per_epoch()), &self.spec); + + // We only need to import and publish columns that we need to sample + // and columns that we haven't already received + let data_columns_to_import_and_publish = all_data_columns + .into_iter() + .filter(|d| { + columns_to_sample.contains(&d.index()) + && !existing_column_indices.contains(&d.index()) + }) + .collect::>(); + + metrics::stop_timer(timer); + metrics::inc_counter_by( + &metrics::DATA_AVAILABILITY_RECONSTRUCTED_COLUMNS, + data_columns_to_import_and_publish.len() as u64, + ); + + debug!( + count = data_columns_to_import_and_publish.len(), + ?block_root, + %slot, + "Reconstructed columns" + ); + + self.availability_cache + .put_kzg_verified_data_columns(*block_root, data_columns_to_import_and_publish.clone()) + .map(|availability| { + DataColumnReconstructionResult::Success(( + availability, + data_columns_to_import_and_publish + .into_iter() + .map(|d| d.clone_arc()) + .collect::>(), + )) + }) + } +} + +impl DataAvailabilityChecker { + pub fn new( + complete_blob_backfill: bool, + slot_clock: T::SlotClock, + kzg: Arc, + store: BeaconStore, + custody_context: Arc>, + spec: Arc, + ) -> Result { + let inner = DataAvailabilityCheckerInner::new( + OVERFLOW_LRU_CAPACITY_NON_ZERO, + store, + custody_context.clone(), + spec.clone(), + )?; + Ok(Self { + complete_blob_backfill, + availability_cache: Arc::new(inner), + slot_clock, + kzg, + custody_context, + spec, + }) + } + + pub fn custody_context(&self) -> &Arc> { + &self.custody_context + } + + /// Checks if the payload associated with the given block root is currently in the availability cache awaiting import because + /// of missing components. + /// + /// Returns the cached payload wrapped in a `PayloadProcessStatus` enum if it exists. + pub fn get_cached_payload( + &self, + block_root: &Hash256, + ) -> Option> { + self.availability_cache.get_cached_payload(block_root) + } + + /// Check if we have all required columns for a payload. Returns `Availability` which has information + /// about whether all components have been received or more are required. + pub fn put_executed_payload( + &self, + executed_payload: AvailabilityPendingExecutedPayload, + ) -> Result, AvailabilityCheckError> { + self.availability_cache + .put_executed_payload(executed_payload) + } + + /// Inserts a pre-execution payload into the cache. + /// This does NOT override an existing executed payload. + pub fn put_pre_execution_payload( + &self, + block_root: Hash256, + payload: Arc>, + source: BlockImportSource, + ) -> Result<(), AvailabilityCheckError> { + self.availability_cache + .put_pre_execution_payload(block_root, payload, source) + } + + /// Removes a pre-execution payload from the cache. + /// This does NOT remove an existing executed payload. + pub fn remove_payload_on_execution_error(&self, block_root: &Hash256) { + self.availability_cache + .remove_pre_execution_payload(block_root); + } + + /// Verifies kzg commitments for an `AvailableBlock`.` + /// + /// WARNING: This function assumes all required blobs are already present, it does NOT + /// check if there are any missing blobs. + pub fn verify_kzg_for_available_payload( + &self, + available_payload: &AvailablePayload, + ) -> Result<(), AvailabilityCheckError> { + let block_data_required = self + .custody_context + .data_columns_required_for_payload(&available_payload.payload); + match available_payload.data() { + AvailablePayloadData::NoData => { + if block_data_required { + return Err(AvailabilityCheckError::MissingCustodyColumns); + } + } + AvailablePayloadData::DataColumns(data_columns) => { + verify_kzg_for_data_column_list(data_columns.iter(), &self.kzg) + .map_err(AvailabilityCheckError::InvalidColumn)?; + } + } + + Ok(()) + } + + /// Performs batch kzg verification for a vector of `AvailablePayloads`. This is more efficient than + /// calling `verify_kzg_for_available_block` in a loop. + /// + /// WARNING: This function assumes all required blobs are already present, it does NOT + /// check if there are any missing blobs. + #[instrument(skip_all)] + pub fn batch_verify_kzg_for_available_payloads( + &self, + available_payloads: &Vec>, + ) -> Result<(), AvailabilityCheckError> { + let all_data_columns = available_payloads + .iter() + .filter(|available_payload| { + self.custody_context + .data_columns_required_for_payload(&available_payload.payload) + }) + // this clone is cheap as it's cloning an Arc + .filter_map(|available_payload| available_payload.column_data.data_columns()) + .flatten() + .collect::>(); + + for available_payload in available_payloads { + let payload_data_required = self + .custody_context + .data_columns_required_for_payload(&available_payload.payload); + if let AvailablePayloadData::NoData = available_payload.data() + && payload_data_required + { + return Err(AvailabilityCheckError::MissingCustodyColumns); + } + } + + // verify kzg for all data columns at once + if !all_data_columns.is_empty() { + // Attributes fault to the specific peer that sent an invalid column + verify_kzg_for_data_column_list(all_data_columns.iter(), &self.kzg) + .map_err(AvailabilityCheckError::InvalidColumn)?; + } + + Ok(()) + } + + /// Collects metrics from the data availability checker. + pub fn metrics(&self) -> DataAvailabilityCheckerMetrics { + DataAvailabilityCheckerMetrics { + state_cache_size: self.availability_cache.state_cache_size(), + payload_cache_size: self.availability_cache.payload_cache_size(), + } + } +} + +/// Helper struct to group data availability checker metrics. +pub struct DataAvailabilityCheckerMetrics { + pub state_cache_size: usize, + pub payload_cache_size: usize, +} + +pub fn start_availability_cache_maintenance_service( + executor: TaskExecutor, + chain: Arc>, +) { + if chain.spec.gloas_fork_epoch.is_some() { + let overflow_cache = chain + .data_availability_checker + .v2() + .availability_cache + .clone(); + executor.spawn( + async move { availability_cache_maintenance_service(chain, overflow_cache).await }, + "availability_cache_service", + ); + } else { + debug!("Gloas fork not configured, not starting availability cache maintenance service"); + } + // TODO(gloas) + // this cache only needs to be maintained if deneb is configured + // if chain.spec.deneb_fork_epoch.is_some() { + // let overflow_cache = chain.data_availability_checker.availability_cache.clone(); + // executor.spawn( + // async move { availability_cache_maintenance_service(chain, overflow_cache).await }, + // "availability_cache_service", + // ); + // } else { + // debug!("Deneb fork not configured, not starting availability cache maintenance service"); + // } +} + +async fn availability_cache_maintenance_service( + chain: Arc>, + overflow_cache: Arc>, +) { + let epoch_duration = chain.slot_clock.slot_duration() * T::EthSpec::slots_per_epoch() as u32; + loop { + match chain + .slot_clock + .duration_to_next_epoch(T::EthSpec::slots_per_epoch()) + { + Some(duration) => { + // this service should run 3/4 of the way through the epoch + let additional_delay = (epoch_duration * 3) / 4; + tokio::time::sleep(duration + additional_delay).await; + + let Some(gloas_fork_epoch) = chain.spec.gloas_fork_epoch else { + // shutdown service if gloas fork epoch not set + break; + }; + + debug!("Availability cache maintenance service firing"); + let Some(current_epoch) = chain + .slot_clock + .now() + .map(|slot| slot.epoch(T::EthSpec::slots_per_epoch())) + else { + continue; + }; + + if current_epoch < gloas_fork_epoch { + // we are not in gloas yet + continue; + } + + let finalized_epoch = chain + .canonical_head + .fork_choice_read_lock() + .finalized_checkpoint() + .epoch; + + let Some(min_epochs_for_blobs) = chain + .spec + .min_epoch_data_availability_boundary(current_epoch) + else { + // Shutdown service if deneb fork epoch not set. Unreachable as the same check is performed above. + break; + }; + + // any data belonging to an epoch before this should be pruned + let cutoff_epoch = std::cmp::max(finalized_epoch + 1, min_epochs_for_blobs); + + if let Err(e) = overflow_cache.do_maintenance(cutoff_epoch) { + error!(error = ?e,"Failed to maintain availability cache"); + } + } + None => { + error!("Failed to read slot clock"); + // If we can't read the slot clock, just wait another slot. + tokio::time::sleep(chain.slot_clock.slot_duration()).await; + } + }; + } +} + +#[derive(Debug, Clone)] +// TODO(gloas) Move this to `payload_verification_types.rs` +pub enum AvailablePayloadData { + /// Payload has zero blobs + NoData, + /// Payload has more than zero blobs + DataColumns(DataColumnSidecarList), +} + +impl AvailablePayloadData { + pub fn new_with_data_columns(columns: DataColumnSidecarList) -> Self { + if columns.is_empty() { + Self::NoData + } else { + Self::DataColumns(columns) + } + } + + pub fn data_columns(&self) -> Option> { + match self { + AvailablePayloadData::NoData => None, + AvailablePayloadData::DataColumns(data_columns) => Some(data_columns.clone()), + } + } + + pub fn data_columns_len(&self) -> usize { + if let Some(data_columns) = self.data_columns() { + data_columns.len() + } else { + 0 + } + } +} + +/// A fully available payload that is ready to be imported into fork choice. +#[derive(Debug, Clone, Educe)] +#[educe(Hash(bound(E: EthSpec)))] +pub struct AvailablePayload { + block_root: Hash256, + block: Arc>, + payload: Arc>, + #[educe(Hash(ignore))] + column_data: AvailablePayloadData, + #[educe(Hash(ignore))] + /// Timestamp at which this payload first became available (UNIX timestamp, time since 1970). + payload_available_timestamp: Option, + #[educe(Hash(ignore))] + pub spec: Arc, +} + +impl AvailablePayload { + /// Constructs an `AvailablePayload` from a payload and optional data. + /// - If `column_data` is `DataColumns`, constructs `AvailablePayload` variant after column validation. + /// - If `column_data` is `NoData`, constructs `AvailablePayload` after verifying that the payload is not expecting columns. + /// Returns `AvailabilityCheckError` if: + /// - `column_data` contains data not required by the block + /// - Required `column_data` is missing + /// - Blob count doesn't match expected + /// - Custody columns are incomplete + pub fn new( + payload: Arc>, + block: Arc>, + column_data: AvailablePayloadData, + da_checker: &DataAvailabilityChecker, + spec: Arc, + ) -> Result + where + T: BeaconChainTypes, + { + // Ensure payload availability + let columns_required = da_checker + .custody_context() + .data_columns_required_for_payload(&payload); + + match &column_data { + AvailablePayloadData::NoData => { + if columns_required { + return Err(AvailabilityCheckError::MissingCustodyColumns); + } + } + AvailablePayloadData::DataColumns(data_columns) => { + if !columns_required { + // TODO(gloas) potential refactor here + return Err(AvailabilityCheckError::MissingCustodyColumns); + } + + let mut column_indices = da_checker + .custody_context + .custody_columns_for_epoch( + Some(payload.message.slot.epoch(T::EthSpec::slots_per_epoch())), + &spec, + ) + .iter() + .collect::>(); + + for data_column in data_columns { + column_indices.remove(&data_column.index); + } + + if !column_indices.is_empty() { + return Err(AvailabilityCheckError::MissingCustodyColumns); + } + } + } + + Ok(Self { + block_root: payload.message.beacon_block_root, + block, + payload, + column_data, + payload_available_timestamp: None, + spec: spec.clone(), + }) + } + + pub fn payload(&self) -> &SignedExecutionPayloadEnvelope { + &self.payload + } + pub fn payload_cloned(&self) -> Arc> { + self.payload.clone() + } + + pub fn payload_available_timestamp(&self) -> Option { + self.payload_available_timestamp + } + + pub fn data(&self) -> &AvailablePayloadData { + &self.column_data + } + + pub fn block_root(&self) -> Hash256 { + self.block_root + } + + #[allow(clippy::type_complexity)] + pub fn deconstruct( + self, + ) -> ( + Hash256, + Arc>, + AvailablePayloadData, + ) { + let AvailablePayload { + block_root, + payload, + column_data, + .. + } = self; + (block_root, payload, column_data) + } + + /// Only used for testing + pub fn __clone_without_recv(&self) -> Self { + Self { + block_root: self.block_root, + payload: self.payload.clone(), + block: self.block.clone(), + column_data: match &self.column_data { + AvailablePayloadData::NoData => AvailablePayloadData::NoData, + AvailablePayloadData::DataColumns(data_columns) => { + AvailablePayloadData::DataColumns(data_columns.clone()) + } + }, + payload_available_timestamp: self.payload_available_timestamp, + spec: self.spec.clone(), + } + } +} + +#[derive(Debug)] +pub enum MaybeAvailablePayload { + /// This payload is fully available. + Available(AvailablePayload), + /// This variant is not fully available and requires blobs to become fully available. + AvailabilityPending { + block_root: Hash256, + payload: Arc>, + }, +} + +impl MaybeAvailablePayload { + pub fn block_cloned(&self) -> Arc> { + match self { + Self::Available(payload) => payload.payload_cloned(), + Self::AvailabilityPending { payload, .. } => payload.clone(), + } + } +} + +// #[cfg(test)] +// mod test { +// use super::*; +// use crate::CustodyContext; +// use crate::block_verification_types::RpcBlock; +// use crate::custody_context::NodeCustodyType; +// use crate::data_column_verification::CustodyDataColumn; +// use crate::test_utils::{ +// EphemeralHarnessType, NumBlobs, generate_data_column_indices_rand_order, +// generate_rand_block_and_data_columns, get_kzg, +// }; +// use rand::SeedableRng; +// use rand::prelude::StdRng; +// use slot_clock::{SlotClock, TestingSlotClock}; +// use std::collections::HashSet; +// use std::sync::Arc; +// use std::time::Duration; +// use store::HotColdDB; +// use types::data::DataColumn; +// use types::{ChainSpec, ColumnIndex, EthSpec, ForkName, MainnetEthSpec, Slot}; + +// type E = MainnetEthSpec; +// type T = EphemeralHarnessType; + +// /// Test to verify any extra RPC columns received that are not part of the "effective" CGC for +// /// the slot are excluded from import. +// #[test] +// fn should_exclude_rpc_columns_not_required_for_sampling() { +// // SETUP +// let spec = Arc::new(ForkName::Fulu.make_genesis_spec(E::default_spec())); +// let mut rng = StdRng::seed_from_u64(0xDEADBEEF0BAD5EEDu64); + +// let da_checker = new_da_checker(spec.clone()); +// let custody_context = &da_checker.custody_context; + +// // GIVEN a single 32 ETH validator is attached slot 0 +// let epoch = Epoch::new(0); +// let validator_0 = 0; +// custody_context.register_validators( +// vec![(validator_0, 32_000_000_000)], +// epoch.start_slot(E::slots_per_epoch()), +// &spec, +// ); +// assert_eq!( +// custody_context.num_of_data_columns_to_sample(epoch, &spec), +// spec.validator_custody_requirement as usize, +// "sampling size should be the minimal custody requirement == 8" +// ); + +// // WHEN additional attached validators result in a CGC increase to 10 at the end slot of the same epoch +// let validator_1 = 1; +// let cgc_change_slot = epoch.end_slot(E::slots_per_epoch()); +// custody_context.register_validators( +// vec![(validator_1, 32_000_000_000 * 9)], +// cgc_change_slot, +// &spec, +// ); +// // AND custody columns (8) and any new extra columns (2) are received via RPC responses. +// // NOTE: block lookup uses the **latest** CGC (10) instead of the effective CGC (8) as the slot is unknown. +// let (_, data_columns) = generate_rand_block_and_data_columns::( +// ForkName::Fulu, +// NumBlobs::Number(1), +// &mut rng, +// &spec, +// ); +// let block_root = Hash256::random(); +// let custody_columns = custody_context.custody_columns_for_epoch(None, &spec); +// let requested_columns = &custody_columns[..10]; +// da_checker +// .put_rpc_custody_columns( +// block_root, +// cgc_change_slot, +// data_columns +// .into_iter() +// .filter(|d| requested_columns.contains(&d.index)) +// .collect(), +// ) +// .expect("should put rpc custody columns"); + +// // THEN the sampling size for the end slot of the same epoch remains unchanged +// let sampling_columns = custody_context.sampling_columns_for_epoch(epoch, &spec); +// assert_eq!( +// sampling_columns.len(), +// spec.validator_custody_requirement as usize // 8 +// ); +// // AND any extra columns received via RPC responses are excluded from import. +// let actual_cached: HashSet = da_checker +// .cached_data_column_indexes(&block_root) +// .expect("should have cached data columns") +// .into_iter() +// .collect(); +// let expected_sampling_columns = sampling_columns.iter().copied().collect::>(); +// assert_eq!( +// actual_cached, expected_sampling_columns, +// "should cache only the effective sampling columns" +// ); +// assert!( +// actual_cached.len() < requested_columns.len(), +// "extra columns should be excluded" +// ) +// } + +// /// Test to verify any extra gossip columns received that are not part of the "effective" CGC for +// /// the slot are excluded from import. +// #[test] +// fn should_exclude_gossip_columns_not_required_for_sampling() { +// // SETUP +// let spec = Arc::new(ForkName::Fulu.make_genesis_spec(E::default_spec())); +// let mut rng = StdRng::seed_from_u64(0xDEADBEEF0BAD5EEDu64); + +// let da_checker = new_da_checker(spec.clone()); +// let custody_context = &da_checker.custody_context; + +// // GIVEN a single 32 ETH validator is attached slot 0 +// let epoch = Epoch::new(0); +// let validator_0 = 0; +// custody_context.register_validators( +// vec![(validator_0, 32_000_000_000)], +// epoch.start_slot(E::slots_per_epoch()), +// &spec, +// ); +// assert_eq!( +// custody_context.num_of_data_columns_to_sample(epoch, &spec), +// spec.validator_custody_requirement as usize, +// "sampling size should be the minimal custody requirement == 8" +// ); + +// // WHEN additional attached validators result in a CGC increase to 10 at the end slot of the same epoch +// let validator_1 = 1; +// let cgc_change_slot = epoch.end_slot(E::slots_per_epoch()); +// custody_context.register_validators( +// vec![(validator_1, 32_000_000_000 * 9)], +// cgc_change_slot, +// &spec, +// ); +// // AND custody columns (8) and any new extra columns (2) are received via gossip. +// // NOTE: CGC updates results in new topics subscriptions immediately, and extra columns may start to +// // arrive via gossip. +// let (_, data_columns) = generate_rand_block_and_data_columns::( +// ForkName::Fulu, +// NumBlobs::Number(1), +// &mut rng, +// &spec, +// ); +// let block_root = Hash256::random(); +// let custody_columns = custody_context.custody_columns_for_epoch(None, &spec); +// let requested_columns = &custody_columns[..10]; +// let gossip_columns = data_columns +// .into_iter() +// .filter(|d| requested_columns.contains(&d.index)) +// .map(GossipVerifiedDataColumn::::__new_for_testing) +// .collect::>(); +// da_checker +// .put_gossip_verified_data_columns(block_root, cgc_change_slot, gossip_columns) +// .expect("should put gossip custody columns"); + +// // THEN the sampling size for the end slot of the same epoch remains unchanged +// let sampling_columns = custody_context.sampling_columns_for_epoch(epoch, &spec); +// assert_eq!( +// sampling_columns.len(), +// spec.validator_custody_requirement as usize // 8 +// ); +// // AND any extra columns received via gossip responses are excluded from import. +// let actual_cached: HashSet = da_checker +// .cached_data_column_indexes(&block_root) +// .expect("should have cached data columns") +// .into_iter() +// .collect(); +// let expected_sampling_columns = sampling_columns.iter().copied().collect::>(); +// assert_eq!( +// actual_cached, expected_sampling_columns, +// "should cache only the effective sampling columns" +// ); +// assert!( +// actual_cached.len() < requested_columns.len(), +// "extra columns should be excluded" +// ) +// } + +// /// Regression test for KZG verification truncation bug (https://github.com/sigp/lighthouse/pull/7927) +// #[test] +// fn verify_kzg_for_rpc_blocks_should_not_truncate_data_columns() { +// let spec = Arc::new(ForkName::Fulu.make_genesis_spec(E::default_spec())); +// let mut rng = StdRng::seed_from_u64(0xDEADBEEF0BAD5EEDu64); +// let da_checker = new_da_checker(spec.clone()); + +// // GIVEN multiple RPC blocks with data columns totalling more than 128 +// let blocks_with_columns = (0..2) +// .map(|index| { +// let (block, data_columns) = generate_rand_block_and_data_columns::( +// ForkName::Fulu, +// NumBlobs::Number(1), +// &mut rng, +// &spec, +// ); + +// let custody_columns = if index == 0 { +// // 128 valid data columns in the first block +// data_columns +// } else { +// // invalid data columns in the second block +// data_columns +// .into_iter() +// .map(|d| { +// let invalid_sidecar = DataColumnSidecar { +// column: DataColumn::::empty(), +// ..d.as_ref().clone() +// }; +// CustodyDataColumn::from_asserted_custody(Arc::new(invalid_sidecar)) +// .as_data_column() +// .clone() +// }) +// .collect::>() +// }; + +// let block_data = AvailableBlockData::new_with_data_columns(custody_columns); +// let da_checker = Arc::new(new_da_checker(spec.clone())); +// RpcBlock::new(Arc::new(block), Some(block_data), &da_checker, spec.clone()) +// .expect("should create RPC block with custody columns") +// }) +// .collect::>(); + +// let available_blocks = blocks_with_columns +// .iter() +// .filter_map(|block| match block { +// RpcBlock::FullyAvailable(available_block) => Some(available_block.clone()), +// RpcBlock::BlockOnly { .. } => None, +// }) +// .collect::>(); + +// // WHEN verifying all blocks together (totalling 256 data columns) +// let verification_result = +// da_checker.batch_verify_kzg_for_available_blocks(&available_blocks); + +// // THEN batch block verification should fail due to 128 invalid columns in the second block +// verification_result.expect_err("should have failed to verify blocks"); +// } + +// #[test] +// fn should_exclude_reconstructed_columns_not_required_for_sampling() { +// // SETUP +// let spec = Arc::new(ForkName::Fulu.make_genesis_spec(E::default_spec())); +// let mut rng = StdRng::seed_from_u64(0xDEADBEEF0BAD5EEDu64); + +// let da_checker = new_da_checker(spec.clone()); +// let custody_context = &da_checker.custody_context; + +// // Set custody requirement to 65 columns (enough to trigger reconstruction) +// let epoch = Epoch::new(1); +// custody_context.register_validators( +// vec![(0, 2_048_000_000_000), (1, 32_000_000_000)], // 64 + 1 +// Slot::new(0), +// &spec, +// ); +// let sampling_requirement = custody_context.num_of_data_columns_to_sample(epoch, &spec); +// assert_eq!( +// sampling_requirement, 65, +// "sampling requirement should be 65" +// ); + +// let (block, data_columns) = generate_rand_block_and_data_columns::( +// ForkName::Fulu, +// NumBlobs::Number(1), +// &mut rng, +// &spec, +// ); +// let block_root = Hash256::random(); +// // Add the block to the DA checker +// da_checker +// .availability_cache +// .put_pre_execution_block(block_root, Arc::new(block), BlockImportSource::Gossip) +// .expect("should put block"); + +// // Add 64 columns to the da checker (enough to be able to reconstruct) +// // Order by all_column_indices_ordered, then take first 64 +// let custody_columns = custody_context.custody_columns_for_epoch(None, &spec); +// let custody_columns = custody_columns +// .iter() +// .filter_map(|&col_idx| data_columns.iter().find(|d| d.index == col_idx).cloned()) +// .take(64) +// .map(|d| { +// KzgVerifiedCustodyDataColumn::from_asserted_custody( +// KzgVerifiedDataColumn::__new_for_testing(d), +// ) +// }) +// .collect::>(); + +// da_checker +// .availability_cache +// .put_kzg_verified_data_columns(block_root, custody_columns) +// .expect("should put custody columns"); + +// // Try reconstrucing +// let reconstruction_result = da_checker +// .reconstruct_data_columns(&block_root) +// .expect("should reconstruct columns"); + +// // Reconstruction should succeed +// let (_availability, reconstructed_columns) = match reconstruction_result { +// DataColumnReconstructionResult::Success(result) => result, +// e => { +// panic!("Expected successful reconstruction {:?}", e); +// } +// }; + +// // Remaining 64 columns should be reconstructed +// assert_eq!( +// reconstructed_columns.len(), +// sampling_requirement - spec.number_of_custody_groups as usize / 2, +// "should reconstruct the remaining 1 columns" +// ); + +// // Only the columns required for custody (65) should be imported into the cache +// let sampling_columns = custody_context.sampling_columns_for_epoch(epoch, &spec); +// let actual_cached: HashSet = da_checker +// .cached_data_column_indexes(&block_root) +// .expect("should have cached data columns") +// .into_iter() +// .collect(); +// let expected_sampling_columns = sampling_columns.iter().copied().collect::>(); +// assert_eq!( +// actual_cached, expected_sampling_columns, +// "should cache only the required custody columns, not all reconstructed columns" +// ); +// } + +// fn new_da_checker(spec: Arc) -> DataAvailabilityChecker { +// let slot_clock = TestingSlotClock::new( +// Slot::new(0), +// Duration::from_secs(0), +// Duration::from_secs(spec.seconds_per_slot), +// ); +// let kzg = get_kzg(&spec); +// let store = Arc::new(HotColdDB::open_ephemeral(<_>::default(), spec.clone()).unwrap()); +// let ordered_custody_column_indices = generate_data_column_indices_rand_order::(); +// let custody_context = Arc::new(CustodyContext::new( +// NodeCustodyType::Fullnode, +// ordered_custody_column_indices, +// &spec, +// )); +// let complete_blob_backfill = false; +// DataAvailabilityChecker::new( +// complete_blob_backfill, +// slot_clock, +// kzg, +// store, +// custody_context, +// spec, +// ) +// .expect("should initialise data availability checker") +// } +// } diff --git a/beacon_node/beacon_chain/src/data_availability_checker_v2/overflow_lru_cache.rs b/beacon_node/beacon_chain/src/data_availability_checker_v2/overflow_lru_cache.rs new file mode 100644 index 0000000000..aace5c91ba --- /dev/null +++ b/beacon_node/beacon_chain/src/data_availability_checker_v2/overflow_lru_cache.rs @@ -0,0 +1,1306 @@ +use super::state_lru_cache::{DietAvailabilityPendingExecutedPayload, StateLRUCache}; +use crate::BeaconChainTypes; +use crate::CustodyContext; +use crate::beacon_chain::BeaconStore; +use crate::data_availability_checker::AvailabilityCheckError; +use crate::data_availability_checker_v2::{Availability, AvailablePayload, AvailablePayloadData}; +use crate::data_column_verification::KzgVerifiedCustodyDataColumn; +use crate::payload_verification_types::PayloadProcessStatus; +use crate::payload_verification_types::{ + AvailabilityPendingExecutedPayload, AvailableExecutedPayload, +}; +use lru::LruCache; +use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard, RwLockWriteGuard}; +use std::cmp::Ordering; +use std::num::NonZeroUsize; +use std::sync::Arc; +use tracing::{Span, debug, debug_span}; +use types::kzg_ext::KzgCommitments; +use types::{ + BlockImportSource, ChainSpec, ColumnIndex, DataColumnSidecar, DataColumnSidecarList, Epoch, + EthSpec, Hash256, SignedBeaconBlock, SignedExecutionPayloadEnvelope, +}; + +#[derive(Clone)] +pub enum CachedPayload { + PreExecution(Arc>, BlockImportSource), + Executed(Box>), +} + +impl CachedPayload { + pub fn get_commitments(&self) -> KzgCommitments { + let payload = self.as_payload(); + payload.message.blob_kzg_commitments.clone() + } + + fn as_payload(&self) -> &SignedExecutionPayloadEnvelope { + match self { + CachedPayload::PreExecution(p, _) => p, + CachedPayload::Executed(p) => p.as_payload(), + } + } + + pub fn num_blobs_expected(&self) -> usize { + self.as_payload().message.blob_kzg_commitments.len() + } +} + +/// This represents the components of a partially available payload +/// +/// The columns are all gossip and kzg verified. +/// The payload has completed all verifications except the availability check. +pub struct PendingComponents { + pub block_root: Hash256, + pub block: Option>>, + pub verified_data_columns: Vec>, + pub payload: Option>, + pub reconstruction_started: bool, + span: Span, +} + +impl PendingComponents { + #[cfg(test)] + fn get_diet_payload(&self) -> Option<&DietAvailabilityPendingExecutedPayload> { + self.payload.as_ref().and_then(|payload| match payload { + CachedPayload::Executed(payload) => Some(payload.as_ref()), + _ => None, + }) + } + + /// Returns an immutable reference to the cached data column. + pub fn get_cached_data_column( + &self, + data_column_index: u64, + ) -> Option>> { + self.verified_data_columns + .iter() + .find(|d| d.index() == data_column_index) + .map(|d| d.clone_arc()) + } + + /// Returns the indices of cached custody columns + pub fn get_cached_data_columns_indices(&self) -> Vec { + self.verified_data_columns + .iter() + .map(|d| d.index()) + .collect() + } + + /// Inserts an executed payload into the cache. + pub fn insert_executed_payload(&mut self, payload: DietAvailabilityPendingExecutedPayload) { + self.payload = Some(CachedPayload::Executed(Box::new(payload))) + } + + /// Inserts a pre-execution payload into the cache. + /// This does NOT override an existing executed payload. + pub fn insert_pre_execution_payload( + &mut self, + payload: Arc>, + source: BlockImportSource, + ) { + if self.payload.is_none() { + self.payload = Some(CachedPayload::PreExecution(payload, source)) + } + } + + /// Merges a given set of data columns into the cache. + fn merge_data_columns>>( + &mut self, + kzg_verified_data_columns: I, + ) -> Result<(), AvailabilityCheckError> { + for data_column in kzg_verified_data_columns { + if self.get_cached_data_column(data_column.index()).is_none() { + self.verified_data_columns.push(data_column); + } + } + + Ok(()) + } + + /// Inserts a new payload. + pub fn merge_payload(&mut self, payload: DietAvailabilityPendingExecutedPayload) { + self.insert_executed_payload(payload); + } + + /// Returns Some if the payload has received all its required data for import. The return value + /// must be persisted in the DB along with the block. + /// + /// WARNING: This function can potentially take a lot of time if the state needs to be + /// reconstructed from disk. Ensure you are not holding any write locks while calling this. + pub fn make_available( + &self, + spec: &Arc, + num_expected_columns: usize, + recover: R, + ) -> Result>, AvailabilityCheckError> + where + R: FnOnce( + DietAvailabilityPendingExecutedPayload, + &Span, + ) -> Result, AvailabilityCheckError>, + { + let Some(CachedPayload::Executed(payload)) = &self.payload else { + // Payload not available yet + return Ok(None); + }; + + let num_expected_blobs = payload.num_blobs_expected(); + let column_data = if num_expected_blobs == 0 { + Some(AvailablePayloadData::NoData) + } else { + let num_received_columns = self.verified_data_columns.len(); + match num_received_columns.cmp(&num_expected_columns) { + Ordering::Greater => { + // Should never happen + return Err(AvailabilityCheckError::Unexpected(format!( + "too many columns got {num_received_columns} expected {num_expected_columns}" + ))); + } + Ordering::Equal => { + // Block is post-peerdas, and we got enough columns + let data_columns = self + .verified_data_columns + .iter() + .map(|d| d.clone().into_inner()) + .collect::>(); + Some(AvailablePayloadData::DataColumns(data_columns)) + } + Ordering::Less => { + // Not enough data columns received yet + None + } + } + }; + + // Payload's data not available yet + let Some(column_data) = column_data else { + return Ok(None); + }; + + let Some(block) = self.block.clone() else { + // This should never happen + return Err(AvailabilityCheckError::Unexpected(format!( + "Payload is being made available but no block exists" + ))); + }; + + // Payload is available, construct `AvailableExecutedPayload` + + let payload_available_timestamp = match column_data { + AvailablePayloadData::NoData => None, + // TODO(gloas): fix with https://github.com/sigp/lighthouse/issues/7477 + AvailablePayloadData::DataColumns(_) => None, + }; + + let AvailabilityPendingExecutedPayload { + payload, + import_data, + payload_verification_outcome, + } = recover(*payload.clone(), &self.span)?; + + let available_payload = AvailablePayload { + block_root: payload.message.beacon_block_root, + payload, + block, + column_data, + payload_available_timestamp, + spec: spec.clone(), + }; + + self.span.in_scope(|| { + debug!("Payload and all data components are available"); + }); + Ok(Some(AvailableExecutedPayload::new( + available_payload, + import_data, + payload_verification_outcome, + ))) + } + + /// Returns an empty `PendingComponents` object with the given block root. + pub fn empty(block_root: Hash256) -> Self { + let span = debug_span!(parent: None, "lh_pending_components", %block_root); + let _guard = span.clone().entered(); + Self { + block_root, + block: None, + verified_data_columns: vec![], + payload: None, + reconstruction_started: false, + span, + } + } + + /// Returns the epoch of: + /// - The payload if it is cached + /// Otherwise, returns None + pub fn epoch(&self) -> Option { + // Get epoch from cached block + if let Some(payload) = &self.payload { + return Some( + payload + .as_payload() + .message + .slot + .epoch(E::slots_per_epoch()), + ); + } + + // Or, get epoch from first data column + if let Some(data_column) = self.verified_data_columns.first() { + return Some(data_column.as_data_column().epoch()); + } + + None + } + + pub fn status_str(&self, num_expected_columns: usize) -> String { + let payload_count = if self.payload.is_some() { 1 } else { 0 }; + format!( + "payload {} data_columns {}/{}", + payload_count, + self.verified_data_columns.len(), + num_expected_columns + ) + } +} + +/// This is the main struct for this module. Outside methods should +/// interact with the cache through this. +pub struct DataAvailabilityCheckerInner { + /// Contains all the data we keep in memory, protected by an RwLock + critical: RwLock>>, + /// This cache holds a limited number of states in memory and reconstructs them + /// from disk when necessary. This is necessary until we merge tree-states + state_cache: StateLRUCache, + custody_context: Arc>, + spec: Arc, +} + +// This enum is only used internally within the crate in the reconstruction function to improve +// readability, so it's OK to not box the variant value, and it shouldn't impact memory much with +// the current usage, as it's deconstructed immediately. +#[allow(clippy::large_enum_variant)] +pub(crate) enum ReconstructColumnsDecision { + Yes(Vec>), + No(&'static str), +} + +impl DataAvailabilityCheckerInner { + pub fn new( + capacity: NonZeroUsize, + beacon_store: BeaconStore, + custody_context: Arc>, + spec: Arc, + ) -> Result { + Ok(Self { + critical: RwLock::new(LruCache::new(capacity)), + state_cache: StateLRUCache::new(beacon_store, spec.clone()), + custody_context, + spec, + }) + } + + /// Returns true if the payload with the given block root is known, without altering the LRU ordering + pub fn get_cached_payload( + &self, + block_root: &Hash256, + ) -> Option> { + self.critical + .read() + .peek(block_root) + .and_then(|pending_components| { + pending_components + .payload + .as_ref() + .map(|payload| match payload { + CachedPayload::PreExecution(p, source) => { + PayloadProcessStatus::NotValidated(p.clone(), *source) + } + CachedPayload::Executed(p) => { + PayloadProcessStatus::ExecutionValidated(p.payload_cloned()) + } + }) + }) + } + + /// Fetch data columns of a given `block_root` from the cache without affecting the LRU ordering + pub fn peek_data_columns( + &self, + block_root: Hash256, + ) -> Option> { + self.critical + .read() + .peek(&block_root) + .map(|pending_components| { + pending_components + .verified_data_columns + .iter() + .map(|col| col.clone_arc()) + .collect() + }) + } + + pub fn peek_pending_components>) -> R>( + &self, + block_root: &Hash256, + f: F, + ) -> R { + f(self.critical.read().peek(block_root)) + } + + #[allow(clippy::type_complexity)] + pub fn put_kzg_verified_data_columns< + I: IntoIterator>, + >( + &self, + block_root: Hash256, + kzg_verified_data_columns: I, + ) -> Result, AvailabilityCheckError> { + let mut kzg_verified_data_columns = kzg_verified_data_columns.into_iter().peekable(); + let Some(epoch) = kzg_verified_data_columns + .peek() + .map(|verified_blob| verified_blob.as_data_column().epoch()) + else { + // No columns are processed. This can occur if all received columns were filtered out + // before this point, e.g. due to a CGC change that caused extra columns to be downloaded + // // before the new CGC took effect. + // Return `Ok` without marking the block as available. + return Ok(Availability::MissingComponents(block_root)); + }; + + let pending_components = self + .update_or_insert_pending_components(block_root, |pending_components| { + pending_components.merge_data_columns(kzg_verified_data_columns) + })?; + + let num_expected_columns = self.get_num_expected_columns(epoch); + + pending_components.span.in_scope(|| { + debug!( + component = "data_columns", + status = pending_components.status_str(num_expected_columns), + "Component added to data availability checker" + ); + }); + + self.check_availability_and_cache_components( + block_root, + pending_components, + num_expected_columns, + ) + } + + fn check_availability_and_cache_components( + &self, + block_root: Hash256, + pending_components: MappedRwLockReadGuard<'_, PendingComponents>, + num_expected_columns: usize, + ) -> Result, AvailabilityCheckError> { + if let Some(available_payload) = pending_components.make_available( + &self.spec, + num_expected_columns, + |payload, span| { + self.state_cache + .recover_pending_executed_payload(payload, span) + }, + )? { + // Explicitly drop read lock before acquiring write lock + drop(pending_components); + if let Some(components) = self.critical.write().get_mut(&block_root) { + // Clean up span now that block is available + components.span = Span::none(); + } + + // We never remove the pending components manually to avoid race conditions. + // This ensures components remain available during and right after payload import, + // preventing a race condition where a component was removed after the payload was + // imported, but re-inserted immediately, causing partial pending components to be + // stored and served to peers. + // Components are only removed via LRU eviction as finality advances. + Ok(Availability::Available(Box::new(available_payload))) + } else { + Ok(Availability::MissingComponents(block_root)) + } + } + + /// Updates or inserts a new `PendingComponents` if it doesn't exist, and then apply the + /// `update_fn` while holding the write lock. + /// + /// Once the update is complete, the write lock is downgraded and a read guard with a + /// reference of the updated `PendingComponents` is returned. + fn update_or_insert_pending_components( + &self, + block_root: Hash256, + update_fn: F, + ) -> Result>, AvailabilityCheckError> + where + F: FnOnce(&mut PendingComponents) -> Result<(), AvailabilityCheckError>, + { + let mut write_lock = self.critical.write(); + + { + let pending_components = + write_lock.get_or_insert_mut(block_root, || PendingComponents::empty(block_root)); + update_fn(pending_components)? + } + + RwLockReadGuard::try_map(RwLockWriteGuard::downgrade(write_lock), |cache| { + cache.peek(&block_root) + }) + .map_err(|_| { + AvailabilityCheckError::Unexpected("pending components should exist".to_string()) + }) + } + + /// Check whether data column reconstruction should be attempted. + /// + /// Potentially trigger reconstruction if all the following satisfy: + /// - Our custody requirement is more than 50% of total columns, + /// - We haven't received all required columns + /// - Reconstruction hasn't been started for the block + /// + /// If reconstruction is required, returns `PendingComponents` which contains the + /// components to be used as inputs to reconstruction, otherwise returns a `reason`. + pub fn check_and_set_reconstruction_started( + &self, + block_root: &Hash256, + ) -> ReconstructColumnsDecision { + let mut write_lock = self.critical.write(); + let Some(pending_components) = write_lock.get_mut(block_root) else { + // Block may have been imported as it does not exist in availability cache. + return ReconstructColumnsDecision::No("block already imported"); + }; + + let Some(epoch) = pending_components + .verified_data_columns + .first() + .map(|c| c.as_data_column().epoch()) + else { + return ReconstructColumnsDecision::No("not enough columns"); + }; + + let total_column_count = T::EthSpec::number_of_columns(); + let sampling_column_count = self + .custody_context + .num_of_data_columns_to_sample(epoch, &self.spec); + let received_column_count = pending_components.verified_data_columns.len(); + + if pending_components.reconstruction_started { + return ReconstructColumnsDecision::No("already started"); + } + if received_column_count >= sampling_column_count { + return ReconstructColumnsDecision::No("all sampling columns received"); + } + if received_column_count < total_column_count / 2 { + return ReconstructColumnsDecision::No("not enough columns"); + } + + pending_components.reconstruction_started = true; + ReconstructColumnsDecision::Yes(pending_components.verified_data_columns.clone()) + } + + /// This could mean some invalid data columns made it through to the `DataAvailabilityChecker`. + /// In this case, we remove all data columns in `PendingComponents`, reset reconstruction + /// status so that we can attempt to retrieve columns from peers again. + pub fn handle_reconstruction_failure(&self, block_root: &Hash256) { + if let Some(pending_components_mut) = self.critical.write().get_mut(block_root) { + pending_components_mut.verified_data_columns = vec![]; + pending_components_mut.reconstruction_started = false; + } + } + + /// Inserts a pre executed payload into the cache. + /// - This does NOT trigger the availability check as the payload still needs to be executed. + /// - This does NOT override an existing cached payload to avoid overwriting an executed payload. + pub fn put_pre_execution_payload( + &self, + block_root: Hash256, + payload: Arc>, + source: BlockImportSource, + ) -> Result<(), AvailabilityCheckError> { + let epoch = payload.message.slot.epoch(T::EthSpec::slots_per_epoch()); + let pending_components = + self.update_or_insert_pending_components(block_root, |pending_components| { + pending_components.insert_pre_execution_payload(payload, source); + Ok(()) + })?; + + let num_expected_columns_opt = self.get_num_expected_columns(epoch); + + pending_components.span.in_scope(|| { + debug!( + component = "pre execution payload", + status = pending_components.status_str(num_expected_columns_opt), + "Component added to data availability checker" + ); + }); + + Ok(()) + } + + /// Removes a pre-execution payload from the cache. + /// This does NOT remove an existing executed payload. + pub fn remove_pre_execution_payload(&self, block_root: &Hash256) { + // The read lock is immediately dropped so we can safely remove the block from the cache. + if let Some(PayloadProcessStatus::NotValidated(_, _)) = self.get_cached_payload(block_root) + { + self.critical.write().pop(block_root); + } + } + + /// Check if we have all the columns for a payload. If we do, return the Availability variant that + /// triggers import of the payload. + pub fn put_executed_payload( + &self, + executed_payload: AvailabilityPendingExecutedPayload, + ) -> Result, AvailabilityCheckError> { + let epoch = executed_payload + .as_payload() + .message + .slot + .epoch(T::EthSpec::slots_per_epoch()); + let block_root = executed_payload.payload.message.beacon_block_root; + + // register the payload to get the diet block + let diet_executed_payload = self + .state_cache + .register_pending_executed_payload(executed_payload); + + let pending_components = + self.update_or_insert_pending_components(block_root, |pending_components| { + pending_components.merge_payload(diet_executed_payload); + Ok(()) + })?; + + let num_expected_columns = self.get_num_expected_columns(epoch); + + pending_components.span.in_scope(|| { + debug!( + component = "payload", + status = pending_components.status_str(num_expected_columns), + "Component added to data availability checker" + ); + }); + + self.check_availability_and_cache_components( + block_root, + pending_components, + num_expected_columns, + ) + } + + fn get_num_expected_columns(&self, epoch: Epoch) -> usize { + self.custody_context + .num_of_data_columns_to_sample(epoch, &self.spec) + } + + /// maintain the cache + pub fn do_maintenance(&self, cutoff_epoch: Epoch) -> Result<(), AvailabilityCheckError> { + // clean up any lingering states in the state cache + self.state_cache.do_maintenance(cutoff_epoch); + + // Collect keys of pending payloads from a previous epoch to cutoff + let mut write_lock = self.critical.write(); + let mut keys_to_remove = vec![]; + for (key, value) in write_lock.iter() { + if let Some(epoch) = value.epoch() + && epoch < cutoff_epoch + { + keys_to_remove.push(*key); + } + } + // Now remove keys + for key in keys_to_remove { + write_lock.pop(&key); + } + + Ok(()) + } + + #[cfg(test)] + /// get the state cache for inspection (used only for tests) + pub fn state_lru_cache(&self) -> &StateLRUCache { + &self.state_cache + } + + /// Number of states stored in memory in the cache. + pub fn state_cache_size(&self) -> usize { + self.state_cache.lru_cache().read().len() + } + + /// Number of pending component entries in memory in the cache. + pub fn payload_cache_size(&self) -> usize { + self.critical.read().len() + } +} + +#[cfg(test)] +mod test { + use super::*; + + use crate::test_utils::generate_data_column_indices_rand_order; + use crate::{ + blob_verification::GossipVerifiedBlob, + block_verification::PayloadVerificationOutcome, + block_verification_types::{AsBlock, BlockImportData}, + custody_context::NodeCustodyType, + data_availability_checker::STATE_LRU_CAPACITY_NON_ZERO, + test_utils::{BaseHarnessType, BeaconChainHarness, DiskHarnessType}, + }; + use fork_choice::PayloadVerificationStatus; + use logging::create_test_tracing_subscriber; + use state_processing::ConsensusContext; + use std::collections::VecDeque; + use store::{HotColdDB, ItemStore, StoreConfig, database::interface::BeaconNodeBackend}; + use tempfile::{TempDir, tempdir}; + use tracing::{debug_span, info}; + use types::new_non_zero_usize; + use types::{ExecPayload, MinimalEthSpec}; + + const LOW_VALIDATOR_COUNT: usize = 32; + const STATE_LRU_CAPACITY: usize = STATE_LRU_CAPACITY_NON_ZERO.get(); + + fn get_store_with_spec( + db_path: &TempDir, + spec: Arc, + ) -> Arc, BeaconNodeBackend>> { + let hot_path = db_path.path().join("hot_db"); + let cold_path = db_path.path().join("cold_db"); + let blobs_path = db_path.path().join("blobs_db"); + let config = StoreConfig::default(); + + HotColdDB::open( + &hot_path, + &cold_path, + &blobs_path, + |_, _, _| Ok(()), + config, + spec, + ) + .expect("disk store should initialize") + } + async fn get_gloas_chain( + db_path: &TempDir, + ) -> BeaconChainHarness> { + let altair_fork_epoch = Epoch::new(0); + let bellatrix_fork_epoch = Epoch::new(0); + let capella_fork_epoch = Epoch::new(0); + let deneb_fork_epoch = Epoch::new(0); + let electra_fork_epoch = Epoch::new(0); + let fulu_fork_epoch = Epoch::new(0); + let gloas_fork_epoch = Epoch::new(0); + + let mut spec = E::default_spec(); + spec.altair_fork_epoch = Some(altair_fork_epoch); + spec.bellatrix_fork_epoch = Some(bellatrix_fork_epoch); + spec.capella_fork_epoch = Some(capella_fork_epoch); + spec.deneb_fork_epoch = Some(deneb_fork_epoch); + spec.electra_fork_epoch = Some(electra_fork_epoch); + spec.fulu_fork_epoch = Some(fulu_fork_epoch); + spec.gloas_fork_epoch = Some(gloas_fork_epoch); + let spec = Arc::new(spec); + + let chain_store = get_store_with_spec::(db_path, spec.clone()); + let validators_keypairs = + types::test_utils::generate_deterministic_keypairs(LOW_VALIDATOR_COUNT); + let harness = BeaconChainHarness::builder(E::default()) + .spec(spec.clone()) + .keypairs(validators_keypairs) + .fresh_disk_store(chain_store) + .mock_execution_layer() + .build(); + + // go to gloas slot + let gloas_fork_slot = gloas_fork_epoch.start_slot(E::slots_per_epoch()); + harness.extend_to_slot(gloas_fork_slot).await; + let gloas_head = &harness.chain.head_snapshot().beacon_block; + assert!(gloas_head.as_gloas().is_ok()); + assert_eq!(gloas_head.slot(), gloas_fork_slot); + assert!( + gloas_head + .message() + .body() + .execution_payload() + .is_err() + "Gloas block has no payload" + ); + harness + } + + async fn availability_pending_payload( + harness: &BeaconChainHarness>, + ) -> ( + AvailabilityPendingExecutedPayload, + Vec>>, + ) + where + E: EthSpec, + Hot: ItemStore, + Cold: ItemStore, + { + let chain = &harness.chain; + let head = chain.head_snapshot(); + let parent_state = head.beacon_state.clone(); + + let target_slot = chain.slot().expect("should get slot") + 1; + let parent_root = head.beacon_block_root; + let parent_block = chain + .get_blinded_block(&parent_root) + .expect("should get block") + .expect("should have block"); + + let (signed_beacon_block_hash, (block, maybe_blobs), state) = harness + .add_block_at_slot(target_slot, parent_state) + .await + .expect("should add block"); + let block_root = signed_beacon_block_hash.into(); + assert_eq!( + block_root, + block.canonical_root(), + "block root should match" + ); + + // log kzg commitments + info!("printing kzg commitments"); + for comm in Vec::from( + block + .message() + .body() + .blob_kzg_commitments() + .expect("should be deneb fork") + .clone(), + ) { + info!(commitment = ?comm, "kzg commitment"); + } + info!("done printing kzg commitments"); + + let gossip_verified_columns = if let Some((kzg_proofs, blobs)) = maybe_blobs { + let sidecars = + DataColumnSidecar::build_sidecars(blobs, &block, kzg_proofs, &chain.spec).unwrap(); + Vec::from(sidecars) + .into_iter() + .map(|sidecar| { + let subnet = sidecar.index; + GossipVerifiedDataColumn::new(sidecar, subnet, &harness.chain) + .expect("should validate column") + }) + .collect() + } else { + vec![] + }; + + let slot = block.slot(); + let consensus_context = ConsensusContext::::new(slot); + let import_data: BlockImportData = BlockImportData { + block_root, + state, + parent_block, + consensus_context, + }; + + let payload_verification_outcome = PayloadVerificationOutcome { + payload_verification_status: PayloadVerificationStatus::Verified, + is_valid_merge_transition_block: false, + }; + + let availability_pending_block = AvailabilityPendingExecutedBlock { + block, + import_data, + payload_verification_outcome, + }; + + (availability_pending_block, gossip_verified_blobs) + } + + async fn setup_harness_and_cache( + capacity: usize, + ) -> ( + BeaconChainHarness>, + Arc>, + TempDir, + ) + where + E: EthSpec, + T: BeaconChainTypes< + HotStore = BeaconNodeBackend, + ColdStore = BeaconNodeBackend, + EthSpec = E, + >, + { + create_test_tracing_subscriber(); + let chain_db_path = tempdir().expect("should get temp dir"); + let harness = get_deneb_chain(&chain_db_path).await; + let spec = harness.spec.clone(); + let test_store = harness.chain.store.clone(); + let capacity_non_zero = new_non_zero_usize(capacity); + let custody_context = Arc::new(CustodyContext::new( + NodeCustodyType::Fullnode, + generate_data_column_indices_rand_order::(), + &spec, + )); + let cache = Arc::new( + DataAvailabilityCheckerInner::::new( + capacity_non_zero, + test_store, + custody_context, + spec.clone(), + ) + .expect("should create cache"), + ); + (harness, cache, chain_db_path) + } + + #[tokio::test] + async fn overflow_cache_test_insert_components() { + type E = MinimalEthSpec; + type T = DiskHarnessType; + let capacity = 4; + let (harness, cache, _path) = setup_harness_and_cache::(capacity).await; + + let (pending_block, blobs) = availability_pending_block(&harness).await; + let root = pending_block.import_data.block_root; + + let blobs_expected = pending_block.num_blobs_expected(); + assert_eq!( + blobs.len(), + blobs_expected, + "should have expected number of blobs" + ); + assert!(cache.critical.read().is_empty(), "cache should be empty"); + let availability = cache + .put_executed_block(pending_block) + .expect("should put block"); + if blobs_expected == 0 { + assert!( + matches!(availability, Availability::Available(_)), + "block doesn't have blobs, should be available" + ); + assert_eq!( + cache.critical.read().len(), + 1, + "cache should still have block as it hasn't been imported yet" + ); + } else { + assert!( + matches!(availability, Availability::MissingComponents(_)), + "should be pending blobs" + ); + assert_eq!( + cache.critical.read().len(), + 1, + "cache should have one block" + ); + assert!( + cache.critical.read().peek(&root).is_some(), + "newly inserted block should exist in memory" + ); + } + + let mut kzg_verified_blobs = Vec::new(); + for (blob_index, gossip_blob) in blobs.into_iter().enumerate() { + kzg_verified_blobs.push(gossip_blob.into_inner()); + let availability = cache + .put_kzg_verified_blobs(root, kzg_verified_blobs.clone()) + .expect("should put blob"); + if blob_index == blobs_expected - 1 { + assert!(matches!(availability, Availability::Available(_))); + } else { + assert!(matches!(availability, Availability::MissingComponents(_))); + assert_eq!(cache.critical.read().len(), 1); + } + } + + let (pending_block, blobs) = availability_pending_block(&harness).await; + let blobs_expected = pending_block.num_blobs_expected(); + assert_eq!( + blobs.len(), + blobs_expected, + "should have expected number of blobs" + ); + let root = pending_block.import_data.block_root; + let mut kzg_verified_blobs = vec![]; + for gossip_blob in blobs { + kzg_verified_blobs.push(gossip_blob.into_inner()); + let availability = cache + .put_kzg_verified_blobs(root, kzg_verified_blobs.clone()) + .expect("should put blob"); + assert!( + matches!(availability, Availability::MissingComponents(_)), + "should be pending block" + ); + assert_eq!( + cache.critical.read().len(), + 2, + "cache should have two blocks now" + ); + } + let availability = cache + .put_executed_block(pending_block) + .expect("should put block"); + assert!( + matches!(availability, Availability::Available(_)), + "block should be available: {:?}", + availability + ); + assert!( + cache.critical.read().len() == 2, + "cache should still have available block" + ); + } + + #[tokio::test] + // ensure the state cache keeps memory usage low and that it can properly recover states + // THIS TEST CAN BE DELETED ONCE TREE STATES IS MERGED AND WE RIP OUT THE STATE CACHE + async fn overflow_cache_test_state_cache() { + type E = MinimalEthSpec; + type T = DiskHarnessType; + let capacity = STATE_LRU_CAPACITY * 2; + let (harness, cache, _path) = setup_harness_and_cache::(capacity).await; + + let mut pending_blocks = VecDeque::new(); + let mut states = Vec::new(); + let mut state_roots = Vec::new(); + // Get enough blocks to fill the cache to capacity, ensuring all blocks have blobs + while pending_blocks.len() < capacity { + let (mut pending_block, _) = availability_pending_block(&harness).await; + if pending_block.num_blobs_expected() == 0 { + // we need blocks with blobs + continue; + } + let state_root = pending_block.import_data.state.canonical_root().unwrap(); + states.push(pending_block.import_data.state.clone()); + pending_blocks.push_back(pending_block); + state_roots.push(state_root); + } + + let state_cache = cache.state_lru_cache().lru_cache(); + let mut pushed_diet_blocks = VecDeque::new(); + + for i in 0..capacity { + let pending_block = pending_blocks.pop_front().expect("should have block"); + let block_root = pending_block.as_block().canonical_root(); + + assert_eq!( + state_cache.read().len(), + std::cmp::min(i, STATE_LRU_CAPACITY), + "state cache should be empty at start" + ); + + if i >= STATE_LRU_CAPACITY { + let lru_root = state_roots[i - STATE_LRU_CAPACITY]; + assert_eq!( + state_cache.read().peek_lru().map(|(root, _)| root), + Some(&lru_root), + "lru block should be in cache" + ); + } + + // put the block in the cache + let availability = cache + .put_executed_block(pending_block) + .expect("should put block"); + + // grab the diet block from the cache for later testing + let diet_block = cache + .critical + .read() + .peek(&block_root) + .and_then(|pending_components| pending_components.get_diet_block().cloned()) + .expect("should exist"); + pushed_diet_blocks.push_back(diet_block); + + // should be unavailable since we made sure all blocks had blobs + assert!( + matches!(availability, Availability::MissingComponents(_)), + "should be pending blobs" + ); + + if i >= STATE_LRU_CAPACITY { + let evicted_index = i - STATE_LRU_CAPACITY; + let evicted_root = state_roots[evicted_index]; + assert!( + state_cache.read().peek(&evicted_root).is_none(), + "lru root should be evicted" + ); + // get the diet block via direct conversion (testing only) + let diet_block = pushed_diet_blocks.pop_front().expect("should have block"); + // reconstruct the pending block by replaying the block on the parent state + let recovered_pending_block = cache + .state_lru_cache() + .recover_pending_executed_block(diet_block, &debug_span!("test")) + .expect("should reconstruct pending block"); + + // assert the recovered state is the same as the original + assert_eq!( + recovered_pending_block.import_data.state, states[evicted_index], + "recovered state should be the same as the original" + ); + } + } + + // now check the last block + let last_block = pushed_diet_blocks.pop_back().expect("should exist").clone(); + // the state should still be in the cache + assert!( + state_cache + .read() + .peek(&last_block.as_block().state_root()) + .is_some(), + "last block state should still be in cache" + ); + // get the diet block via direct conversion (testing only) + let diet_block = last_block.clone(); + // recover the pending block from the cache + let recovered_pending_block = cache + .state_lru_cache() + .recover_pending_executed_block(diet_block, &debug_span!("test")) + .expect("should reconstruct pending block"); + // assert the recovered state is the same as the original + assert_eq!( + Some(&recovered_pending_block.import_data.state), + states.last(), + "recovered state should be the same as the original" + ); + } +} + +#[cfg(test)] +mod pending_components_tests { + use super::*; + use crate::PayloadVerificationOutcome; + use crate::payload_verification_types::PayloadImportData; + use crate::test_utils::{NumBlobs, generate_rand_block_and_blobs, test_spec}; + use fixed_bytes::FixedBytesExtended; + use fork_choice::PayloadVerificationStatus; + use kzg::KzgCommitment; + use rand::SeedableRng; + use rand::rngs::StdRng; + use state_processing::ConsensusContext; + use types::test_utils::TestRandom; + use types::{BeaconState, ForkName, MainnetEthSpec, SignedBeaconBlock, Slot}; + + type E = MainnetEthSpec; + + type Setup = ( + SignedBeaconBlock, + RuntimeFixedVector>>>, + RuntimeFixedVector>>>, + usize, + ); + + pub fn pre_setup() -> Setup { + let mut rng = StdRng::seed_from_u64(0xDEADBEEF0BAD5EEDu64); + let spec = test_spec::(); + let (block, blobs_vec) = + generate_rand_block_and_blobs::(ForkName::Deneb, NumBlobs::Random, &mut rng); + let max_len = spec.max_blobs_per_block(block.epoch()) as usize; + let mut blobs: RuntimeFixedVector>>> = + RuntimeFixedVector::default(max_len); + + for blob in blobs_vec { + if let Some(b) = blobs.get_mut(blob.index as usize) { + *b = Some(Arc::new(blob)); + } + } + + let mut invalid_blobs: RuntimeFixedVector>>> = + RuntimeFixedVector::default(max_len); + for (index, blob) in blobs.iter().enumerate() { + if let Some(invalid_blob) = blob { + let mut blob_copy = invalid_blob.as_ref().clone(); + blob_copy.kzg_commitment = KzgCommitment::random_for_test(&mut rng); + *invalid_blobs.get_mut(index).unwrap() = Some(Arc::new(blob_copy)); + } + } + + (block, blobs, invalid_blobs, max_len) + } + + type PendingComponentsSetup = ( + DietAvailabilityPendingExecutedBlock, + RuntimeFixedVector>>, + RuntimeFixedVector>>, + ); + + pub fn setup_pending_components( + payload: SignedExecutionPayloadEnvelope, + valid_columns: RuntimeFixedVector>>>, + invalid_columns: RuntimeFixedVector>>>, + ) -> PendingComponentsSetup { + let columns = RuntimeFixedVector::new( + valid_columns + .iter() + .map(|column_opt| { + column_opt + .as_ref() + .map(|column| KzgVerifiedDataColumn::__assumed_valid(column.clone())) + }) + .collect::>(), + ); + let invalid_columns = RuntimeFixedVector::new( + invalid_columns + .iter() + .map(|column_opt| { + column_opt + .as_ref() + .map(|column| KzgVerifiedDataColumn::__assumed_valid(column.clone())) + }) + .collect::>(), + ); + let block = AvailabilityPendingExecutedBlock { + payload: Arc::new(payload), + import_data: PayloadImportData { + state: BeaconState::new(0, Default::default(), &ChainSpec::minimal()), + consensus_context: ConsensusContext::new(Slot::new(0)), + }, + payload_verification_outcome: PayloadVerificationOutcome { + payload_verification_status: PayloadVerificationStatus::Verified, + is_valid_merge_transition_block: false, + }, + }; + (payload, columns, invalid_blobs) + } + + pub fn assert_cache_consistent(cache: PendingComponents, max_len: usize) { + if let Some(cached_payload) = &cache.payload { + let cached_payload_commitments = cached_payload.get_commitments(); + for index in 0..max_len { + let payload_commitment = cached_payload_commitments.get(index).copied(); + let column_commitment_opt = cache.get_cached_data_column().get(index).unwrap(); + let column_commitment = column_commitment_opt.as_ref().map(|c| *c.get_commitment()); + assert_eq!(payload_commitment, column_commitment); + } + } else { + panic!("No cached payload") + } + } + + pub fn assert_empty_column_cache(cache: PendingComponents) { + for column_indices in cache.get_cached_data_columns_indices().iter() { + panic!("assert_empty_column_cache failed"); + } + } + + #[test] + fn valid_block_invalid_blobs_valid_blobs() { + let (block_commitments, blobs, random_blobs, max_len) = pre_setup(); + let (block_commitments, blobs, random_blobs) = + setup_pending_components(block_commitments, blobs, random_blobs); + let block_root = Hash256::zero(); + let mut cache = >::empty(block_root, max_len); + cache.merge_block(block_commitments); + cache.merge_blobs(random_blobs); + cache.merge_blobs(blobs); + + assert_cache_consistent(cache, max_len); + } + + #[test] + fn invalid_blobs_block_valid_blobs() { + let (block_commitments, blobs, random_blobs, max_len) = pre_setup(); + let (block_commitments, blobs, random_blobs) = + setup_pending_components(block_commitments, blobs, random_blobs); + let block_root = Hash256::zero(); + let mut cache = >::empty(block_root, max_len); + cache.merge_blobs(random_blobs); + cache.merge_block(block_commitments); + cache.merge_blobs(blobs); + + assert_cache_consistent(cache, max_len); + } + + #[test] + fn invalid_blobs_valid_blobs_block() { + let (block_commitments, blobs, random_blobs, max_len) = pre_setup(); + let (block_commitments, blobs, random_blobs) = + setup_pending_components(block_commitments, blobs, random_blobs); + + let block_root = Hash256::zero(); + let mut cache = >::empty(block_root, max_len); + cache.merge_blobs(random_blobs); + cache.merge_blobs(blobs); + cache.merge_block(block_commitments); + + assert_empty_blob_cache(cache); + } + + #[test] + fn block_valid_blobs_invalid_blobs() { + let (block_commitments, blobs, random_blobs, max_len) = pre_setup(); + let (block_commitments, blobs, random_blobs) = + setup_pending_components(block_commitments, blobs, random_blobs); + + let block_root = Hash256::zero(); + let mut cache = >::empty(block_root, max_len); + cache.merge_block(block_commitments); + cache.merge_blobs(blobs); + cache.merge_blobs(random_blobs); + + assert_cache_consistent(cache, max_len); + } + + #[test] + fn valid_blobs_block_invalid_blobs() { + let (block_commitments, blobs, random_blobs, max_len) = pre_setup(); + let (block_commitments, blobs, random_blobs) = + setup_pending_components(block_commitments, blobs, random_blobs); + + let block_root = Hash256::zero(); + let mut cache = >::empty(block_root, max_len); + cache.merge_blobs(blobs); + cache.merge_block(block_commitments); + cache.merge_blobs(random_blobs); + + assert_cache_consistent(cache, max_len); + } + + #[test] + fn valid_blobs_invalid_blobs_block() { + let (block_commitments, blobs, random_blobs, max_len) = pre_setup(); + let (block_commitments, blobs, random_blobs) = + setup_pending_components(block_commitments, blobs, random_blobs); + + let block_root = Hash256::zero(); + let mut cache = >::empty(block_root, max_len); + cache.merge_blobs(blobs); + cache.merge_blobs(random_blobs); + cache.merge_block(block_commitments); + + assert_cache_consistent(cache, max_len); + } + + #[test] + fn should_not_insert_pre_execution_block_if_executed_block_exists() { + let (pre_execution_block, blobs, random_blobs, max_len) = pre_setup(); + let (executed_block, _blobs, _random_blobs) = + setup_pending_components(pre_execution_block.clone(), blobs, random_blobs); + + let block_root = pre_execution_block.canonical_root(); + let mut pending_component = >::empty(block_root, max_len); + + let pre_execution_block = Arc::new(pre_execution_block); + pending_component + .insert_pre_execution_block(pre_execution_block.clone(), BlockImportSource::Gossip); + assert!( + matches!( + pending_component.block, + Some(CachedBlock::PreExecution(_, _)) + ), + "pre execution block inserted" + ); + + pending_component.insert_executed_block(executed_block); + assert!( + matches!(pending_component.block, Some(CachedBlock::Executed(_))), + "executed block inserted" + ); + + pending_component + .insert_pre_execution_block(pre_execution_block, BlockImportSource::Gossip); + assert!( + matches!(pending_component.block, Some(CachedBlock::Executed(_))), + "executed block should remain" + ); + } +} diff --git a/beacon_node/beacon_chain/src/data_availability_checker_v2/state_lru_cache.rs b/beacon_node/beacon_chain/src/data_availability_checker_v2/state_lru_cache.rs new file mode 100644 index 0000000000..0eb4a7b7e2 --- /dev/null +++ b/beacon_node/beacon_chain/src/data_availability_checker_v2/state_lru_cache.rs @@ -0,0 +1,138 @@ +use crate::payload_verification_types::{AvailabilityPendingExecutedPayload, PayloadImportData}; +use crate::{ + BeaconChainTypes, BeaconStore, PayloadVerificationOutcome, + data_availability_checker_v2::{AvailabilityCheckError, STATE_LRU_CAPACITY_NON_ZERO}, +}; +use lru::LruCache; +use parking_lot::RwLock; +use std::sync::Arc; +use store::OnDiskConsensusContext; +use tracing::{Span, instrument}; +use types::{BeaconState, ChainSpec, Epoch, EthSpec, Hash256, SignedExecutionPayloadEnvelope}; + +/// This mirrors everything in the `AvailabilityPendingExecutedBlock`, except +/// that it is much smaller because it contains only a state root instead of +/// a full `BeaconState`. +#[derive(Clone)] +pub struct DietAvailabilityPendingExecutedPayload { + payload: Arc>, + state_root: Hash256, + consensus_context: OnDiskConsensusContext, + payload_verification_outcome: PayloadVerificationOutcome, +} + +/// Implementing the same methods as `AvailabilityPendingExecutedPayload` +impl DietAvailabilityPendingExecutedPayload { + pub fn as_payload(&self) -> &SignedExecutionPayloadEnvelope { + &self.payload + } + + pub fn payload_cloned(&self) -> Arc> { + self.payload.clone() + } + + pub fn num_blobs_expected(&self) -> usize { + self.payload.message.blob_kzg_commitments.len() + } +} + +/// This LRU cache holds BeaconStates used for payload import. If the cache overflows, +/// the least recently used state will be dropped. If the dropped state is needed +/// later on, it will be recovered from the parent state and replaying the payload. +/// +/// WARNING: This cache assumes the parent block of any `AvailabilityPendingExecutedPayload` +/// has already been imported into ForkChoice. If this is not the case, the cache +/// will fail to recover the state when the cache overflows because it can't load +/// the parent state! +pub struct StateLRUCache { + states: RwLock>>, + store: BeaconStore, + spec: Arc, +} + +impl StateLRUCache { + pub fn new(store: BeaconStore, spec: Arc) -> Self { + Self { + states: RwLock::new(LruCache::new(STATE_LRU_CAPACITY_NON_ZERO)), + store, + spec, + } + } + + /// This will store the state in the LRU cache and return a + /// `DietAvailabilityPendingExecutedPayload` which is much cheaper to + /// keep around in memory. + pub fn register_pending_executed_payload( + &self, + executed_payload: AvailabilityPendingExecutedPayload, + ) -> DietAvailabilityPendingExecutedPayload { + let state = executed_payload.import_data.state; + let state_root = executed_payload.payload.message.state_root; + self.states.write().put(state_root, state); + + DietAvailabilityPendingExecutedPayload { + payload: executed_payload.payload, + state_root, + consensus_context: OnDiskConsensusContext::from_consensus_context( + executed_payload.import_data.consensus_context, + ), + payload_verification_outcome: executed_payload.payload_verification_outcome, + } + } + + /// Recover the `AvailabilityPendingExecutedPayload` from the diet version. + /// This method will first check the cache and if the state is not found + /// it will reconstruct the state by loading the parent state from disk and + /// replaying the block. + #[instrument(skip_all, parent = _span, level = "debug")] + pub fn recover_pending_executed_payload( + &self, + diet_executed_payload: DietAvailabilityPendingExecutedPayload, + _span: &Span, + ) -> Result, AvailabilityCheckError> { + // Keep the state in the cache to prevent reconstruction in race conditions + let state = if let Some(state) = self.states.write().get(&diet_executed_payload.state_root) + { + state.clone() + } else { + self.reconstruct_state(&diet_executed_payload)? + }; + Ok(AvailabilityPendingExecutedPayload { + payload: diet_executed_payload.payload, + import_data: PayloadImportData { + state, + consensus_context: diet_executed_payload + .consensus_context + .into_consensus_context(), + }, + payload_verification_outcome: diet_executed_payload.payload_verification_outcome, + }) + } + + /// Reconstruct the state by loading the parent state from disk and replaying + /// the block. + #[instrument(skip_all, level = "debug")] + fn reconstruct_state( + &self, + diet_executed_block: &DietAvailabilityPendingExecutedPayload, + ) -> Result, AvailabilityCheckError> { + todo!() + } + + /// returns the state cache for inspection + pub fn lru_cache(&self) -> &RwLock>> { + &self.states + } + + /// remove any states from the cache from before the given epoch + pub fn do_maintenance(&self, cutoff_epoch: Epoch) { + let mut write_lock = self.states.write(); + while let Some((_, state)) = write_lock.peek_lru() { + if state.slot().epoch(T::EthSpec::slots_per_epoch()) < cutoff_epoch { + write_lock.pop_lru(); + } else { + break; + } + } + } +} diff --git a/beacon_node/beacon_chain/src/data_column_availability_cache.rs b/beacon_node/beacon_chain/src/data_column_availability_cache.rs new file mode 100644 index 0000000000..e8f7afe229 --- /dev/null +++ b/beacon_node/beacon_chain/src/data_column_availability_cache.rs @@ -0,0 +1,388 @@ +//! Abstraction layer for data column storage across different DA checkers. +//! +//! This module provides a unified interface for data column operations that are shared +//! between the legacy `DataAvailabilityChecker` (v1, for blocks) and +//! `DataAvailabilityChecker` v2 (for payload envelopes after Gloas). +//! +//! ## Design +//! +//! - **Read operations**: Unified via the `DataColumnCache` trait +//! - **Write operations**: Return `AvailabilityOutcome` enum that wraps both checker types +//! - **Processing**: `BeaconChain::process_availability_outcome()` handles both cases +//! +//! After Gloas is fully activated and v1 is deprecated, this can be deleted and we can +//! use the Gloas DA checker directly. + +use crate::BeaconChainTypes; +use crate::custody_context::CustodyContext; +use crate::data_availability_checker::{ + Availability as BlockAvailability, AvailabilityCheckError, + DataColumnReconstructionResult as BlockReconstructionResult, +}; +use crate::data_availability_checker_v2::{ + Availability as PayloadAvailability, + DataColumnReconstructionResult as PayloadReconstructionResult, +}; +use crate::data_column_verification::{GossipVerifiedDataColumn, KzgVerifiedCustodyDataColumn}; +use crate::observed_data_sidecars::ObservationStrategy; +use std::sync::Arc; +use types::{ + ChainSpec, ColumnIndex, DataColumnSidecar, DataColumnSidecarList, EthSpec, ForkName, Hash256, + Slot, +}; + +/// Unified result from write operations that can come from either DA checker. +/// +/// This enum allows callers to handle availability from both v1 (blocks) and v2 (payloads) +/// through a single type, with downstream processing handled by `BeaconChain::process_availability_outcome()`. +#[derive(Debug)] +pub enum AvailabilityOutcome { + /// Block became available (pre-Gloas, from v1 checker) + Block(BlockAvailability), + /// Payload became available (post-Gloas, from v2 checker) + Payload(PayloadAvailability), +} + +impl AvailabilityOutcome { + /// Returns `true` if data is fully available and ready for import. + pub fn is_available(&self) -> bool { + match self { + Self::Block(BlockAvailability::Available(_)) => true, + Self::Block(BlockAvailability::MissingComponents(_)) => false, + Self::Payload(PayloadAvailability::Available(_)) => true, + Self::Payload(PayloadAvailability::MissingComponents(_)) => false, + } + } + + /// Returns the block root, regardless of availability status. + pub fn block_root(&self) -> Hash256 { + match self { + Self::Block(BlockAvailability::Available(block)) => block.import_data.block_root, + Self::Block(BlockAvailability::MissingComponents(root)) => *root, + Self::Payload(PayloadAvailability::Available(payload)) => payload.payload.block_root(), + Self::Payload(PayloadAvailability::MissingComponents(root)) => *root, + } + } + + /// Converts to the inner block availability if this is a block outcome. + pub fn into_block(self) -> Option> { + match self { + Self::Block(avail) => Some(avail), + Self::Payload(_) => None, + } + } + + /// Converts to the inner payload availability if this is a payload outcome. + pub fn into_payload(self) -> Option> { + match self { + Self::Block(_) => None, + Self::Payload(avail) => Some(avail), + } + } +} + +/// Unified result from reconstruction operations. +#[derive(Debug)] +pub enum ReconstructionOutcome { + /// Block reconstruction result (pre-Gloas) + Block(BlockReconstructionResult), + /// Payload reconstruction result (post-Gloas) + Payload(PayloadReconstructionResult), +} + +impl ReconstructionOutcome { + /// Returns the reconstructed columns if successful, regardless of type. + pub fn reconstructed_columns(&self) -> Option<&DataColumnSidecarList> { + match self { + Self::Block(BlockReconstructionResult::Success((_, cols))) => Some(cols), + Self::Payload(PayloadReconstructionResult::Success((_, cols))) => Some(cols), + _ => None, + } + } + + /// Returns true if reconstruction was successful. + pub fn is_success(&self) -> bool { + matches!( + self, + Self::Block(BlockReconstructionResult::Success(_)) + | Self::Payload(PayloadReconstructionResult::Success(_)) + ) + } + + /// Returns the reason if reconstruction was not started or columns not imported. + pub fn reason(&self) -> Option<&'static str> { + match self { + Self::Block(BlockReconstructionResult::NotStarted(r)) => Some(r), + Self::Block(BlockReconstructionResult::RecoveredColumnsNotImported(r)) => Some(r), + Self::Payload(PayloadReconstructionResult::NotStarted(r)) => Some(r), + Self::Payload(PayloadReconstructionResult::RecoveredColumnsNotImported(r)) => Some(r), + _ => None, + } + } +} + +/// Trait for data column operations on availability checkers. +/// +/// Both `DataAvailabilityChecker` (v1) and `DataAvailabilityChecker` (v2) implement +/// this trait. The associated types differ: +/// - V1: Returns `Availability` containing `AvailableExecutedBlock` +/// - V2: Returns `Availability` containing `AvailableExecutedPayload` +pub trait DataColumnCache: Send + Sync { + /// The availability type returned by write operations. + /// V1 returns block availability, V2 returns payload availability. + type Availability; + + /// The reconstruction result type. + /// V1 returns `DataColumnReconstructionResult` with block availability. + /// V2 returns `DataColumnReconstructionResult` with payload availability. + type ReconstructionResult; + + /// Returns the custody context used by this checker. + fn custody_context(&self) -> &Arc>; + + /// Returns all cached data columns for the given block root, if any. + fn get_data_columns(&self, block_root: Hash256) -> Option>; + + /// Returns the indices of cached data columns for the given block root. + fn cached_data_column_indexes(&self, block_root: &Hash256) -> Option>; + + /// Checks if a specific data column is cached for the given block root. + fn is_data_column_cached( + &self, + block_root: &Hash256, + data_column: &DataColumnSidecar, + ) -> bool; + + /// Insert RPC custody columns and check if the block/payload becomes available. + fn put_rpc_custody_columns( + &self, + block_root: Hash256, + slot: Slot, + custody_columns: DataColumnSidecarList, + ) -> Result; + + /// Insert gossip-verified data columns and check availability. + fn put_gossip_verified_data_columns( + &self, + block_root: Hash256, + slot: Slot, + data_columns: Vec>, + ) -> Result; + + /// Insert KZG-verified custody data columns and check availability. + fn put_kzg_verified_custody_data_columns( + &self, + block_root: Hash256, + custody_columns: Vec>, + ) -> Result; + + /// Attempt to reconstruct missing data columns from available ones. + fn reconstruct_data_columns( + &self, + block_root: &Hash256, + ) -> Result; +} + +/// Router that directs data availability checker operations to the appropriate version based on fork. +/// +/// This wraps both the legacy (v1) and Gloas (v2) DA checkers, providing: +/// - Unified read operations that query both checkers +/// - Fork-aware routing for write operations that return `AvailabilityOutcome` +/// +/// After Gloas is fully activated and v1 is deprecated, this router can be deleted and +/// we can use the Gloas DA checker directly. +pub struct DataAvailabilityRouter +where + V1: DataColumnCache< + T, + Availability = BlockAvailability, + ReconstructionResult = BlockReconstructionResult, + >, + V2: DataColumnCache< + T, + Availability = PayloadAvailability, + ReconstructionResult = PayloadReconstructionResult, + >, +{ + /// Legacy DA checker for pre-Gloas blocks + v1: Arc, + /// Gloas DA checker for payload envelopes + v2: Arc, + spec: Arc, + _phantom: std::marker::PhantomData, +} + +impl DataAvailabilityRouter +where + V1: DataColumnCache< + T, + Availability = BlockAvailability, + ReconstructionResult = BlockReconstructionResult, + >, + V2: DataColumnCache< + T, + Availability = PayloadAvailability, + ReconstructionResult = PayloadReconstructionResult, + >, +{ + pub fn new(v1: Arc, v2: Arc, spec: Arc) -> Self { + Self { + v1, + v2, + spec, + _phantom: std::marker::PhantomData, + } + } + + /// Returns true if the given slot is in the Gloas fork or later. + fn is_gloas(&self, slot: Slot) -> bool { + self.spec + .fork_name_at_slot::(slot) + .gloas_enabled() + } + + /// Returns the custody context (same for both checkers). + pub fn custody_context(&self) -> &Arc> { + // Both checkers share the same custody context + self.v1.custody_context() + } + + /// Query data columns from the appropriate checker based on slot. + pub fn get_data_columns( + &self, + block_root: Hash256, + fork_name: ForkName, + ) -> Option> { + if fork_name.gloas_enabled() { + self.v2.get_data_columns(block_root) + } else { + self.v1.get_data_columns(block_root) + } + } + + /// Query data columns from both checkers, returning the first match. + /// + /// Use this when you don't know which fork the block belongs to, or during + /// the transition period when data might be in either checker. + pub fn get_data_columns_any( + &self, + block_root: Hash256, + ) -> Option> { + self.v1 + .get_data_columns(block_root) + .or_else(|| self.v2.get_data_columns(block_root)) + } + + pub fn is_data_column_cached( + &self, + slot: Slot, + block_root: &Hash256, + data_column: &DataColumnSidecar, + ) -> bool { + if self.is_gloas(slot) { + self.v2.is_data_column_cached(block_root, data_column) + } else { + self.v1.is_data_column_cached(block_root, data_column) + } + } + + /// Get cached column indexes from the appropriate checker based on slot. + pub fn cached_data_column_indexes( + &self, + block_root: &Hash256, + slot: Slot, + ) -> Option> { + if self.is_gloas(slot) { + self.v2.cached_data_column_indexes(block_root) + } else { + self.v1.cached_data_column_indexes(block_root) + } + } + + /// Insert RPC custody columns, routing to the correct checker based on fork. + pub fn put_rpc_custody_columns( + &self, + block_root: Hash256, + slot: Slot, + custody_columns: DataColumnSidecarList, + ) -> Result, AvailabilityCheckError> { + if self.is_gloas(slot) { + self.v2 + .put_rpc_custody_columns(block_root, slot, custody_columns) + .map(AvailabilityOutcome::Payload) + } else { + self.v1 + .put_rpc_custody_columns(block_root, slot, custody_columns) + .map(AvailabilityOutcome::Block) + } + } + + /// Insert gossip-verified data columns, routing to the correct checker based on fork. + pub fn put_gossip_verified_data_columns( + &self, + block_root: Hash256, + slot: Slot, + data_columns: Vec>, + ) -> Result, AvailabilityCheckError> { + if self.is_gloas(slot) { + self.v2 + .put_gossip_verified_data_columns(block_root, slot, data_columns) + .map(AvailabilityOutcome::Payload) + } else { + self.v1 + .put_gossip_verified_data_columns(block_root, slot, data_columns) + .map(AvailabilityOutcome::Block) + } + } + + /// Insert KZG-verified custody data columns, routing to the correct checker based on fork. + pub fn put_kzg_verified_custody_data_columns( + &self, + block_root: Hash256, + slot: Slot, + custody_columns: Vec>, + ) -> Result, AvailabilityCheckError> { + if self.is_gloas(slot) { + self.v2 + .put_kzg_verified_custody_data_columns(block_root, custody_columns) + .map(AvailabilityOutcome::Payload) + } else { + self.v1 + .put_kzg_verified_custody_data_columns(block_root, custody_columns) + .map(AvailabilityOutcome::Block) + } + } + + /// Attempt to reconstruct missing data columns, routing to the correct checker based on fork. + pub fn reconstruct_data_columns( + &self, + block_root: &Hash256, + slot: Slot, + ) -> Result, AvailabilityCheckError> { + if self.is_gloas(slot) { + self.v2 + .reconstruct_data_columns(block_root) + .map(ReconstructionOutcome::Payload) + } else { + self.v1 + .reconstruct_data_columns(block_root) + .map(ReconstructionOutcome::Block) + } + } + + /// Direct access to v1 checker (for block-specific operations). + /// + /// Use this for operations that are specific to the legacy block-based DA checker, + /// such as `put_executed_block`, `get_cached_block`, blob operations, etc. + pub fn v1(&self) -> &V1 { + &self.v1 + } + + /// Direct access to v2 checker (for payload-specific operations). + /// + /// Use this for operations that are specific to the Gloas payload-based DA checker, + /// such as `put_executed_payload`, `get_cached_payload`, etc. + pub fn v2(&self) -> &V2 { + &self.v2 + } +} diff --git a/beacon_node/beacon_chain/src/data_column_verification.rs b/beacon_node/beacon_chain/src/data_column_verification.rs index 7bb139756d..752a2350b6 100644 --- a/beacon_node/beacon_chain/src/data_column_verification.rs +++ b/beacon_node/beacon_chain/src/data_column_verification.rs @@ -229,6 +229,7 @@ impl GossipVerifiedDataColumn column_sidecar: Arc>, chain: &BeaconChain, ) -> Result { + let slot = column_sidecar.slot(); verify_data_column_sidecar(&column_sidecar, &chain.spec)?; // Check if the data column is already in the DA checker cache. This happens when data columns @@ -238,10 +239,11 @@ impl GossipVerifiedDataColumn // In this case, we should accept it for gossip propagation. verify_is_unknown_sidecar(chain, &column_sidecar)?; - if chain - .data_availability_checker - .is_data_column_cached(&column_sidecar.block_root(), &column_sidecar) - { + if chain.data_availability_checker.is_data_column_cached( + slot, + &column_sidecar.block_root(), + &column_sidecar, + ) { // Observe this data column so we don't process it again. if O::observe() { observe_gossip_data_column(&column_sidecar, chain)?; @@ -495,10 +497,11 @@ pub fn validate_data_column_sidecar_for_gossip FetchBlobsBeaconAdapter { pub(crate) fn cached_blob_indexes(&self, block_root: &Hash256) -> Option> { self.chain .data_availability_checker + .v1() .cached_blob_indexes(block_root) } - pub(crate) fn cached_data_column_indexes(&self, block_root: &Hash256) -> Option> { + pub(crate) fn cached_data_column_indexes( + &self, + slot: Slot, + block_root: &Hash256, + ) -> Option> { self.chain .data_availability_checker - .cached_data_column_indexes(block_root) + .cached_data_column_indexes(block_root, slot) } pub(crate) async fn process_engine_blobs( diff --git a/beacon_node/beacon_chain/src/fetch_blobs/mod.rs b/beacon_node/beacon_chain/src/fetch_blobs/mod.rs index 6559f24d23..71578807a8 100644 --- a/beacon_node/beacon_chain/src/fetch_blobs/mod.rs +++ b/beacon_node/beacon_chain/src/fetch_blobs/mod.rs @@ -402,7 +402,7 @@ async fn compute_custody_columns_to_import( // Only consider columns that are not already known to data availability. if let Some(known_columns) = - chain_adapter_cloned.cached_data_column_indexes(&block_root) + chain_adapter_cloned.cached_data_column_indexes(block.slot(), &block_root) { custody_columns.retain(|col| !known_columns.contains(&col.index())); if custody_columns.is_empty() { diff --git a/beacon_node/beacon_chain/src/fetch_blobs/tests.rs b/beacon_node/beacon_chain/src/fetch_blobs/tests.rs index cbe2f78fbd..aba35b687a 100644 --- a/beacon_node/beacon_chain/src/fetch_blobs/tests.rs +++ b/beacon_node/beacon_chain/src/fetch_blobs/tests.rs @@ -197,7 +197,7 @@ mod get_blobs_v2 { .returning(|_| None); mock_adapter .expect_cached_data_column_indexes() - .returning(|_| None); + .returning(|_, _| None); mock_process_engine_blobs_result( &mut mock_adapter, Ok(AvailabilityProcessingStatus::Imported(block_root)), diff --git a/beacon_node/beacon_chain/src/kzg_utils.rs b/beacon_node/beacon_chain/src/kzg_utils.rs index a1c255e3b3..8f8b497c91 100644 --- a/beacon_node/beacon_chain/src/kzg_utils.rs +++ b/beacon_node/beacon_chain/src/kzg_utils.rs @@ -11,7 +11,7 @@ use types::kzg_ext::KzgCommitments; use types::{ Blob, BlobSidecar, BlobSidecarList, ChainSpec, DataColumnSidecar, DataColumnSidecarList, EthSpec, Hash256, KzgCommitment, KzgProof, SignedBeaconBlock, SignedBeaconBlockHeader, - SignedBlindedBeaconBlock, + SignedBlindedBeaconBlock, Slot, }; /// Converts a blob ssz List object to an array to be used with the kzg diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index f92030a671..5db200e6ba 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -18,6 +18,8 @@ pub mod canonical_head; pub mod chain_config; pub mod custody_context; pub mod data_availability_checker; +pub mod data_availability_checker_v2; +pub mod data_column_availability_cache; pub mod data_column_verification; mod early_attester_cache; mod errors; @@ -42,6 +44,7 @@ pub mod observed_block_producers; pub mod observed_data_sidecars; pub mod observed_operations; mod observed_slashable; +pub mod payload_verification_types; pub mod persisted_beacon_chain; pub mod persisted_custody; mod persisted_fork_choice; diff --git a/beacon_node/beacon_chain/src/metrics.rs b/beacon_node/beacon_chain/src/metrics.rs index 6be07faa24..8afe32b7c6 100644 --- a/beacon_node/beacon_chain/src/metrics.rs +++ b/beacon_node/beacon_chain/src/metrics.rs @@ -1978,7 +1978,8 @@ pub fn scrape_for_metrics(beacon_chain: &BeaconChain) { beacon_chain.store.state_cache_len(), ); - let da_checker_metrics = beacon_chain.data_availability_checker.metrics(); + let da_checker_metrics = beacon_chain.data_availability_checker.v1().metrics(); + set_gauge_by_usize( &DATA_AVAILABILITY_OVERFLOW_MEMORY_BLOCK_CACHE_SIZE, da_checker_metrics.block_cache_size, diff --git a/beacon_node/beacon_chain/src/payload_verification_types.rs b/beacon_node/beacon_chain/src/payload_verification_types.rs new file mode 100644 index 0000000000..f54c67ecb1 --- /dev/null +++ b/beacon_node/beacon_chain/src/payload_verification_types.rs @@ -0,0 +1,74 @@ +use std::sync::Arc; + +use state_processing::ConsensusContext; +use types::{BeaconState, BlockImportSource, EthSpec, SignedExecutionPayloadEnvelope}; + +use crate::{PayloadVerificationOutcome, data_availability_checker_v2::AvailablePayload}; + +#[derive(Debug, PartialEq)] +pub struct PayloadImportData { + pub state: BeaconState, + pub consensus_context: ConsensusContext, +} + +/// A payload that has completed payload verification by an EL client but does not +/// have all requisite column data to get imported into fork choice. +pub struct AvailabilityPendingExecutedPayload { + pub payload: Arc>, + pub import_data: PayloadImportData, + pub payload_verification_outcome: PayloadVerificationOutcome, +} + +impl AvailabilityPendingExecutedPayload { + pub fn new( + payload: Arc>, + import_data: PayloadImportData, + payload_verification_outcome: PayloadVerificationOutcome, + ) -> Self { + Self { + payload, + import_data, + payload_verification_outcome, + } + } + + pub fn as_payload(&self) -> &SignedExecutionPayloadEnvelope { + &self.payload + } + + pub fn num_blobs_expected(&self) -> usize { + self.payload.message.blob_kzg_commitments.len() + } +} + +/// A payload that has completed all payload verification by an EL client +/// **and** has all requisite column data to be imported into fork choice. +pub struct AvailableExecutedPayload { + pub payload: AvailablePayload, + pub import_data: PayloadImportData, + pub payload_verification_outcome: PayloadVerificationOutcome, +} + +impl AvailableExecutedPayload { + pub fn new( + payload: AvailablePayload, + import_data: PayloadImportData, + payload_verification_outcome: PayloadVerificationOutcome, + ) -> Self { + Self { + payload, + import_data, + payload_verification_outcome, + } + } +} + +pub enum PayloadProcessStatus { + /// Payload is not in any pre-import cache. Payload may be in the data-base or in the fork-choice. + Unknown, + /// Payload is currently processing but not yet validated. + NotValidated(Arc>, BlockImportSource), + /// Payload is fully valid, but not yet imported. It's cached in the da_checker while awaiting + /// columns. + ExecutionValidated(Arc>), +} diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index b6c235a4cb..b49e986762 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -3280,6 +3280,42 @@ macro_rules! add_blob_transactions { }}; } +macro_rules! add_blob_transactions_gloas { + ($message:expr, $num_blobs:expr, $rng:expr, $fork_name:expr) => {{ + let num_blobs = match $num_blobs { + NumBlobs::Random => $rng.random_range(DEFAULT_MIN_BLOBS..=DEFAULT_MAX_BLOBS), + NumBlobs::Number(n) => n, + NumBlobs::None => 0, + }; + let (bundle, transactions) = + execution_layer::test_utils::generate_blobs::(num_blobs, $fork_name).unwrap(); + + let payload = &mut $message.payload; + payload.transactions = <_>::default(); + for tx in Vec::from(transactions) { + payload.transactions.push(tx).unwrap(); + } + $message.blob_kzg_commitments = bundle.commitments.clone(); + bundle + }}; +} + +pub fn generate_rand_payloads_and_columns( + fork_name: ForkName, + num_blobs: NumBlobs, + rng: &mut impl Rng, +) -> (SignedExecutionPayloadEnvelope, Vec>) { + let mut payload = SignedExecutionPayloadEnvelope::random_for_test(rng); + + let mut data_column_sidecars = vec![]; + + let bundle = add_blob_transactions_gloas!(payload.message, num_blobs, rng, fork_name); + + let data_columns = generate_data_column_sidecars_from_block(&block, spec); + + todo!() +} + pub fn generate_rand_block_and_blobs( fork_name: ForkName, num_blobs: NumBlobs, @@ -3398,6 +3434,44 @@ pub fn generate_data_column_sidecars_from_block( .unwrap() } +/// Generate data column sidecars from pre-computed cells and proofs for gloas paylaods. +pub fn generate_data_column_sidecars_from_payload( + payload: &SignedExecutionPayloadEnvelope, + spec: &ChainSpec, +) -> DataColumnSidecarList { + let kzg_commitments = payload.message.blob_kzg_commitments; + if kzg_commitments.is_empty() { + return vec![]; + } + + // load the precomputed column sidecar to avoid computing them for every block in the tests. + let template_data_columns = RuntimeVariableList::>::from_ssz_bytes( + TEST_DATA_COLUMN_SIDECARS_SSZ, + E::number_of_columns(), + ) + .unwrap(); + + let (cells, proofs) = template_data_columns + .into_iter() + .map(|sidecar| { + let DataColumnSidecar { + column, kzg_proofs, .. + } = sidecar; + // There's only one cell per column for a single blob + let cell_bytes: Vec = column.into_iter().next().unwrap().into(); + let kzg_cell = cell_bytes.try_into().unwrap(); + let kzg_proof = kzg_proofs.into_iter().next().unwrap(); + (kzg_cell, kzg_proof) + }) + .collect::<(Vec<_>, Vec<_>)>(); + + // Repeat the cells and proofs for every blob + let blob_cells_and_proofs_vec = + vec![(cells.try_into().unwrap(), proofs.try_into().unwrap()); kzg_commitments.len()]; + + build_data_column_sidecars(kzg_commitments.clone(), blob_cells_and_proofs_vec, spec).unwrap() +} + pub fn generate_data_column_indices_rand_order() -> Vec { let mut indices = (0..E::number_of_columns() as u64).collect::>(); indices.shuffle(&mut StdRng::seed_from_u64(42)); diff --git a/beacon_node/lighthouse_network/src/rpc/codec.rs b/beacon_node/lighthouse_network/src/rpc/codec.rs index 3611f02391..57698a10b9 100644 --- a/beacon_node/lighthouse_network/src/rpc/codec.rs +++ b/beacon_node/lighthouse_network/src/rpc/codec.rs @@ -566,6 +566,7 @@ fn handle_rpc_request( decoded_buffer, spec.max_request_blocks(current_fork), )?, + fork_name: current_fork, }, ))), SupportedProtocol::PingV1 => Ok(Some(RequestType::Ping(Ping { @@ -1089,6 +1090,7 @@ mod tests { spec.max_request_blocks(fork_name), ) .unwrap(), + fork_name, } } diff --git a/beacon_node/lighthouse_network/src/rpc/methods.rs b/beacon_node/lighthouse_network/src/rpc/methods.rs index 0539877c72..da7ce901b0 100644 --- a/beacon_node/lighthouse_network/src/rpc/methods.rs +++ b/beacon_node/lighthouse_network/src/rpc/methods.rs @@ -12,6 +12,7 @@ use std::ops::Deref; use std::sync::Arc; use strum::IntoStaticStr; use superstruct::superstruct; +use types::ForkName; use types::data::BlobIdentifier; use types::light_client::consts::MAX_REQUEST_LIGHT_CLIENT_UPDATES; use types::{ @@ -528,16 +529,21 @@ impl BlobsByRootRequest { pub struct DataColumnsByRootRequest { /// The list of beacon block roots and column indices being requested. pub data_column_ids: RuntimeVariableList>, + pub fork_name: ForkName, } impl DataColumnsByRootRequest { pub fn new( data_column_ids: Vec>, + fork_name: ForkName, max_request_blocks: usize, ) -> Result { let data_column_ids = RuntimeVariableList::new(data_column_ids, max_request_blocks) .map_err(|_| "DataColumnsByRootRequest too many column IDs")?; - Ok(Self { data_column_ids }) + Ok(Self { + data_column_ids, + fork_name, + }) } pub fn max_requested(&self) -> usize { diff --git a/beacon_node/lighthouse_network/tests/rpc_tests.rs b/beacon_node/lighthouse_network/tests/rpc_tests.rs index 2a17a04b90..809c50c002 100644 --- a/beacon_node/lighthouse_network/tests/rpc_tests.rs +++ b/beacon_node/lighthouse_network/tests/rpc_tests.rs @@ -992,6 +992,7 @@ fn test_tcp_columns_by_root_chunked_rpc() { }; max_request_blocks ], + current_fork_name, max_request_blocks, ) .unwrap(); @@ -1002,6 +1003,7 @@ fn test_tcp_columns_by_root_chunked_rpc() { spec.max_request_blocks(current_fork_name), ) .unwrap(), + fork_name: current_fork_name, }; assert_eq!(req, req_decoded); let rpc_request = RequestType::DataColumnsByRoot(req); 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 e51e73b756..cb134fcbdd 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -1028,7 +1028,7 @@ impl NetworkBeaconProcessor { .await; register_process_result_metrics(&result, metrics::BlockSource::Gossip, "data_column"); - match &result { + match result { Ok(availability) => match availability { AvailabilityProcessingStatus::Imported(block_root) => { debug!( @@ -1041,6 +1041,14 @@ impl NetworkBeaconProcessor { &metrics::BEACON_BLOB_DELAY_FULL_VERIFICATION, processing_start_time.elapsed().as_millis() as i64, ); + + // If a block is in the da_checker, sync maybe awaiting for an event when block is finally + // imported. A block can become imported both after processing a block or data column. If a + // importing a block results in `Imported`, notify. Do not notify of data column errors. + self.send_sync_message(SyncMessage::GossipBlockProcessResult { + block_root, + imported: true, + }); } AvailabilityProcessingStatus::MissingComponents(slot, block_root) => { trace!( @@ -1072,11 +1080,14 @@ impl NetworkBeaconProcessor { work: Work::Reprocess( ReprocessQueueMessage::DelayColumnReconstruction( QueuedColumnReconstruction { - block_root, - slot: *slot, + block_root: block_root.into(), + slot, process_fn: Box::pin(async move { cloned_self - .attempt_data_column_reconstruction(block_root) + .attempt_data_column_reconstruction( + slot, + block_root.into(), + ) .await; }), }, @@ -1111,16 +1122,6 @@ impl NetworkBeaconProcessor { ); } } - - // If a block is in the da_checker, sync maybe awaiting for an event when block is finally - // imported. A block can become imported both after processing a block or data column. If a - // importing a block results in `Imported`, notify. Do not notify of data column errors. - if matches!(result, Ok(AvailabilityProcessingStatus::Imported(_))) { - self.send_sync_message(SyncMessage::GossipBlockProcessResult { - block_root, - imported: true, - }); - } } /// Process the beacon block received from the gossip network and: diff --git a/beacon_node/network/src/network_beacon_processor/mod.rs b/beacon_node/network/src/network_beacon_processor/mod.rs index fd9c2c1e55..9c1faefb28 100644 --- a/beacon_node/network/src/network_beacon_processor/mod.rs +++ b/beacon_node/network/src/network_beacon_processor/mod.rs @@ -835,8 +835,8 @@ impl NetworkBeaconProcessor { /// Attempts to reconstruct all data columns if the conditions checked in /// [`DataAvailabilityCheckerInner::check_and_set_reconstruction_started`] are satisfied. #[instrument(level = "debug", skip_all, fields(?block_root))] - async fn attempt_data_column_reconstruction(self: &Arc, block_root: Hash256) { - let result = self.chain.reconstruct_data_columns(block_root).await; + async fn attempt_data_column_reconstruction(self: &Arc, slot: Slot, block_root: Hash256) { + let result = self.chain.reconstruct_data_columns(slot, block_root).await; match result { Ok(Some((availability_processing_status, data_columns_to_publish))) => { 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 7cf7c01416..a711da61c3 100644 --- a/beacon_node/network/src/network_beacon_processor/rpc_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/rpc_methods.rs @@ -306,6 +306,7 @@ impl NetworkBeaconProcessor { let block_root = blob_id.block_root; self.chain .data_availability_checker + .v1() .get_cached_block(&block_root) .and_then(|status| match status { BlockProcessStatus::NotValidated(block, _source) => Some(block), @@ -333,7 +334,7 @@ impl NetworkBeaconProcessor { } // First attempt to get the blobs from the RPC cache. - if let Ok(Some(blob)) = self.chain.data_availability_checker.get_blob(id) { + if let Ok(Some(blob)) = self.chain.data_availability_checker.v1().get_blob(id) { self.send_response( peer_id, inbound_request_id, @@ -443,6 +444,7 @@ impl NetworkBeaconProcessor { match self.chain.get_data_columns_checking_all_caches( data_column_ids_by_root.block_root, &indices_to_retrieve, + request.fork_name, ) { Ok(data_columns) => { send_data_column_count += data_columns.len(); diff --git a/beacon_node/network/src/network_beacon_processor/sync_methods.rs b/beacon_node/network/src/network_beacon_processor/sync_methods.rs index 6ba8bd4d3e..79dd77e61f 100644 --- a/beacon_node/network/src/network_beacon_processor/sync_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/sync_methods.rs @@ -717,9 +717,11 @@ impl NetworkBeaconProcessor { downloaded_blocks: Vec>, ) -> (usize, Result<(), ChainSegmentFailed>) { let total_blocks = downloaded_blocks.len(); + // TODO(gloas) make this work across both v1 and v2 let available_blocks = match self .chain .data_availability_checker + .v1() .verify_kzg_for_rpc_blocks(downloaded_blocks) { Ok(blocks) => blocks diff --git a/beacon_node/network/src/sync/block_lookups/common.rs b/beacon_node/network/src/sync/block_lookups/common.rs index edd99345b4..b209e051bc 100644 --- a/beacon_node/network/src/sync/block_lookups/common.rs +++ b/beacon_node/network/src/sync/block_lookups/common.rs @@ -172,7 +172,7 @@ impl RequestState for CustodyRequestState { _: usize, cx: &mut SyncNetworkContext, ) -> Result { - cx.custody_lookup_request(id, self.block_root, lookup_peers) + cx.custody_lookup_request(id, self.slot, self.block_root, lookup_peers) .map_err(LookupRequestError::SendFailedNetwork) } diff --git a/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs b/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs index 43bfe29a84..0250e52468 100644 --- a/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs +++ b/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs @@ -233,7 +233,7 @@ impl SingleBlockLookup { ); } else if cx.chain.should_fetch_custody_columns(block_epoch) { self.component_requests = ComponentRequests::ActiveCustodyRequest( - CustodyRequestState::new(self.block_root), + CustodyRequestState::new(block.slot(), self.block_root), ); } else { self.component_requests = ComponentRequests::NotNeeded("outside da window"); @@ -391,13 +391,15 @@ impl BlobRequestState { pub struct CustodyRequestState { #[educe(Debug(ignore))] pub block_root: Hash256, + pub slot: Slot, pub state: SingleLookupRequestState>, } impl CustodyRequestState { - pub fn new(block_root: Hash256) -> Self { + pub fn new(slot: Slot, block_root: Hash256) -> Self { Self { block_root, + slot, state: SingleLookupRequestState::new(), } } diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index 069d51764f..a58847319c 100644 --- a/beacon_node/network/src/sync/network_context.rs +++ b/beacon_node/network/src/sync/network_context.rs @@ -964,6 +964,7 @@ impl SyncNetworkContext { let imported_blob_indexes = self .chain .data_availability_checker + .v1() .cached_blob_indexes(&block_root) .unwrap_or_default(); // Include only the blob indexes not yet imported (received through gossip) @@ -1078,13 +1079,14 @@ impl SyncNetworkContext { pub fn custody_lookup_request( &mut self, lookup_id: SingleLookupId, + slot: Slot, block_root: Hash256, lookup_peers: Arc>>, ) -> Result { let custody_indexes_imported = self .chain .data_availability_checker - .cached_data_column_indexes(&block_root) + .cached_data_column_indexes(&block_root, slot) .unwrap_or_default(); let current_epoch = self.chain.epoch().map_err(|e| { @@ -1366,12 +1368,14 @@ impl SyncNetworkContext { if self .chain .data_availability_checker + .v1() .data_columns_required_for_epoch(epoch) { ByRangeRequestType::BlocksAndColumns } else if self .chain .data_availability_checker + .v1() .blobs_required_for_epoch(epoch) { ByRangeRequestType::BlocksAndBlobs diff --git a/beacon_node/network/src/sync/network_context/requests/data_columns_by_root.rs b/beacon_node/network/src/sync/network_context/requests/data_columns_by_root.rs index 34df801eaa..d06645e3af 100644 --- a/beacon_node/network/src/sync/network_context/requests/data_columns_by_root.rs +++ b/beacon_node/network/src/sync/network_context/requests/data_columns_by_root.rs @@ -26,6 +26,7 @@ impl DataColumnsByRootSingleBlockRequest { block_root: self.block_root, columns, }], + fork_name, spec.max_request_blocks(fork_name), ) } diff --git a/beacon_node/network/src/sync/tests/lookups.rs b/beacon_node/network/src/sync/tests/lookups.rs index 715928906e..371c1f260d 100644 --- a/beacon_node/network/src/sync/tests/lookups.rs +++ b/beacon_node/network/src/sync/tests/lookups.rs @@ -1079,6 +1079,7 @@ impl TestRig { .harness .chain .data_availability_checker + .v1() .put_executed_block(executed_block) .unwrap() { @@ -1094,6 +1095,7 @@ impl TestRig { .harness .chain .data_availability_checker + .v1() .put_gossip_verified_blobs( blob.block_root(), std::iter::once(GossipVerifiedBlob::<_, Observe>::__assumed_valid( @@ -1113,6 +1115,7 @@ impl TestRig { self.harness .chain .data_availability_checker + .v1() .put_pre_execution_block(block.canonical_root(), block, BlockImportSource::Gossip) .unwrap(); } @@ -1121,6 +1124,7 @@ impl TestRig { self.harness .chain .data_availability_checker + .v1() .remove_block_on_execution_error(&block_root); self.send_sync_message(SyncMessage::GossipBlockProcessResult { From e9f9ad6c4506d59fe18275ef8ef15baf36eef365 Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Wed, 28 Jan 2026 16:46:25 -0800 Subject: [PATCH 002/118] Small rename --- beacon_node/beacon_chain/src/beacon_chain.rs | 2 +- beacon_node/beacon_chain/src/builder.rs | 2 +- beacon_node/beacon_chain/src/data_availability_checker.rs | 2 +- beacon_node/beacon_chain/src/data_availability_checker_v2.rs | 2 +- ...column_availability_cache.rs => data_availability_router.rs} | 0 beacon_node/beacon_chain/src/lib.rs | 2 +- 6 files changed, 5 insertions(+), 5 deletions(-) rename beacon_node/beacon_chain/src/{data_column_availability_cache.rs => data_availability_router.rs} (100%) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 409fd3ed9b..1789873c6b 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -26,7 +26,7 @@ use crate::data_availability_checker::{ DataAvailabilityChecker, DataColumnReconstructionResult, }; use crate::data_availability_checker_v2::DataAvailabilityChecker as DataAvailabilityCheckerV2; -use crate::data_column_availability_cache::{ +use crate::data_availability_router::{ AvailabilityOutcome, DataAvailabilityRouter, ReconstructionOutcome, }; use crate::data_column_verification::{GossipDataColumnError, GossipVerifiedDataColumn}; diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index 2940c270c4..db15214318 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -7,7 +7,7 @@ use crate::beacon_proposer_cache::BeaconProposerCache; use crate::custody_context::NodeCustodyType; use crate::data_availability_checker::DataAvailabilityChecker; use crate::data_availability_checker_v2::DataAvailabilityChecker as DataAvailabilityCheckerV2; -use crate::data_column_availability_cache::DataAvailabilityRouter; +use crate::data_availability_router::DataAvailabilityRouter; use crate::fork_choice_signal::ForkChoiceSignalTx; use crate::fork_revert::{reset_fork_choice_to_finalization, revert_to_fork_boundary}; use crate::graffiti_calculator::{GraffitiCalculator, GraffitiOrigin}; diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index 31a466f387..e0d165f0b5 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -5,7 +5,7 @@ use crate::block_verification_types::{AvailabilityPendingExecutedBlock, Availabl use crate::data_availability_checker::overflow_lru_cache::{ DataAvailabilityCheckerInner, ReconstructColumnsDecision, }; -use crate::data_column_availability_cache::DataColumnCache; +use crate::data_availability_router::DataColumnCache; use crate::{ BeaconChain, BeaconChainTypes, BeaconStore, BlockProcessStatus, CustodyContext, metrics, }; diff --git a/beacon_node/beacon_chain/src/data_availability_checker_v2.rs b/beacon_node/beacon_chain/src/data_availability_checker_v2.rs index 381d3f4445..6f892f9d2f 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker_v2.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker_v2.rs @@ -3,7 +3,7 @@ use crate::data_availability_checker_v2::overflow_lru_cache::{ }; use crate::data_availability_checker::AvailabilityCheckError; -use crate::data_column_availability_cache::DataColumnCache; +use crate::data_availability_router::DataColumnCache; use crate::payload_verification_types::{ AvailabilityPendingExecutedPayload, AvailableExecutedPayload, PayloadProcessStatus, }; diff --git a/beacon_node/beacon_chain/src/data_column_availability_cache.rs b/beacon_node/beacon_chain/src/data_availability_router.rs similarity index 100% rename from beacon_node/beacon_chain/src/data_column_availability_cache.rs rename to beacon_node/beacon_chain/src/data_availability_router.rs diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index 3eeca72113..a030557c52 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -19,7 +19,7 @@ pub mod chain_config; pub mod custody_context; pub mod data_availability_checker; pub mod data_availability_checker_v2; -pub mod data_column_availability_cache; +pub mod data_availability_router; pub mod data_column_verification; mod early_attester_cache; mod errors; From 3df2cf8f7e052c0e843d47ebfce4a6768c8e013e Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Wed, 28 Jan 2026 18:26:56 -0800 Subject: [PATCH 003/118] Add db boilerplate for payload envelope --- beacon_node/beacon_chain/src/beacon_chain.rs | 7 + beacon_node/beacon_chain/src/migrate.rs | 6 +- beacon_node/store/src/hot_cold_store.rs | 131 ++++++++++++++++++ beacon_node/store/src/impls.rs | 1 + .../signed_execution_payload_envelope.rs | 18 +++ beacon_node/store/src/lib.rs | 12 +- beacon_node/store/src/metrics.rs | 14 ++ 7 files changed, 186 insertions(+), 3 deletions(-) create mode 100644 beacon_node/store/src/impls/signed_execution_payload_envelope.rs diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 148a4f8fcd..1bb1d91a42 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -1295,6 +1295,13 @@ impl BeaconChain { Ok(self.store.get_blinded_block(block_root)?) } + pub fn get_payload( + &self, + block_root: &Hash256, + ) -> Result>, Error> { + Ok(self.store.get_payload_envelope(block_root)?) + } + /// Return the status of a block as it progresses through the various caches of the beacon /// chain. Used by sync to learn the status of a block and prevent repeated downloads / /// processing attempts. diff --git a/beacon_node/beacon_chain/src/migrate.rs b/beacon_node/beacon_chain/src/migrate.rs index bd232f2e8a..f014842be7 100644 --- a/beacon_node/beacon_chain/src/migrate.rs +++ b/beacon_node/beacon_chain/src/migrate.rs @@ -635,9 +635,10 @@ impl, Cold: ItemStore> BackgroundMigrator = HashSet::new(); + let mut payloads_to_prune: HashSet = HashSet::new(); let mut states_to_prune: HashSet<(Slot, Hash256)> = HashSet::new(); let mut kept_summaries_for_hdiff = vec![]; @@ -728,6 +729,7 @@ impl, Cold: ItemStore> BackgroundMigrator, Cold: ItemStore> BackgroundMigrator, Cold: ItemStore> BackgroundMigrator { block_cache: LruCache>, blob_cache: LruCache>, data_column_cache: LruCache>>>, + payload_envelope_cache: LruCache>, data_column_custody_info_cache: Option, } @@ -102,6 +103,7 @@ impl BlockCache { block_cache: LruCache::new(size), blob_cache: LruCache::new(size), data_column_cache: LruCache::new(size), + payload_envelope_cache: LruCache::new(size), data_column_custody_info_cache: None, } } @@ -116,6 +118,14 @@ impl BlockCache { .get_or_insert_mut(block_root, Default::default) .insert(*data_column.index(), data_column); } + pub fn put_payload_envelope( + &mut self, + block_root: Hash256, + payload_envelope: SignedExecutionPayloadEnvelope, + ) { + self.payload_envelope_cache + .put(block_root, payload_envelope); + } pub fn put_data_column_custody_info( &mut self, data_column_custody_info: Option, @@ -139,6 +149,12 @@ impl BlockCache { .get(block_root) .and_then(|map| map.get(column_index).cloned()) } + pub fn get_payload_envelope<'a>( + &'a mut self, + block_root: &Hash256, + ) -> Option<&'a SignedExecutionPayloadEnvelope> { + self.payload_envelope_cache.get(block_root) + } pub fn get_data_column_custody_info(&self) -> Option { self.data_column_custody_info_cache.clone() } @@ -151,10 +167,14 @@ impl BlockCache { pub fn delete_data_columns(&mut self, block_root: &Hash256) { let _ = self.data_column_cache.pop(block_root); } + pub fn delete_payload_envelope(&mut self, block_root: &Hash256) { + let _ = self.payload_envelope_cache.pop(block_root); + } pub fn delete(&mut self, block_root: &Hash256) { self.delete_block(block_root); self.delete_blobs(block_root); self.delete_data_columns(block_root); + self.delete_payload_envelope(block_root); } } @@ -508,6 +528,10 @@ impl, Cold: ItemStore> HotColdDB &metrics::STORE_BEACON_BLOB_CACHE_SIZE, cache.blob_cache.len() as i64, ); + metrics::set_gauge( + &metrics::STORE_BEACON_PAYLOAD_ENVELOPE_CACHE_SIZE, + cache.payload_envelope_cache.len() as i64, + ); } let state_cache = self.state_cache.lock(); metrics::set_gauge( @@ -745,6 +769,57 @@ impl, Cold: ItemStore> HotColdDB .map_err(|e| e.into()) } + pub fn get_payload_envelope( + &self, + block_root: &Hash256, + ) -> Result>, Error> { + // Check the cache. + if let Some(envelope) = self + .block_cache + .as_ref() + .and_then(|cache| cache.lock().get_payload_envelope(block_root).cloned()) + { + metrics::inc_counter(&metrics::BEACON_PAYLOAD_ENVELOPE_CACHE_HIT_COUNT); + return Ok(Some(envelope)); + } + + let key = block_root.as_slice(); + + match self + .hot_db + .get_bytes(SignedExecutionPayloadEnvelope::::db_column(), key)? + { + Some(bytes) => { + let envelope = SignedExecutionPayloadEnvelope::from_ssz_bytes(&bytes)?; + self.block_cache.as_ref().inspect(|cache| { + cache + .lock() + .put_payload_envelope(*block_root, envelope.clone()) + }); + Ok(Some(envelope)) + } + None => Ok(None), + } + } + + /// Check if the payload envelope for a block exists on disk or in cache. + pub fn payload_envelope_exists(&self, block_root: &Hash256) -> Result { + // Check the cache first. + if self + .block_cache + .as_ref() + .and_then(|cache| cache.lock().get_payload_envelope(block_root).cloned()) + .is_some() + { + return Ok(true); + } + + self.hot_db.key_exists( + SignedExecutionPayloadEnvelope::::db_column(), + block_root.as_slice(), + ) + } + /// Load the execution payload for a block from disk. /// This method deserializes with the proper fork. pub fn get_execution_payload( @@ -1027,6 +1102,39 @@ impl, Cold: ItemStore> HotColdDB } } + // TODO(gloas) we should store the execution payload separately like we do for blocks. + /// Prepare a signed execution payload envelope for storage in the database. + pub fn payload_envelope_as_kv_store_ops( + &self, + key: &Hash256, + payload: &SignedExecutionPayloadEnvelope, + ops: &mut Vec, + ) { + ops.push(KeyValueStoreOp::PutKeyValue( + SignedExecutionPayloadEnvelope::::db_column(), + key.as_slice().into(), + payload.as_ssz_bytes(), + )); + } + + pub fn put_payload_envelope( + &self, + block_root: &Hash256, + payload_envelope: SignedExecutionPayloadEnvelope, + ) -> Result<(), Error> { + self.hot_db.put_bytes( + SignedExecutionPayloadEnvelope::::db_column(), + block_root.as_slice(), + &payload_envelope.as_ssz_bytes(), + )?; + self.block_cache.as_ref().inspect(|cache| { + cache + .lock() + .put_payload_envelope(*block_root, payload_envelope) + }); + Ok(()) + } + /// Store a state in the store. pub fn put_state(&self, state_root: &Hash256, state: &BeaconState) -> Result<(), Error> { let mut ops: Vec = Vec::new(); @@ -1283,6 +1391,14 @@ impl, Cold: ItemStore> HotColdDB ); } + StoreOp::PutPayloadEnvelope(block_root, payload_envelope) => { + self.payload_envelope_as_kv_store_ops( + &block_root, + &payload_envelope, + &mut key_value_batch, + ); + } + StoreOp::PutStateSummary(state_root, summary) => { key_value_batch.push(summary.as_kv_store_op(state_root)); } @@ -1309,6 +1425,13 @@ impl, Cold: ItemStore> HotColdDB } } + StoreOp::DeletePayloadEnvelope(block_root) => { + key_value_batch.push(KeyValueStoreOp::DeleteKey( + SignedExecutionPayloadEnvelope::::db_column(), + block_root.as_slice().to_vec(), + )) + } + StoreOp::DeleteState(state_root, slot) => { // Delete the hot state summary. key_value_batch.push(KeyValueStoreOp::DeleteKey( @@ -1528,6 +1651,10 @@ impl, Cold: ItemStore> HotColdDB StoreOp::PutDataColumns(_, _) => (), + StoreOp::PutPayloadEnvelope(block_root, payload_envelope) => { + guard.put_payload_envelope(block_root, (*payload_envelope).clone()); + } + StoreOp::PutState(_, _) => (), StoreOp::PutStateSummary(_, _) => (), @@ -1536,6 +1663,10 @@ impl, Cold: ItemStore> HotColdDB guard.delete_block(&block_root); } + StoreOp::DeletePayloadEnvelope(block_root) => { + guard.delete_payload_envelope(&block_root); + } + StoreOp::DeleteState(_, _) => (), StoreOp::DeleteBlobs(_) => (), diff --git a/beacon_node/store/src/impls.rs b/beacon_node/store/src/impls.rs index 691c79ace7..a2b2f3b2d6 100644 --- a/beacon_node/store/src/impls.rs +++ b/beacon_node/store/src/impls.rs @@ -1 +1,2 @@ pub mod execution_payload; +mod signed_execution_payload_envelope; diff --git a/beacon_node/store/src/impls/signed_execution_payload_envelope.rs b/beacon_node/store/src/impls/signed_execution_payload_envelope.rs new file mode 100644 index 0000000000..3faab4b7d5 --- /dev/null +++ b/beacon_node/store/src/impls/signed_execution_payload_envelope.rs @@ -0,0 +1,18 @@ +use ssz::{Decode, Encode}; +use types::{EthSpec, SignedExecutionPayloadEnvelope}; + +use crate::{DBColumn, Error, StoreItem}; + +impl StoreItem for SignedExecutionPayloadEnvelope { + fn db_column() -> DBColumn { + DBColumn::PayloadEnvelope + } + + fn as_store_bytes(&self) -> Vec { + self.as_ssz_bytes() + } + + fn from_store_bytes(bytes: &[u8]) -> Result { + Ok(Self::from_ssz_bytes(bytes)?) + } +} diff --git a/beacon_node/store/src/lib.rs b/beacon_node/store/src/lib.rs index 83ca43ebaa..ee40d3acbd 100644 --- a/beacon_node/store/src/lib.rs +++ b/beacon_node/store/src/lib.rs @@ -234,12 +234,14 @@ pub enum StoreOp<'a, E: EthSpec> { PutState(Hash256, &'a BeaconState), PutBlobs(Hash256, BlobSidecarList), PutDataColumns(Hash256, DataColumnSidecarList), + PutPayloadEnvelope(Hash256, Arc>), PutStateSummary(Hash256, HotStateSummary), DeleteBlock(Hash256), DeleteBlobs(Hash256), DeleteDataColumns(Hash256, Vec, ForkName), DeleteState(Hash256, Option), DeleteExecutionPayload(Hash256), + DeletePayloadEnvelope(Hash256), DeleteSyncCommitteeBranch(Hash256), KeyValueOp(KeyValueStoreOp), } @@ -307,9 +309,14 @@ pub enum DBColumn { /// non-temporary by the deletion of their state root from this column. #[strum(serialize = "bst")] BeaconStateTemporary, - /// Execution payloads for blocks more recent than the finalized checkpoint. + /// Pre-gloas execution payloads for blocks more recent than the finalized checkpoint. #[strum(serialize = "exp")] ExecPayload, + // TODO(gloas) once finalized envelope pruning is implemented this comment should be updated + // "Post-gloas execution payload envlopes for payloads more recent than the finalized checkpoint" + /// Post-gloas execution payload envelopes. + #[strum(serialize = "pay")] + PayloadEnvelope, /// For persisting in-memory state to the database. #[strum(serialize = "bch")] BeaconChain, @@ -421,7 +428,8 @@ impl DBColumn { | Self::BeaconRestorePoint | Self::DhtEnrs | Self::CustodyContext - | Self::OptimisticTransitionBlock => 32, + | Self::OptimisticTransitionBlock + | Self::PayloadEnvelope => 32, Self::BeaconBlockRoots | Self::BeaconDataColumnCustodyInfo | Self::BeaconBlockRootsChunked diff --git a/beacon_node/store/src/metrics.rs b/beacon_node/store/src/metrics.rs index 93c9840586..59fd583a46 100644 --- a/beacon_node/store/src/metrics.rs +++ b/beacon_node/store/src/metrics.rs @@ -251,6 +251,13 @@ pub static BEACON_BLOBS_CACHE_HIT_COUNT: LazyLock> = LazyLock "Number of hits to the store's blob cache", ) }); +pub static BEACON_PAYLOAD_ENVELOPE_CACHE_HIT_COUNT: LazyLock> = + LazyLock::new(|| { + try_create_int_counter( + "store_beacon_payload_envelope_cache_hit_total", + "Number of hits to the store's payload envelope cache", + ) + }); pub static STORE_BEACON_BLOCK_CACHE_SIZE: LazyLock> = LazyLock::new(|| { try_create_int_gauge( "store_beacon_block_cache_size", @@ -263,6 +270,13 @@ pub static STORE_BEACON_BLOB_CACHE_SIZE: LazyLock> = LazyLock:: "Current count of items in beacon store blob cache", ) }); +pub static STORE_BEACON_PAYLOAD_ENVELOPE_CACHE_SIZE: LazyLock> = + LazyLock::new(|| { + try_create_int_gauge( + "store_beacon_payload_envelope_cache_size", + "Current count of items in beacon store payload envelope cache", + ) + }); pub static STORE_BEACON_STATE_CACHE_SIZE: LazyLock> = LazyLock::new(|| { try_create_int_gauge( "store_beacon_state_cache_size", From c26ad962bfd9c07279902aad85b761e5def35c8e Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Wed, 28 Jan 2026 18:30:46 -0800 Subject: [PATCH 004/118] small fixes --- beacon_node/beacon_chain/src/beacon_chain.rs | 2 +- beacon_node/beacon_chain/src/migrate.rs | 3 --- beacon_node/store/src/lib.rs | 4 +--- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 1bb1d91a42..20a50ad7e9 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -1295,7 +1295,7 @@ impl BeaconChain { Ok(self.store.get_blinded_block(block_root)?) } - pub fn get_payload( + pub fn get_payload_envelope( &self, block_root: &Hash256, ) -> Result>, Error> { diff --git a/beacon_node/beacon_chain/src/migrate.rs b/beacon_node/beacon_chain/src/migrate.rs index f014842be7..cb0ee2ede3 100644 --- a/beacon_node/beacon_chain/src/migrate.rs +++ b/beacon_node/beacon_chain/src/migrate.rs @@ -638,7 +638,6 @@ impl, Cold: ItemStore> BackgroundMigrator = HashSet::new(); - let mut payloads_to_prune: HashSet = HashSet::new(); let mut states_to_prune: HashSet<(Slot, Hash256)> = HashSet::new(); let mut kept_summaries_for_hdiff = vec![]; @@ -729,7 +728,6 @@ impl, Cold: ItemStore> BackgroundMigrator, Cold: ItemStore> BackgroundMigrator Date: Wed, 28 Jan 2026 18:31:47 -0800 Subject: [PATCH 005/118] Fix --- beacon_node/beacon_chain/src/migrate.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beacon_node/beacon_chain/src/migrate.rs b/beacon_node/beacon_chain/src/migrate.rs index cb0ee2ede3..24258d2d31 100644 --- a/beacon_node/beacon_chain/src/migrate.rs +++ b/beacon_node/beacon_chain/src/migrate.rs @@ -635,7 +635,7 @@ impl, Cold: ItemStore> BackgroundMigrator = HashSet::new(); let mut states_to_prune: HashSet<(Slot, Hash256)> = HashSet::new(); From bb9bfafa4cb4e162688f50480a05b882ea39a65f Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Wed, 28 Jan 2026 20:33:04 -0800 Subject: [PATCH 006/118] fix --- beacon_node/beacon_chain/tests/schema_stability.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/beacon_node/beacon_chain/tests/schema_stability.rs b/beacon_node/beacon_chain/tests/schema_stability.rs index db7f7dbdbb..3dc009366d 100644 --- a/beacon_node/beacon_chain/tests/schema_stability.rs +++ b/beacon_node/beacon_chain/tests/schema_stability.rs @@ -106,8 +106,8 @@ fn check_db_columns() { let current_columns: Vec<&'static str> = DBColumn::iter().map(|c| c.as_str()).collect(); let expected_columns = vec![ "bma", "blk", "blb", "bdc", "bdi", "ste", "hsd", "hsn", "bsn", "bsd", "bss", "bs3", "bcs", - "bst", "exp", "bch", "opo", "etc", "frk", "pkc", "brp", "bsx", "bsr", "bbx", "bbr", "bhr", - "brm", "dht", "cus", "otb", "bhs", "olc", "lcu", "scb", "scm", "dmy", + "bst", "exp", "pay", "bch", "opo", "etc", "frk", "pkc", "brp", "bsx", "bsr", "bbx", "bbr", + "bhr", "brm", "dht", "cus", "otb", "bhs", "olc", "lcu", "scb", "scm", "dmy", ]; assert_eq!(expected_columns, current_columns); } From 6ea966846c6873a15af07eb2b6f747634ac01e91 Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Thu, 29 Jan 2026 11:02:55 -0800 Subject: [PATCH 007/118] Some test fixes --- .../src/data_availability_checker.rs | 2 +- .../src/data_availability_checker_v2.rs | 2 +- .../overflow_lru_cache.rs | 161 ++++++------------ .../state_lru_cache.rs | 2 +- beacon_node/beacon_chain/src/test_utils.rs | 16 ++ common/eth2/src/types.rs | 5 + .../types/src/data/data_column_sidecar.rs | 35 ++-- 7 files changed, 91 insertions(+), 132 deletions(-) diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index e0d165f0b5..cc9d165887 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -397,7 +397,7 @@ impl DataColumnCache for DataAvailabilityChecker { self.availability_cache .peek_pending_components(block_root, |components| { components.is_some_and(|components| { - let cached_column_opt = components.get_cached_data_column(data_column.index()); + let cached_column_opt = components.get_cached_data_column(*data_column.index()); cached_column_opt.is_some_and(|cached| *cached == *data_column) }) }) diff --git a/beacon_node/beacon_chain/src/data_availability_checker_v2.rs b/beacon_node/beacon_chain/src/data_availability_checker_v2.rs index 6f892f9d2f..112c9074dc 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker_v2.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker_v2.rs @@ -147,7 +147,7 @@ impl DataColumnCache for DataAvailabilityChecker { self.availability_cache .peek_pending_components(block_root, |components| { components.is_some_and(|components| { - let cached_column_opt = components.get_cached_data_column(data_column.index()); + let cached_column_opt = components.get_cached_data_column(*data_column.index()); cached_column_opt.is_some_and(|cached| *cached == *data_column) }) }) diff --git a/beacon_node/beacon_chain/src/data_availability_checker_v2/overflow_lru_cache.rs b/beacon_node/beacon_chain/src/data_availability_checker_v2/overflow_lru_cache.rs index ec65bf007b..4c6ec7cf93 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker_v2/overflow_lru_cache.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker_v2/overflow_lru_cache.rs @@ -644,7 +644,7 @@ mod test { use crate::test_utils::generate_data_column_indices_rand_order; use crate::{ block_verification::PayloadVerificationOutcome, - block_verification_types::{AsBlock, BlockImportData}, + block_verification_types::AsBlock, custody_context::NodeCustodyType, data_availability_checker_v2::STATE_LRU_CAPACITY_NON_ZERO, test_utils::{BaseHarnessType, BeaconChainHarness, DiskHarnessType}, @@ -657,7 +657,7 @@ mod test { use tempfile::{TempDir, tempdir}; use tracing::{debug_span, info}; use types::new_non_zero_usize; - use types::{ExecPayload, MinimalEthSpec}; + use types::MinimalEthSpec; const LOW_VALIDATOR_COUNT: usize = 32; const STATE_LRU_CAPACITY: usize = STATE_LRU_CAPACITY_NON_ZERO.get(); @@ -719,11 +719,7 @@ mod test { assert!(gloas_head.as_gloas().is_ok()); assert_eq!(gloas_head.slot(), gloas_fork_slot); assert!( - gloas_head - .message() - .body() - .execution_payload() - .is_err(), + gloas_head.message().body().execution_payload().is_err(), "Gloas block has no payload" ); harness @@ -740,77 +736,7 @@ mod test { Hot: ItemStore, Cold: ItemStore, { - let chain = &harness.chain; - let head = chain.head_snapshot(); - let parent_state = head.beacon_state.clone(); - - let target_slot = chain.slot().expect("should get slot") + 1; - let parent_root = head.beacon_block_root; - let parent_block = chain - .get_payload(&parent_root) - .expect("should get block") - .expect("should have block"); - - - let (signed_beacon_block_hash, (block, maybe_blobs), state) = harness - .add_block_at_slot(target_slot, parent_state) - .await - .expect("should add block"); - let block_root = signed_beacon_block_hash.into(); - assert_eq!( - block_root, - block.canonical_root(), - "block root should match" - ); - - // log kzg commitments - info!("printing kzg commitments"); - for comm in Vec::from( - block - .message() - .body() - .blob_kzg_commitments() - .expect("should be deneb fork") - .clone(), - ) { - info!(commitment = ?comm, "kzg commitment"); - } - info!("done printing kzg commitments"); - - let gossip_verified_columns = if let Some((kzg_proofs, blobs)) = maybe_blobs { - let sidecars = - DataColumnSidecar::build_sidecars(blobs, &block, &chain.kzg, &chain.spec).unwrap(); - Vec::from(sidecars) - .into_iter() - .map(|sidecar| { - let subnet = *sidecar.index(); - GossipVerifiedDataColumn::new(sidecar, subnet.into(), &harness.chain) - .expect("should validate column") - }) - .collect() - } else { - vec![] - }; - - let slot = block.slot(); - let consensus_context: ConsensusContext = ConsensusContext::::new(slot); - let import_data: PayloadImportData = PayloadImportData { - state, - consensus_context, - }; - - let payload_verification_outcome = PayloadVerificationOutcome { - payload_verification_status: PayloadVerificationStatus::Verified, - is_valid_merge_transition_block: false, - }; - - let availability_pending_block = AvailabilityPendingExecutedPayload { - payload, - import_data, - payload_verification_outcome, - }; - - (availability_pending_block, gossip_verified_columns) + todo!() } async fn setup_harness_and_cache( @@ -861,7 +787,13 @@ mod test { let (pending_payload, columns) = availability_pending_payload(&harness).await; let root = pending_payload.as_payload().beacon_block_root(); - let expected_column_indices = harness.chain.data_availability_checker.custody_context().custody_columns_for_epoch(None, &harness.chain.spec).iter().collect::>(); + let expected_column_indices = harness + .chain + .data_availability_checker + .custody_context() + .custody_columns_for_epoch(None, &harness.chain.spec) + .iter() + .collect::>(); let columns_expected = pending_payload.num_blobs_expected(); assert_eq!( @@ -918,7 +850,13 @@ mod test { } let (pending_payload, columns) = availability_pending_payload(&harness).await; - let expected_column_indices = harness.chain.data_availability_checker.custody_context().custody_columns_for_epoch(None, &harness.chain.spec).iter().collect::>(); + let expected_column_indices = harness + .chain + .data_availability_checker + .custody_context() + .custody_columns_for_epoch(None, &harness.chain.spec) + .iter() + .collect::>(); let columns_expected = pending_payload.num_blobs_expected(); assert_eq!( columns.len(), @@ -966,28 +904,28 @@ mod test { let capacity = STATE_LRU_CAPACITY * 2; let (harness, cache, _path) = setup_harness_and_cache::(capacity).await; - let mut pending_blocks = VecDeque::new(); + let mut pending_payloads = VecDeque::new(); let mut states = Vec::new(); let mut state_roots = Vec::new(); // Get enough blocks to fill the cache to capacity, ensuring all blocks have blobs - while pending_blocks.len() < capacity { - let (mut pending_block, _) = availability_pending_block(&harness).await; - if pending_block.num_blobs_expected() == 0 { + while pending_payloads.len() < capacity { + let (mut pending_payload, _) = availability_pending_payload(&harness).await; + if pending_payload.num_blobs_expected() == 0 { // we need blocks with blobs continue; } - let state_root = pending_block.import_data.state.canonical_root().unwrap(); - states.push(pending_block.import_data.state.clone()); - pending_blocks.push_back(pending_block); + let state_root = pending_payload.import_data.state.canonical_root().unwrap(); + states.push(pending_payload.import_data.state.clone()); + pending_payloads.push_back(pending_payload); state_roots.push(state_root); } let state_cache = cache.state_lru_cache().lru_cache(); - let mut pushed_diet_blocks = VecDeque::new(); + let mut pushed_diet_payloads = VecDeque::new(); for i in 0..capacity { - let pending_block = pending_blocks.pop_front().expect("should have block"); - let block_root = pending_block.as_block().canonical_root(); + let pending_payload = pending_payloads.pop_front().expect("should have payload"); + let block_root = pending_payload.as_payload().beacon_block_root(); assert_eq!( state_cache.read().len(), @@ -1000,23 +938,23 @@ mod test { assert_eq!( state_cache.read().peek_lru().map(|(root, _)| root), Some(&lru_root), - "lru block should be in cache" + "lru payload should be in cache" ); } // put the block in the cache let availability = cache - .put_executed_block(pending_block) - .expect("should put block"); + .put_executed_payload(pending_payload) + .expect("should put payload"); // grab the diet block from the cache for later testing - let diet_block = cache + let diet_payload = cache .critical .read() .peek(&block_root) - .and_then(|pending_components| pending_components.get_diet_block().cloned()) + .and_then(|pending_components| pending_components.get_diet_payload().cloned()) .expect("should exist"); - pushed_diet_blocks.push_back(diet_block); + pushed_diet_payloads.push_back(diet_payload); // should be unavailable since we made sure all blocks had blobs assert!( @@ -1032,41 +970,41 @@ mod test { "lru root should be evicted" ); // get the diet block via direct conversion (testing only) - let diet_block = pushed_diet_blocks.pop_front().expect("should have block"); + let diet_payload = pushed_diet_payloads.pop_front().expect("should have payload"); // reconstruct the pending block by replaying the block on the parent state - let recovered_pending_block = cache + let recovered_pending_payload = cache .state_lru_cache() - .recover_pending_executed_block(diet_block, &debug_span!("test")) + .recover_pending_executed_payload(diet_payload, &debug_span!("test")) .expect("should reconstruct pending block"); // assert the recovered state is the same as the original assert_eq!( - recovered_pending_block.import_data.state, states[evicted_index], + recovered_pending_payload.import_data.state, states[evicted_index], "recovered state should be the same as the original" ); } } - // now check the last block - let last_block = pushed_diet_blocks.pop_back().expect("should exist").clone(); + // now check the last payload + let last_payload = pushed_diet_payloads.pop_back().expect("should exist").clone(); // the state should still be in the cache assert!( state_cache .read() - .peek(&last_block.as_block().state_root()) + .peek(&last_payload.as_payload().message.state_root) .is_some(), - "last block state should still be in cache" + "last payload state should still be in cache" ); - // get the diet block via direct conversion (testing only) - let diet_block = last_block.clone(); - // recover the pending block from the cache - let recovered_pending_block = cache + // get the diet payload via direct conversion (testing only) + let diet_payload = last_payload.clone(); + // recover the pending payload from the cache + let recovered_pending_payload = cache .state_lru_cache() - .recover_pending_executed_block(diet_block, &debug_span!("test")) - .expect("should reconstruct pending block"); + .recover_pending_executed_payload(diet_payload, &debug_span!("test")) + .expect("should reconstruct pending payload"); // assert the recovered state is the same as the original assert_eq!( - Some(&recovered_pending_block.import_data.state), + Some(&recovered_pending_payload.import_data.state), states.last(), "recovered state should be the same as the original" ); @@ -1084,6 +1022,7 @@ mod pending_components_tests { use kzg::KzgCommitment; use rand::SeedableRng; use rand::rngs::StdRng; + use ssz_types::RuntimeFixedVector; use state_processing::ConsensusContext; use types::test_utils::TestRandom; use types::{BeaconState, ForkName, MainnetEthSpec, SignedBeaconBlock, Slot}; diff --git a/beacon_node/beacon_chain/src/data_availability_checker_v2/state_lru_cache.rs b/beacon_node/beacon_chain/src/data_availability_checker_v2/state_lru_cache.rs index 02dc094177..623a33b020 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker_v2/state_lru_cache.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker_v2/state_lru_cache.rs @@ -1,4 +1,4 @@ -#![allow(dead_code)] +#![allow(dead_code)] use crate::payload_verification_types::{AvailabilityPendingExecutedPayload, PayloadImportData}; use crate::{ BeaconChainTypes, BeaconStore, PayloadVerificationOutcome, diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index 41abee5ed7..931dd17e7a 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -2690,6 +2690,22 @@ where self.chain.slot_clock.set_slot(slot.into()); } + pub async fn add_payload_envelope_at_slot( + &self, + slot: Slot, + state: BeaconState, + ) -> Result< + ( + SignedBeaconBlockHash, + SignedExecutionPayloaContentsTuple, + BeaconState, + ), + BlockError, + > { + self.set_current_slot(slot); + let (block_contents, new_state) = self.make_block(state, slot).await; + } + pub async fn add_block_at_slot( &self, slot: Slot, diff --git a/common/eth2/src/types.rs b/common/eth2/src/types.rs index c1572ca354..f02a4f736d 100644 --- a/common/eth2/src/types.rs +++ b/common/eth2/src/types.rs @@ -1852,6 +1852,11 @@ pub type SignedBlockContentsTuple = ( Option<(KzgProofs, BlobsList)>, ); +pub type SignedPayloadEnvelopeContentsTuple = ( + Arc>, + Option<(KzgProofs, BlobsList)>, +); + fn parse_required_header( headers: &HeaderMap, header_name: &str, diff --git a/consensus/types/src/data/data_column_sidecar.rs b/consensus/types/src/data/data_column_sidecar.rs index 44f3f6ffd0..1fc14aab01 100644 --- a/consensus/types/src/data/data_column_sidecar.rs +++ b/consensus/types/src/data/data_column_sidecar.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use bls::Signature; use context_deserialize::context_deserialize; use educe::Educe; -use kzg::{CellsAndKzgProofs, Kzg, KzgCommitment, KzgProof, BYTES_PER_BLOB}; +use kzg::{BYTES_PER_BLOB, CellsAndKzgProofs, Kzg, KzgCommitment, KzgProof}; use merkle_proof::verify_merkle_proof; use safe_arith::ArithError; use serde::{Deserialize, Serialize}; @@ -17,7 +17,9 @@ use tree_hash::TreeHash; use tree_hash_derive::TreeHash; use crate::{ - block::{BLOB_KZG_COMMITMENTS_INDEX, BeaconBlockHeader, SignedBeaconBlock, SignedBeaconBlockHeader}, + block::{ + BLOB_KZG_COMMITMENTS_INDEX, BeaconBlockHeader, SignedBeaconBlock, SignedBeaconBlockHeader, + }, core::{ChainSpec, Epoch, EthSpec, Hash256, Slot}, data::BlobsList, fork::ForkName, @@ -162,16 +164,13 @@ impl DataColumnSidecar { let blob_cells_and_proofs: Vec = blobs .iter() .map(|blob| { - let blob_bytes: &[u8; BYTES_PER_BLOB] = - blob.as_ref().try_into().map_err(|_| { - DataColumnSidecarError::KzgError(KzgError::InconsistentArrayLength( - format!( - "blob should have size {}, got {}", - BYTES_PER_BLOB, - blob.len() - ), - )) - })?; + let blob_bytes: &[u8; BYTES_PER_BLOB] = blob.as_ref().try_into().map_err(|_| { + DataColumnSidecarError::KzgError(KzgError::InconsistentArrayLength(format!( + "blob should have size {}, got {}", + BYTES_PER_BLOB, + blob.len() + ))) + })?; kzg.compute_cells_and_proofs(blob_bytes) .map_err(DataColumnSidecarError::KzgError) }) @@ -190,15 +189,15 @@ impl DataColumnSidecar { // Arrange cells and proofs into columns for (blob_cells, blob_cell_proofs) in &blob_cells_and_proofs { for col_idx in 0..number_of_columns { - let cell = blob_cells.get(col_idx).ok_or_else(|| { - DataColumnSidecarError::DataColumnIndexOutOfBounds - })?; + let cell = blob_cells + .get(col_idx) + .ok_or_else(|| DataColumnSidecarError::DataColumnIndexOutOfBounds)?; let cell_vec: Vec = cell.to_vec(); let cell = Cell::::try_from(cell_vec)?; - let proof = blob_cell_proofs.get(col_idx).ok_or_else(|| { - DataColumnSidecarError::DataColumnIndexOutOfBounds - })?; + let proof = blob_cell_proofs + .get(col_idx) + .ok_or_else(|| DataColumnSidecarError::DataColumnIndexOutOfBounds)?; columns .get_mut(col_idx) From d122561444872df184960cdc0903551a0e63218f Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Thu, 29 Jan 2026 12:37:36 -0800 Subject: [PATCH 008/118] Test fixes --- .../overflow_lru_cache.rs | 471 ++++++++++-------- .../state_lru_cache.rs | 19 +- .../src/data_availability_router.rs | 8 +- beacon_node/beacon_chain/src/test_utils.rs | 57 ++- .../tests/attestation_production.rs | 2 + .../beacon_chain/tests/block_verification.rs | 33 +- .../tests/payload_invalidation.rs | 8 +- beacon_node/beacon_chain/tests/store_tests.rs | 7 +- beacon_node/http_api/src/publish_blocks.rs | 2 +- .../lighthouse_network/tests/rpc_tests.rs | 4 +- .../src/network_beacon_processor/tests.rs | 4 +- .../src/sync/block_sidecar_coupling.rs | 1 + .../network/src/sync/network_context.rs | 4 +- beacon_node/network/src/sync/tests/lookups.rs | 2 +- beacon_node/network/src/sync/tests/range.rs | 6 +- .../types/src/data/data_column_sidecar.rs | 4 +- testing/ef_tests/src/cases/fork_choice.rs | 4 +- 17 files changed, 365 insertions(+), 271 deletions(-) diff --git a/beacon_node/beacon_chain/src/data_availability_checker_v2/overflow_lru_cache.rs b/beacon_node/beacon_chain/src/data_availability_checker_v2/overflow_lru_cache.rs index 4c6ec7cf93..3e5aa3749e 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker_v2/overflow_lru_cache.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker_v2/overflow_lru_cache.rs @@ -27,6 +27,7 @@ pub enum CachedPayload { Executed(Box>), } +#[allow(dead_code)] impl CachedPayload { pub fn get_commitments(&self) -> KzgCommitments { let payload = self.as_payload(); @@ -49,6 +50,7 @@ impl CachedPayload { /// /// The columns are all gossip and kzg verified. /// The payload has completed all verifications except the availability check. +#[allow(dead_code)] pub struct PendingComponents { pub block_root: Hash256, pub block: Option>>, @@ -123,7 +125,7 @@ impl PendingComponents { } /// Returns Some if the payload has received all its required data for import. The return value - /// must be persisted in the DB along with the block. + /// must be persisted in the DB along with the payload. /// /// WARNING: This function can potentially take a lot of time if the state needs to be /// reconstructed from disk. Ensure you are not holding any write locks while calling this. @@ -157,7 +159,7 @@ impl PendingComponents { ))); } Ordering::Equal => { - // Block is post-peerdas, and we got enough columns + // We have enough columns let data_columns = self .verified_data_columns .iter() @@ -179,9 +181,9 @@ impl PendingComponents { let Some(block) = self.block.clone() else { // This should never happen - return Err(AvailabilityCheckError::Unexpected(format!( - "Payload is being made available but no block exists" - ))); + return Err(AvailabilityCheckError::Unexpected( + "Block doesn't exist for the payload being made available".to_owned(), + )); }; // Payload is available, construct `AvailableExecutedPayload` @@ -235,7 +237,7 @@ impl PendingComponents { /// - The payload if it is cached /// Otherwise, returns None pub fn epoch(&self) -> Option { - // Get epoch from cached block + // Get epoch from cached payload if let Some(payload) = &self.payload { return Some( payload @@ -365,7 +367,7 @@ impl DataAvailabilityCheckerInner { // No columns are processed. This can occur if all received columns were filtered out // before this point, e.g. due to a CGC change that caused extra columns to be downloaded // // before the new CGC took effect. - // Return `Ok` without marking the block as available. + // Return `Ok` without marking the payload as available. return Ok(Availability::MissingComponents(block_root)); }; @@ -408,7 +410,7 @@ impl DataAvailabilityCheckerInner { // Explicitly drop read lock before acquiring write lock drop(pending_components); if let Some(components) = self.critical.write().get_mut(&block_root) { - // Clean up span now that block is available + // Clean up span now that payload is available components.span = Span::none(); } @@ -458,7 +460,7 @@ impl DataAvailabilityCheckerInner { /// Potentially trigger reconstruction if all the following satisfy: /// - Our custody requirement is more than 50% of total columns, /// - We haven't received all required columns - /// - Reconstruction hasn't been started for the block + /// - Reconstruction hasn't been started for the payload /// /// If reconstruction is required, returns `PendingComponents` which contains the /// components to be used as inputs to reconstruction, otherwise returns a `reason`. @@ -468,8 +470,8 @@ impl DataAvailabilityCheckerInner { ) -> ReconstructColumnsDecision { let mut write_lock = self.critical.write(); let Some(pending_components) = write_lock.get_mut(block_root) else { - // Block may have been imported as it does not exist in availability cache. - return ReconstructColumnsDecision::No("block already imported"); + // Payload may have been imported as it does not exist in availability cache. + return ReconstructColumnsDecision::No("payload already imported"); }; let Some(epoch) = pending_components @@ -542,7 +544,7 @@ impl DataAvailabilityCheckerInner { /// Removes a pre-execution payload from the cache. /// This does NOT remove an existing executed payload. pub fn remove_pre_execution_payload(&self, block_root: &Hash256) { - // The read lock is immediately dropped so we can safely remove the block from the cache. + // The read lock is immediately dropped so we can safely remove the payload from the cache. if let Some(PayloadProcessStatus::NotValidated(_, _)) = self.get_cached_payload(block_root) { self.critical.write().pop(block_root); @@ -562,7 +564,7 @@ impl DataAvailabilityCheckerInner { .epoch(T::EthSpec::slots_per_epoch()); let block_root = executed_payload.payload.message.beacon_block_root; - // register the payload to get the diet block + // register the payload to get the diet payload let diet_executed_payload = self .state_cache .register_pending_executed_payload(executed_payload); @@ -639,25 +641,21 @@ impl DataAvailabilityCheckerInner { mod test { use super::*; - use crate::data_column_verification::GossipVerifiedDataColumn; - use crate::payload_verification_types::PayloadImportData; + use crate::data_column_verification::{GossipVerifiedDataColumn, KzgVerifiedCustodyDataColumn}; use crate::test_utils::generate_data_column_indices_rand_order; use crate::{ - block_verification::PayloadVerificationOutcome, block_verification_types::AsBlock, custody_context::NodeCustodyType, data_availability_checker_v2::STATE_LRU_CAPACITY_NON_ZERO, test_utils::{BaseHarnessType, BeaconChainHarness, DiskHarnessType}, }; - use fork_choice::PayloadVerificationStatus; use logging::create_test_tracing_subscriber; - use state_processing::ConsensusContext; use std::collections::{HashSet, VecDeque}; use store::{HotColdDB, ItemStore, StoreConfig, database::interface::BeaconNodeBackend}; use tempfile::{TempDir, tempdir}; - use tracing::{debug_span, info}; - use types::new_non_zero_usize; + use tracing::debug_span; use types::MinimalEthSpec; + use types::new_non_zero_usize; const LOW_VALIDATOR_COUNT: usize = 32; const STATE_LRU_CAPACITY: usize = STATE_LRU_CAPACITY_NON_ZERO.get(); @@ -726,7 +724,7 @@ mod test { } async fn availability_pending_payload( - harness: &BeaconChainHarness>, + _harness: &BeaconChainHarness>, ) -> ( AvailabilityPendingExecutedPayload, Vec>>, @@ -787,7 +785,7 @@ mod test { let (pending_payload, columns) = availability_pending_payload(&harness).await; let root = pending_payload.as_payload().beacon_block_root(); - let expected_column_indices = harness + let mut expected_column_indices = harness .chain .data_availability_checker .custody_context() @@ -804,7 +802,7 @@ mod test { assert!(cache.critical.read().is_empty(), "cache should be empty"); let availability = cache .put_executed_payload(pending_payload) - .expect("should put block"); + .expect("should put payload"); if columns_expected == 0 { assert!( matches!(availability, Availability::Available(_)), @@ -834,12 +832,15 @@ mod test { let mut kzg_verified_columns = Vec::new(); for gossip_column in columns.into_iter() { - kzg_verified_columns.push(gossip_column.into_inner()); + let col_index = gossip_column.index(); + kzg_verified_columns.push(KzgVerifiedCustodyDataColumn::from_asserted_custody( + gossip_column.into_inner(), + )); let availability = cache .put_kzg_verified_data_columns(root, kzg_verified_columns.clone().into_iter()) .expect("should put column"); - expected_column_indices.remove(&gossip_column.index()); + expected_column_indices.remove(&col_index); if expected_column_indices.is_empty() { assert!(matches!(availability, Availability::Available(_))); @@ -857,17 +858,19 @@ mod test { .custody_columns_for_epoch(None, &harness.chain.spec) .iter() .collect::>(); - let columns_expected = pending_payload.num_blobs_expected(); + assert_eq!( columns.len(), expected_column_indices.len(), - "should have expected number of blobs" + "should have expected number of columns" ); let root = pending_payload.as_payload().beacon_block_root(); let mut kzg_verified_columns = vec![]; for gossip_column in columns { - kzg_verified_columns.push(gossip_column.into_inner()); + kzg_verified_columns.push(KzgVerifiedCustodyDataColumn::from_asserted_custody( + gossip_column.into_inner(), + )); let availability = cache .put_kzg_verified_data_columns(root, kzg_verified_columns.clone()) .expect("should put column"); @@ -907,11 +910,11 @@ mod test { let mut pending_payloads = VecDeque::new(); let mut states = Vec::new(); let mut state_roots = Vec::new(); - // Get enough blocks to fill the cache to capacity, ensuring all blocks have blobs + // Get enough payload to fill the cache to capacity, ensuring all payloads have blobs while pending_payloads.len() < capacity { let (mut pending_payload, _) = availability_pending_payload(&harness).await; if pending_payload.num_blobs_expected() == 0 { - // we need blocks with blobs + // we need payloads with blobs continue; } let state_root = pending_payload.import_data.state.canonical_root().unwrap(); @@ -942,12 +945,12 @@ mod test { ); } - // put the block in the cache + // put the payload in the cache let availability = cache .put_executed_payload(pending_payload) .expect("should put payload"); - // grab the diet block from the cache for later testing + // grab the diet payload from the cache for later testing let diet_payload = cache .critical .read() @@ -956,7 +959,7 @@ mod test { .expect("should exist"); pushed_diet_payloads.push_back(diet_payload); - // should be unavailable since we made sure all blocks had blobs + // should be unavailable since we made sure all payloads had blobs assert!( matches!(availability, Availability::MissingComponents(_)), "should be pending blobs" @@ -969,13 +972,15 @@ mod test { state_cache.read().peek(&evicted_root).is_none(), "lru root should be evicted" ); - // get the diet block via direct conversion (testing only) - let diet_payload = pushed_diet_payloads.pop_front().expect("should have payload"); - // reconstruct the pending block by replaying the block on the parent state + // get the diet payload via direct conversion (testing only) + let diet_payload = pushed_diet_payloads + .pop_front() + .expect("should have payload"); + // reconstruct the pending payload by replaying the payload on the parent state let recovered_pending_payload = cache .state_lru_cache() .recover_pending_executed_payload(diet_payload, &debug_span!("test")) - .expect("should reconstruct pending block"); + .expect("should reconstruct pending payload"); // assert the recovered state is the same as the original assert_eq!( @@ -986,7 +991,10 @@ mod test { } // now check the last payload - let last_payload = pushed_diet_payloads.pop_back().expect("should exist").clone(); + let last_payload = pushed_diet_payloads + .pop_back() + .expect("should exist") + .clone(); // the state should still be in the cache assert!( state_cache @@ -1015,239 +1023,294 @@ mod test { mod pending_components_tests { use super::*; use crate::PayloadVerificationOutcome; - use crate::payload_verification_types::PayloadImportData; - use crate::test_utils::{NumBlobs, generate_rand_block_and_blobs, test_spec}; - use fixed_bytes::FixedBytesExtended; + use crate::data_column_verification::KzgVerifiedDataColumn; + use crate::test_utils::{NumBlobs, generate_data_column_sidecars_from_payload, test_spec}; use fork_choice::PayloadVerificationStatus; use kzg::KzgCommitment; - use rand::SeedableRng; use rand::rngs::StdRng; - use ssz_types::RuntimeFixedVector; - use state_processing::ConsensusContext; + use rand::{Rng, SeedableRng}; + use ssz_types::VariableList; use types::test_utils::TestRandom; - use types::{BeaconState, ForkName, MainnetEthSpec, SignedBeaconBlock, Slot}; + use types::{ForkName, MainnetEthSpec, SignedExecutionPayloadEnvelope}; type E = MainnetEthSpec; - type Setup = ( - SignedBeaconBlock, - RuntimeFixedVector>>>, - RuntimeFixedVector>>>, - usize, + type Setup = ( + SignedExecutionPayloadEnvelope, + DataColumnSidecarList, + DataColumnSidecarList, ); - pub fn pre_setup() -> Setup { + fn pre_setup() -> Setup { let mut rng = StdRng::seed_from_u64(0xDEADBEEF0BAD5EEDu64); let spec = test_spec::(); - let (block, blobs_vec) = - generate_rand_block_and_blobs::(ForkName::Deneb, NumBlobs::Random, &mut rng); - let max_len = spec.max_blobs_per_block(block.epoch()) as usize; - let mut blobs: RuntimeFixedVector>>> = - RuntimeFixedVector::default(max_len); + // Ensure spec supports gloas so build_data_column_sidecars_gloas succeeds + let spec = ForkName::Gloas.make_genesis_spec(spec); - for blob in blobs_vec { - if let Some(b) = blobs.get_mut(blob.index as usize) { - *b = Some(Arc::new(blob)); - } + let mut payload = SignedExecutionPayloadEnvelope::::random_for_test(&mut rng); + // Generate blob transactions to populate blob_kzg_commitments + let num_blobs = match NumBlobs::Random { + NumBlobs::Random => rng.random_range(1..=6usize), + NumBlobs::Number(n) => n, + NumBlobs::None => 0, + }; + let (bundle, transactions) = + execution_layer::test_utils::generate_blobs::(num_blobs, ForkName::Gloas).unwrap(); + payload.message.payload.transactions = <_>::default(); + for tx in Vec::from(transactions) { + payload.message.payload.transactions.push(tx).unwrap(); } + payload.message.blob_kzg_commitments = bundle.commitments.clone(); - let mut invalid_blobs: RuntimeFixedVector>>> = - RuntimeFixedVector::default(max_len); - for (index, blob) in blobs.iter().enumerate() { - if let Some(invalid_blob) = blob { - let mut blob_copy = invalid_blob.as_ref().clone(); - blob_copy.kzg_commitment = KzgCommitment::random_for_test(&mut rng); - *invalid_blobs.get_mut(index).unwrap() = Some(Arc::new(blob_copy)); - } - } + let columns = generate_data_column_sidecars_from_payload(&payload, &spec); - (block, blobs, invalid_blobs, max_len) + // Create invalid columns by mutating kzg_commitments + let invalid_columns: DataColumnSidecarList = columns + .iter() + .map(|col| { + let mut col_clone = col.as_ref().clone(); + // Mutate commitments to make them invalid + let mut commitments: Vec<_> = col_clone.kzg_commitments().iter().copied().collect(); + for commitment in commitments.iter_mut() { + *commitment = KzgCommitment::random_for_test(&mut rng); + } + let new_commitments = + VariableList::try_from(commitments).expect("commitments within bounds"); + match &mut col_clone { + DataColumnSidecar::Gloas(gloas) => { + gloas.kzg_commitments = new_commitments; + } + DataColumnSidecar::Fulu(fulu) => { + fulu.kzg_commitments = new_commitments; + } + } + Arc::new(col_clone) + }) + .collect(); + + (payload, columns, invalid_columns) } - type PendingComponentsSetup = ( - DietAvailabilityPendingExecutedBlock, - RuntimeFixedVector>>, - RuntimeFixedVector>>, + type PendingComponentsSetup = ( + DietAvailabilityPendingExecutedPayload, + Vec>, + Vec>, ); - pub fn setup_pending_components( + fn setup_pending_components( payload: SignedExecutionPayloadEnvelope, - valid_columns: RuntimeFixedVector>>>, - invalid_columns: RuntimeFixedVector>>>, - ) -> PendingComponentsSetup { - let columns = RuntimeFixedVector::new( - valid_columns - .iter() - .map(|column_opt| { - column_opt - .as_ref() - .map(|column| KzgVerifiedDataColumn::__assumed_valid(column.clone())) - }) - .collect::>(), - ); - let invalid_columns = RuntimeFixedVector::new( - invalid_columns - .iter() - .map(|column_opt| { - column_opt - .as_ref() - .map(|column| KzgVerifiedDataColumn::__assumed_valid(column.clone())) - }) - .collect::>(), - ); - let block = AvailabilityPendingExecutedBlock { - payload: Arc::new(payload), - import_data: PayloadImportData { - state: BeaconState::new(0, Default::default(), &ChainSpec::minimal()), - consensus_context: ConsensusContext::new(Slot::new(0)), - }, - payload_verification_outcome: PayloadVerificationOutcome { + valid_columns: DataColumnSidecarList, + invalid_columns: DataColumnSidecarList, + ) -> PendingComponentsSetup { + let columns: Vec> = valid_columns + .into_iter() + .map(|col| { + KzgVerifiedCustodyDataColumn::from_asserted_custody( + KzgVerifiedDataColumn::__new_for_testing(col), + ) + }) + .collect(); + + let invalid_columns: Vec> = invalid_columns + .into_iter() + .map(|col| { + KzgVerifiedCustodyDataColumn::from_asserted_custody( + KzgVerifiedDataColumn::__new_for_testing(col), + ) + }) + .collect(); + + let diet_payload = DietAvailabilityPendingExecutedPayload::new_for_testing( + Arc::new(payload), + PayloadVerificationOutcome { payload_verification_status: PayloadVerificationStatus::Verified, is_valid_merge_transition_block: false, }, - }; - (payload, columns, invalid_blobs) + ); + + (diet_payload, columns, invalid_columns) } - pub fn assert_cache_consistent(cache: PendingComponents, max_len: usize) { - if let Some(cached_payload) = &cache.payload { - let cached_payload_commitments = cached_payload.get_commitments(); - for index in 0..max_len { - let payload_commitment = cached_payload_commitments.get(index).copied(); - let column_commitment_opt = cache.get_cached_data_column().get(index).unwrap(); - let column_commitment = column_commitment_opt.as_ref().map(|c| *c.get_commitment()); - assert_eq!(payload_commitment, column_commitment); - } - } else { - panic!("No cached payload") + fn assert_cache_consistent(cache: &PendingComponents) { + let cached_payload = cache + .payload + .as_ref() + .expect("expected cached payload to be present"); + let payload_commitments = cached_payload.get_commitments(); + // Each column should have commitments matching the payload + for col in &cache.verified_data_columns { + let col_commitments = col.as_data_column().kzg_commitments(); + assert_eq!( + payload_commitments, + *col_commitments, + "column {} commitments should match payload commitments", + col.index() + ); } } - pub fn assert_empty_column_cache(cache: PendingComponents) { - for column_indices in cache.get_cached_data_columns_indices().iter() { - panic!("assert_empty_column_cache failed"); - } + #[allow(dead_code)] + fn assert_empty_column_cache(cache: &PendingComponents) { + assert!( + cache.verified_data_columns.is_empty(), + "expected empty column cache but found {} columns", + cache.verified_data_columns.len() + ); + } + + // v2 merge_data_columns deduplicates by index (first-in wins). When invalid columns + // are merged first, they persist even after valid columns are merged at the same indices. + + #[test] + fn payload_invalid_columns_valid_columns() { + let (payload, columns, invalid_columns) = pre_setup(); + let (diet_payload, columns, invalid_columns) = + setup_pending_components(payload, columns, invalid_columns); + let block_root = Hash256::ZERO; + let mut cache = >::empty(block_root); + cache.merge_payload(diet_payload); + cache + .merge_data_columns(invalid_columns) + .expect("merge should succeed"); + cache + .merge_data_columns(columns) + .expect("merge should succeed"); + + // Invalid columns were inserted first, valid columns are deduplicated away. + // The cache has columns but they have invalid commitments (not matching payload). + assert!(!cache.verified_data_columns.is_empty()); } #[test] - fn valid_block_invalid_blobs_valid_blobs() { - let (block_commitments, blobs, random_blobs, max_len) = pre_setup(); - let (block_commitments, blobs, random_blobs) = - setup_pending_components(block_commitments, blobs, random_blobs); - let block_root = Hash256::zero(); - let mut cache = >::empty(block_root, max_len); - cache.merge_block(block_commitments); - cache.merge_blobs(random_blobs); - cache.merge_blobs(blobs); + fn invalid_columns_payload_valid_columns() { + let (payload, columns, invalid_columns) = pre_setup(); + let (diet_payload, columns, invalid_columns) = + setup_pending_components(payload, columns, invalid_columns); + let block_root = Hash256::ZERO; + let mut cache = >::empty(block_root); + cache + .merge_data_columns(invalid_columns) + .expect("merge should succeed"); + cache.merge_payload(diet_payload); + cache + .merge_data_columns(columns) + .expect("merge should succeed"); - assert_cache_consistent(cache, max_len); + // Invalid columns were first, valid ones deduplicated away. + assert!(!cache.verified_data_columns.is_empty()); } #[test] - fn invalid_blobs_block_valid_blobs() { - let (block_commitments, blobs, random_blobs, max_len) = pre_setup(); - let (block_commitments, blobs, random_blobs) = - setup_pending_components(block_commitments, blobs, random_blobs); - let block_root = Hash256::zero(); - let mut cache = >::empty(block_root, max_len); - cache.merge_blobs(random_blobs); - cache.merge_block(block_commitments); - cache.merge_blobs(blobs); + fn invalid_columns_valid_columns_payload() { + let (payload, columns, invalid_columns) = pre_setup(); + let (diet_payload, columns, invalid_columns) = + setup_pending_components(payload, columns, invalid_columns); - assert_cache_consistent(cache, max_len); + let block_root = Hash256::ZERO; + let mut cache = >::empty(block_root); + cache + .merge_data_columns(invalid_columns) + .expect("merge should succeed"); + cache + .merge_data_columns(columns) + .expect("merge should succeed"); + cache.merge_payload(diet_payload); + + // Invalid columns were first, valid ones deduplicated away. + assert!(!cache.verified_data_columns.is_empty()); } #[test] - fn invalid_blobs_valid_blobs_block() { - let (block_commitments, blobs, random_blobs, max_len) = pre_setup(); - let (block_commitments, blobs, random_blobs) = - setup_pending_components(block_commitments, blobs, random_blobs); + fn payload_valid_columns_invalid_columns() { + let (payload, columns, invalid_columns) = pre_setup(); + let (diet_payload, columns, invalid_columns) = + setup_pending_components(payload, columns, invalid_columns); - let block_root = Hash256::zero(); - let mut cache = >::empty(block_root, max_len); - cache.merge_blobs(random_blobs); - cache.merge_blobs(blobs); - cache.merge_block(block_commitments); + let block_root = Hash256::ZERO; + let mut cache = >::empty(block_root); + cache.merge_payload(diet_payload); + cache + .merge_data_columns(columns) + .expect("merge should succeed"); + cache + .merge_data_columns(invalid_columns) + .expect("merge should succeed"); - assert_empty_blob_cache(cache); + // Valid columns were inserted first, so they persist. Cache should be consistent. + assert_cache_consistent(&cache); } #[test] - fn block_valid_blobs_invalid_blobs() { - let (block_commitments, blobs, random_blobs, max_len) = pre_setup(); - let (block_commitments, blobs, random_blobs) = - setup_pending_components(block_commitments, blobs, random_blobs); + fn valid_columns_payload_invalid_columns() { + let (payload, columns, invalid_columns) = pre_setup(); + let (diet_payload, columns, invalid_columns) = + setup_pending_components(payload, columns, invalid_columns); - let block_root = Hash256::zero(); - let mut cache = >::empty(block_root, max_len); - cache.merge_block(block_commitments); - cache.merge_blobs(blobs); - cache.merge_blobs(random_blobs); + let block_root = Hash256::ZERO; + let mut cache = >::empty(block_root); + cache + .merge_data_columns(columns) + .expect("merge should succeed"); + cache.merge_payload(diet_payload); + cache + .merge_data_columns(invalid_columns) + .expect("merge should succeed"); - assert_cache_consistent(cache, max_len); + // Valid columns were inserted first, so they persist. Cache should be consistent. + assert_cache_consistent(&cache); } #[test] - fn valid_blobs_block_invalid_blobs() { - let (block_commitments, blobs, random_blobs, max_len) = pre_setup(); - let (block_commitments, blobs, random_blobs) = - setup_pending_components(block_commitments, blobs, random_blobs); + fn valid_columns_invalid_columns_payload() { + let (payload, columns, invalid_columns) = pre_setup(); + let (diet_payload, columns, invalid_columns) = + setup_pending_components(payload, columns, invalid_columns); - let block_root = Hash256::zero(); - let mut cache = >::empty(block_root, max_len); - cache.merge_blobs(blobs); - cache.merge_block(block_commitments); - cache.merge_blobs(random_blobs); + let block_root = Hash256::ZERO; + let mut cache = >::empty(block_root); + cache + .merge_data_columns(columns) + .expect("merge should succeed"); + cache + .merge_data_columns(invalid_columns) + .expect("merge should succeed"); + cache.merge_payload(diet_payload); - assert_cache_consistent(cache, max_len); + // Valid columns were inserted first, so they persist. Cache should be consistent. + assert_cache_consistent(&cache); } #[test] - fn valid_blobs_invalid_blobs_block() { - let (block_commitments, blobs, random_blobs, max_len) = pre_setup(); - let (block_commitments, blobs, random_blobs) = - setup_pending_components(block_commitments, blobs, random_blobs); + fn should_not_insert_pre_execution_payload_if_executed_payload_exists() { + let (payload, _columns, _invalid_columns) = pre_setup(); + let (diet_payload, _columns, _invalid_columns) = + setup_pending_components(payload.clone(), _columns, _invalid_columns); - let block_root = Hash256::zero(); - let mut cache = >::empty(block_root, max_len); - cache.merge_blobs(blobs); - cache.merge_blobs(random_blobs); - cache.merge_block(block_commitments); + let block_root = Hash256::ZERO; + let mut pending_component = >::empty(block_root); - assert_cache_consistent(cache, max_len); - } - - #[test] - fn should_not_insert_pre_execution_block_if_executed_block_exists() { - let (pre_execution_block, blobs, random_blobs, max_len) = pre_setup(); - let (executed_block, _blobs, _random_blobs) = - setup_pending_components(pre_execution_block.clone(), blobs, random_blobs); - - let block_root = pre_execution_block.canonical_root(); - let mut pending_component = >::empty(block_root, max_len); - - let pre_execution_block = Arc::new(pre_execution_block); + let pre_execution_payload = Arc::new(payload); pending_component - .insert_pre_execution_block(pre_execution_block.clone(), BlockImportSource::Gossip); + .insert_pre_execution_payload(pre_execution_payload.clone(), BlockImportSource::Gossip); assert!( matches!( - pending_component.block, - Some(CachedBlock::PreExecution(_, _)) + pending_component.payload, + Some(CachedPayload::PreExecution(_, _)) ), - "pre execution block inserted" + "pre execution payload inserted" ); - pending_component.insert_executed_block(executed_block); + pending_component.insert_executed_payload(diet_payload); assert!( - matches!(pending_component.block, Some(CachedBlock::Executed(_))), - "executed block inserted" + matches!(pending_component.payload, Some(CachedPayload::Executed(_))), + "executed payload inserted" ); pending_component - .insert_pre_execution_block(pre_execution_block, BlockImportSource::Gossip); + .insert_pre_execution_payload(pre_execution_payload, BlockImportSource::Gossip); assert!( - matches!(pending_component.block, Some(CachedBlock::Executed(_))), - "executed block should remain" + matches!(pending_component.payload, Some(CachedPayload::Executed(_))), + "executed payload should remain" ); } } diff --git a/beacon_node/beacon_chain/src/data_availability_checker_v2/state_lru_cache.rs b/beacon_node/beacon_chain/src/data_availability_checker_v2/state_lru_cache.rs index 623a33b020..4d1e02990c 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker_v2/state_lru_cache.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker_v2/state_lru_cache.rs @@ -24,6 +24,23 @@ pub struct DietAvailabilityPendingExecutedPayload { /// Implementing the same methods as `AvailabilityPendingExecutedPayload` impl DietAvailabilityPendingExecutedPayload { + #[cfg(test)] + pub fn new_for_testing( + payload: Arc>, + payload_verification_outcome: PayloadVerificationOutcome, + ) -> Self { + use state_processing::ConsensusContext; + use types::Slot; + Self { + payload, + state_root: Hash256::ZERO, + consensus_context: OnDiskConsensusContext::from_consensus_context( + ConsensusContext::new(Slot::new(0)), + ), + payload_verification_outcome, + } + } + pub fn as_payload(&self) -> &SignedExecutionPayloadEnvelope { &self.payload } @@ -115,7 +132,7 @@ impl StateLRUCache { #[instrument(skip_all, level = "debug")] fn reconstruct_state( &self, - diet_executed_block: &DietAvailabilityPendingExecutedPayload, + _diet_executed_block: &DietAvailabilityPendingExecutedPayload, ) -> Result, AvailabilityCheckError> { todo!() } diff --git a/beacon_node/beacon_chain/src/data_availability_router.rs b/beacon_node/beacon_chain/src/data_availability_router.rs index e8f7afe229..847c25ce76 100644 --- a/beacon_node/beacon_chain/src/data_availability_router.rs +++ b/beacon_node/beacon_chain/src/data_availability_router.rs @@ -374,15 +374,15 @@ where /// /// Use this for operations that are specific to the legacy block-based DA checker, /// such as `put_executed_block`, `get_cached_block`, blob operations, etc. - pub fn v1(&self) -> &V1 { - &self.v1 + pub fn v1(&self) -> Arc { + self.v1.clone() } /// Direct access to v2 checker (for payload-specific operations). /// /// Use this for operations that are specific to the Gloas payload-based DA checker, /// such as `put_executed_payload`, `get_cached_payload`, etc. - pub fn v2(&self) -> &V2 { - &self.v2 + pub fn v2(&self) -> Arc { + self.v2.clone() } } diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index 931dd17e7a..315c1a2c6a 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -24,7 +24,7 @@ use bls::get_withdrawal_credentials; use bls::{ AggregateSignature, Keypair, PublicKey, PublicKeyBytes, SecretKey, Signature, SignatureBytes, }; -use eth2::types::{GraffitiPolicy, SignedBlockContentsTuple}; +use eth2::types::{GraffitiPolicy, SignedBlockContentsTuple, SignedPayloadEnvelopeContentsTuple}; use execution_layer::test_utils::generate_genesis_header; use execution_layer::{ ExecutionLayer, @@ -2483,7 +2483,7 @@ where return RpcBlock::new( block, Some(AvailableBlockData::NoData), - &self.chain.data_availability_checker, + &self.chain.data_availability_checker.v1(), self.chain.spec.clone(), ) .unwrap(); @@ -2502,7 +2502,7 @@ where RpcBlock::new( block, Some(block_data), - &self.chain.data_availability_checker, + &self.chain.data_availability_checker.v1(), self.chain.spec.clone(), ) .unwrap() @@ -2517,7 +2517,7 @@ where RpcBlock::new( block, Some(block_data), - &self.chain.data_availability_checker, + &self.chain.data_availability_checker.v1(), self.chain.spec.clone(), ) .unwrap() @@ -2548,14 +2548,14 @@ where RpcBlock::new( block, Some(block_data), - &self.chain.data_availability_checker, + &self.chain.data_availability_checker.v1(), self.chain.spec.clone(), )? } else { RpcBlock::new( block, None, - &self.chain.data_availability_checker, + &self.chain.data_availability_checker.v1(), self.chain.spec.clone(), )? } @@ -2563,14 +2563,14 @@ where RpcBlock::new( block, Some(AvailableBlockData::NoData), - &self.chain.data_availability_checker, + &self.chain.data_availability_checker.v1(), self.chain.spec.clone(), )? } else { RpcBlock::new( block, None, - &self.chain.data_availability_checker, + &self.chain.data_availability_checker.v1(), self.chain.spec.clone(), )? } @@ -2591,14 +2591,14 @@ where RpcBlock::new( block, Some(block_data), - &self.chain.data_availability_checker, + &self.chain.data_availability_checker.v1(), self.chain.spec.clone(), )? } else { RpcBlock::new( block, None, - &self.chain.data_availability_checker, + &self.chain.data_availability_checker.v1(), self.chain.spec.clone(), )? } @@ -2690,20 +2690,23 @@ where self.chain.slot_clock.set_slot(slot.into()); } + // TODO(gloas) this is a stub implementation for now + // we need payload processing functionality for this function + // to work pub async fn add_payload_envelope_at_slot( &self, slot: Slot, - state: BeaconState, + _state: BeaconState, ) -> Result< ( SignedBeaconBlockHash, - SignedExecutionPayloaContentsTuple, + SignedPayloadEnvelopeContentsTuple, BeaconState, ), BlockError, > { self.set_current_slot(slot); - let (block_contents, new_state) = self.make_block(state, slot).await; + todo!() } pub async fn add_block_at_slot( @@ -3437,16 +3440,15 @@ pub fn generate_rand_payloads_and_columns( fork_name: ForkName, num_blobs: NumBlobs, rng: &mut impl Rng, -) -> (SignedExecutionPayloadEnvelope, Vec>) { + spec: &ChainSpec, +) -> (SignedExecutionPayloadEnvelope, DataColumnSidecarList) { let mut payload = SignedExecutionPayloadEnvelope::random_for_test(rng); - let mut data_column_sidecars = vec![]; + let _bundle = add_blob_transactions_gloas!(payload.message, num_blobs, rng, fork_name); - let bundle = add_blob_transactions_gloas!(payload.message, num_blobs, rng, fork_name); + let data_columns = generate_data_column_sidecars_from_payload(&payload, spec); - let data_columns = generate_data_column_sidecars_from_block(&block, spec); - - todo!() + (payload, data_columns) } pub fn generate_rand_block_and_blobs( @@ -3604,18 +3606,18 @@ pub fn generate_data_column_sidecars_from_block( } } -/// Generate data column sidecars from pre-computed cells and proofs for gloas paylaods. +/// Generate data column sidecars from pre-computed cells and proofs for gloas payloads. pub fn generate_data_column_sidecars_from_payload( payload: &SignedExecutionPayloadEnvelope, spec: &ChainSpec, ) -> DataColumnSidecarList { - let kzg_commitments = payload.message.blob_kzg_commitments; + let kzg_commitments = payload.message.blob_kzg_commitments.clone(); if kzg_commitments.is_empty() { return vec![]; } // load the precomputed column sidecar to avoid computing them for every block in the tests. - let template_data_columns = RuntimeVariableList::>::from_ssz_bytes( + let template_data_columns = RuntimeVariableList::>::from_ssz_bytes( TEST_DATA_COLUMN_SIDECARS_SSZ, E::number_of_columns(), ) @@ -3624,7 +3626,7 @@ pub fn generate_data_column_sidecars_from_payload( let (cells, proofs) = template_data_columns .into_iter() .map(|sidecar| { - let DataColumnSidecar { + let DataColumnSidecarGloas { column, kzg_proofs, .. } = sidecar; // There's only one cell per column for a single blob @@ -3639,7 +3641,14 @@ pub fn generate_data_column_sidecars_from_payload( let blob_cells_and_proofs_vec = vec![(cells.try_into().unwrap(), proofs.try_into().unwrap()); kzg_commitments.len()]; - build_data_column_sidecars(kzg_commitments.clone(), blob_cells_and_proofs_vec, spec).unwrap() + build_data_column_sidecars_gloas( + kzg_commitments, + payload.message.beacon_block_root, + payload.message.slot, + blob_cells_and_proofs_vec, + spec, + ) + .unwrap() } pub fn generate_data_column_indices_rand_order() -> Vec { diff --git a/beacon_node/beacon_chain/tests/attestation_production.rs b/beacon_node/beacon_chain/tests/attestation_production.rs index a1922f32a4..5fb2d0897d 100644 --- a/beacon_node/beacon_chain/tests/attestation_production.rs +++ b/beacon_node/beacon_chain/tests/attestation_production.rs @@ -230,6 +230,7 @@ async fn produces_attestations() { RpcBlock::FullyAvailable(available_block) => { chain .data_availability_checker + .v1() .verify_kzg_for_available_block(&available_block) .unwrap(); available_block @@ -300,6 +301,7 @@ async fn early_attester_cache_old_request() { harness .chain .data_availability_checker + .v1() .verify_kzg_for_available_block(&available_block) .unwrap(); available_block diff --git a/beacon_node/beacon_chain/tests/block_verification.rs b/beacon_node/beacon_chain/tests/block_verification.rs index 440c0be3e4..f1bc135445 100644 --- a/beacon_node/beacon_chain/tests/block_verification.rs +++ b/beacon_node/beacon_chain/tests/block_verification.rs @@ -165,7 +165,7 @@ where RpcBlock::new( block, Some(block_data), - &chain.data_availability_checker, + &chain.data_availability_checker.v1(), chain.spec.clone(), ) .unwrap() @@ -180,7 +180,7 @@ where RpcBlock::new( block, Some(block_data), - &chain.data_availability_checker, + &chain.data_availability_checker.v1(), chain.spec.clone(), ) .unwrap() @@ -188,7 +188,7 @@ where None => RpcBlock::new( block, Some(AvailableBlockData::NoData), - &chain.data_availability_checker, + &chain.data_availability_checker.v1(), chain.spec.clone(), ) .unwrap(), @@ -417,7 +417,7 @@ async fn chain_segment_non_linear_parent_roots() { blocks[3] = RpcBlock::new( Arc::new(SignedBeaconBlock::from_block(block, signature)), blocks[3].block_data().cloned(), - &harness.chain.data_availability_checker, + &harness.chain.data_availability_checker.v1(), harness.spec.clone(), ) .unwrap(); @@ -457,7 +457,7 @@ async fn chain_segment_non_linear_slots() { blocks[3] = RpcBlock::new( Arc::new(SignedBeaconBlock::from_block(block, signature)), blocks[3].block_data().cloned(), - &harness.chain.data_availability_checker, + &harness.chain.data_availability_checker.v1(), harness.spec.clone(), ) .unwrap(); @@ -487,7 +487,7 @@ async fn chain_segment_non_linear_slots() { blocks[3] = RpcBlock::new( Arc::new(SignedBeaconBlock::from_block(block, signature)), blocks[3].block_data().cloned(), - &harness.chain.data_availability_checker, + &harness.chain.data_availability_checker.v1(), harness.chain.spec.clone(), ) .unwrap(); @@ -634,7 +634,7 @@ async fn invalid_signature_gossip_block() { let rpc_block = RpcBlock::new( Arc::new(signed_block), None, - &harness.chain.data_availability_checker, + &harness.chain.data_availability_checker.v1(), harness.spec.clone(), ) .unwrap(); @@ -1645,7 +1645,7 @@ async fn add_base_block_to_altair_chain() { let base_rpc_block = RpcBlock::new( Arc::new(base_block.clone()), None, - &harness.chain.data_availability_checker, + &harness.chain.data_availability_checker.v1(), harness.spec.clone(), ) .unwrap(); @@ -1676,7 +1676,7 @@ async fn add_base_block_to_altair_chain() { RpcBlock::new( Arc::new(base_block), None, - &harness.chain.data_availability_checker, + &harness.chain.data_availability_checker.v1(), harness.spec.clone() ) .unwrap() @@ -1796,7 +1796,7 @@ async fn add_altair_block_to_base_chain() { let altair_rpc_block = RpcBlock::new( Arc::new(altair_block.clone()), None, - &harness.chain.data_availability_checker, + &harness.chain.data_availability_checker.v1(), harness.spec.clone(), ) .unwrap(); @@ -1827,7 +1827,7 @@ async fn add_altair_block_to_base_chain() { RpcBlock::new( Arc::new(altair_block), None, - &harness.chain.data_availability_checker, + &harness.chain.data_availability_checker.v1(), harness.spec.clone() ) .unwrap() @@ -1897,7 +1897,7 @@ async fn import_duplicate_block_unrealized_justification() { let rpc_block = RpcBlock::new( block.clone(), Some(AvailableBlockData::NoData), - &harness.chain.data_availability_checker, + &harness.chain.data_availability_checker.v1(), harness.spec.clone(), ) .unwrap(); @@ -2000,7 +2000,7 @@ async fn signature_verify_mixed_rpc_block_variants() { RpcBlock::new( block, None, - &harness.chain.data_availability_checker, + &harness.chain.data_availability_checker.v1(), harness.chain.spec.clone(), ) .unwrap() @@ -2070,7 +2070,7 @@ async fn rpc_block_construction_fails_with_wrong_blob_count() { let result = RpcBlock::new( Arc::new(block), Some(block_data), - &harness.chain.data_availability_checker, + &harness.chain.data_availability_checker.v1(), harness.chain.spec.clone(), ); @@ -2145,7 +2145,7 @@ async fn rpc_block_rejects_missing_custody_columns() { let result = RpcBlock::new( Arc::new(block), Some(block_data), - &harness.chain.data_availability_checker, + &harness.chain.data_availability_checker.v1(), harness.chain.spec.clone(), ); @@ -2219,6 +2219,7 @@ async fn rpc_block_allows_construction_past_da_boundary() { let da_boundary = harness .chain .data_availability_checker + .v1() .data_availability_boundary() .expect("DA boundary should be set"); assert!( @@ -2233,7 +2234,7 @@ async fn rpc_block_allows_construction_past_da_boundary() { let result = RpcBlock::new( Arc::new(block), Some(AvailableBlockData::NoData), - &harness.chain.data_availability_checker, + &harness.chain.data_availability_checker.v1(), harness.chain.spec.clone(), ); diff --git a/beacon_node/beacon_chain/tests/payload_invalidation.rs b/beacon_node/beacon_chain/tests/payload_invalidation.rs index 1204412d65..b02e832eae 100644 --- a/beacon_node/beacon_chain/tests/payload_invalidation.rs +++ b/beacon_node/beacon_chain/tests/payload_invalidation.rs @@ -688,7 +688,7 @@ async fn invalidates_all_descendants() { let fork_rpc_block = RpcBlock::new( fork_block.clone(), None, - &rig.harness.chain.data_availability_checker, + &rig.harness.chain.data_availability_checker.v1(), rig.harness.chain.spec.clone(), ) .unwrap(); @@ -796,7 +796,7 @@ async fn switches_heads() { let fork_rpc_block = RpcBlock::new( fork_block.clone(), None, - &rig.harness.chain.data_availability_checker, + &rig.harness.chain.data_availability_checker.v1(), rig.harness.chain.spec.clone(), ) .unwrap(); @@ -1074,7 +1074,7 @@ async fn invalid_parent() { let rpc_block = RpcBlock::new( block.clone(), None, - &rig.harness.chain.data_availability_checker, + &rig.harness.chain.data_availability_checker.v1(), rig.harness.chain.spec.clone(), ) .unwrap(); @@ -1405,7 +1405,7 @@ async fn recover_from_invalid_head_by_importing_blocks() { let fork_rpc_block = RpcBlock::new( fork_block.clone(), None, - &rig.harness.chain.data_availability_checker, + &rig.harness.chain.data_availability_checker.v1(), rig.harness.chain.spec.clone(), ) .unwrap(); diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index 14e9deb62a..3bc23283ac 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -3183,6 +3183,7 @@ async fn weak_subjectivity_sync_test( harness .chain .data_availability_checker + .v1() .verify_kzg_for_available_block(&available_block) .expect("should verify kzg"); available_blocks.push(available_block); @@ -3202,7 +3203,7 @@ async fn weak_subjectivity_sync_test( AvailableBlock::new( Arc::new(corrupt_block), data, - &beacon_chain.data_availability_checker, + &beacon_chain.data_availability_checker.v1(), Arc::new(spec), ) .expect("available block") @@ -3752,7 +3753,7 @@ async fn process_blocks_and_attestations_for_unaligned_checkpoint() { let invalid_fork_rpc_block = RpcBlock::new( invalid_fork_block.clone(), None, - &harness.chain.data_availability_checker, + &harness.chain.data_availability_checker.v1(), harness.spec.clone(), ) .unwrap(); @@ -3774,7 +3775,7 @@ async fn process_blocks_and_attestations_for_unaligned_checkpoint() { let valid_fork_rpc_block = RpcBlock::new( valid_fork_block.clone(), None, - &harness.chain.data_availability_checker, + &harness.chain.data_availability_checker.v1(), harness.spec.clone(), ) .unwrap(); diff --git a/beacon_node/http_api/src/publish_blocks.rs b/beacon_node/http_api/src/publish_blocks.rs index 1887dee640..bfa6e8c0cb 100644 --- a/beacon_node/http_api/src/publish_blocks.rs +++ b/beacon_node/http_api/src/publish_blocks.rs @@ -317,7 +317,7 @@ pub async fn publish_block>( let Ok(rpc_block) = RpcBlock::new( block.clone(), None, - &chain.data_availability_checker, + &chain.data_availability_checker.v1(), chain.spec.clone(), ) else { return Err(warp_utils::reject::custom_bad_request( diff --git a/beacon_node/lighthouse_network/tests/rpc_tests.rs b/beacon_node/lighthouse_network/tests/rpc_tests.rs index dc8c88b7ed..c569d7172b 100644 --- a/beacon_node/lighthouse_network/tests/rpc_tests.rs +++ b/beacon_node/lighthouse_network/tests/rpc_tests.rs @@ -993,7 +993,7 @@ fn test_tcp_columns_by_root_chunked_rpc_for_fork(fork_name: ForkName) { }; max_request_blocks ], - current_fork_name, + fork_name, max_request_blocks, ) .unwrap(); @@ -1004,7 +1004,7 @@ fn test_tcp_columns_by_root_chunked_rpc_for_fork(fork_name: ForkName) { spec.max_request_blocks(fork_name), ) .unwrap(), - fork_name: current_fork_name, + fork_name, }; assert_eq!(req, req_decoded); let rpc_request = RequestType::DataColumnsByRoot(req); diff --git a/beacon_node/network/src/network_beacon_processor/tests.rs b/beacon_node/network/src/network_beacon_processor/tests.rs index 49b1c0c262..05297015e3 100644 --- a/beacon_node/network/src/network_beacon_processor/tests.rs +++ b/beacon_node/network/src/network_beacon_processor/tests.rs @@ -404,7 +404,7 @@ impl TestRig { RpcBlock::new( self.next_block.clone(), None, - &self._harness.chain.data_availability_checker, + &self._harness.chain.data_availability_checker.v1(), self._harness.spec.clone(), ) .unwrap(), @@ -422,7 +422,7 @@ impl TestRig { RpcBlock::new( self.next_block.clone(), None, - &self._harness.chain.data_availability_checker, + &self._harness.chain.data_availability_checker.v1(), self._harness.spec.clone(), ) .unwrap(), diff --git a/beacon_node/network/src/sync/block_sidecar_coupling.rs b/beacon_node/network/src/sync/block_sidecar_coupling.rs index a287771854..9a86cb64fb 100644 --- a/beacon_node/network/src/sync/block_sidecar_coupling.rs +++ b/beacon_node/network/src/sync/block_sidecar_coupling.rs @@ -490,6 +490,7 @@ mod tests { use super::RangeBlockComponentsRequest; use beacon_chain::custody_context::NodeCustodyType; + use beacon_chain::data_availability_router::DataColumnCache; use beacon_chain::test_utils::{ NumBlobs, generate_rand_block_and_blobs, generate_rand_block_and_data_columns, test_da_checker, test_spec, diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index a99a0a7000..bdeff68855 100644 --- a/beacon_node/network/src/sync/network_context.rs +++ b/beacon_node/network/src/sync/network_context.rs @@ -777,7 +777,7 @@ impl SyncNetworkContext { let range_req = entry.get_mut(); if let Some(blocks_result) = range_req.responses( - self.chain.data_availability_checker.clone(), + self.chain.data_availability_checker.v1().clone(), self.chain.spec.clone(), ) { if let Err(CouplingError::DataColumnPeerFailure { @@ -1615,7 +1615,7 @@ impl SyncNetworkContext { let block = RpcBlock::new( block, None, - &self.chain.data_availability_checker, + &self.chain.data_availability_checker.v1(), self.chain.spec.clone(), ) .map_err(|_| SendErrorProcessor::SendError)?; diff --git a/beacon_node/network/src/sync/tests/lookups.rs b/beacon_node/network/src/sync/tests/lookups.rs index e2e038c10a..12fd2035da 100644 --- a/beacon_node/network/src/sync/tests/lookups.rs +++ b/beacon_node/network/src/sync/tests/lookups.rs @@ -2294,7 +2294,7 @@ mod deneb_only { let block = RpcBlock::new( block, None, - &self.rig.harness.chain.data_availability_checker, + &self.rig.harness.chain.data_availability_checker.v1(), self.rig.harness.chain.spec.clone(), ) .unwrap(); diff --git a/beacon_node/network/src/sync/tests/range.rs b/beacon_node/network/src/sync/tests/range.rs index 6f129bc8f0..86c3652742 100644 --- a/beacon_node/network/src/sync/tests/range.rs +++ b/beacon_node/network/src/sync/tests/range.rs @@ -454,7 +454,7 @@ fn build_rpc_block( RpcBlock::new( block, Some(block_data), - &chain.data_availability_checker, + &chain.data_availability_checker.v1(), chain.spec.clone(), ) .unwrap() @@ -469,7 +469,7 @@ fn build_rpc_block( RpcBlock::new( block, Some(block_data), - &chain.data_availability_checker, + &chain.data_availability_checker.v1(), chain.spec.clone(), ) .unwrap() @@ -478,7 +478,7 @@ fn build_rpc_block( None => RpcBlock::new( block, Some(AvailableBlockData::NoData), - &chain.data_availability_checker, + &chain.data_availability_checker.v1(), chain.spec.clone(), ) .unwrap(), diff --git a/consensus/types/src/data/data_column_sidecar.rs b/consensus/types/src/data/data_column_sidecar.rs index 1fc14aab01..14dc2e828f 100644 --- a/consensus/types/src/data/data_column_sidecar.rs +++ b/consensus/types/src/data/data_column_sidecar.rs @@ -191,13 +191,13 @@ impl DataColumnSidecar { for col_idx in 0..number_of_columns { let cell = blob_cells .get(col_idx) - .ok_or_else(|| DataColumnSidecarError::DataColumnIndexOutOfBounds)?; + .ok_or(DataColumnSidecarError::DataColumnIndexOutOfBounds)?; let cell_vec: Vec = cell.to_vec(); let cell = Cell::::try_from(cell_vec)?; let proof = blob_cell_proofs .get(col_idx) - .ok_or_else(|| DataColumnSidecarError::DataColumnIndexOutOfBounds)?; + .ok_or(DataColumnSidecarError::DataColumnIndexOutOfBounds)?; columns .get_mut(col_idx) diff --git a/testing/ef_tests/src/cases/fork_choice.rs b/testing/ef_tests/src/cases/fork_choice.rs index a3c2fab468..ae61206102 100644 --- a/testing/ef_tests/src/cases/fork_choice.rs +++ b/testing/ef_tests/src/cases/fork_choice.rs @@ -567,7 +567,7 @@ impl Tester { RpcBlock::new( block.clone(), None, - &self.harness.chain.data_availability_checker, + &self.harness.chain.data_availability_checker.v1(), self.harness.chain.spec.clone(), ) .map_err(|e| Error::InternalError(format!("{:?}", e)))?, @@ -665,7 +665,7 @@ impl Tester { RpcBlock::new( block.clone(), None, - &self.harness.chain.data_availability_checker, + &self.harness.chain.data_availability_checker.v1(), self.harness.chain.spec.clone(), ) .map_err(|e| Error::InternalError(format!("{:?}", e)))?, From 72f0a7b9c1990f9514408066aee5ec022b1a9d1c Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Thu, 29 Jan 2026 12:59:52 -0800 Subject: [PATCH 009/118] move commitments to bid --- beacon_node/lighthouse_network/src/types/pubsub.rs | 2 +- .../src/network_beacon_processor/gossip_methods.rs | 2 +- .../network/src/network_beacon_processor/mod.rs | 2 +- consensus/types/src/block/beacon_block_body.rs | 2 +- .../types/src/execution/execution_payload_bid.rs | 12 +++++++----- .../src/execution/execution_payload_envelope.rs | 4 +--- .../src/execution/signed_execution_payload_bid.rs | 12 +++++++----- consensus/types/src/state/beacon_state.rs | 2 +- 8 files changed, 20 insertions(+), 18 deletions(-) diff --git a/beacon_node/lighthouse_network/src/types/pubsub.rs b/beacon_node/lighthouse_network/src/types/pubsub.rs index d1df7face7..12567907f6 100644 --- a/beacon_node/lighthouse_network/src/types/pubsub.rs +++ b/beacon_node/lighthouse_network/src/types/pubsub.rs @@ -49,7 +49,7 @@ pub enum PubsubMessage { /// Gossipsub message providing notification of a payload attestation message. PayloadAttestation(Box), /// Gossipsub message providing notification of a signed execution payload bid. - ExecutionPayloadBid(Box), + ExecutionPayloadBid(Box>), /// Gossipsub message providing notification of signed proposer preferences. ProposerPreferences(Box), /// Gossipsub message providing notification of a light client finality update. 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 fec557ec04..efbf7bfaef 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -3249,7 +3249,7 @@ impl NetworkBeaconProcessor { self: &Arc, message_id: MessageId, peer_id: PeerId, - payload_bid: SignedExecutionPayloadBid, + payload_bid: SignedExecutionPayloadBid, ) { // TODO(EIP-7732): Implement proper payload bid gossip processing. // This should integrate with a payload execution bid verification module once it's implemented. diff --git a/beacon_node/network/src/network_beacon_processor/mod.rs b/beacon_node/network/src/network_beacon_processor/mod.rs index c326dfd597..fd67fcde82 100644 --- a/beacon_node/network/src/network_beacon_processor/mod.rs +++ b/beacon_node/network/src/network_beacon_processor/mod.rs @@ -448,7 +448,7 @@ impl NetworkBeaconProcessor { self: &Arc, message_id: MessageId, peer_id: PeerId, - execution_payload_bid: Box, + execution_payload_bid: Box>, ) -> Result<(), Error> { let processor = self.clone(); let process_fn = move || { diff --git a/consensus/types/src/block/beacon_block_body.rs b/consensus/types/src/block/beacon_block_body.rs index a113f85fd3..fd5d976c9b 100644 --- a/consensus/types/src/block/beacon_block_body.rs +++ b/consensus/types/src/block/beacon_block_body.rs @@ -167,7 +167,7 @@ pub struct BeaconBlockBody = FullPay #[superstruct(only(Electra, Fulu))] pub execution_requests: ExecutionRequests, #[superstruct(only(Gloas))] - pub signed_execution_payload_bid: SignedExecutionPayloadBid, + pub signed_execution_payload_bid: SignedExecutionPayloadBid, #[superstruct(only(Gloas))] pub payload_attestations: VariableList, E::MaxPayloadAttestations>, #[superstruct(only(Base, Altair, Gloas))] diff --git a/consensus/types/src/execution/execution_payload_bid.rs b/consensus/types/src/execution/execution_payload_bid.rs index f0056463e9..7d80bb48a9 100644 --- a/consensus/types/src/execution/execution_payload_bid.rs +++ b/consensus/types/src/execution/execution_payload_bid.rs @@ -1,5 +1,5 @@ use crate::test_utils::TestRandom; -use crate::{Address, ExecutionBlockHash, ForkName, Hash256, SignedRoot, Slot}; +use crate::{Address, EthSpec, ExecutionBlockHash, ForkName, Hash256, KzgCommitments, SignedRoot, Slot}; use context_deserialize::context_deserialize; use educe::Educe; use serde::{Deserialize, Serialize}; @@ -12,9 +12,10 @@ use tree_hash_derive::TreeHash; )] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[educe(PartialEq, Hash)] +#[serde(bound = "E: EthSpec")] #[context_deserialize(ForkName)] // https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/beacon-chain.md#executionpayloadbid -pub struct ExecutionPayloadBid { +pub struct ExecutionPayloadBid { pub parent_block_hash: ExecutionBlockHash, pub parent_block_root: Hash256, pub block_hash: ExecutionBlockHash, @@ -30,14 +31,15 @@ pub struct ExecutionPayloadBid { pub value: u64, #[serde(with = "serde_utils::quoted_u64")] pub execution_payment: u64, - pub blob_kzg_commitments_root: Hash256, + pub blob_kzg_commitments: KzgCommitments, } -impl SignedRoot for ExecutionPayloadBid {} +impl SignedRoot for ExecutionPayloadBid {} #[cfg(test)] mod tests { use super::*; + use crate::MainnetEthSpec; - ssz_and_tree_hash_tests!(ExecutionPayloadBid); + ssz_and_tree_hash_tests!(ExecutionPayloadBid); } diff --git a/consensus/types/src/execution/execution_payload_envelope.rs b/consensus/types/src/execution/execution_payload_envelope.rs index 64e03cec5a..cf3315a58a 100644 --- a/consensus/types/src/execution/execution_payload_envelope.rs +++ b/consensus/types/src/execution/execution_payload_envelope.rs @@ -1,7 +1,6 @@ use crate::test_utils::TestRandom; use crate::{ - EthSpec, ExecutionPayloadGloas, ExecutionRequests, ForkName, Hash256, KzgCommitments, - SignedRoot, Slot, + EthSpec, ExecutionPayloadGloas, ExecutionRequests, ForkName, Hash256, SignedRoot, Slot, }; use context_deserialize::context_deserialize; use educe::Educe; @@ -21,7 +20,6 @@ pub struct ExecutionPayloadEnvelope { pub builder_index: u64, pub beacon_block_root: Hash256, pub slot: Slot, - pub blob_kzg_commitments: KzgCommitments, pub state_root: Hash256, } diff --git a/consensus/types/src/execution/signed_execution_payload_bid.rs b/consensus/types/src/execution/signed_execution_payload_bid.rs index 29dfd03ba0..1fe26ba1c6 100644 --- a/consensus/types/src/execution/signed_execution_payload_bid.rs +++ b/consensus/types/src/execution/signed_execution_payload_bid.rs @@ -1,5 +1,5 @@ use crate::test_utils::TestRandom; -use crate::{ExecutionPayloadBid, ForkName}; +use crate::{EthSpec, ExecutionPayloadBid, ForkName}; use bls::Signature; use context_deserialize::context_deserialize; use educe::Educe; @@ -11,14 +11,15 @@ use tree_hash_derive::TreeHash; #[derive(TestRandom, TreeHash, Debug, Clone, Encode, Decode, Serialize, Deserialize, Educe)] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[educe(PartialEq, Hash)] +#[serde(bound = "E: EthSpec")] #[context_deserialize(ForkName)] // https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/beacon-chain.md#signedexecutionpayloadbid -pub struct SignedExecutionPayloadBid { - pub message: ExecutionPayloadBid, +pub struct SignedExecutionPayloadBid { + pub message: ExecutionPayloadBid, pub signature: Signature, } -impl SignedExecutionPayloadBid { +impl SignedExecutionPayloadBid { pub fn empty() -> Self { Self { message: ExecutionPayloadBid::default(), @@ -30,6 +31,7 @@ impl SignedExecutionPayloadBid { #[cfg(test)] mod tests { use super::*; + use crate::MainnetEthSpec; - ssz_and_tree_hash_tests!(SignedExecutionPayloadBid); + ssz_and_tree_hash_tests!(SignedExecutionPayloadBid); } diff --git a/consensus/types/src/state/beacon_state.rs b/consensus/types/src/state/beacon_state.rs index 3f8fa4cfff..2c639160a4 100644 --- a/consensus/types/src/state/beacon_state.rs +++ b/consensus/types/src/state/beacon_state.rs @@ -547,7 +547,7 @@ where pub latest_execution_payload_header: ExecutionPayloadHeaderFulu, #[superstruct(only(Gloas))] #[metastruct(exclude_from(tree_lists))] - pub latest_execution_payload_bid: ExecutionPayloadBid, + pub latest_execution_payload_bid: ExecutionPayloadBid, #[superstruct(only(Capella, Deneb, Electra, Fulu, Gloas), partial_getter(copy))] #[serde(with = "serde_utils::quoted_u64")] #[metastruct(exclude_from(tree_lists))] From 4e00426c4b400a433994872417ec0fd6a8b103d7 Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Thu, 29 Jan 2026 13:40:41 -0800 Subject: [PATCH 010/118] Test only execute for glosa --- .../src/data_availability_checker_v2.rs | 2 - .../overflow_lru_cache.rs | 62 ++++++--- beacon_node/beacon_chain/src/test_utils.rs | 10 +- .../types/src/data/data_column_sidecar.rs | 127 +----------------- 4 files changed, 51 insertions(+), 150 deletions(-) diff --git a/beacon_node/beacon_chain/src/data_availability_checker_v2.rs b/beacon_node/beacon_chain/src/data_availability_checker_v2.rs index 112c9074dc..098bdc7916 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker_v2.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker_v2.rs @@ -612,8 +612,6 @@ impl AvailablePayload { /// Returns `AvailabilityCheckError` if: /// - `column_data` contains data not required by the block /// - Required `column_data` is missing - /// - Blob count doesn't match expected - /// - Custody columns are incomplete pub fn new( payload: Arc>, block: Arc>, diff --git a/beacon_node/beacon_chain/src/data_availability_checker_v2/overflow_lru_cache.rs b/beacon_node/beacon_chain/src/data_availability_checker_v2/overflow_lru_cache.rs index 3e5aa3749e..258bc10875 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker_v2/overflow_lru_cache.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker_v2/overflow_lru_cache.rs @@ -776,6 +776,7 @@ mod test { } #[tokio::test] + #[ignore] // TODO(gloas): Implement availability_pending_payload async fn overflow_cache_test_insert_components() { type E = MinimalEthSpec; type T = DiskHarnessType; @@ -899,6 +900,7 @@ mod test { } #[tokio::test] + #[ignore] // TODO(gloas): Implement availability_pending_payload // ensure the state cache keeps memory usage low and that it can properly recover states // THIS TEST CAN BE DELETED ONCE TREE STATES IS MERGED AND WE RIP OUT THE STATE CACHE async fn overflow_cache_test_state_cache() { @@ -1024,14 +1026,14 @@ mod pending_components_tests { use super::*; use crate::PayloadVerificationOutcome; use crate::data_column_verification::KzgVerifiedDataColumn; - use crate::test_utils::{NumBlobs, generate_data_column_sidecars_from_payload, test_spec}; + use crate::test_utils::{NumBlobs, generate_rand_payload_and_columns, test_spec}; use fork_choice::PayloadVerificationStatus; use kzg::KzgCommitment; + use rand::SeedableRng; use rand::rngs::StdRng; - use rand::{Rng, SeedableRng}; use ssz_types::VariableList; use types::test_utils::TestRandom; - use types::{ForkName, MainnetEthSpec, SignedExecutionPayloadEnvelope}; + use types::{ForkName, MainnetEthSpec, SignedExecutionPayloadEnvelope, Slot}; type E = MainnetEthSpec; @@ -1041,28 +1043,27 @@ mod pending_components_tests { DataColumnSidecarList, ); + /// Returns true if gloas is enabled for testing. Tests should skip if this returns false. + fn is_gloas_enabled() -> bool { + let spec = test_spec::(); + spec.fork_name_at_slot::(Slot::new(0)).gloas_enabled() + } + fn pre_setup() -> Setup { let mut rng = StdRng::seed_from_u64(0xDEADBEEF0BAD5EEDu64); let spec = test_spec::(); - // Ensure spec supports gloas so build_data_column_sidecars_gloas succeeds - let spec = ForkName::Gloas.make_genesis_spec(spec); - let mut payload = SignedExecutionPayloadEnvelope::::random_for_test(&mut rng); - // Generate blob transactions to populate blob_kzg_commitments - let num_blobs = match NumBlobs::Random { - NumBlobs::Random => rng.random_range(1..=6usize), - NumBlobs::Number(n) => n, - NumBlobs::None => 0, - }; - let (bundle, transactions) = - execution_layer::test_utils::generate_blobs::(num_blobs, ForkName::Gloas).unwrap(); - payload.message.payload.transactions = <_>::default(); - for tx in Vec::from(transactions) { - payload.message.payload.transactions.push(tx).unwrap(); - } - payload.message.blob_kzg_commitments = bundle.commitments.clone(); + assert!( + spec.fork_name_at_slot::(Slot::new(0)).gloas_enabled(), + "pre_setup() only works after gloas" + ); - let columns = generate_data_column_sidecars_from_payload(&payload, &spec); + let (payload, columns) = generate_rand_payload_and_columns::( + ForkName::Gloas, + NumBlobs::Random, + &mut rng, + &spec, + ); // Create invalid columns by mutating kzg_commitments let invalid_columns: DataColumnSidecarList = columns @@ -1163,6 +1164,9 @@ mod pending_components_tests { #[test] fn payload_invalid_columns_valid_columns() { + if !is_gloas_enabled() { + return; + } let (payload, columns, invalid_columns) = pre_setup(); let (diet_payload, columns, invalid_columns) = setup_pending_components(payload, columns, invalid_columns); @@ -1183,6 +1187,9 @@ mod pending_components_tests { #[test] fn invalid_columns_payload_valid_columns() { + if !is_gloas_enabled() { + return; + } let (payload, columns, invalid_columns) = pre_setup(); let (diet_payload, columns, invalid_columns) = setup_pending_components(payload, columns, invalid_columns); @@ -1202,6 +1209,9 @@ mod pending_components_tests { #[test] fn invalid_columns_valid_columns_payload() { + if !is_gloas_enabled() { + return; + } let (payload, columns, invalid_columns) = pre_setup(); let (diet_payload, columns, invalid_columns) = setup_pending_components(payload, columns, invalid_columns); @@ -1222,6 +1232,9 @@ mod pending_components_tests { #[test] fn payload_valid_columns_invalid_columns() { + if !is_gloas_enabled() { + return; + } let (payload, columns, invalid_columns) = pre_setup(); let (diet_payload, columns, invalid_columns) = setup_pending_components(payload, columns, invalid_columns); @@ -1242,6 +1255,9 @@ mod pending_components_tests { #[test] fn valid_columns_payload_invalid_columns() { + if !is_gloas_enabled() { + return; + } let (payload, columns, invalid_columns) = pre_setup(); let (diet_payload, columns, invalid_columns) = setup_pending_components(payload, columns, invalid_columns); @@ -1262,6 +1278,9 @@ mod pending_components_tests { #[test] fn valid_columns_invalid_columns_payload() { + if !is_gloas_enabled() { + return; + } let (payload, columns, invalid_columns) = pre_setup(); let (diet_payload, columns, invalid_columns) = setup_pending_components(payload, columns, invalid_columns); @@ -1282,6 +1301,9 @@ mod pending_components_tests { #[test] fn should_not_insert_pre_execution_payload_if_executed_payload_exists() { + if !is_gloas_enabled() { + return; + } let (payload, _columns, _invalid_columns) = pre_setup(); let (diet_payload, _columns, _invalid_columns) = setup_pending_components(payload.clone(), _columns, _invalid_columns); diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index 315c1a2c6a..29ed742ac0 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -3436,7 +3436,7 @@ macro_rules! add_blob_transactions_gloas { }}; } -pub fn generate_rand_payloads_and_columns( +pub fn generate_rand_payload_and_columns( fork_name: ForkName, num_blobs: NumBlobs, rng: &mut impl Rng, @@ -3616,8 +3616,10 @@ pub fn generate_data_column_sidecars_from_payload( return vec![]; } - // load the precomputed column sidecar to avoid computing them for every block in the tests. - let template_data_columns = RuntimeVariableList::>::from_ssz_bytes( + // Load the precomputed column sidecar to avoid computing them for every block in the tests. + // TODO(gloas): The fixture is currently in Fulu format. We should generate a Gloas-specific + // fixture once the format is finalized, or compute columns dynamically for Gloas tests. + let template_data_columns = RuntimeVariableList::>::from_ssz_bytes( TEST_DATA_COLUMN_SIDECARS_SSZ, E::number_of_columns(), ) @@ -3626,7 +3628,7 @@ pub fn generate_data_column_sidecars_from_payload( let (cells, proofs) = template_data_columns .into_iter() .map(|sidecar| { - let DataColumnSidecarGloas { + let DataColumnSidecarFulu { column, kzg_proofs, .. } = sidecar; // There's only one cell per column for a single blob diff --git a/consensus/types/src/data/data_column_sidecar.rs b/consensus/types/src/data/data_column_sidecar.rs index 14dc2e828f..d98470297a 100644 --- a/consensus/types/src/data/data_column_sidecar.rs +++ b/consensus/types/src/data/data_column_sidecar.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use bls::Signature; use context_deserialize::context_deserialize; use educe::Educe; -use kzg::{BYTES_PER_BLOB, CellsAndKzgProofs, Kzg, KzgCommitment, KzgProof}; +use kzg::{KzgCommitment, KzgProof}; use merkle_proof::verify_merkle_proof; use safe_arith::ArithError; use serde::{Deserialize, Serialize}; @@ -17,11 +17,8 @@ use tree_hash::TreeHash; use tree_hash_derive::TreeHash; use crate::{ - block::{ - BLOB_KZG_COMMITMENTS_INDEX, BeaconBlockHeader, SignedBeaconBlock, SignedBeaconBlockHeader, - }, - core::{ChainSpec, Epoch, EthSpec, Hash256, Slot}, - data::BlobsList, + block::{BLOB_KZG_COMMITMENTS_INDEX, BeaconBlockHeader, SignedBeaconBlockHeader}, + core::{Epoch, EthSpec, Hash256, Slot}, fork::ForkName, kzg_ext::{KzgCommitments, KzgError}, state::BeaconStateError, @@ -137,124 +134,6 @@ impl DataColumnSidecar { )), } } - - /// Build data column sidecars from blobs and a signed beacon block. - /// - /// This method computes cells and cell proofs from the blobs using KZG, - /// then constructs the appropriate data column sidecar variant (Fulu or Gloas) - /// based on the block's fork. - pub fn build_sidecars( - blobs: BlobsList, - block: &SignedBeaconBlock, - kzg: &Kzg, - spec: &ChainSpec, - ) -> Result, DataColumnSidecarError> { - if blobs.is_empty() { - return Ok(vec![]); - } - - let kzg_commitments = block - .message() - .body() - .blob_kzg_commitments() - .map_err(|_| DataColumnSidecarError::PreDeneb)? - .clone(); - - // Compute cells and proofs for each blob - let blob_cells_and_proofs: Vec = blobs - .iter() - .map(|blob| { - let blob_bytes: &[u8; BYTES_PER_BLOB] = blob.as_ref().try_into().map_err(|_| { - DataColumnSidecarError::KzgError(KzgError::InconsistentArrayLength(format!( - "blob should have size {}, got {}", - BYTES_PER_BLOB, - blob.len() - ))) - })?; - kzg.compute_cells_and_proofs(blob_bytes) - .map_err(DataColumnSidecarError::KzgError) - }) - .collect::, _>>()?; - - let number_of_columns = E::number_of_columns(); - let max_blobs_per_block = - spec.max_blobs_per_block(block.slot().epoch(E::slots_per_epoch())) as usize; - - // Initialize columns and proofs vectors - let mut columns: Vec>> = - vec![Vec::with_capacity(max_blobs_per_block); number_of_columns]; - let mut column_kzg_proofs: Vec> = - vec![Vec::with_capacity(max_blobs_per_block); number_of_columns]; - - // Arrange cells and proofs into columns - for (blob_cells, blob_cell_proofs) in &blob_cells_and_proofs { - for col_idx in 0..number_of_columns { - let cell = blob_cells - .get(col_idx) - .ok_or(DataColumnSidecarError::DataColumnIndexOutOfBounds)?; - let cell_vec: Vec = cell.to_vec(); - let cell = Cell::::try_from(cell_vec)?; - - let proof = blob_cell_proofs - .get(col_idx) - .ok_or(DataColumnSidecarError::DataColumnIndexOutOfBounds)?; - - columns - .get_mut(col_idx) - .ok_or(DataColumnSidecarError::DataColumnIndexOutOfBounds)? - .push(cell); - column_kzg_proofs - .get_mut(col_idx) - .ok_or(DataColumnSidecarError::DataColumnIndexOutOfBounds)? - .push(*proof); - } - } - - // Build sidecars based on fork - let fork_name = block.fork_name_unchecked(); - if fork_name.gloas_enabled() { - // Gloas variant - let beacon_block_root = block.canonical_root(); - let slot = block.slot(); - - columns - .into_iter() - .zip(column_kzg_proofs) - .enumerate() - .map(|(index, (col, proofs))| { - Ok(Arc::new(DataColumnSidecar::Gloas(DataColumnSidecarGloas { - index: index as u64, - column: DataColumn::::try_from(col)?, - kzg_commitments: kzg_commitments.clone(), - kzg_proofs: VariableList::try_from(proofs)?, - slot, - beacon_block_root, - }))) - }) - .collect() - } else { - // Fulu variant - let signed_block_header = block.signed_block_header(); - let kzg_commitments_inclusion_proof = - block.message().body().kzg_commitments_merkle_proof()?; - - columns - .into_iter() - .zip(column_kzg_proofs) - .enumerate() - .map(|(index, (col, proofs))| { - Ok(Arc::new(DataColumnSidecar::Fulu(DataColumnSidecarFulu { - index: index as u64, - column: DataColumn::::try_from(col)?, - kzg_commitments: kzg_commitments.clone(), - kzg_proofs: VariableList::try_from(proofs)?, - signed_block_header: signed_block_header.clone(), - kzg_commitments_inclusion_proof: kzg_commitments_inclusion_proof.clone(), - }))) - }) - .collect() - } - } } impl DataColumnSidecarFulu { From 0b923795328012a8c2b83c916421e4f246276b3a Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Thu, 29 Jan 2026 21:11:59 -0800 Subject: [PATCH 011/118] Remove state LRU cache for da checker v2 --- beacon_node/beacon_chain/src/builder.rs | 1 - .../src/data_availability_checker/error.rs | 2 + .../src/data_availability_checker_v2.rs | 8 +- .../overflow_lru_cache.rs | 265 ++++-------------- .../state_lru_cache.rs | 156 ----------- .../src/payload_verification_types.rs | 3 +- 6 files changed, 55 insertions(+), 380 deletions(-) delete mode 100644 beacon_node/beacon_chain/src/data_availability_checker_v2/state_lru_cache.rs diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index db15214318..ecc55b19a9 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -996,7 +996,6 @@ where complete_blob_backfill, slot_clock.clone(), self.kzg.clone(), - store.clone(), custody_context.clone(), self.spec.clone(), ) diff --git a/beacon_node/beacon_chain/src/data_availability_checker/error.rs b/beacon_node/beacon_chain/src/data_availability_checker/error.rs index af3cb72c03..881cbe8569 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker/error.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker/error.rs @@ -19,6 +19,7 @@ pub enum Error { StoreError(store::Error), DecodeError(ssz::DecodeError), ParentStateMissing(Hash256), + StateMissing(Hash256), BlockReplayError(state_processing::BlockReplayError), RebuildingStateCaches(BeaconStateError), SlotClockError, @@ -43,6 +44,7 @@ impl Error { | Error::DecodeError(_) | Error::Unexpected(_) | Error::ParentStateMissing(_) + | Error::StateMissing(_) | Error::BlockReplayError(_) | Error::RebuildingStateCaches(_) | Error::SlotClockError diff --git a/beacon_node/beacon_chain/src/data_availability_checker_v2.rs b/beacon_node/beacon_chain/src/data_availability_checker_v2.rs index 098bdc7916..187cfc54b0 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker_v2.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker_v2.rs @@ -7,7 +7,7 @@ use crate::data_availability_router::DataColumnCache; use crate::payload_verification_types::{ AvailabilityPendingExecutedPayload, AvailableExecutedPayload, PayloadProcessStatus, }; -use crate::{BeaconChain, BeaconChainTypes, BeaconStore, CustodyContext, metrics}; +use crate::{BeaconChain, BeaconChainTypes, CustodyContext, metrics}; use educe::Educe; use kzg::Kzg; use slot_clock::SlotClock; @@ -25,7 +25,6 @@ use types::{ }; mod overflow_lru_cache; -mod state_lru_cache; use crate::data_column_verification::{ GossipVerifiedDataColumn, KzgVerifiedCustodyDataColumn, KzgVerifiedDataColumn, @@ -46,7 +45,6 @@ use types::new_non_zero_usize; /// `PendingComponents` are now never removed from the cache manually are only removed via LRU /// eviction to prevent race conditions (#7961), so we expect this cache to be full all the time. const OVERFLOW_LRU_CAPACITY_NON_ZERO: NonZeroUsize = new_non_zero_usize(32); -const STATE_LRU_CAPACITY_NON_ZERO: NonZeroUsize = new_non_zero_usize(32); /// Cache to hold fully valid data that can't be imported to fork-choice yet. After the Gloas hard-fork /// beacon blocks can be immediately imported into fork choice. The execution payload is now separated out from @@ -314,13 +312,11 @@ impl DataAvailabilityChecker { complete_blob_backfill: bool, slot_clock: T::SlotClock, kzg: Arc, - store: BeaconStore, custody_context: Arc>, spec: Arc, ) -> Result { let inner = DataAvailabilityCheckerInner::new( OVERFLOW_LRU_CAPACITY_NON_ZERO, - store, custody_context.clone(), spec.clone(), )?; @@ -449,7 +445,6 @@ impl DataAvailabilityChecker { /// Collects metrics from the data availability checker. pub fn metrics(&self) -> DataAvailabilityCheckerMetrics { DataAvailabilityCheckerMetrics { - state_cache_size: self.availability_cache.state_cache_size(), payload_cache_size: self.availability_cache.payload_cache_size(), } } @@ -457,7 +452,6 @@ impl DataAvailabilityChecker { /// Helper struct to group data availability checker metrics. pub struct DataAvailabilityCheckerMetrics { - pub state_cache_size: usize, pub payload_cache_size: usize, } diff --git a/beacon_node/beacon_chain/src/data_availability_checker_v2/overflow_lru_cache.rs b/beacon_node/beacon_chain/src/data_availability_checker_v2/overflow_lru_cache.rs index 258bc10875..fe5c0860f0 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker_v2/overflow_lru_cache.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker_v2/overflow_lru_cache.rs @@ -1,7 +1,5 @@ -use super::state_lru_cache::{DietAvailabilityPendingExecutedPayload, StateLRUCache}; use crate::BeaconChainTypes; use crate::CustodyContext; -use crate::beacon_chain::BeaconStore; use crate::data_availability_checker::AvailabilityCheckError; use crate::data_availability_checker_v2::{Availability, AvailablePayload, AvailablePayloadData}; use crate::data_column_verification::KzgVerifiedCustodyDataColumn; @@ -24,7 +22,7 @@ use types::{ #[derive(Clone)] pub enum CachedPayload { PreExecution(Arc>, BlockImportSource), - Executed(Box>), + Executed(Box>), } #[allow(dead_code)] @@ -37,13 +35,20 @@ impl CachedPayload { fn as_payload(&self) -> &SignedExecutionPayloadEnvelope { match self { CachedPayload::PreExecution(p, _) => p, - CachedPayload::Executed(p) => p.as_payload(), + CachedPayload::Executed(p) => &p.payload, } } pub fn num_blobs_expected(&self) -> usize { self.as_payload().message.blob_kzg_commitments.len() } + + pub fn payload_cloned(&self) -> Arc> { + match self { + CachedPayload::PreExecution(p, _) => p.clone(), + CachedPayload::Executed(p) => p.payload.clone(), + } + } } /// This represents the components of a partially available payload @@ -61,14 +66,6 @@ pub struct PendingComponents { } impl PendingComponents { - #[cfg(test)] - fn get_diet_payload(&self) -> Option<&DietAvailabilityPendingExecutedPayload> { - self.payload.as_ref().and_then(|payload| match payload { - CachedPayload::Executed(payload) => Some(payload.as_ref()), - _ => None, - }) - } - /// Returns an immutable reference to the cached data column. pub fn get_cached_data_column( &self, @@ -89,7 +86,7 @@ impl PendingComponents { } /// Inserts an executed payload into the cache. - pub fn insert_executed_payload(&mut self, payload: DietAvailabilityPendingExecutedPayload) { + pub fn insert_executed_payload(&mut self, payload: AvailabilityPendingExecutedPayload) { self.payload = Some(CachedPayload::Executed(Box::new(payload))) } @@ -120,33 +117,23 @@ impl PendingComponents { } /// Inserts a new payload. - pub fn merge_payload(&mut self, payload: DietAvailabilityPendingExecutedPayload) { + pub fn merge_payload(&mut self, payload: AvailabilityPendingExecutedPayload) { self.insert_executed_payload(payload); } /// Returns Some if the payload has received all its required data for import. The return value /// must be persisted in the DB along with the payload. - /// - /// WARNING: This function can potentially take a lot of time if the state needs to be - /// reconstructed from disk. Ensure you are not holding any write locks while calling this. - pub fn make_available( + pub fn make_available( &self, spec: &Arc, num_expected_columns: usize, - recover: R, - ) -> Result>, AvailabilityCheckError> - where - R: FnOnce( - DietAvailabilityPendingExecutedPayload, - &Span, - ) -> Result, AvailabilityCheckError>, - { - let Some(CachedPayload::Executed(payload)) = &self.payload else { + ) -> Result>, AvailabilityCheckError> { + let Some(CachedPayload::Executed(executed_payload)) = &self.payload else { // Payload not available yet return Ok(None); }; - let num_expected_blobs = payload.num_blobs_expected(); + let num_expected_blobs = executed_payload.num_blobs_expected(); let column_data = if num_expected_blobs == 0 { Some(AvailablePayloadData::NoData) } else { @@ -198,7 +185,7 @@ impl PendingComponents { payload, import_data, payload_verification_outcome, - } = recover(*payload.clone(), &self.span)?; + } = executed_payload.as_ref().clone(); let available_payload = AvailablePayload { block_root: payload.message.beacon_block_root, @@ -272,9 +259,6 @@ impl PendingComponents { pub struct DataAvailabilityCheckerInner { /// Contains all the data we keep in memory, protected by an RwLock critical: RwLock>>, - /// This cache holds a limited number of states in memory and reconstructs them - /// from disk when necessary. This is necessary until we merge tree-states - state_cache: StateLRUCache, custody_context: Arc>, spec: Arc, } @@ -291,13 +275,11 @@ pub(crate) enum ReconstructColumnsDecision { impl DataAvailabilityCheckerInner { pub fn new( capacity: NonZeroUsize, - beacon_store: BeaconStore, custody_context: Arc>, spec: Arc, ) -> Result { Ok(Self { critical: RwLock::new(LruCache::new(capacity)), - state_cache: StateLRUCache::new(beacon_store, spec.clone()), custody_context, spec, }) @@ -320,7 +302,7 @@ impl DataAvailabilityCheckerInner { PayloadProcessStatus::NotValidated(p.clone(), *source) } CachedPayload::Executed(p) => { - PayloadProcessStatus::ExecutionValidated(p.payload_cloned()) + PayloadProcessStatus::ExecutionValidated(p.payload.clone()) } }) }) @@ -399,14 +381,9 @@ impl DataAvailabilityCheckerInner { pending_components: MappedRwLockReadGuard<'_, PendingComponents>, num_expected_columns: usize, ) -> Result, AvailabilityCheckError> { - if let Some(available_payload) = pending_components.make_available( - &self.spec, - num_expected_columns, - |payload, span| { - self.state_cache - .recover_pending_executed_payload(payload, span) - }, - )? { + if let Some(available_payload) = + pending_components.make_available(&self.spec, num_expected_columns)? + { // Explicitly drop read lock before acquiring write lock drop(pending_components); if let Some(components) = self.critical.write().get_mut(&block_root) { @@ -564,14 +541,9 @@ impl DataAvailabilityCheckerInner { .epoch(T::EthSpec::slots_per_epoch()); let block_root = executed_payload.payload.message.beacon_block_root; - // register the payload to get the diet payload - let diet_executed_payload = self - .state_cache - .register_pending_executed_payload(executed_payload); - let pending_components = self.update_or_insert_pending_components(block_root, |pending_components| { - pending_components.merge_payload(diet_executed_payload); + pending_components.merge_payload(executed_payload); Ok(()) })?; @@ -599,9 +571,6 @@ impl DataAvailabilityCheckerInner { /// maintain the cache pub fn do_maintenance(&self, cutoff_epoch: Epoch) -> Result<(), AvailabilityCheckError> { - // clean up any lingering states in the state cache - self.state_cache.do_maintenance(cutoff_epoch); - // Collect keys of pending payloads from a previous epoch to cutoff let mut write_lock = self.critical.write(); let mut keys_to_remove = vec![]; @@ -620,17 +589,6 @@ impl DataAvailabilityCheckerInner { Ok(()) } - #[cfg(test)] - /// get the state cache for inspection (used only for tests) - pub fn state_lru_cache(&self) -> &StateLRUCache { - &self.state_cache - } - - /// Number of states stored in memory in the cache. - pub fn state_cache_size(&self) -> usize { - self.state_cache.lru_cache().read().len() - } - /// Number of pending component entries in memory in the cache. pub fn payload_cache_size(&self) -> usize { self.critical.read().len() @@ -646,19 +604,16 @@ mod test { use crate::{ block_verification_types::AsBlock, custody_context::NodeCustodyType, - data_availability_checker_v2::STATE_LRU_CAPACITY_NON_ZERO, test_utils::{BaseHarnessType, BeaconChainHarness, DiskHarnessType}, }; use logging::create_test_tracing_subscriber; - use std::collections::{HashSet, VecDeque}; + use std::collections::HashSet; use store::{HotColdDB, ItemStore, StoreConfig, database::interface::BeaconNodeBackend}; use tempfile::{TempDir, tempdir}; - use tracing::debug_span; use types::MinimalEthSpec; use types::new_non_zero_usize; const LOW_VALIDATOR_COUNT: usize = 32; - const STATE_LRU_CAPACITY: usize = STATE_LRU_CAPACITY_NON_ZERO.get(); fn get_store_with_spec( db_path: &TempDir, @@ -756,7 +711,7 @@ mod test { let chain_db_path = tempdir().expect("should get temp dir"); let harness = get_gloas_chain(&chain_db_path).await; let spec = harness.spec.clone(); - let test_store = harness.chain.store.clone(); + let _test_store = harness.chain.store.clone(); let capacity_non_zero = new_non_zero_usize(capacity); let custody_context = Arc::new(CustodyContext::new( NodeCustodyType::Fullnode, @@ -764,13 +719,8 @@ mod test { &spec, )); let cache = Arc::new( - DataAvailabilityCheckerInner::::new( - capacity_non_zero, - test_store, - custody_context, - spec.clone(), - ) - .expect("should create cache"), + DataAvailabilityCheckerInner::::new(capacity_non_zero, custody_context, spec.clone()) + .expect("should create cache"), ); (harness, cache, chain_db_path) } @@ -898,127 +848,6 @@ mod test { "cache should still have available payload" ); } - - #[tokio::test] - #[ignore] // TODO(gloas): Implement availability_pending_payload - // ensure the state cache keeps memory usage low and that it can properly recover states - // THIS TEST CAN BE DELETED ONCE TREE STATES IS MERGED AND WE RIP OUT THE STATE CACHE - async fn overflow_cache_test_state_cache() { - type E = MinimalEthSpec; - type T = DiskHarnessType; - let capacity = STATE_LRU_CAPACITY * 2; - let (harness, cache, _path) = setup_harness_and_cache::(capacity).await; - - let mut pending_payloads = VecDeque::new(); - let mut states = Vec::new(); - let mut state_roots = Vec::new(); - // Get enough payload to fill the cache to capacity, ensuring all payloads have blobs - while pending_payloads.len() < capacity { - let (mut pending_payload, _) = availability_pending_payload(&harness).await; - if pending_payload.num_blobs_expected() == 0 { - // we need payloads with blobs - continue; - } - let state_root = pending_payload.import_data.state.canonical_root().unwrap(); - states.push(pending_payload.import_data.state.clone()); - pending_payloads.push_back(pending_payload); - state_roots.push(state_root); - } - - let state_cache = cache.state_lru_cache().lru_cache(); - let mut pushed_diet_payloads = VecDeque::new(); - - for i in 0..capacity { - let pending_payload = pending_payloads.pop_front().expect("should have payload"); - let block_root = pending_payload.as_payload().beacon_block_root(); - - assert_eq!( - state_cache.read().len(), - std::cmp::min(i, STATE_LRU_CAPACITY), - "state cache should be empty at start" - ); - - if i >= STATE_LRU_CAPACITY { - let lru_root = state_roots[i - STATE_LRU_CAPACITY]; - assert_eq!( - state_cache.read().peek_lru().map(|(root, _)| root), - Some(&lru_root), - "lru payload should be in cache" - ); - } - - // put the payload in the cache - let availability = cache - .put_executed_payload(pending_payload) - .expect("should put payload"); - - // grab the diet payload from the cache for later testing - let diet_payload = cache - .critical - .read() - .peek(&block_root) - .and_then(|pending_components| pending_components.get_diet_payload().cloned()) - .expect("should exist"); - pushed_diet_payloads.push_back(diet_payload); - - // should be unavailable since we made sure all payloads had blobs - assert!( - matches!(availability, Availability::MissingComponents(_)), - "should be pending blobs" - ); - - if i >= STATE_LRU_CAPACITY { - let evicted_index = i - STATE_LRU_CAPACITY; - let evicted_root = state_roots[evicted_index]; - assert!( - state_cache.read().peek(&evicted_root).is_none(), - "lru root should be evicted" - ); - // get the diet payload via direct conversion (testing only) - let diet_payload = pushed_diet_payloads - .pop_front() - .expect("should have payload"); - // reconstruct the pending payload by replaying the payload on the parent state - let recovered_pending_payload = cache - .state_lru_cache() - .recover_pending_executed_payload(diet_payload, &debug_span!("test")) - .expect("should reconstruct pending payload"); - - // assert the recovered state is the same as the original - assert_eq!( - recovered_pending_payload.import_data.state, states[evicted_index], - "recovered state should be the same as the original" - ); - } - } - - // now check the last payload - let last_payload = pushed_diet_payloads - .pop_back() - .expect("should exist") - .clone(); - // the state should still be in the cache - assert!( - state_cache - .read() - .peek(&last_payload.as_payload().message.state_root) - .is_some(), - "last payload state should still be in cache" - ); - // get the diet payload via direct conversion (testing only) - let diet_payload = last_payload.clone(); - // recover the pending payload from the cache - let recovered_pending_payload = cache - .state_lru_cache() - .recover_pending_executed_payload(diet_payload, &debug_span!("test")) - .expect("should reconstruct pending payload"); - // assert the recovered state is the same as the original - assert_eq!( - Some(&recovered_pending_payload.import_data.state), - states.last(), - "recovered state should be the same as the original" - ); - } } #[cfg(test)] @@ -1026,14 +855,16 @@ mod pending_components_tests { use super::*; use crate::PayloadVerificationOutcome; use crate::data_column_verification::KzgVerifiedDataColumn; + use crate::payload_verification_types::PayloadImportData; use crate::test_utils::{NumBlobs, generate_rand_payload_and_columns, test_spec}; use fork_choice::PayloadVerificationStatus; use kzg::KzgCommitment; use rand::SeedableRng; use rand::rngs::StdRng; use ssz_types::VariableList; + use state_processing::ConsensusContext; use types::test_utils::TestRandom; - use types::{ForkName, MainnetEthSpec, SignedExecutionPayloadEnvelope, Slot}; + use types::{BeaconState, ForkName, MainnetEthSpec, SignedExecutionPayloadEnvelope, Slot}; type E = MainnetEthSpec; @@ -1093,7 +924,7 @@ mod pending_components_tests { } type PendingComponentsSetup = ( - DietAvailabilityPendingExecutedPayload, + AvailabilityPendingExecutedPayload, Vec>, Vec>, ); @@ -1121,15 +952,19 @@ mod pending_components_tests { }) .collect(); - let diet_payload = DietAvailabilityPendingExecutedPayload::new_for_testing( - Arc::new(payload), + let executed_payload = AvailabilityPendingExecutedPayload::new( + Arc::new(payload.clone()), + PayloadImportData { + state: BeaconState::new(0, Default::default(), &test_spec::()), + consensus_context: ConsensusContext::new(payload.message.slot), + }, PayloadVerificationOutcome { payload_verification_status: PayloadVerificationStatus::Verified, is_valid_merge_transition_block: false, }, ); - (diet_payload, columns, invalid_columns) + (executed_payload, columns, invalid_columns) } fn assert_cache_consistent(cache: &PendingComponents) { @@ -1168,11 +1003,11 @@ mod pending_components_tests { return; } let (payload, columns, invalid_columns) = pre_setup(); - let (diet_payload, columns, invalid_columns) = + let (executed_payload, columns, invalid_columns) = setup_pending_components(payload, columns, invalid_columns); let block_root = Hash256::ZERO; let mut cache = >::empty(block_root); - cache.merge_payload(diet_payload); + cache.merge_payload(executed_payload); cache .merge_data_columns(invalid_columns) .expect("merge should succeed"); @@ -1191,14 +1026,14 @@ mod pending_components_tests { return; } let (payload, columns, invalid_columns) = pre_setup(); - let (diet_payload, columns, invalid_columns) = + let (executed_payload, columns, invalid_columns) = setup_pending_components(payload, columns, invalid_columns); let block_root = Hash256::ZERO; let mut cache = >::empty(block_root); cache .merge_data_columns(invalid_columns) .expect("merge should succeed"); - cache.merge_payload(diet_payload); + cache.merge_payload(executed_payload); cache .merge_data_columns(columns) .expect("merge should succeed"); @@ -1213,7 +1048,7 @@ mod pending_components_tests { return; } let (payload, columns, invalid_columns) = pre_setup(); - let (diet_payload, columns, invalid_columns) = + let (executed_payload, columns, invalid_columns) = setup_pending_components(payload, columns, invalid_columns); let block_root = Hash256::ZERO; @@ -1224,7 +1059,7 @@ mod pending_components_tests { cache .merge_data_columns(columns) .expect("merge should succeed"); - cache.merge_payload(diet_payload); + cache.merge_payload(executed_payload); // Invalid columns were first, valid ones deduplicated away. assert!(!cache.verified_data_columns.is_empty()); @@ -1236,12 +1071,12 @@ mod pending_components_tests { return; } let (payload, columns, invalid_columns) = pre_setup(); - let (diet_payload, columns, invalid_columns) = + let (executed_payload, columns, invalid_columns) = setup_pending_components(payload, columns, invalid_columns); let block_root = Hash256::ZERO; let mut cache = >::empty(block_root); - cache.merge_payload(diet_payload); + cache.merge_payload(executed_payload); cache .merge_data_columns(columns) .expect("merge should succeed"); @@ -1259,7 +1094,7 @@ mod pending_components_tests { return; } let (payload, columns, invalid_columns) = pre_setup(); - let (diet_payload, columns, invalid_columns) = + let (executed_payload, columns, invalid_columns) = setup_pending_components(payload, columns, invalid_columns); let block_root = Hash256::ZERO; @@ -1267,7 +1102,7 @@ mod pending_components_tests { cache .merge_data_columns(columns) .expect("merge should succeed"); - cache.merge_payload(diet_payload); + cache.merge_payload(executed_payload); cache .merge_data_columns(invalid_columns) .expect("merge should succeed"); @@ -1282,7 +1117,7 @@ mod pending_components_tests { return; } let (payload, columns, invalid_columns) = pre_setup(); - let (diet_payload, columns, invalid_columns) = + let (executed_payload, columns, invalid_columns) = setup_pending_components(payload, columns, invalid_columns); let block_root = Hash256::ZERO; @@ -1293,7 +1128,7 @@ mod pending_components_tests { cache .merge_data_columns(invalid_columns) .expect("merge should succeed"); - cache.merge_payload(diet_payload); + cache.merge_payload(executed_payload); // Valid columns were inserted first, so they persist. Cache should be consistent. assert_cache_consistent(&cache); @@ -1305,7 +1140,7 @@ mod pending_components_tests { return; } let (payload, _columns, _invalid_columns) = pre_setup(); - let (diet_payload, _columns, _invalid_columns) = + let (executed_payload, _columns, _invalid_columns) = setup_pending_components(payload.clone(), _columns, _invalid_columns); let block_root = Hash256::ZERO; @@ -1322,7 +1157,7 @@ mod pending_components_tests { "pre execution payload inserted" ); - pending_component.insert_executed_payload(diet_payload); + pending_component.insert_executed_payload(executed_payload); assert!( matches!(pending_component.payload, Some(CachedPayload::Executed(_))), "executed payload inserted" diff --git a/beacon_node/beacon_chain/src/data_availability_checker_v2/state_lru_cache.rs b/beacon_node/beacon_chain/src/data_availability_checker_v2/state_lru_cache.rs deleted file mode 100644 index 4d1e02990c..0000000000 --- a/beacon_node/beacon_chain/src/data_availability_checker_v2/state_lru_cache.rs +++ /dev/null @@ -1,156 +0,0 @@ -#![allow(dead_code)] -use crate::payload_verification_types::{AvailabilityPendingExecutedPayload, PayloadImportData}; -use crate::{ - BeaconChainTypes, BeaconStore, PayloadVerificationOutcome, - data_availability_checker_v2::{AvailabilityCheckError, STATE_LRU_CAPACITY_NON_ZERO}, -}; -use lru::LruCache; -use parking_lot::RwLock; -use std::sync::Arc; -use store::OnDiskConsensusContext; -use tracing::{Span, instrument}; -use types::{BeaconState, ChainSpec, Epoch, EthSpec, Hash256, SignedExecutionPayloadEnvelope}; - -/// This mirrors everything in the `AvailabilityPendingExecutedBlock`, except -/// that it is much smaller because it contains only a state root instead of -/// a full `BeaconState`. -#[derive(Clone)] -pub struct DietAvailabilityPendingExecutedPayload { - payload: Arc>, - state_root: Hash256, - consensus_context: OnDiskConsensusContext, - payload_verification_outcome: PayloadVerificationOutcome, -} - -/// Implementing the same methods as `AvailabilityPendingExecutedPayload` -impl DietAvailabilityPendingExecutedPayload { - #[cfg(test)] - pub fn new_for_testing( - payload: Arc>, - payload_verification_outcome: PayloadVerificationOutcome, - ) -> Self { - use state_processing::ConsensusContext; - use types::Slot; - Self { - payload, - state_root: Hash256::ZERO, - consensus_context: OnDiskConsensusContext::from_consensus_context( - ConsensusContext::new(Slot::new(0)), - ), - payload_verification_outcome, - } - } - - pub fn as_payload(&self) -> &SignedExecutionPayloadEnvelope { - &self.payload - } - - pub fn payload_cloned(&self) -> Arc> { - self.payload.clone() - } - - pub fn num_blobs_expected(&self) -> usize { - self.payload.message.blob_kzg_commitments.len() - } -} - -/// This LRU cache holds BeaconStates used for payload import. If the cache overflows, -/// the least recently used state will be dropped. If the dropped state is needed -/// later on, it will be recovered from the parent state and replaying the payload. -/// -/// WARNING: This cache assumes the parent block of any `AvailabilityPendingExecutedPayload` -/// has already been imported into ForkChoice. If this is not the case, the cache -/// will fail to recover the state when the cache overflows because it can't load -/// the parent state! -pub struct StateLRUCache { - states: RwLock>>, - store: BeaconStore, - spec: Arc, -} - -impl StateLRUCache { - pub fn new(store: BeaconStore, spec: Arc) -> Self { - Self { - states: RwLock::new(LruCache::new(STATE_LRU_CAPACITY_NON_ZERO)), - store, - spec, - } - } - - /// This will store the state in the LRU cache and return a - /// `DietAvailabilityPendingExecutedPayload` which is much cheaper to - /// keep around in memory. - pub fn register_pending_executed_payload( - &self, - executed_payload: AvailabilityPendingExecutedPayload, - ) -> DietAvailabilityPendingExecutedPayload { - let state = executed_payload.import_data.state; - let state_root = executed_payload.payload.message.state_root; - self.states.write().put(state_root, state); - - DietAvailabilityPendingExecutedPayload { - payload: executed_payload.payload, - state_root, - consensus_context: OnDiskConsensusContext::from_consensus_context( - executed_payload.import_data.consensus_context, - ), - payload_verification_outcome: executed_payload.payload_verification_outcome, - } - } - - /// Recover the `AvailabilityPendingExecutedPayload` from the diet version. - /// This method will first check the cache and if the state is not found - /// it will reconstruct the state by loading the parent state from disk and - /// replaying the block. - #[instrument(skip_all, parent = _span, level = "debug")] - pub fn recover_pending_executed_payload( - &self, - diet_executed_payload: DietAvailabilityPendingExecutedPayload, - _span: &Span, - ) -> Result, AvailabilityCheckError> { - // Keep the state in the cache to prevent reconstruction in race conditions - let state = if let Some(state) = self.states.write().get(&diet_executed_payload.state_root) - { - state.clone() - } else { - self.reconstruct_state(&diet_executed_payload)? - }; - Ok(AvailabilityPendingExecutedPayload { - payload: diet_executed_payload.payload, - import_data: PayloadImportData { - state, - consensus_context: diet_executed_payload - .consensus_context - .into_consensus_context(), - }, - payload_verification_outcome: diet_executed_payload.payload_verification_outcome, - }) - } - - /// Reconstruct the state by loading the parent state from disk and replaying - /// the block. - #[instrument(skip_all, level = "debug")] - fn reconstruct_state( - &self, - _diet_executed_block: &DietAvailabilityPendingExecutedPayload, - ) -> Result, AvailabilityCheckError> { - todo!() - } - - /// returns the state cache for inspection - pub fn lru_cache(&self) -> &RwLock>> { - &self.states - } - - /// remove any states from the cache from before the given epoch - pub fn do_maintenance(&self, cutoff_epoch: Epoch) { - let mut write_lock = self.states.write(); - while let Some((_, state)) = write_lock.peek_lru() { - if state.slot().epoch(T::EthSpec::slots_per_epoch()) < cutoff_epoch { - write_lock.pop_lru(); - } else { - break; - } - } - } -} diff --git a/beacon_node/beacon_chain/src/payload_verification_types.rs b/beacon_node/beacon_chain/src/payload_verification_types.rs index f54c67ecb1..02868bb597 100644 --- a/beacon_node/beacon_chain/src/payload_verification_types.rs +++ b/beacon_node/beacon_chain/src/payload_verification_types.rs @@ -5,7 +5,7 @@ use types::{BeaconState, BlockImportSource, EthSpec, SignedExecutionPayloadEnvel use crate::{PayloadVerificationOutcome, data_availability_checker_v2::AvailablePayload}; -#[derive(Debug, PartialEq)] +#[derive(Debug, Clone, PartialEq)] pub struct PayloadImportData { pub state: BeaconState, pub consensus_context: ConsensusContext, @@ -13,6 +13,7 @@ pub struct PayloadImportData { /// A payload that has completed payload verification by an EL client but does not /// have all requisite column data to get imported into fork choice. +#[derive(Clone)] pub struct AvailabilityPendingExecutedPayload { pub payload: Arc>, pub import_data: PayloadImportData, From dee394cf0f6c2a5dc9d1637ea1801cd82211f575 Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Thu, 29 Jan 2026 21:23:21 -0800 Subject: [PATCH 012/118] Lint fixes --- consensus/types/src/execution/execution_payload_bid.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/consensus/types/src/execution/execution_payload_bid.rs b/consensus/types/src/execution/execution_payload_bid.rs index 7d80bb48a9..e81add5177 100644 --- a/consensus/types/src/execution/execution_payload_bid.rs +++ b/consensus/types/src/execution/execution_payload_bid.rs @@ -1,5 +1,7 @@ use crate::test_utils::TestRandom; -use crate::{Address, EthSpec, ExecutionBlockHash, ForkName, Hash256, KzgCommitments, SignedRoot, Slot}; +use crate::{ + Address, EthSpec, ExecutionBlockHash, ForkName, Hash256, KzgCommitments, SignedRoot, Slot, +}; use context_deserialize::context_deserialize; use educe::Educe; use serde::{Deserialize, Serialize}; From d047ace41fe9e22ab645316c11a085f1e2cc7244 Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Fri, 30 Jan 2026 09:24:41 -0800 Subject: [PATCH 013/118] new da checker doesn't need payloads --- beacon_node/beacon_chain/src/builder.rs | 1 - .../src/data_availability_checker_v2.rs | 782 +---------- .../overflow_lru_cache.rs | 1195 +++++++---------- .../src/data_availability_router.rs | 5 +- .../src/payload_verification_types.rs | 32 +- beacon_node/beacon_chain/src/test_utils.rs | 15 +- 6 files changed, 555 insertions(+), 1475 deletions(-) diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index ecc55b19a9..04aba2d2a4 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -993,7 +993,6 @@ where let da_checker_v2 = Arc::new( DataAvailabilityCheckerV2::new( - complete_blob_backfill, slot_clock.clone(), self.kzg.clone(), custody_context.clone(), diff --git a/beacon_node/beacon_chain/src/data_availability_checker_v2.rs b/beacon_node/beacon_chain/src/data_availability_checker_v2.rs index 187cfc54b0..a386d1f5a0 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker_v2.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker_v2.rs @@ -4,24 +4,18 @@ use crate::data_availability_checker_v2::overflow_lru_cache::{ use crate::data_availability_checker::AvailabilityCheckError; use crate::data_availability_router::DataColumnCache; -use crate::payload_verification_types::{ - AvailabilityPendingExecutedPayload, AvailableExecutedPayload, PayloadProcessStatus, -}; use crate::{BeaconChain, BeaconChainTypes, CustodyContext, metrics}; -use educe::Educe; use kzg::Kzg; use slot_clock::SlotClock; -use std::collections::HashSet; use std::fmt; use std::fmt::Debug; use std::num::NonZeroUsize; use std::sync::Arc; -use std::time::Duration; use task_executor::TaskExecutor; use tracing::{debug, error, instrument}; use types::{ - BlockImportSource, ChainSpec, ColumnIndex, DataColumnSidecar, DataColumnSidecarList, EthSpec, - Hash256, SignedBeaconBlock, SignedExecutionPayloadEnvelope, Slot, + ChainSpec, ColumnIndex, DataColumnSidecar, DataColumnSidecarList, EthSpec, Hash256, + SignedBeaconBlock, Slot, }; mod overflow_lru_cache; @@ -36,50 +30,33 @@ use crate::metrics::{ use crate::observed_data_sidecars::ObservationStrategy; use types::new_non_zero_usize; -/// The LRU Cache stores `PendingComponents`, which store payload and its associated column data: +/// The LRU Cache stores `PendingComponents`, which store block and its associated column data. +/// Setting this to 32 keeps memory usage reasonable. /// -/// With `MAX_BLOBS_PER_BLOCK` = 48 for exa,ple, the maximum size of data columns -/// in `PendingComponents` is ~12.29 MB. Setting this to 32 means the maximum size of the cache is -/// approximately 0.4 GB. -/// -/// `PendingComponents` are now never removed from the cache manually are only removed via LRU +/// `PendingComponents` are now never removed from the cache manually and are only removed via LRU /// eviction to prevent race conditions (#7961), so we expect this cache to be full all the time. const OVERFLOW_LRU_CAPACITY_NON_ZERO: NonZeroUsize = new_non_zero_usize(32); -/// Cache to hold fully valid data that can't be imported to fork-choice yet. After the Gloas hard-fork -/// beacon blocks can be immediately imported into fork choice. The execution payload is now separated out from -/// the beacon block. The payload envelope and data columns are received separately from the network. The block -/// is now always considered "available". Availability checks are now made on the payload and it is considered -/// "fully available" when the payload and all required columns are inserted into this cache. +/// Represents available data for a block - the block root and its data columns. +pub type AvailableData = (Hash256, DataColumnSidecarList); + +/// This type is returned after adding a block / column to the `DataAvailabilityChecker`. /// -/// Usually a payload becomes available on its slot within a second of receiving its first component -/// over gossip. However, a payload may never become available if a malicious proposer does not -/// publish its data, or there are network issues that prevent us from receiving it. If the payload -/// does not become available after some time we can safely forget about it. Consider these two -/// cases: -/// -/// - Global unavailability: If nobody has received the payload components it's likely that the -/// builder never made the payload available. So we can safely forget about the payload as it will -/// never become available. -/// - Local unavailability: Some fraction of the network has received all payload components, but not us. -/// Some of our peers will eventually attest to a descendant of that block and lookup sync will -/// fetch its components. Therefore it's not strictly necessary to hold to the partially available -/// payload for too long as we can recover from other peers. -/// -/// Even in periods of non-finality, the builder is expected to publish the payload's data -/// immediately. Because this cache only holds fully valid data, its capacity is bound to 1 block -/// per slot and fork: before inserting into this cache we check the proposer signature and correct -/// proposer. Having a capacity > 1 is an optimization to prevent sync lookup from having re-fetch -/// data during moments of unstable network conditions. -pub struct DataAvailabilityChecker { - #[allow(dead_code)] - complete_blob_backfill: bool, - availability_cache: Arc>, - #[allow(dead_code)] - slot_clock: T::SlotClock, - kzg: Arc, - custody_context: Arc>, - spec: Arc, +/// Indicates if the block's data is fully `Available` or if we need more columns. +pub enum Availability { + MissingComponents(Hash256), + Available(Box>), +} + +impl Debug for Availability { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::MissingComponents(block_root) => { + write!(f, "MissingComponents({})", block_root) + } + Self::Available(data) => write!(f, "Available({}, {} columns)", data.0, data.1.len()), + } + } } pub type AvailabilityAndReconstructedColumns = (Availability, DataColumnSidecarList); @@ -91,24 +68,22 @@ pub enum DataColumnReconstructionResult { RecoveredColumnsNotImported(&'static str), } -/// This type is returned after adding a payload / column to the `DataAvailabilityChecker`. +/// Cache to hold data columns for blocks pending data availability. /// -/// Indicates if the payload is fully `Available` or if we need columns or payload -/// to "complete" the requirements for an `AvailablePayload`. -pub enum Availability { - MissingComponents(Hash256), - Available(Box>), -} - -impl Debug for Availability { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - Self::MissingComponents(block_root) => { - write!(f, "MissingComponents({})", block_root) - } - Self::Available(payload) => write!(f, "Available({:?})", payload.payload.block_root), - } - } +/// In Gloas, beacon blocks can be immediately imported into fork choice. The execution payload +/// is separated from the beacon block. This cache tracks data columns for payloads until all +/// required columns are received. +/// +/// Usually data becomes available on its slot within a second of receiving its first component +/// over gossip. However, data may never become available if a malicious proposer does not +/// publish its data, or there are network issues. Components are only removed via LRU eviction. +pub struct DataAvailabilityChecker { + availability_cache: Arc>, + #[allow(dead_code)] + slot_clock: T::SlotClock, + kzg: Arc, + custody_context: Arc>, + spec: Arc, } impl DataColumnCache for DataAvailabilityChecker { @@ -151,7 +126,7 @@ impl DataColumnCache for DataAvailabilityChecker { }) } - /// Insert RPC custody columns and check if the block/payload becomes available. + /// Insert RPC custody columns and check if the block becomes available. #[instrument(skip_all, level = "trace")] fn put_rpc_custody_columns( &self, @@ -165,9 +140,6 @@ impl DataColumnCache for DataAvailabilityChecker { .map_err(AvailabilityCheckError::InvalidColumn)?; // Filter out columns that aren't required for custody for this slot - // This is required because `data_columns_by_root` requests the **latest** CGC that _may_ - // not be yet effective for data availability check, as CGC changes are only effecive from - // a new epoch. let epoch = slot.epoch(T::EthSpec::slots_per_epoch()); let sampling_columns = self .custody_context @@ -182,11 +154,9 @@ impl DataColumnCache for DataAvailabilityChecker { .put_kzg_verified_data_columns(block_root, verified_custody_columns) } - /// Check if we've cached other data columns for this payload. If it satisfies the custody requirement and we also - /// have a payload cached, return the `Availability` variant triggering payload import. - /// Otherwise cache the data column sidecar. - /// - /// This should only accept gossip verified data columns, so we should not have to worry about dupes. + /// Check if we've cached other data columns for this block. If it satisfies the custody + /// requirement and we also have the block cached, return the `Availability` variant + /// triggering import. Otherwise cache the data column sidecar. #[instrument(skip_all, level = "trace")] fn put_gossip_verified_data_columns( &self, @@ -309,7 +279,6 @@ impl DataColumnCache for DataAvailabilityChecker { impl DataAvailabilityChecker { pub fn new( - complete_blob_backfill: bool, slot_clock: T::SlotClock, kzg: Arc, custody_context: Arc>, @@ -321,7 +290,6 @@ impl DataAvailabilityChecker { spec.clone(), )?; Ok(Self { - complete_blob_backfill, availability_cache: Arc::new(inner), slot_clock, kzg, @@ -334,125 +302,38 @@ impl DataAvailabilityChecker { &self.custody_context } - /// Checks if the payload associated with the given block root is currently in the availability cache awaiting import because - /// of missing components. - /// - /// Returns the cached payload wrapped in a `PayloadProcessStatus` enum if it exists. - pub fn get_cached_payload( - &self, - block_root: &Hash256, - ) -> Option> { - self.availability_cache.get_cached_payload(block_root) - } - - /// Check if we have all required columns for a payload. Returns `Availability` which has information - /// about whether all components have been received or more are required. - pub fn put_executed_payload( - &self, - executed_payload: AvailabilityPendingExecutedPayload, - ) -> Result, AvailabilityCheckError> { - self.availability_cache - .put_executed_payload(executed_payload) - } - - /// Inserts a pre-execution payload into the cache. - /// This does NOT override an existing executed payload. - pub fn put_pre_execution_payload( + /// Insert a block into the cache and check if data becomes available. + pub fn put_block( &self, block_root: Hash256, - payload: Arc>, - source: BlockImportSource, - ) -> Result<(), AvailabilityCheckError> { - self.availability_cache - .put_pre_execution_payload(block_root, payload, source) + block: Arc>, + ) -> Result, AvailabilityCheckError> { + self.availability_cache.put_block(block_root, block) } - /// Removes a pre-execution payload from the cache. - /// This does NOT remove an existing executed payload. - pub fn remove_payload_on_execution_error(&self, block_root: &Hash256) { - self.availability_cache - .remove_pre_execution_payload(block_root); - } - - /// Verifies kzg commitments for an `AvailableBlock`.` - /// - /// WARNING: This function assumes all required blobs are already present, it does NOT - /// check if there are any missing blobs. - pub fn verify_kzg_for_available_payload( + /// Verifies kzg commitments for data columns. + pub fn verify_kzg_for_data_columns( &self, - available_payload: &AvailablePayload, + data_columns: &DataColumnSidecarList, ) -> Result<(), AvailabilityCheckError> { - let block_data_required = self - .custody_context - .data_columns_required_for_payload(&available_payload.payload); - match available_payload.data() { - AvailablePayloadData::NoData => { - if block_data_required { - return Err(AvailabilityCheckError::MissingCustodyColumns); - } - } - AvailablePayloadData::DataColumns(data_columns) => { - verify_kzg_for_data_column_list(data_columns.iter(), &self.kzg) - .map_err(AvailabilityCheckError::InvalidColumn)?; - } - } - - Ok(()) - } - - /// Performs batch kzg verification for a vector of `AvailablePayloads`. This is more efficient than - /// calling `verify_kzg_for_available_block` in a loop. - /// - /// WARNING: This function assumes all required blobs are already present, it does NOT - /// check if there are any missing blobs. - #[instrument(skip_all)] - pub fn batch_verify_kzg_for_available_payloads( - &self, - available_payloads: &Vec>, - ) -> Result<(), AvailabilityCheckError> { - let all_data_columns = available_payloads - .iter() - .filter(|available_payload| { - self.custody_context - .data_columns_required_for_payload(&available_payload.payload) - }) - // this clone is cheap as it's cloning an Arc - .filter_map(|available_payload| available_payload.column_data.data_columns()) - .flatten() - .collect::>(); - - for available_payload in available_payloads { - let payload_data_required = self - .custody_context - .data_columns_required_for_payload(&available_payload.payload); - if let AvailablePayloadData::NoData = available_payload.data() - && payload_data_required - { - return Err(AvailabilityCheckError::MissingCustodyColumns); - } - } - - // verify kzg for all data columns at once - if !all_data_columns.is_empty() { - // Attributes fault to the specific peer that sent an invalid column - verify_kzg_for_data_column_list(all_data_columns.iter(), &self.kzg) + if !data_columns.is_empty() { + verify_kzg_for_data_column_list(data_columns.iter(), &self.kzg) .map_err(AvailabilityCheckError::InvalidColumn)?; } - Ok(()) } /// Collects metrics from the data availability checker. pub fn metrics(&self) -> DataAvailabilityCheckerMetrics { DataAvailabilityCheckerMetrics { - payload_cache_size: self.availability_cache.payload_cache_size(), + block_cache_size: self.availability_cache.block_cache_size(), } } } /// Helper struct to group data availability checker metrics. pub struct DataAvailabilityCheckerMetrics { - pub payload_cache_size: usize, + pub block_cache_size: usize, } pub fn start_availability_cache_maintenance_service( @@ -472,17 +353,6 @@ pub fn start_availability_cache_maintenance_service( } else { debug!("Gloas fork not configured, not starting availability cache maintenance service"); } - // TODO(gloas) - // this cache only needs to be maintained if deneb is configured - // if chain.spec.deneb_fork_epoch.is_some() { - // let overflow_cache = chain.data_availability_checker.availability_cache.clone(); - // executor.spawn( - // async move { availability_cache_maintenance_service(chain, overflow_cache).await }, - // "availability_cache_service", - // ); - // } else { - // debug!("Deneb fork not configured, not starting availability cache maintenance service"); - // } } async fn availability_cache_maintenance_service( @@ -529,7 +399,7 @@ async fn availability_cache_maintenance_service( .spec .min_epoch_data_availability_boundary(current_epoch) else { - // Shutdown service if deneb fork epoch not set. Unreachable as the same check is performed above. + // Shutdown service if deneb fork epoch not set. break; }; @@ -548,543 +418,3 @@ async fn availability_cache_maintenance_service( }; } } - -#[derive(Debug, Clone)] -// TODO(gloas) Move this to `payload_verification_types.rs` -pub enum AvailablePayloadData { - /// Payload has zero blobs - NoData, - /// Payload has more than zero blobs - DataColumns(DataColumnSidecarList), -} - -impl AvailablePayloadData { - pub fn new_with_data_columns(columns: DataColumnSidecarList) -> Self { - if columns.is_empty() { - Self::NoData - } else { - Self::DataColumns(columns) - } - } - - pub fn data_columns(&self) -> Option> { - match self { - AvailablePayloadData::NoData => None, - AvailablePayloadData::DataColumns(data_columns) => Some(data_columns.clone()), - } - } - - pub fn data_columns_len(&self) -> usize { - if let Some(data_columns) = self.data_columns() { - data_columns.len() - } else { - 0 - } - } -} - -/// A fully available payload that is ready to be imported into fork choice. -#[derive(Debug, Clone, Educe)] -#[educe(Hash(bound(E: EthSpec)))] -pub struct AvailablePayload { - block_root: Hash256, - block: Arc>, - payload: Arc>, - #[educe(Hash(ignore))] - column_data: AvailablePayloadData, - #[educe(Hash(ignore))] - /// Timestamp at which this payload first became available (UNIX timestamp, time since 1970). - payload_available_timestamp: Option, - #[educe(Hash(ignore))] - pub spec: Arc, -} - -impl AvailablePayload { - /// Constructs an `AvailablePayload` from a payload and optional data. - /// - If `column_data` is `DataColumns`, constructs `AvailablePayload` variant after column validation. - /// - If `column_data` is `NoData`, constructs `AvailablePayload` after verifying that the payload is not expecting columns. - /// Returns `AvailabilityCheckError` if: - /// - `column_data` contains data not required by the block - /// - Required `column_data` is missing - pub fn new( - payload: Arc>, - block: Arc>, - column_data: AvailablePayloadData, - da_checker: &DataAvailabilityChecker, - spec: Arc, - ) -> Result - where - T: BeaconChainTypes, - { - // Ensure payload availability - let columns_required = da_checker - .custody_context() - .data_columns_required_for_payload(&payload); - - match &column_data { - AvailablePayloadData::NoData => { - if columns_required { - return Err(AvailabilityCheckError::MissingCustodyColumns); - } - } - AvailablePayloadData::DataColumns(data_columns) => { - if !columns_required { - // TODO(gloas) potential refactor here - return Err(AvailabilityCheckError::MissingCustodyColumns); - } - - let mut column_indices = da_checker - .custody_context - .custody_columns_for_epoch( - Some(payload.message.slot.epoch(T::EthSpec::slots_per_epoch())), - &spec, - ) - .iter() - .collect::>(); - - for data_column in data_columns { - column_indices.remove(&data_column.index()); - } - - if !column_indices.is_empty() { - return Err(AvailabilityCheckError::MissingCustodyColumns); - } - } - } - - Ok(Self { - block_root: payload.message.beacon_block_root, - block, - payload, - column_data, - payload_available_timestamp: None, - spec: spec.clone(), - }) - } - - pub fn payload(&self) -> &SignedExecutionPayloadEnvelope { - &self.payload - } - pub fn payload_cloned(&self) -> Arc> { - self.payload.clone() - } - - pub fn payload_available_timestamp(&self) -> Option { - self.payload_available_timestamp - } - - pub fn data(&self) -> &AvailablePayloadData { - &self.column_data - } - - pub fn block_root(&self) -> Hash256 { - self.block_root - } - - #[allow(clippy::type_complexity)] - pub fn deconstruct( - self, - ) -> ( - Hash256, - Arc>, - AvailablePayloadData, - ) { - let AvailablePayload { - block_root, - payload, - column_data, - .. - } = self; - (block_root, payload, column_data) - } - - /// Only used for testing - pub fn __clone_without_recv(&self) -> Self { - Self { - block_root: self.block_root, - payload: self.payload.clone(), - block: self.block.clone(), - column_data: match &self.column_data { - AvailablePayloadData::NoData => AvailablePayloadData::NoData, - AvailablePayloadData::DataColumns(data_columns) => { - AvailablePayloadData::DataColumns(data_columns.clone()) - } - }, - payload_available_timestamp: self.payload_available_timestamp, - spec: self.spec.clone(), - } - } -} - -#[derive(Debug)] -pub enum MaybeAvailablePayload { - /// This payload is fully available. - Available(AvailablePayload), - /// This variant is not fully available and requires blobs to become fully available. - AvailabilityPending { - block_root: Hash256, - payload: Arc>, - }, -} - -impl MaybeAvailablePayload { - pub fn block_cloned(&self) -> Arc> { - match self { - Self::Available(payload) => payload.payload_cloned(), - Self::AvailabilityPending { payload, .. } => payload.clone(), - } - } -} - -// #[cfg(test)] -// mod test { -// use super::*; -// use crate::CustodyContext; -// use crate::block_verification_types::RpcBlock; -// use crate::custody_context::NodeCustodyType; -// use crate::data_column_verification::CustodyDataColumn; -// use crate::test_utils::{ -// EphemeralHarnessType, NumBlobs, generate_data_column_indices_rand_order, -// generate_rand_block_and_data_columns, get_kzg, -// }; -// use rand::SeedableRng; -// use rand::prelude::StdRng; -// use slot_clock::{SlotClock, TestingSlotClock}; -// use std::collections::HashSet; -// use std::sync::Arc; -// use std::time::Duration; -// use store::HotColdDB; -// use types::data::DataColumn; -// use types::{ChainSpec, ColumnIndex, EthSpec, ForkName, MainnetEthSpec, Slot}; - -// type E = MainnetEthSpec; -// type T = EphemeralHarnessType; - -// /// Test to verify any extra RPC columns received that are not part of the "effective" CGC for -// /// the slot are excluded from import. -// #[test] -// fn should_exclude_rpc_columns_not_required_for_sampling() { -// // SETUP -// let spec = Arc::new(ForkName::Fulu.make_genesis_spec(E::default_spec())); -// let mut rng = StdRng::seed_from_u64(0xDEADBEEF0BAD5EEDu64); - -// let da_checker = new_da_checker(spec.clone()); -// let custody_context = &da_checker.custody_context; - -// // GIVEN a single 32 ETH validator is attached slot 0 -// let epoch = Epoch::new(0); -// let validator_0 = 0; -// custody_context.register_validators( -// vec![(validator_0, 32_000_000_000)], -// epoch.start_slot(E::slots_per_epoch()), -// &spec, -// ); -// assert_eq!( -// custody_context.num_of_data_columns_to_sample(epoch, &spec), -// spec.validator_custody_requirement as usize, -// "sampling size should be the minimal custody requirement == 8" -// ); - -// // WHEN additional attached validators result in a CGC increase to 10 at the end slot of the same epoch -// let validator_1 = 1; -// let cgc_change_slot = epoch.end_slot(E::slots_per_epoch()); -// custody_context.register_validators( -// vec![(validator_1, 32_000_000_000 * 9)], -// cgc_change_slot, -// &spec, -// ); -// // AND custody columns (8) and any new extra columns (2) are received via RPC responses. -// // NOTE: block lookup uses the **latest** CGC (10) instead of the effective CGC (8) as the slot is unknown. -// let (_, data_columns) = generate_rand_block_and_data_columns::( -// ForkName::Fulu, -// NumBlobs::Number(1), -// &mut rng, -// &spec, -// ); -// let block_root = Hash256::random(); -// let custody_columns = custody_context.custody_columns_for_epoch(None, &spec); -// let requested_columns = &custody_columns[..10]; -// da_checker -// .put_rpc_custody_columns( -// block_root, -// cgc_change_slot, -// data_columns -// .into_iter() -// .filter(|d| requested_columns.contains(&d.index)) -// .collect(), -// ) -// .expect("should put rpc custody columns"); - -// // THEN the sampling size for the end slot of the same epoch remains unchanged -// let sampling_columns = custody_context.sampling_columns_for_epoch(epoch, &spec); -// assert_eq!( -// sampling_columns.len(), -// spec.validator_custody_requirement as usize // 8 -// ); -// // AND any extra columns received via RPC responses are excluded from import. -// let actual_cached: HashSet = da_checker -// .cached_data_column_indexes(&block_root) -// .expect("should have cached data columns") -// .into_iter() -// .collect(); -// let expected_sampling_columns = sampling_columns.iter().copied().collect::>(); -// assert_eq!( -// actual_cached, expected_sampling_columns, -// "should cache only the effective sampling columns" -// ); -// assert!( -// actual_cached.len() < requested_columns.len(), -// "extra columns should be excluded" -// ) -// } - -// /// Test to verify any extra gossip columns received that are not part of the "effective" CGC for -// /// the slot are excluded from import. -// #[test] -// fn should_exclude_gossip_columns_not_required_for_sampling() { -// // SETUP -// let spec = Arc::new(ForkName::Fulu.make_genesis_spec(E::default_spec())); -// let mut rng = StdRng::seed_from_u64(0xDEADBEEF0BAD5EEDu64); - -// let da_checker = new_da_checker(spec.clone()); -// let custody_context = &da_checker.custody_context; - -// // GIVEN a single 32 ETH validator is attached slot 0 -// let epoch = Epoch::new(0); -// let validator_0 = 0; -// custody_context.register_validators( -// vec![(validator_0, 32_000_000_000)], -// epoch.start_slot(E::slots_per_epoch()), -// &spec, -// ); -// assert_eq!( -// custody_context.num_of_data_columns_to_sample(epoch, &spec), -// spec.validator_custody_requirement as usize, -// "sampling size should be the minimal custody requirement == 8" -// ); - -// // WHEN additional attached validators result in a CGC increase to 10 at the end slot of the same epoch -// let validator_1 = 1; -// let cgc_change_slot = epoch.end_slot(E::slots_per_epoch()); -// custody_context.register_validators( -// vec![(validator_1, 32_000_000_000 * 9)], -// cgc_change_slot, -// &spec, -// ); -// // AND custody columns (8) and any new extra columns (2) are received via gossip. -// // NOTE: CGC updates results in new topics subscriptions immediately, and extra columns may start to -// // arrive via gossip. -// let (_, data_columns) = generate_rand_block_and_data_columns::( -// ForkName::Fulu, -// NumBlobs::Number(1), -// &mut rng, -// &spec, -// ); -// let block_root = Hash256::random(); -// let custody_columns = custody_context.custody_columns_for_epoch(None, &spec); -// let requested_columns = &custody_columns[..10]; -// let gossip_columns = data_columns -// .into_iter() -// .filter(|d| requested_columns.contains(&d.index)) -// .map(GossipVerifiedDataColumn::::__new_for_testing) -// .collect::>(); -// da_checker -// .put_gossip_verified_data_columns(block_root, cgc_change_slot, gossip_columns) -// .expect("should put gossip custody columns"); - -// // THEN the sampling size for the end slot of the same epoch remains unchanged -// let sampling_columns = custody_context.sampling_columns_for_epoch(epoch, &spec); -// assert_eq!( -// sampling_columns.len(), -// spec.validator_custody_requirement as usize // 8 -// ); -// // AND any extra columns received via gossip responses are excluded from import. -// let actual_cached: HashSet = da_checker -// .cached_data_column_indexes(&block_root) -// .expect("should have cached data columns") -// .into_iter() -// .collect(); -// let expected_sampling_columns = sampling_columns.iter().copied().collect::>(); -// assert_eq!( -// actual_cached, expected_sampling_columns, -// "should cache only the effective sampling columns" -// ); -// assert!( -// actual_cached.len() < requested_columns.len(), -// "extra columns should be excluded" -// ) -// } - -// /// Regression test for KZG verification truncation bug (https://github.com/sigp/lighthouse/pull/7927) -// #[test] -// fn verify_kzg_for_rpc_blocks_should_not_truncate_data_columns() { -// let spec = Arc::new(ForkName::Fulu.make_genesis_spec(E::default_spec())); -// let mut rng = StdRng::seed_from_u64(0xDEADBEEF0BAD5EEDu64); -// let da_checker = new_da_checker(spec.clone()); - -// // GIVEN multiple RPC blocks with data columns totalling more than 128 -// let blocks_with_columns = (0..2) -// .map(|index| { -// let (block, data_columns) = generate_rand_block_and_data_columns::( -// ForkName::Fulu, -// NumBlobs::Number(1), -// &mut rng, -// &spec, -// ); - -// let custody_columns = if index == 0 { -// // 128 valid data columns in the first block -// data_columns -// } else { -// // invalid data columns in the second block -// data_columns -// .into_iter() -// .map(|d| { -// let invalid_sidecar = DataColumnSidecar { -// column: DataColumn::::empty(), -// ..d.as_ref().clone() -// }; -// CustodyDataColumn::from_asserted_custody(Arc::new(invalid_sidecar)) -// .as_data_column() -// .clone() -// }) -// .collect::>() -// }; - -// let block_data = AvailableBlockData::new_with_data_columns(custody_columns); -// let da_checker = Arc::new(new_da_checker(spec.clone())); -// RpcBlock::new(Arc::new(block), Some(block_data), &da_checker, spec.clone()) -// .expect("should create RPC block with custody columns") -// }) -// .collect::>(); - -// let available_blocks = blocks_with_columns -// .iter() -// .filter_map(|block| match block { -// RpcBlock::FullyAvailable(available_block) => Some(available_block.clone()), -// RpcBlock::BlockOnly { .. } => None, -// }) -// .collect::>(); - -// // WHEN verifying all blocks together (totalling 256 data columns) -// let verification_result = -// da_checker.batch_verify_kzg_for_available_blocks(&available_blocks); - -// // THEN batch block verification should fail due to 128 invalid columns in the second block -// verification_result.expect_err("should have failed to verify blocks"); -// } - -// #[test] -// fn should_exclude_reconstructed_columns_not_required_for_sampling() { -// // SETUP -// let spec = Arc::new(ForkName::Fulu.make_genesis_spec(E::default_spec())); -// let mut rng = StdRng::seed_from_u64(0xDEADBEEF0BAD5EEDu64); - -// let da_checker = new_da_checker(spec.clone()); -// let custody_context = &da_checker.custody_context; - -// // Set custody requirement to 65 columns (enough to trigger reconstruction) -// let epoch = Epoch::new(1); -// custody_context.register_validators( -// vec![(0, 2_048_000_000_000), (1, 32_000_000_000)], // 64 + 1 -// Slot::new(0), -// &spec, -// ); -// let sampling_requirement = custody_context.num_of_data_columns_to_sample(epoch, &spec); -// assert_eq!( -// sampling_requirement, 65, -// "sampling requirement should be 65" -// ); - -// let (block, data_columns) = generate_rand_block_and_data_columns::( -// ForkName::Fulu, -// NumBlobs::Number(1), -// &mut rng, -// &spec, -// ); -// let block_root = Hash256::random(); -// // Add the block to the DA checker -// da_checker -// .availability_cache -// .put_pre_execution_block(block_root, Arc::new(block), BlockImportSource::Gossip) -// .expect("should put block"); - -// // Add 64 columns to the da checker (enough to be able to reconstruct) -// // Order by all_column_indices_ordered, then take first 64 -// let custody_columns = custody_context.custody_columns_for_epoch(None, &spec); -// let custody_columns = custody_columns -// .iter() -// .filter_map(|&col_idx| data_columns.iter().find(|d| d.index == col_idx).cloned()) -// .take(64) -// .map(|d| { -// KzgVerifiedCustodyDataColumn::from_asserted_custody( -// KzgVerifiedDataColumn::__new_for_testing(d), -// ) -// }) -// .collect::>(); - -// da_checker -// .availability_cache -// .put_kzg_verified_data_columns(block_root, custody_columns) -// .expect("should put custody columns"); - -// // Try reconstrucing -// let reconstruction_result = da_checker -// .reconstruct_data_columns(&block_root) -// .expect("should reconstruct columns"); - -// // Reconstruction should succeed -// let (_availability, reconstructed_columns) = match reconstruction_result { -// DataColumnReconstructionResult::Success(result) => result, -// e => { -// panic!("Expected successful reconstruction {:?}", e); -// } -// }; - -// // Remaining 64 columns should be reconstructed -// assert_eq!( -// reconstructed_columns.len(), -// sampling_requirement - spec.number_of_custody_groups as usize / 2, -// "should reconstruct the remaining 1 columns" -// ); - -// // Only the columns required for custody (65) should be imported into the cache -// let sampling_columns = custody_context.sampling_columns_for_epoch(epoch, &spec); -// let actual_cached: HashSet = da_checker -// .cached_data_column_indexes(&block_root) -// .expect("should have cached data columns") -// .into_iter() -// .collect(); -// let expected_sampling_columns = sampling_columns.iter().copied().collect::>(); -// assert_eq!( -// actual_cached, expected_sampling_columns, -// "should cache only the required custody columns, not all reconstructed columns" -// ); -// } - -// fn new_da_checker(spec: Arc) -> DataAvailabilityChecker { -// let slot_clock = TestingSlotClock::new( -// Slot::new(0), -// Duration::from_secs(0), -// Duration::from_secs(spec.seconds_per_slot), -// ); -// let kzg = get_kzg(&spec); -// let store = Arc::new(HotColdDB::open_ephemeral(<_>::default(), spec.clone()).unwrap()); -// let ordered_custody_column_indices = generate_data_column_indices_rand_order::(); -// let custody_context = Arc::new(CustodyContext::new( -// NodeCustodyType::Fullnode, -// ordered_custody_column_indices, -// &spec, -// )); -// let complete_blob_backfill = false; -// DataAvailabilityChecker::new( -// complete_blob_backfill, -// slot_clock, -// kzg, -// store, -// custody_context, -// spec, -// ) -// .expect("should initialise data availability checker") -// } -// } diff --git a/beacon_node/beacon_chain/src/data_availability_checker_v2/overflow_lru_cache.rs b/beacon_node/beacon_chain/src/data_availability_checker_v2/overflow_lru_cache.rs index fe5c0860f0..b88378648e 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker_v2/overflow_lru_cache.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker_v2/overflow_lru_cache.rs @@ -1,66 +1,29 @@ use crate::BeaconChainTypes; use crate::CustodyContext; use crate::data_availability_checker::AvailabilityCheckError; -use crate::data_availability_checker_v2::{Availability, AvailablePayload, AvailablePayloadData}; +use crate::data_availability_checker_v2::Availability; use crate::data_column_verification::KzgVerifiedCustodyDataColumn; -use crate::payload_verification_types::PayloadProcessStatus; -use crate::payload_verification_types::{ - AvailabilityPendingExecutedPayload, AvailableExecutedPayload, -}; use lru::LruCache; use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard, RwLockWriteGuard}; use std::cmp::Ordering; use std::num::NonZeroUsize; use std::sync::Arc; use tracing::{Span, debug, debug_span}; -use types::kzg_ext::KzgCommitments; use types::{ - BlockImportSource, ChainSpec, ColumnIndex, DataColumnSidecar, DataColumnSidecarList, Epoch, - EthSpec, Hash256, SignedBeaconBlock, SignedExecutionPayloadEnvelope, + ChainSpec, ColumnIndex, DataColumnSidecar, DataColumnSidecarList, Epoch, EthSpec, Hash256, + SignedBeaconBlock, }; -#[derive(Clone)] -pub enum CachedPayload { - PreExecution(Arc>, BlockImportSource), - Executed(Box>), -} - -#[allow(dead_code)] -impl CachedPayload { - pub fn get_commitments(&self) -> KzgCommitments { - let payload = self.as_payload(); - payload.message.blob_kzg_commitments.clone() - } - - fn as_payload(&self) -> &SignedExecutionPayloadEnvelope { - match self { - CachedPayload::PreExecution(p, _) => p, - CachedPayload::Executed(p) => &p.payload, - } - } - - pub fn num_blobs_expected(&self) -> usize { - self.as_payload().message.blob_kzg_commitments.len() - } - - pub fn payload_cloned(&self) -> Arc> { - match self { - CachedPayload::PreExecution(p, _) => p.clone(), - CachedPayload::Executed(p) => p.payload.clone(), - } - } -} - -/// This represents the components of a partially available payload +/// This represents the components of a payload pending data availability. /// /// The columns are all gossip and kzg verified. -/// The payload has completed all verifications except the availability check. -#[allow(dead_code)] +/// The payload is considered "available" when all required columns are received. pub struct PendingComponents { + /// The block root is stored for tracing context in the span. + #[allow(dead_code)] pub block_root: Hash256, pub block: Option>>, pub verified_data_columns: Vec>, - pub payload: Option>, pub reconstruction_started: bool, span: Span, } @@ -85,23 +48,6 @@ impl PendingComponents { .collect() } - /// Inserts an executed payload into the cache. - pub fn insert_executed_payload(&mut self, payload: AvailabilityPendingExecutedPayload) { - self.payload = Some(CachedPayload::Executed(Box::new(payload))) - } - - /// Inserts a pre-execution payload into the cache. - /// This does NOT override an existing executed payload. - pub fn insert_pre_execution_payload( - &mut self, - payload: Arc>, - source: BlockImportSource, - ) { - if self.payload.is_none() { - self.payload = Some(CachedPayload::PreExecution(payload, source)) - } - } - /// Merges a given set of data columns into the cache. fn merge_data_columns>>( &mut self, @@ -116,94 +62,80 @@ impl PendingComponents { Ok(()) } - /// Inserts a new payload. - pub fn merge_payload(&mut self, payload: AvailabilityPendingExecutedPayload) { - self.insert_executed_payload(payload); + /// Inserts a block into the cache. + pub fn insert_block(&mut self, block: Arc>) { + self.block = Some(block); } - /// Returns Some if the payload has received all its required data for import. The return value - /// must be persisted in the DB along with the payload. + /// Returns the number of blobs expected for this block by reading the bid's kzg commitments. + /// Returns an error if the block is not cached or not a Gloas block. + pub fn num_blobs_expected(&self) -> Result { + let block = self.block.as_ref().ok_or_else(|| { + AvailabilityCheckError::Unexpected("No block available".to_string()) + })?; + + let bid = block + .message() + .body() + .signed_execution_payload_bid() + .map_err(|_| { + AvailabilityCheckError::Unexpected( + "Block does not have execution payload bid (not a Gloas block?)".to_string(), + ) + })?; + + Ok(bid.message.blob_kzg_commitments.len()) + } + + /// Returns Some if all required data columns have been received. pub fn make_available( &self, - spec: &Arc, num_expected_columns: usize, - ) -> Result>, AvailabilityCheckError> { - let Some(CachedPayload::Executed(executed_payload)) = &self.payload else { - // Payload not available yet + ) -> Result>, AvailabilityCheckError> { + // Check if we have a block - if not, still waiting + if self.block.is_none() { return Ok(None); - }; + } - let num_expected_blobs = executed_payload.num_blobs_expected(); - let column_data = if num_expected_blobs == 0 { - Some(AvailablePayloadData::NoData) - } else { - let num_received_columns = self.verified_data_columns.len(); - match num_received_columns.cmp(&num_expected_columns) { - Ordering::Greater => { - // Should never happen - return Err(AvailabilityCheckError::Unexpected(format!( - "too many columns got {num_received_columns} expected {num_expected_columns}" - ))); - } - Ordering::Equal => { - // We have enough columns - let data_columns = self - .verified_data_columns - .iter() - .map(|d| d.clone().into_inner()) - .collect::>(); - Some(AvailablePayloadData::DataColumns(data_columns)) - } - Ordering::Less => { - // Not enough data columns received yet - None - } + // Get the number of blobs expected from the block's bid + // This will error if the block doesn't have a bid (not Gloas) + let num_expected_blobs = self.num_blobs_expected()?; + + if num_expected_blobs == 0 { + // No blobs expected, data is available (empty) + self.span.in_scope(|| { + debug!("Block has no blobs, data is available"); + }); + return Ok(Some(vec![])); + } + + let num_received_columns = self.verified_data_columns.len(); + match num_received_columns.cmp(&num_expected_columns) { + Ordering::Greater => { + // Should never happen + Err(AvailabilityCheckError::Unexpected(format!( + "too many columns got {num_received_columns} expected {num_expected_columns}" + ))) } - }; + Ordering::Equal => { + // We have enough columns + let data_columns = self + .verified_data_columns + .iter() + .map(|d| d.clone().into_inner()) + .collect::>(); - // Payload's data not available yet - let Some(column_data) = column_data else { - return Ok(None); - }; + self.span.in_scope(|| { + debug!("All data columns received, data is available"); + }); - let Some(block) = self.block.clone() else { - // This should never happen - return Err(AvailabilityCheckError::Unexpected( - "Block doesn't exist for the payload being made available".to_owned(), - )); - }; - - // Payload is available, construct `AvailableExecutedPayload` - - let payload_available_timestamp = match column_data { - AvailablePayloadData::NoData => None, - // TODO(gloas): fix with https://github.com/sigp/lighthouse/issues/7477 - AvailablePayloadData::DataColumns(_) => None, - }; - - let AvailabilityPendingExecutedPayload { - payload, - import_data, - payload_verification_outcome, - } = executed_payload.as_ref().clone(); - - let available_payload = AvailablePayload { - block_root: payload.message.beacon_block_root, - payload, - block, - column_data, - payload_available_timestamp, - spec: spec.clone(), - }; - - self.span.in_scope(|| { - debug!("Payload and all data components are available"); - }); - Ok(Some(AvailableExecutedPayload::new( - available_payload, - import_data, - payload_verification_outcome, - ))) + Ok(Some(data_columns)) + } + Ordering::Less => { + // Not enough data columns received yet + Ok(None) + } + } } /// Returns an empty `PendingComponents` object with the given block root. @@ -214,25 +146,16 @@ impl PendingComponents { block_root, block: None, verified_data_columns: vec![], - payload: None, reconstruction_started: false, span, } } - /// Returns the epoch of: - /// - The payload if it is cached - /// Otherwise, returns None + /// Returns the epoch of the block or first data column, if available. pub fn epoch(&self) -> Option { - // Get epoch from cached payload - if let Some(payload) = &self.payload { - return Some( - payload - .as_payload() - .message - .slot - .epoch(E::slots_per_epoch()), - ); + // Get epoch from block + if let Some(block) = &self.block { + return Some(block.slot().epoch(E::slots_per_epoch())); } // Or, get epoch from first data column @@ -244,10 +167,10 @@ impl PendingComponents { } pub fn status_str(&self, num_expected_columns: usize) -> String { - let payload_count = if self.payload.is_some() { 1 } else { 0 }; + let block_status = if self.block.is_some() { "yes" } else { "no" }; format!( - "payload {} data_columns {}/{}", - payload_count, + "block {} data_columns {}/{}", + block_status, self.verified_data_columns.len(), num_expected_columns ) @@ -285,29 +208,6 @@ impl DataAvailabilityCheckerInner { }) } - /// Returns true if the payload with the given block root is known, without altering the LRU ordering - pub fn get_cached_payload( - &self, - block_root: &Hash256, - ) -> Option> { - self.critical - .read() - .peek(block_root) - .and_then(|pending_components| { - pending_components - .payload - .as_ref() - .map(|payload| match payload { - CachedPayload::PreExecution(p, source) => { - PayloadProcessStatus::NotValidated(p.clone(), *source) - } - CachedPayload::Executed(p) => { - PayloadProcessStatus::ExecutionValidated(p.payload.clone()) - } - }) - }) - } - /// Fetch data columns of a given `block_root` from the cache without affecting the LRU ordering pub fn peek_data_columns( &self, @@ -333,6 +233,33 @@ impl DataAvailabilityCheckerInner { f(self.critical.read().peek(block_root)) } + /// Insert a block into the cache and check if data becomes available. + pub fn put_block( + &self, + block_root: Hash256, + block: Arc>, + ) -> Result, AvailabilityCheckError> { + let epoch = block.slot().epoch(T::EthSpec::slots_per_epoch()); + + let pending_components = + self.update_or_insert_pending_components(block_root, |pending_components| { + pending_components.insert_block(block); + Ok(()) + })?; + + let num_expected_columns = self.get_num_expected_columns(epoch); + + pending_components.span.in_scope(|| { + debug!( + component = "block", + status = pending_components.status_str(num_expected_columns), + "Component added to data availability checker" + ); + }); + + self.check_availability(block_root, pending_components, num_expected_columns) + } + #[allow(clippy::type_complexity)] pub fn put_kzg_verified_data_columns< I: IntoIterator>, @@ -344,12 +271,12 @@ impl DataAvailabilityCheckerInner { let mut kzg_verified_data_columns = kzg_verified_data_columns.into_iter().peekable(); let Some(epoch) = kzg_verified_data_columns .peek() - .map(|verified_blob| verified_blob.as_data_column().epoch()) + .map(|verified_col| verified_col.as_data_column().epoch()) else { // No columns are processed. This can occur if all received columns were filtered out // before this point, e.g. due to a CGC change that caused extra columns to be downloaded - // // before the new CGC took effect. - // Return `Ok` without marking the payload as available. + // before the new CGC took effect. + // Return `Ok` without marking the block as available. return Ok(Availability::MissingComponents(block_root)); }; @@ -368,36 +295,30 @@ impl DataAvailabilityCheckerInner { ); }); - self.check_availability_and_cache_components( - block_root, - pending_components, - num_expected_columns, - ) + self.check_availability(block_root, pending_components, num_expected_columns) } - fn check_availability_and_cache_components( + fn check_availability( &self, block_root: Hash256, pending_components: MappedRwLockReadGuard<'_, PendingComponents>, num_expected_columns: usize, ) -> Result, AvailabilityCheckError> { - if let Some(available_payload) = - pending_components.make_available(&self.spec, num_expected_columns)? - { + if let Some(columns) = pending_components.make_available(num_expected_columns)? { // Explicitly drop read lock before acquiring write lock drop(pending_components); if let Some(components) = self.critical.write().get_mut(&block_root) { - // Clean up span now that payload is available + // Clean up span now that data is available components.span = Span::none(); } // We never remove the pending components manually to avoid race conditions. - // This ensures components remain available during and right after payload import, - // preventing a race condition where a component was removed after the payload was + // This ensures components remain available during and right after block import, + // preventing a race condition where a component was removed after the block was // imported, but re-inserted immediately, causing partial pending components to be // stored and served to peers. // Components are only removed via LRU eviction as finality advances. - Ok(Availability::Available(Box::new(available_payload))) + Ok(Availability::Available(Box::new((block_root, columns)))) } else { Ok(Availability::MissingComponents(block_root)) } @@ -437,7 +358,7 @@ impl DataAvailabilityCheckerInner { /// Potentially trigger reconstruction if all the following satisfy: /// - Our custody requirement is more than 50% of total columns, /// - We haven't received all required columns - /// - Reconstruction hasn't been started for the payload + /// - Reconstruction hasn't been started for the block /// /// If reconstruction is required, returns `PendingComponents` which contains the /// components to be used as inputs to reconstruction, otherwise returns a `reason`. @@ -447,8 +368,8 @@ impl DataAvailabilityCheckerInner { ) -> ReconstructColumnsDecision { let mut write_lock = self.critical.write(); let Some(pending_components) = write_lock.get_mut(block_root) else { - // Payload may have been imported as it does not exist in availability cache. - return ReconstructColumnsDecision::No("payload already imported"); + // Block may have been imported as it does not exist in availability cache. + return ReconstructColumnsDecision::No("block already imported"); }; let Some(epoch) = pending_components @@ -489,81 +410,6 @@ impl DataAvailabilityCheckerInner { } } - /// Inserts a pre executed payload into the cache. - /// - This does NOT trigger the availability check as the payload still needs to be executed. - /// - This does NOT override an existing cached payload to avoid overwriting an executed payload. - pub fn put_pre_execution_payload( - &self, - block_root: Hash256, - payload: Arc>, - source: BlockImportSource, - ) -> Result<(), AvailabilityCheckError> { - let epoch = payload.message.slot.epoch(T::EthSpec::slots_per_epoch()); - let pending_components = - self.update_or_insert_pending_components(block_root, |pending_components| { - pending_components.insert_pre_execution_payload(payload, source); - Ok(()) - })?; - - let num_expected_columns_opt = self.get_num_expected_columns(epoch); - - pending_components.span.in_scope(|| { - debug!( - component = "pre execution payload", - status = pending_components.status_str(num_expected_columns_opt), - "Component added to data availability checker" - ); - }); - - Ok(()) - } - - /// Removes a pre-execution payload from the cache. - /// This does NOT remove an existing executed payload. - pub fn remove_pre_execution_payload(&self, block_root: &Hash256) { - // The read lock is immediately dropped so we can safely remove the payload from the cache. - if let Some(PayloadProcessStatus::NotValidated(_, _)) = self.get_cached_payload(block_root) - { - self.critical.write().pop(block_root); - } - } - - /// Check if we have all the columns for a payload. If we do, return the Availability variant that - /// triggers import of the payload. - pub fn put_executed_payload( - &self, - executed_payload: AvailabilityPendingExecutedPayload, - ) -> Result, AvailabilityCheckError> { - let epoch = executed_payload - .as_payload() - .message - .slot - .epoch(T::EthSpec::slots_per_epoch()); - let block_root = executed_payload.payload.message.beacon_block_root; - - let pending_components = - self.update_or_insert_pending_components(block_root, |pending_components| { - pending_components.merge_payload(executed_payload); - Ok(()) - })?; - - let num_expected_columns = self.get_num_expected_columns(epoch); - - pending_components.span.in_scope(|| { - debug!( - component = "payload", - status = pending_components.status_str(num_expected_columns), - "Component added to data availability checker" - ); - }); - - self.check_availability_and_cache_components( - block_root, - pending_components, - num_expected_columns, - ) - } - fn get_num_expected_columns(&self, epoch: Epoch) -> usize { self.custody_context .num_of_data_columns_to_sample(epoch, &self.spec) @@ -571,7 +417,7 @@ impl DataAvailabilityCheckerInner { /// maintain the cache pub fn do_maintenance(&self, cutoff_epoch: Epoch) -> Result<(), AvailabilityCheckError> { - // Collect keys of pending payloads from a previous epoch to cutoff + // Collect keys of pending blocks from a previous epoch to cutoff let mut write_lock = self.critical.write(); let mut keys_to_remove = vec![]; for (key, value) in write_lock.iter() { @@ -590,31 +436,111 @@ impl DataAvailabilityCheckerInner { } /// Number of pending component entries in memory in the cache. - pub fn payload_cache_size(&self) -> usize { + pub fn block_cache_size(&self) -> usize { self.critical.read().len() } } #[cfg(test)] -mod test { +mod pending_components_tests { + use super::*; + use types::MinimalEthSpec; + + type E = MinimalEthSpec; + + #[test] + fn test_empty_pending_components() { + let block_root = Hash256::random(); + let components = PendingComponents::::empty(block_root); + + assert_eq!(components.block_root, block_root); + assert!(components.block.is_none()); + assert!(components.verified_data_columns.is_empty()); + assert!(!components.reconstruction_started); + assert!(components.epoch().is_none()); + } + + #[test] + fn test_get_cached_data_columns_indices_empty() { + let block_root = Hash256::random(); + let components = PendingComponents::::empty(block_root); + + let indices = components.get_cached_data_columns_indices(); + assert!(indices.is_empty()); + } + + #[test] + fn test_status_str_no_block() { + let block_root = Hash256::random(); + let components = PendingComponents::::empty(block_root); + + let status = components.status_str(10); + assert_eq!(status, "block no data_columns 0/10"); + } + + #[test] + fn test_num_blobs_expected_no_block() { + let block_root = Hash256::random(); + let components = PendingComponents::::empty(block_root); + + let result = components.num_blobs_expected(); + assert!(result.is_err()); + // Error should be AvailabilityCheckError::Unexpected + assert!(matches!( + result.unwrap_err(), + AvailabilityCheckError::Unexpected(_) + )); + } + + #[test] + fn test_make_available_no_block_returns_none() { + let block_root = Hash256::random(); + let components = PendingComponents::::empty(block_root); + + // Without a block, make_available should return Ok(None) + let result = components.make_available(10); + assert!(result.is_ok()); + assert!(result.unwrap().is_none()); + } +} + +#[cfg(test)] +mod data_availability_checker_tests { use super::*; - use crate::data_column_verification::{GossipVerifiedDataColumn, KzgVerifiedCustodyDataColumn}; - use crate::test_utils::generate_data_column_indices_rand_order; + use crate::data_column_verification::{KzgVerifiedCustodyDataColumn, KzgVerifiedDataColumn}; + use crate::test_utils::{ + generate_data_column_indices_rand_order, test_spec, NumBlobs, + generate_rand_block_and_data_columns, + }; use crate::{ - block_verification_types::AsBlock, custody_context::NodeCustodyType, - test_utils::{BaseHarnessType, BeaconChainHarness, DiskHarnessType}, + test_utils::{BeaconChainHarness, DiskHarnessType}, }; use logging::create_test_tracing_subscriber; - use std::collections::HashSet; - use store::{HotColdDB, ItemStore, StoreConfig, database::interface::BeaconNodeBackend}; + use store::{HotColdDB, StoreConfig, database::interface::BeaconNodeBackend}; use tempfile::{TempDir, tempdir}; - use types::MinimalEthSpec; + use types::{ForkName, MinimalEthSpec, Slot}; use types::new_non_zero_usize; + use rand::SeedableRng; + use rand::rngs::StdRng; + + type E = MinimalEthSpec; const LOW_VALIDATOR_COUNT: usize = 32; + fn gloas_spec() -> Arc { + let mut spec = E::default_spec(); + spec.altair_fork_epoch = Some(Epoch::new(0)); + spec.bellatrix_fork_epoch = Some(Epoch::new(0)); + spec.capella_fork_epoch = Some(Epoch::new(0)); + spec.deneb_fork_epoch = Some(Epoch::new(0)); + spec.electra_fork_epoch = Some(Epoch::new(0)); + spec.fulu_fork_epoch = Some(Epoch::new(0)); + spec.gloas_fork_epoch = Some(Epoch::new(0)); + Arc::new(spec) + } + fn get_store_with_spec( db_path: &TempDir, spec: Arc, @@ -634,26 +560,11 @@ mod test { ) .expect("disk store should initialize") } + async fn get_gloas_chain( db_path: &TempDir, ) -> BeaconChainHarness> { - let altair_fork_epoch = Epoch::new(0); - let bellatrix_fork_epoch = Epoch::new(0); - let capella_fork_epoch = Epoch::new(0); - let deneb_fork_epoch = Epoch::new(0); - let electra_fork_epoch = Epoch::new(0); - let fulu_fork_epoch = Epoch::new(0); - let gloas_fork_epoch = Epoch::new(0); - - let mut spec = E::default_spec(); - spec.altair_fork_epoch = Some(altair_fork_epoch); - spec.bellatrix_fork_epoch = Some(bellatrix_fork_epoch); - spec.capella_fork_epoch = Some(capella_fork_epoch); - spec.deneb_fork_epoch = Some(deneb_fork_epoch); - spec.electra_fork_epoch = Some(electra_fork_epoch); - spec.fulu_fork_epoch = Some(fulu_fork_epoch); - spec.gloas_fork_epoch = Some(gloas_fork_epoch); - let spec = Arc::new(spec); + let spec = gloas_spec::(); let chain_store = get_store_with_spec::(db_path, spec.clone()); let validators_keypairs = @@ -666,33 +577,12 @@ mod test { .build(); // go to gloas slot - let gloas_fork_slot = gloas_fork_epoch.start_slot(E::slots_per_epoch()); + let gloas_fork_slot = Slot::new(0); harness.extend_to_slot(gloas_fork_slot).await; - let gloas_head = &harness.chain.head_snapshot().beacon_block; - assert!(gloas_head.as_gloas().is_ok()); - assert_eq!(gloas_head.slot(), gloas_fork_slot); - assert!( - gloas_head.message().body().execution_payload().is_err(), - "Gloas block has no payload" - ); harness } - async fn availability_pending_payload( - _harness: &BeaconChainHarness>, - ) -> ( - AvailabilityPendingExecutedPayload, - Vec>>, - ) - where - E: EthSpec, - Hot: ItemStore, - Cold: ItemStore, - { - todo!() - } - - async fn setup_harness_and_cache( + async fn setup_harness_and_cache( capacity: usize, ) -> ( BeaconChainHarness>, @@ -700,7 +590,6 @@ mod test { TempDir, ) where - E: EthSpec, T: BeaconChainTypes< HotStore = BeaconNodeBackend, ColdStore = BeaconNodeBackend, @@ -711,7 +600,6 @@ mod test { let chain_db_path = tempdir().expect("should get temp dir"); let harness = get_gloas_chain(&chain_db_path).await; let spec = harness.spec.clone(); - let _test_store = harness.chain.store.clone(); let capacity_non_zero = new_non_zero_usize(capacity); let custody_context = Arc::new(CustodyContext::new( NodeCustodyType::Fullnode, @@ -725,216 +613,145 @@ mod test { (harness, cache, chain_db_path) } - #[tokio::test] - #[ignore] // TODO(gloas): Implement availability_pending_payload - async fn overflow_cache_test_insert_components() { - type E = MinimalEthSpec; - type T = DiskHarnessType; - let capacity = 4; - let (harness, cache, _path) = setup_harness_and_cache::(capacity).await; - - let (pending_payload, columns) = availability_pending_payload(&harness).await; - let root = pending_payload.as_payload().beacon_block_root(); - - let mut expected_column_indices = harness - .chain - .data_availability_checker - .custody_context() - .custody_columns_for_epoch(None, &harness.chain.spec) - .iter() - .collect::>(); - let columns_expected = pending_payload.num_blobs_expected(); - - assert_eq!( - columns.len(), - expected_column_indices.len(), - "should have expected number of blobs" - ); - assert!(cache.critical.read().is_empty(), "cache should be empty"); - let availability = cache - .put_executed_payload(pending_payload) - .expect("should put payload"); - if columns_expected == 0 { - assert!( - matches!(availability, Availability::Available(_)), - "payload doesn't have columns, should be available" - ); - assert_eq!( - cache.critical.read().len(), - 1, - "cache should still have payload as it hasn't been imported yet" - ); - } else { - assert!( - matches!(availability, Availability::MissingComponents(_)), - "should be pending columns" - ); - assert_eq!( - cache.critical.read().len(), - 1, - "cache should have one payload" - ); - assert!( - cache.critical.read().peek(&root).is_some(), - "newly inserted payload should exist in memory" - ); - } - - let mut kzg_verified_columns = Vec::new(); - - for gossip_column in columns.into_iter() { - let col_index = gossip_column.index(); - kzg_verified_columns.push(KzgVerifiedCustodyDataColumn::from_asserted_custody( - gossip_column.into_inner(), - )); - let availability = cache - .put_kzg_verified_data_columns(root, kzg_verified_columns.clone().into_iter()) - .expect("should put column"); - - expected_column_indices.remove(&col_index); - - if expected_column_indices.is_empty() { - assert!(matches!(availability, Availability::Available(_))); - } else { - assert!(matches!(availability, Availability::MissingComponents(_))); - assert_eq!(cache.critical.read().len(), 1); - } - } - - let (pending_payload, columns) = availability_pending_payload(&harness).await; - let expected_column_indices = harness - .chain - .data_availability_checker - .custody_context() - .custody_columns_for_epoch(None, &harness.chain.spec) - .iter() - .collect::>(); - - assert_eq!( - columns.len(), - expected_column_indices.len(), - "should have expected number of columns" - ); - let root = pending_payload.as_payload().beacon_block_root(); - - let mut kzg_verified_columns = vec![]; - for gossip_column in columns { - kzg_verified_columns.push(KzgVerifiedCustodyDataColumn::from_asserted_custody( - gossip_column.into_inner(), - )); - let availability = cache - .put_kzg_verified_data_columns(root, kzg_verified_columns.clone()) - .expect("should put column"); - assert!( - matches!(availability, Availability::MissingComponents(_)), - "should be pending payload" - ); - assert_eq!( - cache.critical.read().len(), - 2, - "cache should have two payloads now" - ); - } - let availability = cache - .put_executed_payload(pending_payload) - .expect("should put payload"); - assert!( - matches!(availability, Availability::Available(_)), - "payload should be available: {:?}", - availability - ); - assert!( - cache.critical.read().len() == 2, - "cache should still have available payload" - ); - } -} - -#[cfg(test)] -mod pending_components_tests { - use super::*; - use crate::PayloadVerificationOutcome; - use crate::data_column_verification::KzgVerifiedDataColumn; - use crate::payload_verification_types::PayloadImportData; - use crate::test_utils::{NumBlobs, generate_rand_payload_and_columns, test_spec}; - use fork_choice::PayloadVerificationStatus; - use kzg::KzgCommitment; - use rand::SeedableRng; - use rand::rngs::StdRng; - use ssz_types::VariableList; - use state_processing::ConsensusContext; - use types::test_utils::TestRandom; - use types::{BeaconState, ForkName, MainnetEthSpec, SignedExecutionPayloadEnvelope, Slot}; - - type E = MainnetEthSpec; - - type Setup = ( - SignedExecutionPayloadEnvelope, - DataColumnSidecarList, - DataColumnSidecarList, - ); - - /// Returns true if gloas is enabled for testing. Tests should skip if this returns false. fn is_gloas_enabled() -> bool { let spec = test_spec::(); spec.fork_name_at_slot::(Slot::new(0)).gloas_enabled() } - fn pre_setup() -> Setup { - let mut rng = StdRng::seed_from_u64(0xDEADBEEF0BAD5EEDu64); - let spec = test_spec::(); + #[tokio::test] + async fn test_cache_creation() { + if !is_gloas_enabled() { + return; + } - assert!( - spec.fork_name_at_slot::(Slot::new(0)).gloas_enabled(), - "pre_setup() only works after gloas" - ); + type T = DiskHarnessType; + let capacity = 4; + let (_harness, cache, _path) = setup_harness_and_cache::(capacity).await; + assert_eq!(cache.block_cache_size(), 0); + } - let (payload, columns) = generate_rand_payload_and_columns::( - ForkName::Gloas, - NumBlobs::Random, + #[tokio::test] + async fn test_put_columns_creates_pending_components() { + if !is_gloas_enabled() { + return; + } + + type T = DiskHarnessType; + let capacity = 4; + let (harness, cache, _path) = setup_harness_and_cache::(capacity).await; + + let mut rng = StdRng::seed_from_u64(0xDEADBEEF); + let spec = harness.spec.clone(); + + // Generate a block with data columns + let (_block, data_columns) = generate_rand_block_and_data_columns::( + ForkName::Fulu, // Use Fulu for now as Gloas generation may not be ready + NumBlobs::Number(1), &mut rng, &spec, ); - // Create invalid columns by mutating kzg_commitments - let invalid_columns: DataColumnSidecarList = columns - .iter() + let block_root = Hash256::random(); + + // Convert to KzgVerifiedCustodyDataColumn + let verified_columns: Vec<_> = data_columns + .into_iter() + .take(1) // Just take one column for the test .map(|col| { - let mut col_clone = col.as_ref().clone(); - // Mutate commitments to make them invalid - let mut commitments: Vec<_> = col_clone.kzg_commitments().iter().copied().collect(); - for commitment in commitments.iter_mut() { - *commitment = KzgCommitment::random_for_test(&mut rng); - } - let new_commitments = - VariableList::try_from(commitments).expect("commitments within bounds"); - match &mut col_clone { - DataColumnSidecar::Gloas(gloas) => { - gloas.kzg_commitments = new_commitments; - } - DataColumnSidecar::Fulu(fulu) => { - fulu.kzg_commitments = new_commitments; - } - } - Arc::new(col_clone) + KzgVerifiedCustodyDataColumn::from_asserted_custody( + KzgVerifiedDataColumn::__new_for_testing(col), + ) }) .collect(); - (payload, columns, invalid_columns) + // Put columns into cache + let result = cache.put_kzg_verified_data_columns(block_root, verified_columns); + assert!(result.is_ok()); + + // Check that pending components were created + assert_eq!(cache.block_cache_size(), 1); + + // Verify columns are cached + let cached_indices = cache.peek_pending_components(&block_root, |components| { + components.map(|c| c.get_cached_data_columns_indices()) + }); + assert!(cached_indices.is_some()); + assert_eq!(cached_indices.unwrap().len(), 1); } - type PendingComponentsSetup = ( - AvailabilityPendingExecutedPayload, - Vec>, - Vec>, - ); + #[tokio::test] + async fn test_column_deduplication() { + if !is_gloas_enabled() { + return; + } - fn setup_pending_components( - payload: SignedExecutionPayloadEnvelope, - valid_columns: DataColumnSidecarList, - invalid_columns: DataColumnSidecarList, - ) -> PendingComponentsSetup { - let columns: Vec> = valid_columns + type T = DiskHarnessType; + let capacity = 4; + let (harness, cache, _path) = setup_harness_and_cache::(capacity).await; + + let mut rng = StdRng::seed_from_u64(0xDEADBEEF); + let spec = harness.spec.clone(); + + let (_block, data_columns) = generate_rand_block_and_data_columns::( + ForkName::Fulu, + NumBlobs::Number(1), + &mut rng, + &spec, + ); + + let block_root = Hash256::random(); + + // Get the first column + let first_column = data_columns.first().cloned().expect("should have column"); + let column_index = *first_column.index(); + + let verified_column = KzgVerifiedCustodyDataColumn::from_asserted_custody( + KzgVerifiedDataColumn::__new_for_testing(first_column.clone()), + ); + + // Insert the same column twice + cache + .put_kzg_verified_data_columns(block_root, vec![verified_column.clone()]) + .expect("should put column"); + + cache + .put_kzg_verified_data_columns(block_root, vec![verified_column]) + .expect("should put column again"); + + // Check that we still only have one column (deduplicated) + let cached_indices = cache.peek_pending_components(&block_root, |components| { + components.map(|c| c.get_cached_data_columns_indices()) + }); + assert!(cached_indices.is_some()); + let indices = cached_indices.unwrap(); + assert_eq!(indices.len(), 1); + assert_eq!(indices[0], column_index); + } + + #[tokio::test] + async fn test_columns_without_block_not_available() { + if !is_gloas_enabled() { + return; + } + + type T = DiskHarnessType; + let capacity = 4; + let (harness, cache, _path) = setup_harness_and_cache::(capacity).await; + + let mut rng = StdRng::seed_from_u64(0xDEADBEEF); + let spec = harness.spec.clone(); + + let (_block, data_columns) = generate_rand_block_and_data_columns::( + ForkName::Fulu, + NumBlobs::Number(1), + &mut rng, + &spec, + ); + + let block_root = Hash256::random(); + + // Add all columns + let verified_columns: Vec<_> = data_columns .into_iter() .map(|col| { KzgVerifiedCustodyDataColumn::from_asserted_custody( @@ -943,8 +760,40 @@ mod pending_components_tests { }) .collect(); - let invalid_columns: Vec> = invalid_columns + let result = cache + .put_kzg_verified_data_columns(block_root, verified_columns) + .expect("should put columns"); + + // Without a block, should still be missing components + assert!(matches!(result, Availability::MissingComponents(_))); + } + + #[tokio::test] + async fn test_reconstruction_started_flag() { + if !is_gloas_enabled() { + return; + } + + type T = DiskHarnessType; + let capacity = 4; + let (harness, cache, _path) = setup_harness_and_cache::(capacity).await; + + let mut rng = StdRng::seed_from_u64(0xDEADBEEF); + let spec = harness.spec.clone(); + + let (_block, data_columns) = generate_rand_block_and_data_columns::( + ForkName::Fulu, + NumBlobs::Number(1), + &mut rng, + &spec, + ); + + let block_root = Hash256::random(); + + // Add some columns (not enough for reconstruction threshold) + let verified_columns: Vec<_> = data_columns .into_iter() + .take(10) // Not enough for reconstruction .map(|col| { KzgVerifiedCustodyDataColumn::from_asserted_custody( KzgVerifiedDataColumn::__new_for_testing(col), @@ -952,222 +801,138 @@ mod pending_components_tests { }) .collect(); - let executed_payload = AvailabilityPendingExecutedPayload::new( - Arc::new(payload.clone()), - PayloadImportData { - state: BeaconState::new(0, Default::default(), &test_spec::()), - consensus_context: ConsensusContext::new(payload.message.slot), - }, - PayloadVerificationOutcome { - payload_verification_status: PayloadVerificationStatus::Verified, - is_valid_merge_transition_block: false, - }, + cache + .put_kzg_verified_data_columns(block_root, verified_columns) + .expect("should put columns"); + + // Check reconstruction decision - should say "not enough columns" + let decision = cache.check_and_set_reconstruction_started(&block_root); + assert!(matches!(decision, ReconstructColumnsDecision::No(_))); + } + + #[tokio::test] + async fn test_handle_reconstruction_failure_clears_columns() { + if !is_gloas_enabled() { + return; + } + + type T = DiskHarnessType; + let capacity = 4; + let (harness, cache, _path) = setup_harness_and_cache::(capacity).await; + + let mut rng = StdRng::seed_from_u64(0xDEADBEEF); + let spec = harness.spec.clone(); + + let (_block, data_columns) = generate_rand_block_and_data_columns::( + ForkName::Fulu, + NumBlobs::Number(1), + &mut rng, + &spec, ); - (executed_payload, columns, invalid_columns) + let block_root = Hash256::random(); + + // Add some columns + let verified_columns: Vec<_> = data_columns + .into_iter() + .take(5) + .map(|col| { + KzgVerifiedCustodyDataColumn::from_asserted_custody( + KzgVerifiedDataColumn::__new_for_testing(col), + ) + }) + .collect(); + + cache + .put_kzg_verified_data_columns(block_root, verified_columns) + .expect("should put columns"); + + // Verify columns are cached + let cached_count = cache.peek_pending_components(&block_root, |components| { + components.map(|c| c.verified_data_columns.len()) + }); + assert_eq!(cached_count, Some(5)); + + // Handle reconstruction failure + cache.handle_reconstruction_failure(&block_root); + + // Verify columns are cleared + let cached_count_after = cache.peek_pending_components(&block_root, |components| { + components.map(|c| c.verified_data_columns.len()) + }); + assert_eq!(cached_count_after, Some(0)); } - fn assert_cache_consistent(cache: &PendingComponents) { - let cached_payload = cache - .payload - .as_ref() - .expect("expected cached payload to be present"); - let payload_commitments = cached_payload.get_commitments(); - // Each column should have commitments matching the payload - for col in &cache.verified_data_columns { - let col_commitments = col.as_data_column().kzg_commitments(); - assert_eq!( - payload_commitments, - *col_commitments, - "column {} commitments should match payload commitments", - col.index() - ); - } - } - - #[allow(dead_code)] - fn assert_empty_column_cache(cache: &PendingComponents) { - assert!( - cache.verified_data_columns.is_empty(), - "expected empty column cache but found {} columns", - cache.verified_data_columns.len() - ); - } - - // v2 merge_data_columns deduplicates by index (first-in wins). When invalid columns - // are merged first, they persist even after valid columns are merged at the same indices. - - #[test] - fn payload_invalid_columns_valid_columns() { + #[tokio::test] + async fn test_maintenance_removes_old_entries() { if !is_gloas_enabled() { return; } - let (payload, columns, invalid_columns) = pre_setup(); - let (executed_payload, columns, invalid_columns) = - setup_pending_components(payload, columns, invalid_columns); - let block_root = Hash256::ZERO; - let mut cache = >::empty(block_root); - cache.merge_payload(executed_payload); - cache - .merge_data_columns(invalid_columns) - .expect("merge should succeed"); - cache - .merge_data_columns(columns) - .expect("merge should succeed"); - // Invalid columns were inserted first, valid columns are deduplicated away. - // The cache has columns but they have invalid commitments (not matching payload). - assert!(!cache.verified_data_columns.is_empty()); + type T = DiskHarnessType; + let capacity = 4; + let (_harness, cache, _path) = setup_harness_and_cache::(capacity).await; + + let block_root = Hash256::random(); + + // Create an empty entry in the cache + let _ = cache.peek_pending_components(&block_root, |_| {}); + + // Manually insert a pending component by putting empty columns + // This will create an entry but it won't have an epoch + // For this test, we need an entry with a known epoch + + // Run maintenance with a future cutoff epoch + let cutoff_epoch = Epoch::new(100); + cache.do_maintenance(cutoff_epoch).expect("maintenance should succeed"); + + // Cache should still be empty since we didn't add anything with an epoch + assert_eq!(cache.block_cache_size(), 0); } - #[test] - fn invalid_columns_payload_valid_columns() { + #[tokio::test] + async fn test_peek_data_columns() { if !is_gloas_enabled() { return; } - let (payload, columns, invalid_columns) = pre_setup(); - let (executed_payload, columns, invalid_columns) = - setup_pending_components(payload, columns, invalid_columns); - let block_root = Hash256::ZERO; - let mut cache = >::empty(block_root); - cache - .merge_data_columns(invalid_columns) - .expect("merge should succeed"); - cache.merge_payload(executed_payload); - cache - .merge_data_columns(columns) - .expect("merge should succeed"); - // Invalid columns were first, valid ones deduplicated away. - assert!(!cache.verified_data_columns.is_empty()); - } + type T = DiskHarnessType; + let capacity = 4; + let (harness, cache, _path) = setup_harness_and_cache::(capacity).await; - #[test] - fn invalid_columns_valid_columns_payload() { - if !is_gloas_enabled() { - return; - } - let (payload, columns, invalid_columns) = pre_setup(); - let (executed_payload, columns, invalid_columns) = - setup_pending_components(payload, columns, invalid_columns); + let mut rng = StdRng::seed_from_u64(0xDEADBEEF); + let spec = harness.spec.clone(); - let block_root = Hash256::ZERO; - let mut cache = >::empty(block_root); - cache - .merge_data_columns(invalid_columns) - .expect("merge should succeed"); - cache - .merge_data_columns(columns) - .expect("merge should succeed"); - cache.merge_payload(executed_payload); - - // Invalid columns were first, valid ones deduplicated away. - assert!(!cache.verified_data_columns.is_empty()); - } - - #[test] - fn payload_valid_columns_invalid_columns() { - if !is_gloas_enabled() { - return; - } - let (payload, columns, invalid_columns) = pre_setup(); - let (executed_payload, columns, invalid_columns) = - setup_pending_components(payload, columns, invalid_columns); - - let block_root = Hash256::ZERO; - let mut cache = >::empty(block_root); - cache.merge_payload(executed_payload); - cache - .merge_data_columns(columns) - .expect("merge should succeed"); - cache - .merge_data_columns(invalid_columns) - .expect("merge should succeed"); - - // Valid columns were inserted first, so they persist. Cache should be consistent. - assert_cache_consistent(&cache); - } - - #[test] - fn valid_columns_payload_invalid_columns() { - if !is_gloas_enabled() { - return; - } - let (payload, columns, invalid_columns) = pre_setup(); - let (executed_payload, columns, invalid_columns) = - setup_pending_components(payload, columns, invalid_columns); - - let block_root = Hash256::ZERO; - let mut cache = >::empty(block_root); - cache - .merge_data_columns(columns) - .expect("merge should succeed"); - cache.merge_payload(executed_payload); - cache - .merge_data_columns(invalid_columns) - .expect("merge should succeed"); - - // Valid columns were inserted first, so they persist. Cache should be consistent. - assert_cache_consistent(&cache); - } - - #[test] - fn valid_columns_invalid_columns_payload() { - if !is_gloas_enabled() { - return; - } - let (payload, columns, invalid_columns) = pre_setup(); - let (executed_payload, columns, invalid_columns) = - setup_pending_components(payload, columns, invalid_columns); - - let block_root = Hash256::ZERO; - let mut cache = >::empty(block_root); - cache - .merge_data_columns(columns) - .expect("merge should succeed"); - cache - .merge_data_columns(invalid_columns) - .expect("merge should succeed"); - cache.merge_payload(executed_payload); - - // Valid columns were inserted first, so they persist. Cache should be consistent. - assert_cache_consistent(&cache); - } - - #[test] - fn should_not_insert_pre_execution_payload_if_executed_payload_exists() { - if !is_gloas_enabled() { - return; - } - let (payload, _columns, _invalid_columns) = pre_setup(); - let (executed_payload, _columns, _invalid_columns) = - setup_pending_components(payload.clone(), _columns, _invalid_columns); - - let block_root = Hash256::ZERO; - let mut pending_component = >::empty(block_root); - - let pre_execution_payload = Arc::new(payload); - pending_component - .insert_pre_execution_payload(pre_execution_payload.clone(), BlockImportSource::Gossip); - assert!( - matches!( - pending_component.payload, - Some(CachedPayload::PreExecution(_, _)) - ), - "pre execution payload inserted" + let (_block, data_columns) = generate_rand_block_and_data_columns::( + ForkName::Fulu, + NumBlobs::Number(1), + &mut rng, + &spec, ); - pending_component.insert_executed_payload(executed_payload); - assert!( - matches!(pending_component.payload, Some(CachedPayload::Executed(_))), - "executed payload inserted" - ); + let block_root = Hash256::random(); - pending_component - .insert_pre_execution_payload(pre_execution_payload, BlockImportSource::Gossip); - assert!( - matches!(pending_component.payload, Some(CachedPayload::Executed(_))), - "executed payload should remain" - ); + // No columns yet + assert!(cache.peek_data_columns(block_root).is_none()); + + // Add columns + let verified_columns: Vec<_> = data_columns + .into_iter() + .take(3) + .map(|col| { + KzgVerifiedCustodyDataColumn::from_asserted_custody( + KzgVerifiedDataColumn::__new_for_testing(col), + ) + }) + .collect(); + + cache + .put_kzg_verified_data_columns(block_root, verified_columns) + .expect("should put columns"); + + // Now columns should be returned + let peeked = cache.peek_data_columns(block_root); + assert!(peeked.is_some()); + assert_eq!(peeked.unwrap().len(), 3); } } diff --git a/beacon_node/beacon_chain/src/data_availability_router.rs b/beacon_node/beacon_chain/src/data_availability_router.rs index 847c25ce76..95716559b2 100644 --- a/beacon_node/beacon_chain/src/data_availability_router.rs +++ b/beacon_node/beacon_chain/src/data_availability_router.rs @@ -59,7 +59,8 @@ impl AvailabilityOutcome { match self { Self::Block(BlockAvailability::Available(block)) => block.import_data.block_root, Self::Block(BlockAvailability::MissingComponents(root)) => *root, - Self::Payload(PayloadAvailability::Available(payload)) => payload.payload.block_root(), + // For payload availability, the first element of the tuple is the block root + Self::Payload(PayloadAvailability::Available(available_data)) => available_data.0, Self::Payload(PayloadAvailability::MissingComponents(root)) => *root, } } @@ -126,7 +127,7 @@ impl ReconstructionOutcome { /// Both `DataAvailabilityChecker` (v1) and `DataAvailabilityChecker` (v2) implement /// this trait. The associated types differ: /// - V1: Returns `Availability` containing `AvailableExecutedBlock` -/// - V2: Returns `Availability` containing `AvailableExecutedPayload` +/// - V2: Returns `Availability` containing `(Hash256, DataColumnSidecarList)` (block root + columns) pub trait DataColumnCache: Send + Sync { /// The availability type returned by write operations. /// V1 returns block availability, V2 returns payload availability. diff --git a/beacon_node/beacon_chain/src/payload_verification_types.rs b/beacon_node/beacon_chain/src/payload_verification_types.rs index 02868bb597..94c8b6cb5e 100644 --- a/beacon_node/beacon_chain/src/payload_verification_types.rs +++ b/beacon_node/beacon_chain/src/payload_verification_types.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use state_processing::ConsensusContext; use types::{BeaconState, BlockImportSource, EthSpec, SignedExecutionPayloadEnvelope}; -use crate::{PayloadVerificationOutcome, data_availability_checker_v2::AvailablePayload}; +use crate::PayloadVerificationOutcome; #[derive(Debug, Clone, PartialEq)] pub struct PayloadImportData { @@ -13,6 +13,10 @@ pub struct PayloadImportData { /// A payload that has completed payload verification by an EL client but does not /// have all requisite column data to get imported into fork choice. +/// +/// Note: The number of expected blobs is not available from this type directly since +/// blob commitments are in the block's execution payload bid, not the payload envelope. +/// Use the associated block to get this information. #[derive(Clone)] pub struct AvailabilityPendingExecutedPayload { pub payload: Arc>, @@ -36,32 +40,6 @@ impl AvailabilityPendingExecutedPayload { pub fn as_payload(&self) -> &SignedExecutionPayloadEnvelope { &self.payload } - - pub fn num_blobs_expected(&self) -> usize { - self.payload.message.blob_kzg_commitments.len() - } -} - -/// A payload that has completed all payload verification by an EL client -/// **and** has all requisite column data to be imported into fork choice. -pub struct AvailableExecutedPayload { - pub payload: AvailablePayload, - pub import_data: PayloadImportData, - pub payload_verification_outcome: PayloadVerificationOutcome, -} - -impl AvailableExecutedPayload { - pub fn new( - payload: AvailablePayload, - import_data: PayloadImportData, - payload_verification_outcome: PayloadVerificationOutcome, - ) -> Self { - Self { - payload, - import_data, - payload_verification_outcome, - } - } } pub enum PayloadProcessStatus { diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index 29ed742ac0..e818f72f1c 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -3431,7 +3431,8 @@ macro_rules! add_blob_transactions_gloas { for tx in Vec::from(transactions) { payload.transactions.push(tx).unwrap(); } - $message.blob_kzg_commitments = bundle.commitments.clone(); + // Note: In Gloas, blob_kzg_commitments are in the bid (block body), not the payload envelope. + // The commitments are returned via the bundle for the caller to use. bundle }}; } @@ -3444,9 +3445,12 @@ pub fn generate_rand_payload_and_columns( ) -> (SignedExecutionPayloadEnvelope, DataColumnSidecarList) { let mut payload = SignedExecutionPayloadEnvelope::random_for_test(rng); - let _bundle = add_blob_transactions_gloas!(payload.message, num_blobs, rng, fork_name); + let bundle = add_blob_transactions_gloas!(payload.message, num_blobs, rng, fork_name); - let data_columns = generate_data_column_sidecars_from_payload(&payload, spec); + // In Gloas, blob_kzg_commitments are in the bid (block body), not the payload envelope. + // We pass them from the bundle to generate the data columns. + let kzg_commitments = bundle.commitments; + let data_columns = generate_data_column_sidecars_from_payload(&payload, kzg_commitments, spec); (payload, data_columns) } @@ -3607,11 +3611,14 @@ pub fn generate_data_column_sidecars_from_block( } /// Generate data column sidecars from pre-computed cells and proofs for gloas payloads. +/// +/// Note: In Gloas, `blob_kzg_commitments` are in the bid (block body), not the payload envelope. +/// The caller must provide the commitments separately. pub fn generate_data_column_sidecars_from_payload( payload: &SignedExecutionPayloadEnvelope, + kzg_commitments: KzgCommitments, spec: &ChainSpec, ) -> DataColumnSidecarList { - let kzg_commitments = payload.message.blob_kzg_commitments.clone(); if kzg_commitments.is_empty() { return vec![]; } From 78c61a0621df26ea3734945e40bff31baf1ef22c Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Fri, 30 Jan 2026 10:59:43 -0800 Subject: [PATCH 014/118] DA cache updated --- .../src/data_availability_checker.rs | 17 +- .../src/data_availability_checker_v2.rs | 28 +-- .../overflow_lru_cache.rs | 62 ++++--- .../src/data_availability_router.rs | 40 +++-- beacon_node/beacon_chain/src/metrics.rs | 1 + beacon_node/beacon_chain/src/test_utils.rs | 164 ++++++------------ beacon_node/client/src/builder.rs | 5 + .../src/sync/block_sidecar_coupling.rs | 2 +- 8 files changed, 139 insertions(+), 180 deletions(-) diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index cc9d165887..846a55557a 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -5,7 +5,7 @@ use crate::block_verification_types::{AvailabilityPendingExecutedBlock, Availabl use crate::data_availability_checker::overflow_lru_cache::{ DataAvailabilityCheckerInner, ReconstructColumnsDecision, }; -use crate::data_availability_router::DataColumnCache; +use crate::data_availability_router::AvailabilityCache; use crate::{ BeaconChain, BeaconChainTypes, BeaconStore, BlockProcessStatus, CustodyContext, metrics, }; @@ -366,7 +366,7 @@ impl DataAvailabilityChecker { } } -impl DataColumnCache for DataAvailabilityChecker { +impl AvailabilityCache for DataAvailabilityChecker { type Availability = Availability; type ReconstructionResult = DataColumnReconstructionResult; @@ -559,6 +559,18 @@ impl DataColumnCache for DataAvailabilityChecker { )) }) } + + /// Verifies KZG commitments for data columns. + fn verify_kzg_for_data_columns( + &self, + data_columns: &DataColumnSidecarList, + ) -> Result<(), AvailabilityCheckError> { + if !data_columns.is_empty() { + verify_kzg_for_data_column_list(data_columns.iter(), &self.kzg) + .map_err(AvailabilityCheckError::InvalidColumn)?; + } + Ok(()) + } } /// Helper struct to group data availability checker metrics. @@ -587,6 +599,7 @@ pub fn start_availability_cache_maintenance_service( } } +// TODO(gloas) we can shut down this service once we reach the gloas fork epoch async fn availability_cache_maintenance_service( chain: Arc>, overflow_cache: Arc>, diff --git a/beacon_node/beacon_chain/src/data_availability_checker_v2.rs b/beacon_node/beacon_chain/src/data_availability_checker_v2.rs index a386d1f5a0..63629dbfb7 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker_v2.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker_v2.rs @@ -3,7 +3,7 @@ use crate::data_availability_checker_v2::overflow_lru_cache::{ }; use crate::data_availability_checker::AvailabilityCheckError; -use crate::data_availability_router::DataColumnCache; +use crate::data_availability_router::AvailabilityCache; use crate::{BeaconChain, BeaconChainTypes, CustodyContext, metrics}; use kzg::Kzg; use slot_clock::SlotClock; @@ -86,7 +86,7 @@ pub struct DataAvailabilityChecker { spec: Arc, } -impl DataColumnCache for DataAvailabilityChecker { +impl AvailabilityCache for DataAvailabilityChecker { type Availability = Availability; type ReconstructionResult = DataColumnReconstructionResult; @@ -275,6 +275,18 @@ impl DataColumnCache for DataAvailabilityChecker { )) }) } + + /// Verifies KZG commitments for data columns. + fn verify_kzg_for_data_columns( + &self, + data_columns: &DataColumnSidecarList, + ) -> Result<(), AvailabilityCheckError> { + if !data_columns.is_empty() { + verify_kzg_for_data_column_list(data_columns.iter(), &self.kzg) + .map_err(AvailabilityCheckError::InvalidColumn)?; + } + Ok(()) + } } impl DataAvailabilityChecker { @@ -311,18 +323,6 @@ impl DataAvailabilityChecker { self.availability_cache.put_block(block_root, block) } - /// Verifies kzg commitments for data columns. - pub fn verify_kzg_for_data_columns( - &self, - data_columns: &DataColumnSidecarList, - ) -> Result<(), AvailabilityCheckError> { - if !data_columns.is_empty() { - verify_kzg_for_data_column_list(data_columns.iter(), &self.kzg) - .map_err(AvailabilityCheckError::InvalidColumn)?; - } - Ok(()) - } - /// Collects metrics from the data availability checker. pub fn metrics(&self) -> DataAvailabilityCheckerMetrics { DataAvailabilityCheckerMetrics { diff --git a/beacon_node/beacon_chain/src/data_availability_checker_v2/overflow_lru_cache.rs b/beacon_node/beacon_chain/src/data_availability_checker_v2/overflow_lru_cache.rs index b88378648e..fa7ac913c1 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker_v2/overflow_lru_cache.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker_v2/overflow_lru_cache.rs @@ -70,9 +70,10 @@ impl PendingComponents { /// Returns the number of blobs expected for this block by reading the bid's kzg commitments. /// Returns an error if the block is not cached or not a Gloas block. pub fn num_blobs_expected(&self) -> Result { - let block = self.block.as_ref().ok_or_else(|| { - AvailabilityCheckError::Unexpected("No block available".to_string()) - })?; + let block = self + .block + .as_ref() + .ok_or_else(|| AvailabilityCheckError::Unexpected("No block available".to_string()))?; let bid = block .message() @@ -167,10 +168,8 @@ impl PendingComponents { } pub fn status_str(&self, num_expected_columns: usize) -> String { - let block_status = if self.block.is_some() { "yes" } else { "no" }; format!( - "block {} data_columns {}/{}", - block_status, + "data_columns {}/{}", self.verified_data_columns.len(), num_expected_columns ) @@ -475,7 +474,7 @@ mod pending_components_tests { let components = PendingComponents::::empty(block_root); let status = components.status_str(10); - assert_eq!(status, "block no data_columns 0/10"); + assert_eq!(status, "data_columns 0/10"); } #[test] @@ -510,20 +509,20 @@ mod data_availability_checker_tests { use crate::data_column_verification::{KzgVerifiedCustodyDataColumn, KzgVerifiedDataColumn}; use crate::test_utils::{ - generate_data_column_indices_rand_order, test_spec, NumBlobs, - generate_rand_block_and_data_columns, + NumBlobs, generate_data_column_indices_rand_order, generate_rand_block_and_data_columns, + test_spec, }; use crate::{ custody_context::NodeCustodyType, test_utils::{BeaconChainHarness, DiskHarnessType}, }; use logging::create_test_tracing_subscriber; - use store::{HotColdDB, StoreConfig, database::interface::BeaconNodeBackend}; - use tempfile::{TempDir, tempdir}; - use types::{ForkName, MinimalEthSpec, Slot}; - use types::new_non_zero_usize; use rand::SeedableRng; use rand::rngs::StdRng; + use store::{HotColdDB, StoreConfig, database::interface::BeaconNodeBackend}; + use tempfile::{TempDir, tempdir}; + use types::new_non_zero_usize; + use types::{ForkName, MinimalEthSpec, Slot}; type E = MinimalEthSpec; @@ -569,17 +568,12 @@ mod data_availability_checker_tests { let chain_store = get_store_with_spec::(db_path, spec.clone()); let validators_keypairs = types::test_utils::generate_deterministic_keypairs(LOW_VALIDATOR_COUNT); - let harness = BeaconChainHarness::builder(E::default()) + BeaconChainHarness::builder(E::default()) .spec(spec.clone()) .keypairs(validators_keypairs) .fresh_disk_store(chain_store) .mock_execution_layer() - .build(); - - // go to gloas slot - let gloas_fork_slot = Slot::new(0); - harness.extend_to_slot(gloas_fork_slot).await; - harness + .build() } async fn setup_harness_and_cache( @@ -607,8 +601,12 @@ mod data_availability_checker_tests { &spec, )); let cache = Arc::new( - DataAvailabilityCheckerInner::::new(capacity_non_zero, custody_context, spec.clone()) - .expect("should create cache"), + DataAvailabilityCheckerInner::::new( + capacity_non_zero, + custody_context, + spec.clone(), + ) + .expect("should create cache"), ); (harness, cache, chain_db_path) } @@ -643,9 +641,8 @@ mod data_availability_checker_tests { let mut rng = StdRng::seed_from_u64(0xDEADBEEF); let spec = harness.spec.clone(); - // Generate a block with data columns let (_block, data_columns) = generate_rand_block_and_data_columns::( - ForkName::Fulu, // Use Fulu for now as Gloas generation may not be ready + ForkName::Gloas, NumBlobs::Number(1), &mut rng, &spec, @@ -653,7 +650,6 @@ mod data_availability_checker_tests { let block_root = Hash256::random(); - // Convert to KzgVerifiedCustodyDataColumn let verified_columns: Vec<_> = data_columns .into_iter() .take(1) // Just take one column for the test @@ -693,7 +689,7 @@ mod data_availability_checker_tests { let spec = harness.spec.clone(); let (_block, data_columns) = generate_rand_block_and_data_columns::( - ForkName::Fulu, + ForkName::Gloas, NumBlobs::Number(1), &mut rng, &spec, @@ -742,7 +738,7 @@ mod data_availability_checker_tests { let spec = harness.spec.clone(); let (_block, data_columns) = generate_rand_block_and_data_columns::( - ForkName::Fulu, + ForkName::Gloas, NumBlobs::Number(1), &mut rng, &spec, @@ -782,7 +778,7 @@ mod data_availability_checker_tests { let spec = harness.spec.clone(); let (_block, data_columns) = generate_rand_block_and_data_columns::( - ForkName::Fulu, + ForkName::Gloas, NumBlobs::Number(1), &mut rng, &spec, @@ -824,7 +820,7 @@ mod data_availability_checker_tests { let spec = harness.spec.clone(); let (_block, data_columns) = generate_rand_block_and_data_columns::( - ForkName::Fulu, + ForkName::Gloas, NumBlobs::Number(1), &mut rng, &spec, @@ -876,7 +872,7 @@ mod data_availability_checker_tests { let block_root = Hash256::random(); // Create an empty entry in the cache - let _ = cache.peek_pending_components(&block_root, |_| {}); + cache.peek_pending_components(&block_root, |_| {}); // Manually insert a pending component by putting empty columns // This will create an entry but it won't have an epoch @@ -884,7 +880,9 @@ mod data_availability_checker_tests { // Run maintenance with a future cutoff epoch let cutoff_epoch = Epoch::new(100); - cache.do_maintenance(cutoff_epoch).expect("maintenance should succeed"); + cache + .do_maintenance(cutoff_epoch) + .expect("maintenance should succeed"); // Cache should still be empty since we didn't add anything with an epoch assert_eq!(cache.block_cache_size(), 0); @@ -904,7 +902,7 @@ mod data_availability_checker_tests { let spec = harness.spec.clone(); let (_block, data_columns) = generate_rand_block_and_data_columns::( - ForkName::Fulu, + ForkName::Gloas, NumBlobs::Number(1), &mut rng, &spec, diff --git a/beacon_node/beacon_chain/src/data_availability_router.rs b/beacon_node/beacon_chain/src/data_availability_router.rs index 95716559b2..630a2bbb93 100644 --- a/beacon_node/beacon_chain/src/data_availability_router.rs +++ b/beacon_node/beacon_chain/src/data_availability_router.rs @@ -1,14 +1,14 @@ -//! Abstraction layer for data column storage across different DA checkers. +//! Abstraction layer for data availability operations across different DA checkers. //! -//! This module provides a unified interface for data column operations that are shared +//! This module provides a unified interface for availability operations that are shared //! between the legacy `DataAvailabilityChecker` (v1, for blocks) and //! `DataAvailabilityChecker` v2 (for payload envelopes after Gloas). //! //! ## Design //! -//! - **Read operations**: Unified via the `DataColumnCache` trait -//! - **Write operations**: Return `AvailabilityOutcome` enum that wraps both checker types -//! - **Processing**: `BeaconChain::process_availability_outcome()` handles both cases +//! - **Unified operations**: Via the `AvailabilityCache` trait (blocks, columns, availability checks) +//! - **Fork-aware routing**: `DataAvailabilityRouter` dispatches to v1 or v2 based on slot +//! - **Processing**: `BeaconChain::process_availability_outcome()` handles both result types //! //! After Gloas is fully activated and v1 is deprecated, this can be deleted and we can //! use the Gloas DA checker directly. @@ -122,13 +122,13 @@ impl ReconstructionOutcome { } } -/// Trait for data column operations on availability checkers. +/// Trait for data availability operations on availability checkers. /// /// Both `DataAvailabilityChecker` (v1) and `DataAvailabilityChecker` (v2) implement /// this trait. The associated types differ: /// - V1: Returns `Availability` containing `AvailableExecutedBlock` /// - V2: Returns `Availability` containing `(Hash256, DataColumnSidecarList)` (block root + columns) -pub trait DataColumnCache: Send + Sync { +pub trait AvailabilityCache: Send + Sync { /// The availability type returned by write operations. /// V1 returns block availability, V2 returns payload availability. type Availability; @@ -182,24 +182,30 @@ pub trait DataColumnCache: Send + Sync { &self, block_root: &Hash256, ) -> Result; + + /// Verifies KZG commitments for a list of data columns. + fn verify_kzg_for_data_columns( + &self, + data_columns: &DataColumnSidecarList, + ) -> Result<(), AvailabilityCheckError>; } /// Router that directs data availability checker operations to the appropriate version based on fork. /// /// This wraps both the legacy (v1) and Gloas (v2) DA checkers, providing: -/// - Unified read operations that query both checkers +/// - Unified operations that dispatch to the correct checker based on fork /// - Fork-aware routing for write operations that return `AvailabilityOutcome` /// /// After Gloas is fully activated and v1 is deprecated, this router can be deleted and /// we can use the Gloas DA checker directly. pub struct DataAvailabilityRouter where - V1: DataColumnCache< + V1: AvailabilityCache< T, Availability = BlockAvailability, ReconstructionResult = BlockReconstructionResult, >, - V2: DataColumnCache< + V2: AvailabilityCache< T, Availability = PayloadAvailability, ReconstructionResult = PayloadReconstructionResult, @@ -215,12 +221,12 @@ where impl DataAvailabilityRouter where - V1: DataColumnCache< + V1: AvailabilityCache< T, Availability = BlockAvailability, ReconstructionResult = BlockReconstructionResult, >, - V2: DataColumnCache< + V2: AvailabilityCache< T, Availability = PayloadAvailability, ReconstructionResult = PayloadReconstructionResult, @@ -371,18 +377,16 @@ where } } - /// Direct access to v1 checker (for block-specific operations). + /// Direct access to v1 checker for block execution/availability checks. /// - /// Use this for operations that are specific to the legacy block-based DA checker, - /// such as `put_executed_block`, `get_cached_block`, blob operations, etc. + /// Use this for operations that are specific to the legacy DA checker, pub fn v1(&self) -> Arc { self.v1.clone() } - /// Direct access to v2 checker (for payload-specific operations). + /// Direct access to v2 checker for payload availability checks. /// - /// Use this for operations that are specific to the Gloas payload-based DA checker, - /// such as `put_executed_payload`, `get_cached_payload`, etc. + /// Use this for operations that are specific to the Gloas DA checker, pub fn v2(&self) -> Arc { self.v2.clone() } diff --git a/beacon_node/beacon_chain/src/metrics.rs b/beacon_node/beacon_chain/src/metrics.rs index 8afe32b7c6..7e4878c3d1 100644 --- a/beacon_node/beacon_chain/src/metrics.rs +++ b/beacon_node/beacon_chain/src/metrics.rs @@ -1978,6 +1978,7 @@ pub fn scrape_for_metrics(beacon_chain: &BeaconChain) { beacon_chain.store.state_cache_len(), ); + // TODO(gloas) configure v2 metrics let da_checker_metrics = beacon_chain.data_availability_checker.v1().metrics(); set_gauge_by_usize( diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index e818f72f1c..d68613cdfe 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -3416,45 +3416,6 @@ macro_rules! add_blob_transactions { }}; } -macro_rules! add_blob_transactions_gloas { - ($message:expr, $num_blobs:expr, $rng:expr, $fork_name:expr) => {{ - let num_blobs = match $num_blobs { - NumBlobs::Random => $rng.random_range(DEFAULT_MIN_BLOBS..=DEFAULT_MAX_BLOBS), - NumBlobs::Number(n) => n, - NumBlobs::None => 0, - }; - let (bundle, transactions) = - execution_layer::test_utils::generate_blobs::(num_blobs, $fork_name).unwrap(); - - let payload = &mut $message.payload; - payload.transactions = <_>::default(); - for tx in Vec::from(transactions) { - payload.transactions.push(tx).unwrap(); - } - // Note: In Gloas, blob_kzg_commitments are in the bid (block body), not the payload envelope. - // The commitments are returned via the bundle for the caller to use. - bundle - }}; -} - -pub fn generate_rand_payload_and_columns( - fork_name: ForkName, - num_blobs: NumBlobs, - rng: &mut impl Rng, - spec: &ChainSpec, -) -> (SignedExecutionPayloadEnvelope, DataColumnSidecarList) { - let mut payload = SignedExecutionPayloadEnvelope::random_for_test(rng); - - let bundle = add_blob_transactions_gloas!(payload.message, num_blobs, rng, fork_name); - - // In Gloas, blob_kzg_commitments are in the bid (block body), not the payload envelope. - // We pass them from the bundle to generate the data columns. - let kzg_commitments = bundle.commitments; - let data_columns = generate_data_column_sidecars_from_payload(&payload, kzg_commitments, spec); - - (payload, data_columns) -} - pub fn generate_rand_block_and_blobs( fork_name: ForkName, num_blobs: NumBlobs, @@ -3475,7 +3436,26 @@ pub fn generate_rand_block_and_blobs( SignedBeaconBlock::Fulu(SignedBeaconBlockFulu { ref mut message, .. }) => add_blob_transactions!(message, FullPayloadFulu, num_blobs, rng, fork_name), - // TODO(EIP-7732) Add `SignedBeaconBlock::Gloas` variant + SignedBeaconBlock::Gloas(SignedBeaconBlockGloas { + ref mut message, .. + }) => { + // For Gloas, commitments are in the bid, not directly in the body. + // BlobSidecars cannot be created for Gloas because there's no merkle proof + // from the block body to the commitments. Return early with empty blob_sidecars. + let num_blobs = match num_blobs { + NumBlobs::Random => rng.random_range(DEFAULT_MIN_BLOBS..=DEFAULT_MAX_BLOBS), + NumBlobs::Number(n) => n, + NumBlobs::None => 0, + }; + let (bundle, _transactions) = + execution_layer::test_utils::generate_blobs::(num_blobs, fork_name).unwrap(); + message + .body + .signed_execution_payload_bid + .message + .blob_kzg_commitments = bundle.commitments.clone(); + return (block, blob_sidecars); + } _ => return (block, blob_sidecars), }; @@ -3526,32 +3506,37 @@ pub fn generate_data_column_sidecars_from_block( block: &SignedBeaconBlock, spec: &ChainSpec, ) -> DataColumnSidecarList { - let kzg_commitments = block.message().body().blob_kzg_commitments().unwrap(); - if kzg_commitments.is_empty() { - return vec![]; - } - - let kzg_commitments_inclusion_proof = block - .message() - .body() - .kzg_commitments_merkle_proof() - .unwrap(); let signed_block_header = block.signed_block_header(); + // load the precomputed column sidecar to avoid computing them for every block in the tests. + let template_data_columns = RuntimeVariableList::>::from_ssz_bytes( + TEST_DATA_COLUMN_SIDECARS_SSZ, + E::number_of_columns(), + ) + .unwrap(); + // Load the precomputed column sidecar to avoid computing them for every block in the tests. // Then repeat the cells and proofs for every blob if block.fork_name_unchecked().gloas_enabled() { - let template_data_columns = - RuntimeVariableList::>::from_ssz_bytes( - TEST_DATA_COLUMN_SIDECARS_SSZ, - E::number_of_columns(), - ) - .unwrap(); + // For Gloas, commitments are in the bid, not the block body + let kzg_commitments = block + .message() + .body() + .signed_execution_payload_bid() + .unwrap() + .message + .blob_kzg_commitments + .clone(); + if kzg_commitments.is_empty() { + return vec![]; + } + // TODO(gloas): The fixture is Fulu format. Generate Gloas-specific fixture once format + // is finalized, or compute columns dynamically for Gloas tests. let (cells, proofs) = template_data_columns .into_iter() .map(|sidecar| { - let DataColumnSidecarGloas { + let DataColumnSidecarFulu { column, kzg_proofs, .. } = sidecar; // There's only one cell per column for a single blob @@ -3574,12 +3559,15 @@ pub fn generate_data_column_sidecars_from_block( ) .unwrap() } else { - // load the precomputed column sidecar to avoid computing them for every block in the tests. - let template_data_columns = - RuntimeVariableList::>::from_ssz_bytes( - TEST_DATA_COLUMN_SIDECARS_SSZ, - E::number_of_columns(), - ) + // For pre-Gloas forks, commitments are in the block body + let kzg_commitments = block.message().body().blob_kzg_commitments().unwrap(); + if kzg_commitments.is_empty() { + return vec![]; + } + let kzg_commitments_inclusion_proof = block + .message() + .body() + .kzg_commitments_merkle_proof() .unwrap(); let (cells, proofs) = template_data_columns @@ -3610,56 +3598,6 @@ pub fn generate_data_column_sidecars_from_block( } } -/// Generate data column sidecars from pre-computed cells and proofs for gloas payloads. -/// -/// Note: In Gloas, `blob_kzg_commitments` are in the bid (block body), not the payload envelope. -/// The caller must provide the commitments separately. -pub fn generate_data_column_sidecars_from_payload( - payload: &SignedExecutionPayloadEnvelope, - kzg_commitments: KzgCommitments, - spec: &ChainSpec, -) -> DataColumnSidecarList { - if kzg_commitments.is_empty() { - return vec![]; - } - - // Load the precomputed column sidecar to avoid computing them for every block in the tests. - // TODO(gloas): The fixture is currently in Fulu format. We should generate a Gloas-specific - // fixture once the format is finalized, or compute columns dynamically for Gloas tests. - let template_data_columns = RuntimeVariableList::>::from_ssz_bytes( - TEST_DATA_COLUMN_SIDECARS_SSZ, - E::number_of_columns(), - ) - .unwrap(); - - let (cells, proofs) = template_data_columns - .into_iter() - .map(|sidecar| { - let DataColumnSidecarFulu { - column, kzg_proofs, .. - } = sidecar; - // There's only one cell per column for a single blob - let cell_bytes: Vec = column.into_iter().next().unwrap().into(); - let kzg_cell = cell_bytes.try_into().unwrap(); - let kzg_proof = kzg_proofs.into_iter().next().unwrap(); - (kzg_cell, kzg_proof) - }) - .collect::<(Vec<_>, Vec<_>)>(); - - // Repeat the cells and proofs for every blob - let blob_cells_and_proofs_vec = - vec![(cells.try_into().unwrap(), proofs.try_into().unwrap()); kzg_commitments.len()]; - - build_data_column_sidecars_gloas( - kzg_commitments, - payload.message.beacon_block_root, - payload.message.slot, - blob_cells_and_proofs_vec, - spec, - ) - .unwrap() -} - pub fn generate_data_column_indices_rand_order() -> Vec { let mut indices = (0..E::number_of_columns() as u64).collect::>(); indices.shuffle(&mut StdRng::seed_from_u64(42)); diff --git a/beacon_node/client/src/builder.rs b/beacon_node/client/src/builder.rs index ba90cbd8be..28481f8c40 100644 --- a/beacon_node/client/src/builder.rs +++ b/beacon_node/client/src/builder.rs @@ -6,6 +6,7 @@ use crate::config::{ClientGenesis, Config as ClientConfig}; use crate::notifier::spawn_notifier; use beacon_chain::attestation_simulator::start_attestation_simulator_service; use beacon_chain::data_availability_checker::start_availability_cache_maintenance_service; +use beacon_chain::data_availability_checker_v2::start_availability_cache_maintenance_service as start_availability_cache_maintenance_service_v2; use beacon_chain::graffiti_calculator::start_engine_version_cache_refresh_service; use beacon_chain::proposer_prep_service::start_proposer_prep_service; use beacon_chain::schema_change::migrate_schema; @@ -786,6 +787,10 @@ where runtime_context.executor.clone(), beacon_chain.clone(), ); + start_availability_cache_maintenance_service_v2( + runtime_context.executor.clone(), + beacon_chain.clone(), + ); start_engine_version_cache_refresh_service( beacon_chain.as_ref(), runtime_context.executor.clone(), diff --git a/beacon_node/network/src/sync/block_sidecar_coupling.rs b/beacon_node/network/src/sync/block_sidecar_coupling.rs index 9a86cb64fb..88ac863482 100644 --- a/beacon_node/network/src/sync/block_sidecar_coupling.rs +++ b/beacon_node/network/src/sync/block_sidecar_coupling.rs @@ -490,7 +490,7 @@ mod tests { use super::RangeBlockComponentsRequest; use beacon_chain::custody_context::NodeCustodyType; - use beacon_chain::data_availability_router::DataColumnCache; + use beacon_chain::data_availability_router::AvailabilityCache; use beacon_chain::test_utils::{ NumBlobs, generate_rand_block_and_blobs, generate_rand_block_and_data_columns, test_da_checker, test_spec, From e1439e61e05805f66418781a79554157fac41bf4 Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Fri, 30 Jan 2026 11:08:11 -0800 Subject: [PATCH 015/118] Use module level imports --- consensus/types/src/execution/execution_payload_bid.rs | 5 ++--- consensus/types/src/execution/execution_payload_envelope.rs | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/consensus/types/src/execution/execution_payload_bid.rs b/consensus/types/src/execution/execution_payload_bid.rs index e81add5177..6dfed9f6e9 100644 --- a/consensus/types/src/execution/execution_payload_bid.rs +++ b/consensus/types/src/execution/execution_payload_bid.rs @@ -1,7 +1,6 @@ +use crate::kzg_ext::KzgCommitments; use crate::test_utils::TestRandom; -use crate::{ - Address, EthSpec, ExecutionBlockHash, ForkName, Hash256, KzgCommitments, SignedRoot, Slot, -}; +use crate::{Address, EthSpec, ExecutionBlockHash, ForkName, Hash256, SignedRoot, Slot}; use context_deserialize::context_deserialize; use educe::Educe; use serde::{Deserialize, Serialize}; diff --git a/consensus/types/src/execution/execution_payload_envelope.rs b/consensus/types/src/execution/execution_payload_envelope.rs index cf3315a58a..7f68dae037 100644 --- a/consensus/types/src/execution/execution_payload_envelope.rs +++ b/consensus/types/src/execution/execution_payload_envelope.rs @@ -1,7 +1,6 @@ +use crate::execution::{ExecutionPayloadGloas, ExecutionRequests}; use crate::test_utils::TestRandom; -use crate::{ - EthSpec, ExecutionPayloadGloas, ExecutionRequests, ForkName, Hash256, SignedRoot, Slot, -}; +use crate::{EthSpec, ForkName, Hash256, SignedRoot, Slot}; use context_deserialize::context_deserialize; use educe::Educe; use serde::{Deserialize, Serialize}; From 479fa3ff6a3c167772b460837d1df1ef8457b37f Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Fri, 30 Jan 2026 11:19:25 -0800 Subject: [PATCH 016/118] Remove unused error type --- beacon_node/beacon_chain/src/data_availability_checker/error.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/beacon_node/beacon_chain/src/data_availability_checker/error.rs b/beacon_node/beacon_chain/src/data_availability_checker/error.rs index 881cbe8569..af3cb72c03 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker/error.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker/error.rs @@ -19,7 +19,6 @@ pub enum Error { StoreError(store::Error), DecodeError(ssz::DecodeError), ParentStateMissing(Hash256), - StateMissing(Hash256), BlockReplayError(state_processing::BlockReplayError), RebuildingStateCaches(BeaconStateError), SlotClockError, @@ -44,7 +43,6 @@ impl Error { | Error::DecodeError(_) | Error::Unexpected(_) | Error::ParentStateMissing(_) - | Error::StateMissing(_) | Error::BlockReplayError(_) | Error::RebuildingStateCaches(_) | Error::SlotClockError From 4e04399e2178a8ede15f31d0b3230f05933a9e5d Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Fri, 30 Jan 2026 11:21:47 -0800 Subject: [PATCH 017/118] Use module level imports --- consensus/types/src/execution/signed_execution_payload_bid.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/consensus/types/src/execution/signed_execution_payload_bid.rs b/consensus/types/src/execution/signed_execution_payload_bid.rs index 1fe26ba1c6..6f73c2d34d 100644 --- a/consensus/types/src/execution/signed_execution_payload_bid.rs +++ b/consensus/types/src/execution/signed_execution_payload_bid.rs @@ -1,5 +1,6 @@ +use crate::execution::ExecutionPayloadBid; use crate::test_utils::TestRandom; -use crate::{EthSpec, ExecutionPayloadBid, ForkName}; +use crate::{EthSpec, ForkName}; use bls::Signature; use context_deserialize::context_deserialize; use educe::Educe; From 63e1e26ea3ca0ac1dc97032abcd901dd9aa085f1 Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Fri, 30 Jan 2026 14:10:28 -0800 Subject: [PATCH 018/118] Cache the bid instead of the block --- .../beacon_chain/src/block_verification.rs | 6 +- .../beacon_chain/src/custody_context.rs | 9 +-- .../src/data_availability_checker_v2.rs | 30 +++---- .../overflow_lru_cache.rs | 81 +++++++++---------- .../network_beacon_processor/sync_methods.rs | 3 +- 5 files changed, 58 insertions(+), 71 deletions(-) diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 3a55e96be5..b769a40d5a 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -671,7 +671,8 @@ pub fn signature_verify_chain_segment( } } } - // TODO(gloas) make this work across both v1 and v2 + // TODO(gloas) When implementing range and backfill sync for gloas + // we need a batch verify kzg function in the new da checker as well. chain .data_availability_checker .v1() @@ -1311,7 +1312,8 @@ impl IntoExecutionPendingBlock for RpcBlock let maybe_available_block = match &self { RpcBlock::FullyAvailable(available_block) => { - // TODO(gloas) make this work across both v1 and v2 + // TODO(gloas) when implementing sync for gloas we need a verify kzg function + // added to the new da checker as well. chain .data_availability_checker .v1() diff --git a/beacon_node/beacon_chain/src/custody_context.rs b/beacon_node/beacon_chain/src/custody_context.rs index cebb256a02..c512ce616a 100644 --- a/beacon_node/beacon_chain/src/custody_context.rs +++ b/beacon_node/beacon_chain/src/custody_context.rs @@ -7,7 +7,7 @@ use std::{ sync::atomic::{AtomicU64, Ordering}, }; use tracing::{debug, warn}; -use types::{ChainSpec, ColumnIndex, Epoch, EthSpec, SignedExecutionPayloadEnvelope, Slot}; +use types::{ChainSpec, ColumnIndex, Epoch, EthSpec, Slot}; /// A delay before making the CGC change effective to the data availability checker. pub const CUSTODY_CHANGE_DA_EFFECTIVE_DELAY_SECONDS: u64 = 30; @@ -527,13 +527,6 @@ impl CustodyContext { .write() .reset_validator_custody_requirements(effective_epoch); } - - pub fn data_columns_required_for_payload( - &self, - _payload: &SignedExecutionPayloadEnvelope, - ) -> bool { - todo!() - } } /// Indicates that the custody group count (CGC) has increased. diff --git a/beacon_node/beacon_chain/src/data_availability_checker_v2.rs b/beacon_node/beacon_chain/src/data_availability_checker_v2.rs index 63629dbfb7..fcff5605a4 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker_v2.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker_v2.rs @@ -15,7 +15,7 @@ use task_executor::TaskExecutor; use tracing::{debug, error, instrument}; use types::{ ChainSpec, ColumnIndex, DataColumnSidecar, DataColumnSidecarList, EthSpec, Hash256, - SignedBeaconBlock, Slot, + SignedExecutionPayloadBid, Slot, }; mod overflow_lru_cache; @@ -30,19 +30,20 @@ use crate::metrics::{ use crate::observed_data_sidecars::ObservationStrategy; use types::new_non_zero_usize; -/// The LRU Cache stores `PendingComponents`, which store block and its associated column data. +/// The LRU Cache stores `PendingComponents`, which store the block root, the execution payload bid, and its associated column data. +/// The execution payload bid stores the kzg commitments which we use to verify against incoming column data. /// Setting this to 32 keeps memory usage reasonable. /// /// `PendingComponents` are now never removed from the cache manually and are only removed via LRU /// eviction to prevent race conditions (#7961), so we expect this cache to be full all the time. const OVERFLOW_LRU_CAPACITY_NON_ZERO: NonZeroUsize = new_non_zero_usize(32); -/// Represents available data for a block - the block root and its data columns. +/// Represents available data for a payload - its block root and its data columns. pub type AvailableData = (Hash256, DataColumnSidecarList); -/// This type is returned after adding a block / column to the `DataAvailabilityChecker`. +/// This type is returned after adding a bid / column to the `DataAvailabilityChecker`. /// -/// Indicates if the block's data is fully `Available` or if we need more columns. +/// Indicates if the payloads data is fully `Available` or if we need more columns. pub enum Availability { MissingComponents(Hash256), Available(Box>), @@ -68,10 +69,10 @@ pub enum DataColumnReconstructionResult { RecoveredColumnsNotImported(&'static str), } -/// Cache to hold data columns for blocks pending data availability. +/// Cache to hold data columns for payloads pending data availability. /// /// In Gloas, beacon blocks can be immediately imported into fork choice. The execution payload -/// is separated from the beacon block. This cache tracks data columns for payloads until all +/// bid contains the payloads kzg commitments. This cache tracks data columns for payloads until all /// required columns are received. /// /// Usually data becomes available on its slot within a second of receiving its first component @@ -126,7 +127,7 @@ impl AvailabilityCache for DataAvailabilityChecker { }) } - /// Insert RPC custody columns and check if the block becomes available. + /// Insert RPC custody columns and check if the payload becomes available. #[instrument(skip_all, level = "trace")] fn put_rpc_custody_columns( &self, @@ -154,9 +155,8 @@ impl AvailabilityCache for DataAvailabilityChecker { .put_kzg_verified_data_columns(block_root, verified_custody_columns) } - /// Check if we've cached other data columns for this block. If it satisfies the custody - /// requirement and we also have the block cached, return the `Availability` variant - /// triggering import. Otherwise cache the data column sidecar. + /// Check if we've cached other data columns for this block root. If it satisfies the custody + /// requirement, return the `Availability::Available` variant. Otherwise cache the data column sidecar. #[instrument(skip_all, level = "trace")] fn put_gossip_verified_data_columns( &self, @@ -314,13 +314,13 @@ impl DataAvailabilityChecker { &self.custody_context } - /// Insert a block into the cache and check if data becomes available. - pub fn put_block( + /// Insert an execution payload bid into the cache and check if data becomes available. + pub fn put_bid( &self, block_root: Hash256, - block: Arc>, + bid: Arc>, ) -> Result, AvailabilityCheckError> { - self.availability_cache.put_block(block_root, block) + self.availability_cache.put_bid(block_root, bid) } /// Collects metrics from the data availability checker. diff --git a/beacon_node/beacon_chain/src/data_availability_checker_v2/overflow_lru_cache.rs b/beacon_node/beacon_chain/src/data_availability_checker_v2/overflow_lru_cache.rs index fa7ac913c1..51a19554dd 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker_v2/overflow_lru_cache.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker_v2/overflow_lru_cache.rs @@ -11,7 +11,7 @@ use std::sync::Arc; use tracing::{Span, debug, debug_span}; use types::{ ChainSpec, ColumnIndex, DataColumnSidecar, DataColumnSidecarList, Epoch, EthSpec, Hash256, - SignedBeaconBlock, + SignedExecutionPayloadBid, }; /// This represents the components of a payload pending data availability. @@ -22,7 +22,8 @@ pub struct PendingComponents { /// The block root is stored for tracing context in the span. #[allow(dead_code)] pub block_root: Hash256, - pub block: Option>>, + /// The execution payload bid containing blob_kzg_commitments. + pub bid: Option>>, pub verified_data_columns: Vec>, pub reconstruction_started: bool, span: Span, @@ -62,50 +63,40 @@ impl PendingComponents { Ok(()) } - /// Inserts a block into the cache. - pub fn insert_block(&mut self, block: Arc>) { - self.block = Some(block); + /// Inserts an execution payload bid into the cache. + pub fn insert_bid(&mut self, bid: Arc>) { + self.bid = Some(bid); } - /// Returns the number of blobs expected for this block by reading the bid's kzg commitments. - /// Returns an error if the block is not cached or not a Gloas block. + /// Returns the number of blobs expected by reading the bid's kzg commitments. + /// Returns an error if the bid is not cached. This function should only be called + /// after ensuring that the bid has been cached. pub fn num_blobs_expected(&self) -> Result { - let block = self - .block + let bid = self + .bid .as_ref() - .ok_or_else(|| AvailabilityCheckError::Unexpected("No block available".to_string()))?; - - let bid = block - .message() - .body() - .signed_execution_payload_bid() - .map_err(|_| { - AvailabilityCheckError::Unexpected( - "Block does not have execution payload bid (not a Gloas block?)".to_string(), - ) - })?; + .ok_or_else(|| AvailabilityCheckError::Unexpected("No bid available".to_string()))?; Ok(bid.message.blob_kzg_commitments.len()) } - /// Returns Some if all required data columns have been received. + /// Returns `Some` if the bid and all required data columns have been received. pub fn make_available( &self, num_expected_columns: usize, ) -> Result>, AvailabilityCheckError> { - // Check if we have a block - if not, still waiting - if self.block.is_none() { + // Check if we have a bid - if not, still waiting + if self.bid.is_none() { return Ok(None); } - // Get the number of blobs expected from the block's bid - // This will error if the block doesn't have a bid (not Gloas) + // Get the number of blobs expected from the bid let num_expected_blobs = self.num_blobs_expected()?; if num_expected_blobs == 0 { // No blobs expected, data is available (empty) self.span.in_scope(|| { - debug!("Block has no blobs, data is available"); + debug!("Bid has no blobs, data is available"); }); return Ok(Some(vec![])); } @@ -145,18 +136,18 @@ impl PendingComponents { let _guard = span.clone().entered(); Self { block_root, - block: None, + bid: None, verified_data_columns: vec![], reconstruction_started: false, span, } } - /// Returns the epoch of the block or first data column, if available. + /// Returns the epoch of the bid or first data column, if available. pub fn epoch(&self) -> Option { - // Get epoch from block - if let Some(block) = &self.block { - return Some(block.slot().epoch(E::slots_per_epoch())); + // Get epoch from bid + if let Some(bid) = &self.bid { + return Some(bid.message.slot.epoch(E::slots_per_epoch())); } // Or, get epoch from first data column @@ -232,17 +223,17 @@ impl DataAvailabilityCheckerInner { f(self.critical.read().peek(block_root)) } - /// Insert a block into the cache and check if data becomes available. - pub fn put_block( + /// Insert an execution payload bid into the cache and check if data becomes available. + pub fn put_bid( &self, block_root: Hash256, - block: Arc>, + bid: Arc>, ) -> Result, AvailabilityCheckError> { - let epoch = block.slot().epoch(T::EthSpec::slots_per_epoch()); + let epoch = bid.message.slot.epoch(T::EthSpec::slots_per_epoch()); let pending_components = self.update_or_insert_pending_components(block_root, |pending_components| { - pending_components.insert_block(block); + pending_components.insert_bid(bid); Ok(()) })?; @@ -250,7 +241,7 @@ impl DataAvailabilityCheckerInner { pending_components.span.in_scope(|| { debug!( - component = "block", + component = "bid", status = pending_components.status_str(num_expected_columns), "Component added to data availability checker" ); @@ -312,8 +303,8 @@ impl DataAvailabilityCheckerInner { } // We never remove the pending components manually to avoid race conditions. - // This ensures components remain available during and right after block import, - // preventing a race condition where a component was removed after the block was + // This ensures components remain available during and right after payload import, + // preventing a race condition where a component was removed after the payload was // imported, but re-inserted immediately, causing partial pending components to be // stored and served to peers. // Components are only removed via LRU eviction as finality advances. @@ -453,7 +444,7 @@ mod pending_components_tests { let components = PendingComponents::::empty(block_root); assert_eq!(components.block_root, block_root); - assert!(components.block.is_none()); + assert!(components.bid.is_none()); assert!(components.verified_data_columns.is_empty()); assert!(!components.reconstruction_started); assert!(components.epoch().is_none()); @@ -469,7 +460,7 @@ mod pending_components_tests { } #[test] - fn test_status_str_no_block() { + fn test_status_str_no_bid() { let block_root = Hash256::random(); let components = PendingComponents::::empty(block_root); @@ -478,7 +469,7 @@ mod pending_components_tests { } #[test] - fn test_num_blobs_expected_no_block() { + fn test_num_blobs_expected_no_bid() { let block_root = Hash256::random(); let components = PendingComponents::::empty(block_root); @@ -492,11 +483,11 @@ mod pending_components_tests { } #[test] - fn test_make_available_no_block_returns_none() { + fn test_make_available_no_bid_returns_none() { let block_root = Hash256::random(); let components = PendingComponents::::empty(block_root); - // Without a block, make_available should return Ok(None) + // Without a bid, make_available should return Ok(None) let result = components.make_available(10); assert!(result.is_ok()); assert!(result.unwrap().is_none()); @@ -760,7 +751,7 @@ mod data_availability_checker_tests { .put_kzg_verified_data_columns(block_root, verified_columns) .expect("should put columns"); - // Without a block, should still be missing components + // Without a bid, should still be missing components assert!(matches!(result, Availability::MissingComponents(_))); } diff --git a/beacon_node/network/src/network_beacon_processor/sync_methods.rs b/beacon_node/network/src/network_beacon_processor/sync_methods.rs index c4476bb508..dff8fbabff 100644 --- a/beacon_node/network/src/network_beacon_processor/sync_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/sync_methods.rs @@ -734,7 +734,8 @@ impl NetworkBeaconProcessor { } } - // TODO(gloas) make this work across both v1 and v2 + // TODO(gloas) when implementing backfill sync for gloas + // we need a batch verify kzg function in the new da checker match self .chain .data_availability_checker From 047599aac9c7939124c55c0380db0ad5c8de4e58 Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Fri, 30 Jan 2026 14:36:56 -0800 Subject: [PATCH 019/118] Fix CI --- consensus/types/src/execution/execution_payload_bid.rs | 6 +++++- .../types/src/execution/signed_execution_payload_bid.rs | 9 +++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/consensus/types/src/execution/execution_payload_bid.rs b/consensus/types/src/execution/execution_payload_bid.rs index 6dfed9f6e9..5c8771993e 100644 --- a/consensus/types/src/execution/execution_payload_bid.rs +++ b/consensus/types/src/execution/execution_payload_bid.rs @@ -11,7 +11,11 @@ use tree_hash_derive::TreeHash; #[derive( Default, Debug, Clone, Serialize, Encode, Decode, Deserialize, TreeHash, Educe, TestRandom, )] -#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec") +)] #[educe(PartialEq, Hash)] #[serde(bound = "E: EthSpec")] #[context_deserialize(ForkName)] diff --git a/consensus/types/src/execution/signed_execution_payload_bid.rs b/consensus/types/src/execution/signed_execution_payload_bid.rs index 1fe26ba1c6..48da445332 100644 --- a/consensus/types/src/execution/signed_execution_payload_bid.rs +++ b/consensus/types/src/execution/signed_execution_payload_bid.rs @@ -1,5 +1,6 @@ +use crate::execution::ExecutionPayloadBid; use crate::test_utils::TestRandom; -use crate::{EthSpec, ExecutionPayloadBid, ForkName}; +use crate::{EthSpec, ForkName}; use bls::Signature; use context_deserialize::context_deserialize; use educe::Educe; @@ -9,7 +10,11 @@ use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; #[derive(TestRandom, TreeHash, Debug, Clone, Encode, Decode, Serialize, Deserialize, Educe)] -#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec") +)] #[educe(PartialEq, Hash)] #[serde(bound = "E: EthSpec")] #[context_deserialize(ForkName)] From 16611452c5d090347bb1bdfb1d2d5e125c518e91 Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Tue, 3 Feb 2026 22:07:49 -0800 Subject: [PATCH 020/118] Clean up --- beacon_node/beacon_chain/src/beacon_chain.rs | 3 +- .../src/data_availability_checker_v2.rs | 2 +- .../src/data_availability_router.rs | 24 ++------- beacon_node/beacon_chain/src/lib.rs | 1 - .../src/payload_verification_types.rs | 53 ------------------- beacon_node/beacon_chain/src/test_utils.rs | 19 ------- beacon_node/store/src/metrics.rs | 7 --- 7 files changed, 8 insertions(+), 101 deletions(-) delete mode 100644 beacon_node/beacon_chain/src/payload_verification_types.rs diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index ae160431cd..ac55d5e55d 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -3353,7 +3353,8 @@ impl BeaconChain { } } } - ReconstructionOutcome::Payload(_data_column_reconstruction_result) => todo!(), + // TODO(gloas) handle data column reconstruction for gloas. + ReconstructionOutcome::Payload(_data_column_reconstruction_result) => return Err(BlockError::InternalError("Not yet implemented for gloas".to_owned())), } } diff --git a/beacon_node/beacon_chain/src/data_availability_checker_v2.rs b/beacon_node/beacon_chain/src/data_availability_checker_v2.rs index fcff5605a4..d499ef35ce 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker_v2.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker_v2.rs @@ -91,7 +91,7 @@ impl AvailabilityCache for DataAvailabilityChecker { type Availability = Availability; type ReconstructionResult = DataColumnReconstructionResult; - /// Returns the custody context used by this checker. + /// Returns the custody context. fn custody_context(&self) -> &Arc> { &self.custody_context } diff --git a/beacon_node/beacon_chain/src/data_availability_router.rs b/beacon_node/beacon_chain/src/data_availability_router.rs index 630a2bbb93..c2ed0b1ddc 100644 --- a/beacon_node/beacon_chain/src/data_availability_router.rs +++ b/beacon_node/beacon_chain/src/data_availability_router.rs @@ -31,7 +31,7 @@ use types::{ Slot, }; -/// Unified result from write operations that can come from either DA checker. +/// Unified result from operations that can come from either DA checker. /// /// This enum allows callers to handle availability from both v1 (blocks) and v2 (payloads) /// through a single type, with downstream processing handled by `BeaconChain::process_availability_outcome()`. @@ -138,7 +138,7 @@ pub trait AvailabilityCache: Send + Sync { /// V2 returns `DataColumnReconstructionResult` with payload availability. type ReconstructionResult; - /// Returns the custody context used by this checker. + /// Returns the custody context. fn custody_context(&self) -> &Arc>; /// Returns all cached data columns for the given block root, if any. @@ -192,12 +192,11 @@ pub trait AvailabilityCache: Send + Sync { /// Router that directs data availability checker operations to the appropriate version based on fork. /// -/// This wraps both the legacy (v1) and Gloas (v2) DA checkers, providing: -/// - Unified operations that dispatch to the correct checker based on fork -/// - Fork-aware routing for write operations that return `AvailabilityOutcome` +/// This wraps both the legacy (v1) and Gloas (v2) DA checkers, providing unified operations +/// that dispatch to the correct checker based on fork. /// /// After Gloas is fully activated and v1 is deprecated, this router can be deleted and -/// we can use the Gloas DA checker directly. +/// we can use the V2 DA checker directly. pub struct DataAvailabilityRouter where V1: AvailabilityCache< @@ -267,19 +266,6 @@ where } } - /// Query data columns from both checkers, returning the first match. - /// - /// Use this when you don't know which fork the block belongs to, or during - /// the transition period when data might be in either checker. - pub fn get_data_columns_any( - &self, - block_root: Hash256, - ) -> Option> { - self.v1 - .get_data_columns(block_root) - .or_else(|| self.v2.get_data_columns(block_root)) - } - pub fn is_data_column_cached( &self, slot: Slot, diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index a030557c52..2c7746c896 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -44,7 +44,6 @@ pub mod observed_block_producers; pub mod observed_data_sidecars; pub mod observed_operations; mod observed_slashable; -pub mod payload_verification_types; pub mod persisted_beacon_chain; pub mod persisted_custody; mod persisted_fork_choice; diff --git a/beacon_node/beacon_chain/src/payload_verification_types.rs b/beacon_node/beacon_chain/src/payload_verification_types.rs deleted file mode 100644 index 94c8b6cb5e..0000000000 --- a/beacon_node/beacon_chain/src/payload_verification_types.rs +++ /dev/null @@ -1,53 +0,0 @@ -use std::sync::Arc; - -use state_processing::ConsensusContext; -use types::{BeaconState, BlockImportSource, EthSpec, SignedExecutionPayloadEnvelope}; - -use crate::PayloadVerificationOutcome; - -#[derive(Debug, Clone, PartialEq)] -pub struct PayloadImportData { - pub state: BeaconState, - pub consensus_context: ConsensusContext, -} - -/// A payload that has completed payload verification by an EL client but does not -/// have all requisite column data to get imported into fork choice. -/// -/// Note: The number of expected blobs is not available from this type directly since -/// blob commitments are in the block's execution payload bid, not the payload envelope. -/// Use the associated block to get this information. -#[derive(Clone)] -pub struct AvailabilityPendingExecutedPayload { - pub payload: Arc>, - pub import_data: PayloadImportData, - pub payload_verification_outcome: PayloadVerificationOutcome, -} - -impl AvailabilityPendingExecutedPayload { - pub fn new( - payload: Arc>, - import_data: PayloadImportData, - payload_verification_outcome: PayloadVerificationOutcome, - ) -> Self { - Self { - payload, - import_data, - payload_verification_outcome, - } - } - - pub fn as_payload(&self) -> &SignedExecutionPayloadEnvelope { - &self.payload - } -} - -pub enum PayloadProcessStatus { - /// Payload is not in any pre-import cache. Payload may be in the data-base or in the fork-choice. - Unknown, - /// Payload is currently processing but not yet validated. - NotValidated(Arc>, BlockImportSource), - /// Payload is fully valid, but not yet imported. It's cached in the da_checker while awaiting - /// columns. - ExecutionValidated(Arc>), -} diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index 6f82c672d2..d9d5a2f289 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -2697,25 +2697,6 @@ where self.chain.slot_clock.set_slot(slot.into()); } - // TODO(gloas) this is a stub implementation for now - // we need payload processing functionality for this function - // to work - pub async fn add_payload_envelope_at_slot( - &self, - slot: Slot, - _state: BeaconState, - ) -> Result< - ( - SignedBeaconBlockHash, - SignedPayloadEnvelopeContentsTuple, - BeaconState, - ), - BlockError, - > { - self.set_current_slot(slot); - todo!() - } - pub async fn add_block_at_slot( &self, slot: Slot, diff --git a/beacon_node/store/src/metrics.rs b/beacon_node/store/src/metrics.rs index 2f83457b63..93c9840586 100644 --- a/beacon_node/store/src/metrics.rs +++ b/beacon_node/store/src/metrics.rs @@ -251,13 +251,6 @@ pub static BEACON_BLOBS_CACHE_HIT_COUNT: LazyLock> = LazyLock "Number of hits to the store's blob cache", ) }); -pub static BEACON_PAYLOAD_ENVELOPE_CACHE_HIT_COUNT: LazyLock> = - LazyLock::new(|| { - try_create_int_counter( - "store_beacon_payload_envelope_cache_hit_total", - "Number of hits to the store's payload envelope cache", - ) - }); pub static STORE_BEACON_BLOCK_CACHE_SIZE: LazyLock> = LazyLock::new(|| { try_create_int_gauge( "store_beacon_block_cache_size", From 2abb5f122a0853524c74101af33c32dee9befd70 Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Tue, 3 Feb 2026 22:08:32 -0800 Subject: [PATCH 021/118] fmt --- beacon_node/beacon_chain/src/beacon_chain.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index ac55d5e55d..3f5d9a4bc5 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -3354,7 +3354,11 @@ impl BeaconChain { } } // TODO(gloas) handle data column reconstruction for gloas. - ReconstructionOutcome::Payload(_data_column_reconstruction_result) => return Err(BlockError::InternalError("Not yet implemented for gloas".to_owned())), + ReconstructionOutcome::Payload(_data_column_reconstruction_result) => { + return Err(BlockError::InternalError( + "Not yet implemented for gloas".to_owned(), + )); + } } } From b5b5b0c654f8e0954bc0b4f8b0c2ab66b0669ead Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Tue, 3 Feb 2026 22:17:52 -0800 Subject: [PATCH 022/118] Rename --- beacon_node/beacon_chain/src/beacon_chain.rs | 8 +++----- .../mod.rs} | 4 ++-- ...{overflow_lru_cache.rs => pending_components_cache.rs} | 0 beacon_node/beacon_chain/src/test_utils.rs | 2 +- 4 files changed, 6 insertions(+), 8 deletions(-) rename beacon_node/beacon_chain/src/{data_availability_checker_v2.rs => data_availability_checker_v2/mod.rs} (99%) rename beacon_node/beacon_chain/src/data_availability_checker_v2/{overflow_lru_cache.rs => pending_components_cache.rs} (100%) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 3f5d9a4bc5..77a252b9e2 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -3354,11 +3354,9 @@ impl BeaconChain { } } // TODO(gloas) handle data column reconstruction for gloas. - ReconstructionOutcome::Payload(_data_column_reconstruction_result) => { - return Err(BlockError::InternalError( - "Not yet implemented for gloas".to_owned(), - )); - } + ReconstructionOutcome::Payload(_data_column_reconstruction_result) => Err( + BlockError::InternalError("Not yet implemented for gloas".to_owned()), + ), } } diff --git a/beacon_node/beacon_chain/src/data_availability_checker_v2.rs b/beacon_node/beacon_chain/src/data_availability_checker_v2/mod.rs similarity index 99% rename from beacon_node/beacon_chain/src/data_availability_checker_v2.rs rename to beacon_node/beacon_chain/src/data_availability_checker_v2/mod.rs index d499ef35ce..65e22d0a2f 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker_v2.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker_v2/mod.rs @@ -1,4 +1,4 @@ -use crate::data_availability_checker_v2::overflow_lru_cache::{ +use crate::data_availability_checker_v2::pending_components_cache::{ DataAvailabilityCheckerInner, ReconstructColumnsDecision, }; @@ -18,7 +18,7 @@ use types::{ SignedExecutionPayloadBid, Slot, }; -mod overflow_lru_cache; +mod pending_components_cache; use crate::data_column_verification::{ GossipVerifiedDataColumn, KzgVerifiedCustodyDataColumn, KzgVerifiedDataColumn, diff --git a/beacon_node/beacon_chain/src/data_availability_checker_v2/overflow_lru_cache.rs b/beacon_node/beacon_chain/src/data_availability_checker_v2/pending_components_cache.rs similarity index 100% rename from beacon_node/beacon_chain/src/data_availability_checker_v2/overflow_lru_cache.rs rename to beacon_node/beacon_chain/src/data_availability_checker_v2/pending_components_cache.rs diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index d9d5a2f289..2bf04a8401 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -24,7 +24,7 @@ use bls::get_withdrawal_credentials; use bls::{ AggregateSignature, Keypair, PublicKey, PublicKeyBytes, SecretKey, Signature, SignatureBytes, }; -use eth2::types::{GraffitiPolicy, SignedBlockContentsTuple, SignedPayloadEnvelopeContentsTuple}; +use eth2::types::{GraffitiPolicy, SignedBlockContentsTuple}; use execution_layer::test_utils::generate_genesis_header; use execution_layer::{ ExecutionLayer, From fa33577bce79581bf8450ecccff63d84e61d13d9 Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Wed, 4 Feb 2026 11:19:30 -0800 Subject: [PATCH 023/118] Remove TODO --- beacon_node/beacon_chain/src/beacon_chain.rs | 36 +++++++++++++++++--- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 77a252b9e2..1a0e1cb702 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -22,10 +22,12 @@ pub use crate::canonical_head::CanonicalHead; use crate::chain_config::ChainConfig; use crate::custody_context::CustodyContextSsz; use crate::data_availability_checker::{ - Availability, AvailabilityCheckError, AvailableBlock, AvailableBlockData, + Availability as BlockAvailability, AvailabilityCheckError, AvailableBlock, AvailableBlockData, DataAvailabilityChecker, DataColumnReconstructionResult, }; -use crate::data_availability_checker_v2::DataAvailabilityChecker as DataAvailabilityCheckerV2; +use crate::data_availability_checker_v2::{ + Availability as PayloadAvailability, DataAvailabilityChecker as DataAvailabilityCheckerV2, +}; use crate::data_availability_router::{ AvailabilityOutcome, DataAvailabilityRouter, ReconstructionOutcome, }; @@ -3792,20 +3794,44 @@ impl BeaconChain { match availability { AvailabilityOutcome::Block(availability) => { match availability { - Availability::Available(block) => { + BlockAvailability::Available(block) => { publish_fn()?; // Block is fully available, import into fork choice self.import_available_block(block).await } - Availability::MissingComponents(block_root) => Ok( + BlockAvailability::MissingComponents(block_root) => Ok( AvailabilityProcessingStatus::MissingComponents(slot, block_root), ), } } - AvailabilityOutcome::Payload(_availability) => todo!(), + AvailabilityOutcome::Payload(availability) => match availability { + PayloadAvailability::Available(available_payload_data) => { + // TODO(gloas) execution publish_fn + // publish_fn()?; + + // Payload data is fully available + let (block_root, data_columns) = *available_payload_data; + self.import_available_payload_data(block_root, data_columns) + .await + } + PayloadAvailability::MissingComponents(block_root) => Ok( + AvailabilityProcessingStatus::MissingComponents(slot, block_root), + ), + }, } } + #[instrument(skip_all)] + pub async fn import_available_payload_data( + self: &Arc, + block_root: Hash256, + _data_columns: Vec>>, + ) -> Result { + // TODO(gloas) this is just a stub implementation + // this function should mark payload data as available somehow + Ok(AvailabilityProcessingStatus::Imported(block_root)) + } + #[instrument(skip_all)] pub async fn import_available_block( self: &Arc, From a6cdc4187fb32cb6c92a09a4b0cbdd20d229c88e Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Mon, 9 Feb 2026 19:24:42 -0800 Subject: [PATCH 024/118] Update beacon_node/network/src/network_beacon_processor/gossip_methods.rs Co-authored-by: Pawan Dhananjay --- .../network/src/network_beacon_processor/gossip_methods.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 368ff55811..1b01ea3f39 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -1055,7 +1055,7 @@ impl NetworkBeaconProcessor { ); // If a block is in the da_checker, sync maybe awaiting for an event when block is finally - // imported. A block can become imported both after processing a block or data column. If a + // imported. A block can become imported both after processing a block or data column. If // importing a block results in `Imported`, notify. Do not notify of data column errors. self.send_sync_message(SyncMessage::GossipBlockProcessResult { block_root, From abf0c33e12e1698a7119f36d7a08e8d34f5d47b1 Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Tue, 10 Feb 2026 21:08:31 -0800 Subject: [PATCH 025/118] Refactor --- beacon_node/beacon_chain/src/beacon_chain.rs | 40 +-- .../beacon_chain/src/block_verification.rs | 2 - .../src/data_availability_checker.rs | 27 +- .../src/data_availability_checker_v2/mod.rs | 72 +++-- .../src/data_availability_router.rs | 246 ++++++++++-------- .../fetch_blobs/fetch_blobs_beacon_adapter.rs | 1 - .../tests/attestation_production.rs | 2 - .../beacon_chain/tests/block_verification.rs | 1 - beacon_node/beacon_chain/tests/store_tests.rs | 1 - .../gossip_methods.rs | 2 +- .../network_beacon_processor/rpc_methods.rs | 3 +- .../network_beacon_processor/sync_methods.rs | 1 - .../src/sync/block_sidecar_coupling.rs | 1 - .../network/src/sync/network_context.rs | 3 - beacon_node/network/src/sync/tests/lookups.rs | 4 - 15 files changed, 193 insertions(+), 213 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 1a0e1cb702..b41db18ccf 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -23,11 +23,9 @@ use crate::chain_config::ChainConfig; use crate::custody_context::CustodyContextSsz; use crate::data_availability_checker::{ Availability as BlockAvailability, AvailabilityCheckError, AvailableBlock, AvailableBlockData, - DataAvailabilityChecker, DataColumnReconstructionResult, -}; -use crate::data_availability_checker_v2::{ - Availability as PayloadAvailability, DataAvailabilityChecker as DataAvailabilityCheckerV2, + DataColumnReconstructionResult, }; +use crate::data_availability_checker_v2::Availability as PayloadAvailability; use crate::data_availability_router::{ AvailabilityOutcome, DataAvailabilityRouter, ReconstructionOutcome, }; @@ -484,8 +482,7 @@ pub struct BeaconChain { pub genesis_backfill_slot: Slot, /// Provides a KZG verification and temporary storage for blocks and blobs as /// they are collected and combined. - pub data_availability_checker: - Arc, DataAvailabilityCheckerV2>>, + pub data_availability_checker: Arc>, /// The KZG trusted setup used by this chain. pub kzg: Arc, /// RNG instance used by the chain. Currently used for shuffling column sidecars in block publishing. @@ -1315,11 +1312,7 @@ impl BeaconChain { /// chain. Used by sync to learn the status of a block and prevent repeated downloads / /// processing attempts. pub fn get_block_process_status(&self, block_root: &Hash256) -> BlockProcessStatus { - if let Some(cached_block) = self - .data_availability_checker - .v1() - .get_cached_block(block_root) - { + if let Some(cached_block) = self.data_availability_checker.get_cached_block(block_root) { return cached_block; } @@ -3190,7 +3183,6 @@ impl BeaconChain { { let imported_blobs = self .data_availability_checker - .v1() .cached_blob_indexes(block_root) .unwrap_or_default(); let new_blobs = blobs_iter.filter(|b| !imported_blobs.contains(&b.index)); @@ -3407,9 +3399,11 @@ impl BeaconChain { ); } - self.data_availability_checker - .v1() - .put_pre_execution_block(block_root, unverified_block.block_cloned(), block_source)?; + self.data_availability_checker.put_pre_execution_block( + block_root, + unverified_block.block_cloned(), + block_source, + )?; // Start the Prometheus timer. let _full_timer = metrics::start_timer(&metrics::BLOCK_PROCESSING_TIMES); @@ -3444,7 +3438,6 @@ impl BeaconChain { // chain to get stuck temporarily if the block is canonical. Therefore we remove // it from the cache if execution fails. self.data_availability_checker - .v1() .remove_block_on_execution_error(&block_root); })?; @@ -3572,11 +3565,8 @@ impl BeaconChain { block: AvailabilityPendingExecutedBlock, ) -> Result { let slot = block.block.slot(); - let availability = AvailabilityOutcome::Block( - self.data_availability_checker - .v1() - .put_executed_block(block)?, - ); + let availability = + AvailabilityOutcome::Block(self.data_availability_checker.put_executed_block(block)?); self.process_availability(slot, availability, || Ok(())) .await } @@ -3593,7 +3583,6 @@ impl BeaconChain { } let availability = AvailabilityOutcome::Block( self.data_availability_checker - .v1() .put_gossip_verified_blobs(blob.block_root(), std::iter::once(blob))?, ); @@ -3672,7 +3661,6 @@ impl BeaconChain { )?; let availability = AvailabilityOutcome::Block( self.data_availability_checker - .v1() .put_rpc_blobs(block_root, blobs)?, ); @@ -3694,7 +3682,6 @@ impl BeaconChain { )?; let availability = self .data_availability_checker - .v1() .put_kzg_verified_blobs(block_root, blobs)?; AvailabilityOutcome::Block(availability) @@ -7469,15 +7456,12 @@ impl BeaconChain { /// The epoch at which we require a data availability check in block processing. /// `None` if the `Deneb` fork is disabled. pub fn data_availability_boundary(&self) -> Option { - self.data_availability_checker - .v1() - .data_availability_boundary() + self.data_availability_checker.data_availability_boundary() } /// Returns true if epoch is within the data availability boundary pub fn da_check_required_for_epoch(&self, epoch: Epoch) -> bool { self.data_availability_checker - .v1() .da_check_required_for_epoch(epoch) } diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index ae33c829be..6edb6e1ada 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -675,7 +675,6 @@ pub fn signature_verify_chain_segment( // we need a batch verify kzg function in the new da checker as well. chain .data_availability_checker - .v1() .batch_verify_kzg_for_available_blocks(&available_blocks)?; // verify signatures @@ -1316,7 +1315,6 @@ impl IntoExecutionPendingBlock for RpcBlock // added to the new da checker as well. chain .data_availability_checker - .v1() .verify_kzg_for_available_block(available_block) .map_err(|e| { BlockSlashInfo::SignatureNotChecked( diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index a7738b0a3e..75b0760b4e 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -5,7 +5,6 @@ use crate::block_verification_types::{AvailabilityPendingExecutedBlock, Availabl use crate::data_availability_checker::overflow_lru_cache::{ DataAvailabilityCheckerInner, ReconstructColumnsDecision, }; -use crate::data_availability_router::AvailabilityCache; use crate::{BeaconChain, BeaconChainTypes, BlockProcessStatus, CustodyContext, metrics}; use educe::Educe; use kzg::Kzg; @@ -359,22 +358,22 @@ impl DataAvailabilityChecker { } } -impl AvailabilityCache for DataAvailabilityChecker { - type Availability = Availability; - type ReconstructionResult = DataColumnReconstructionResult; - - fn custody_context(&self) -> &Arc> { +impl DataAvailabilityChecker { + pub fn custody_context(&self) -> &Arc> { &self.custody_context } /// Get data columns for a block from the availability cache. - fn get_data_columns(&self, block_root: Hash256) -> Option> { + pub fn get_data_columns( + &self, + block_root: Hash256, + ) -> Option> { self.availability_cache.peek_data_columns(block_root) } /// Return the set of cached custody column indices for `block_root`. Returns None if there is /// no block component for `block_root`. - fn cached_data_column_indexes(&self, block_root: &Hash256) -> Option> { + pub fn cached_data_column_indexes(&self, block_root: &Hash256) -> Option> { self.availability_cache .peek_pending_components(block_root, |components| { components.map(|components| components.get_cached_data_columns_indices()) @@ -382,7 +381,7 @@ impl AvailabilityCache for DataAvailabilityChecker { } /// Check if the exact data column is in the availability cache. - fn is_data_column_cached( + pub fn is_data_column_cached( &self, block_root: &Hash256, data_column: &DataColumnSidecar, @@ -400,7 +399,7 @@ impl AvailabilityCache for DataAvailabilityChecker { /// verification on the blobs in the list. #[allow(clippy::type_complexity)] #[instrument(skip_all, level = "trace")] - fn put_rpc_custody_columns( + pub fn put_rpc_custody_columns( &self, block_root: Hash256, slot: Slot, @@ -435,7 +434,7 @@ impl AvailabilityCache for DataAvailabilityChecker { /// /// This should only accept gossip verified data columns, so we should not have to worry about dupes. #[instrument(skip_all, level = "trace")] - fn put_gossip_verified_data_columns( + pub fn put_gossip_verified_data_columns( &self, block_root: Hash256, slot: Slot, @@ -456,7 +455,7 @@ impl AvailabilityCache for DataAvailabilityChecker { } #[instrument(skip_all, level = "trace")] - fn put_kzg_verified_custody_data_columns( + pub fn put_kzg_verified_custody_data_columns( &self, block_root: Hash256, custody_columns: Vec>, @@ -466,7 +465,7 @@ impl AvailabilityCache for DataAvailabilityChecker { } #[instrument(skip_all, level = "debug")] - fn reconstruct_data_columns( + pub fn reconstruct_data_columns( &self, block_root: &Hash256, ) -> Result, AvailabilityCheckError> { @@ -554,7 +553,7 @@ impl AvailabilityCache for DataAvailabilityChecker { } /// Verifies KZG commitments for data columns. - fn verify_kzg_for_data_columns( + pub fn verify_kzg_for_data_columns( &self, data_columns: &DataColumnSidecarList, ) -> Result<(), AvailabilityCheckError> { diff --git a/beacon_node/beacon_chain/src/data_availability_checker_v2/mod.rs b/beacon_node/beacon_chain/src/data_availability_checker_v2/mod.rs index 65e22d0a2f..cad8070a65 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker_v2/mod.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker_v2/mod.rs @@ -3,7 +3,6 @@ use crate::data_availability_checker_v2::pending_components_cache::{ }; use crate::data_availability_checker::AvailabilityCheckError; -use crate::data_availability_router::AvailabilityCache; use crate::{BeaconChain, BeaconChainTypes, CustodyContext, metrics}; use kzg::Kzg; use slot_clock::SlotClock; @@ -87,24 +86,43 @@ pub struct DataAvailabilityChecker { spec: Arc, } -impl AvailabilityCache for DataAvailabilityChecker { - type Availability = Availability; - type ReconstructionResult = DataColumnReconstructionResult; +impl DataAvailabilityChecker { + pub fn new( + slot_clock: T::SlotClock, + kzg: Arc, + custody_context: Arc>, + spec: Arc, + ) -> Result { + let inner = DataAvailabilityCheckerInner::new( + OVERFLOW_LRU_CAPACITY_NON_ZERO, + custody_context.clone(), + spec.clone(), + )?; + Ok(Self { + availability_cache: Arc::new(inner), + slot_clock, + kzg, + custody_context, + spec, + }) + } - /// Returns the custody context. - fn custody_context(&self) -> &Arc> { + pub fn custody_context(&self) -> &Arc> { &self.custody_context } /// Returns all cached data columns for the given block root, if any. #[instrument(skip_all, level = "trace")] - fn get_data_columns(&self, block_root: Hash256) -> Option> { + pub fn get_data_columns( + &self, + block_root: Hash256, + ) -> Option> { self.availability_cache.peek_data_columns(block_root) } /// Returns the indices of cached data columns for the given block root. #[instrument(skip_all, level = "trace")] - fn cached_data_column_indexes(&self, block_root: &Hash256) -> Option> { + pub fn cached_data_column_indexes(&self, block_root: &Hash256) -> Option> { self.availability_cache .peek_pending_components(block_root, |components| { components.map(|components| components.get_cached_data_columns_indices()) @@ -113,7 +131,7 @@ impl AvailabilityCache for DataAvailabilityChecker { /// Checks if a specific data column is cached for the given block root. #[instrument(skip_all, level = "trace")] - fn is_data_column_cached( + pub fn is_data_column_cached( &self, block_root: &Hash256, data_column: &DataColumnSidecar, @@ -129,7 +147,7 @@ impl AvailabilityCache for DataAvailabilityChecker { /// Insert RPC custody columns and check if the payload becomes available. #[instrument(skip_all, level = "trace")] - fn put_rpc_custody_columns( + pub fn put_rpc_custody_columns( &self, block_root: Hash256, slot: Slot, @@ -158,7 +176,7 @@ impl AvailabilityCache for DataAvailabilityChecker { /// Check if we've cached other data columns for this block root. If it satisfies the custody /// requirement, return the `Availability::Available` variant. Otherwise cache the data column sidecar. #[instrument(skip_all, level = "trace")] - fn put_gossip_verified_data_columns( + pub fn put_gossip_verified_data_columns( &self, block_root: Hash256, slot: Slot, @@ -179,7 +197,7 @@ impl AvailabilityCache for DataAvailabilityChecker { } #[instrument(skip_all, level = "trace")] - fn put_kzg_verified_custody_data_columns( + pub fn put_kzg_verified_custody_data_columns( &self, block_root: Hash256, custody_columns: Vec>, @@ -189,7 +207,7 @@ impl AvailabilityCache for DataAvailabilityChecker { } #[instrument(skip_all, level = "debug")] - fn reconstruct_data_columns( + pub fn reconstruct_data_columns( &self, block_root: &Hash256, ) -> Result, AvailabilityCheckError> { @@ -277,7 +295,7 @@ impl AvailabilityCache for DataAvailabilityChecker { } /// Verifies KZG commitments for data columns. - fn verify_kzg_for_data_columns( + pub fn verify_kzg_for_data_columns( &self, data_columns: &DataColumnSidecarList, ) -> Result<(), AvailabilityCheckError> { @@ -287,32 +305,6 @@ impl AvailabilityCache for DataAvailabilityChecker { } Ok(()) } -} - -impl DataAvailabilityChecker { - pub fn new( - slot_clock: T::SlotClock, - kzg: Arc, - custody_context: Arc>, - spec: Arc, - ) -> Result { - let inner = DataAvailabilityCheckerInner::new( - OVERFLOW_LRU_CAPACITY_NON_ZERO, - custody_context.clone(), - spec.clone(), - )?; - Ok(Self { - availability_cache: Arc::new(inner), - slot_clock, - kzg, - custody_context, - spec, - }) - } - - pub fn custody_context(&self) -> &Arc> { - &self.custody_context - } /// Insert an execution payload bid into the cache and check if data becomes available. pub fn put_bid( diff --git a/beacon_node/beacon_chain/src/data_availability_router.rs b/beacon_node/beacon_chain/src/data_availability_router.rs index c2ed0b1ddc..b480cce5b0 100644 --- a/beacon_node/beacon_chain/src/data_availability_router.rs +++ b/beacon_node/beacon_chain/src/data_availability_router.rs @@ -6,7 +6,7 @@ //! //! ## Design //! -//! - **Unified operations**: Via the `AvailabilityCache` trait (blocks, columns, availability checks) +//! - **Unified operations**: Shared column operations dispatched to v1 or v2 //! - **Fork-aware routing**: `DataAvailabilityRouter` dispatches to v1 or v2 based on slot //! - **Processing**: `BeaconChain::process_availability_outcome()` handles both result types //! @@ -14,21 +14,25 @@ //! use the Gloas DA checker directly. use crate::BeaconChainTypes; +use crate::BlockProcessStatus; +use crate::blob_verification::{GossipVerifiedBlob, KzgVerifiedBlob}; +use crate::block_verification_types::AvailabilityPendingExecutedBlock; use crate::custody_context::CustodyContext; use crate::data_availability_checker::{ - Availability as BlockAvailability, AvailabilityCheckError, - DataColumnReconstructionResult as BlockReconstructionResult, + Availability as BlockAvailability, AvailabilityCheckError, AvailableBlock, + DataAvailabilityChecker, DataColumnReconstructionResult as BlockReconstructionResult, }; use crate::data_availability_checker_v2::{ - Availability as PayloadAvailability, + Availability as PayloadAvailability, DataAvailabilityChecker as DataAvailabilityCheckerV2, DataColumnReconstructionResult as PayloadReconstructionResult, }; use crate::data_column_verification::{GossipVerifiedDataColumn, KzgVerifiedCustodyDataColumn}; use crate::observed_data_sidecars::ObservationStrategy; use std::sync::Arc; +use types::data::{BlobIdentifier, FixedBlobSidecarList}; use types::{ - ChainSpec, ColumnIndex, DataColumnSidecar, DataColumnSidecarList, EthSpec, ForkName, Hash256, - Slot, + BlobSidecar, BlockImportSource, ChainSpec, ColumnIndex, DataColumnSidecar, + DataColumnSidecarList, Epoch, EthSpec, ForkName, Hash256, SignedBeaconBlock, Slot, }; /// Unified result from operations that can come from either DA checker. @@ -122,74 +126,6 @@ impl ReconstructionOutcome { } } -/// Trait for data availability operations on availability checkers. -/// -/// Both `DataAvailabilityChecker` (v1) and `DataAvailabilityChecker` (v2) implement -/// this trait. The associated types differ: -/// - V1: Returns `Availability` containing `AvailableExecutedBlock` -/// - V2: Returns `Availability` containing `(Hash256, DataColumnSidecarList)` (block root + columns) -pub trait AvailabilityCache: Send + Sync { - /// The availability type returned by write operations. - /// V1 returns block availability, V2 returns payload availability. - type Availability; - - /// The reconstruction result type. - /// V1 returns `DataColumnReconstructionResult` with block availability. - /// V2 returns `DataColumnReconstructionResult` with payload availability. - type ReconstructionResult; - - /// Returns the custody context. - fn custody_context(&self) -> &Arc>; - - /// Returns all cached data columns for the given block root, if any. - fn get_data_columns(&self, block_root: Hash256) -> Option>; - - /// Returns the indices of cached data columns for the given block root. - fn cached_data_column_indexes(&self, block_root: &Hash256) -> Option>; - - /// Checks if a specific data column is cached for the given block root. - fn is_data_column_cached( - &self, - block_root: &Hash256, - data_column: &DataColumnSidecar, - ) -> bool; - - /// Insert RPC custody columns and check if the block/payload becomes available. - fn put_rpc_custody_columns( - &self, - block_root: Hash256, - slot: Slot, - custody_columns: DataColumnSidecarList, - ) -> Result; - - /// Insert gossip-verified data columns and check availability. - fn put_gossip_verified_data_columns( - &self, - block_root: Hash256, - slot: Slot, - data_columns: Vec>, - ) -> Result; - - /// Insert KZG-verified custody data columns and check availability. - fn put_kzg_verified_custody_data_columns( - &self, - block_root: Hash256, - custody_columns: Vec>, - ) -> Result; - - /// Attempt to reconstruct missing data columns from available ones. - fn reconstruct_data_columns( - &self, - block_root: &Hash256, - ) -> Result; - - /// Verifies KZG commitments for a list of data columns. - fn verify_kzg_for_data_columns( - &self, - data_columns: &DataColumnSidecarList, - ) -> Result<(), AvailabilityCheckError>; -} - /// Router that directs data availability checker operations to the appropriate version based on fork. /// /// This wraps both the legacy (v1) and Gloas (v2) DA checkers, providing unified operations @@ -197,47 +133,21 @@ pub trait AvailabilityCache: Send + Sync { /// /// After Gloas is fully activated and v1 is deprecated, this router can be deleted and /// we can use the V2 DA checker directly. -pub struct DataAvailabilityRouter -where - V1: AvailabilityCache< - T, - Availability = BlockAvailability, - ReconstructionResult = BlockReconstructionResult, - >, - V2: AvailabilityCache< - T, - Availability = PayloadAvailability, - ReconstructionResult = PayloadReconstructionResult, - >, -{ +pub struct DataAvailabilityRouter { /// Legacy DA checker for pre-Gloas blocks - v1: Arc, + v1: Arc>, /// Gloas DA checker for payload envelopes - v2: Arc, + v2: Arc>, spec: Arc, - _phantom: std::marker::PhantomData, } -impl DataAvailabilityRouter -where - V1: AvailabilityCache< - T, - Availability = BlockAvailability, - ReconstructionResult = BlockReconstructionResult, - >, - V2: AvailabilityCache< - T, - Availability = PayloadAvailability, - ReconstructionResult = PayloadReconstructionResult, - >, -{ - pub fn new(v1: Arc, v2: Arc, spec: Arc) -> Self { - Self { - v1, - v2, - spec, - _phantom: std::marker::PhantomData, - } +impl DataAvailabilityRouter { + pub fn new( + v1: Arc>, + v2: Arc>, + spec: Arc, + ) -> Self { + Self { v1, v2, spec } } /// Returns true if the given slot is in the Gloas fork or later. @@ -247,6 +157,8 @@ where .gloas_enabled() } + // ── Shared methods (dispatched to v1 or v2 based on fork) ── + /// Returns the custody context (same for both checkers). pub fn custody_context(&self) -> &Arc> { // Both checkers share the same custody context @@ -363,17 +275,127 @@ where } } + // ── V1-only methods (blobs, blocks, boundary queries) ── + + /// Returns the data availability boundary epoch (v1). + pub fn data_availability_boundary(&self) -> Option { + self.v1.data_availability_boundary() + } + + /// Returns whether a DA check is required for the given epoch (v1). + pub fn da_check_required_for_epoch(&self, epoch: Epoch) -> bool { + self.v1.da_check_required_for_epoch(epoch) + } + + /// Returns whether blobs are required for the given epoch (v1). + pub fn blobs_required_for_epoch(&self, epoch: Epoch) -> bool { + self.v1.blobs_required_for_epoch(epoch) + } + + /// Returns whether data columns are required for the given epoch (v1). + pub fn data_columns_required_for_epoch(&self, epoch: Epoch) -> bool { + self.v1.data_columns_required_for_epoch(epoch) + } + + /// Verifies KZG commitments for a single available block (v1). + pub fn verify_kzg_for_available_block( + &self, + available_block: &AvailableBlock, + ) -> Result<(), AvailabilityCheckError> { + self.v1.verify_kzg_for_available_block(available_block) + } + + /// Batch verifies KZG commitments for multiple available blocks (v1). + pub fn batch_verify_kzg_for_available_blocks( + &self, + available_blocks: &[AvailableBlock], + ) -> Result<(), AvailabilityCheckError> { + self.v1 + .batch_verify_kzg_for_available_blocks(available_blocks) + } + + /// Get a blob from the availability cache (v1). + pub fn get_blob( + &self, + blob_id: &BlobIdentifier, + ) -> Result>>, AvailabilityCheckError> { + self.v1.get_blob(blob_id) + } + + /// Returns the cached blob indexes for a given block root (v1). + pub fn cached_blob_indexes(&self, block_root: &Hash256) -> Option> { + self.v1.cached_blob_indexes(block_root) + } + + /// Returns the cached block for a given block root (v1). + pub fn get_cached_block(&self, block_root: &Hash256) -> Option> { + self.v1.get_cached_block(block_root) + } + + /// Inserts a pre-execution block into the cache (v1). + pub fn put_pre_execution_block( + &self, + block_root: Hash256, + block: Arc>, + source: BlockImportSource, + ) -> Result<(), AvailabilityCheckError> { + self.v1.put_pre_execution_block(block_root, block, source) + } + + /// Insert an executed block and check availability (v1). + pub fn put_executed_block( + &self, + executed_block: AvailabilityPendingExecutedBlock, + ) -> Result, AvailabilityCheckError> { + self.v1.put_executed_block(executed_block) + } + + /// Removes a pre-execution block from the cache on execution error (v1). + pub fn remove_block_on_execution_error(&self, block_root: &Hash256) { + self.v1.remove_block_on_execution_error(block_root) + } + + /// Insert blobs received via RPC and check availability (v1). + pub fn put_rpc_blobs( + &self, + block_root: Hash256, + blobs: FixedBlobSidecarList, + ) -> Result, AvailabilityCheckError> { + self.v1.put_rpc_blobs(block_root, blobs) + } + + /// Insert KZG-verified blobs and check availability (v1). + pub fn put_kzg_verified_blobs>>( + &self, + block_root: Hash256, + blobs: I, + ) -> Result, AvailabilityCheckError> { + self.v1.put_kzg_verified_blobs(block_root, blobs) + } + + /// Insert gossip-verified blobs into the v1 checker. + pub fn put_gossip_verified_blobs< + I: IntoIterator>, + O: ObservationStrategy, + >( + &self, + block_root: Hash256, + blobs: I, + ) -> Result, AvailabilityCheckError> { + self.v1.put_gossip_verified_blobs(block_root, blobs) + } + /// Direct access to v1 checker for block execution/availability checks. /// /// Use this for operations that are specific to the legacy DA checker, - pub fn v1(&self) -> Arc { + pub fn v1(&self) -> Arc> { self.v1.clone() } /// Direct access to v2 checker for payload availability checks. /// /// Use this for operations that are specific to the Gloas DA checker, - pub fn v2(&self) -> Arc { + pub fn v2(&self) -> Arc> { self.v2.clone() } } diff --git a/beacon_node/beacon_chain/src/fetch_blobs/fetch_blobs_beacon_adapter.rs b/beacon_node/beacon_chain/src/fetch_blobs/fetch_blobs_beacon_adapter.rs index 71d2b65bb5..8575e0d8de 100644 --- a/beacon_node/beacon_chain/src/fetch_blobs/fetch_blobs_beacon_adapter.rs +++ b/beacon_node/beacon_chain/src/fetch_blobs/fetch_blobs_beacon_adapter.rs @@ -92,7 +92,6 @@ impl FetchBlobsBeaconAdapter { pub(crate) fn cached_blob_indexes(&self, block_root: &Hash256) -> Option> { self.chain .data_availability_checker - .v1() .cached_blob_indexes(block_root) } diff --git a/beacon_node/beacon_chain/tests/attestation_production.rs b/beacon_node/beacon_chain/tests/attestation_production.rs index 5fb2d0897d..a1922f32a4 100644 --- a/beacon_node/beacon_chain/tests/attestation_production.rs +++ b/beacon_node/beacon_chain/tests/attestation_production.rs @@ -230,7 +230,6 @@ async fn produces_attestations() { RpcBlock::FullyAvailable(available_block) => { chain .data_availability_checker - .v1() .verify_kzg_for_available_block(&available_block) .unwrap(); available_block @@ -301,7 +300,6 @@ async fn early_attester_cache_old_request() { harness .chain .data_availability_checker - .v1() .verify_kzg_for_available_block(&available_block) .unwrap(); available_block diff --git a/beacon_node/beacon_chain/tests/block_verification.rs b/beacon_node/beacon_chain/tests/block_verification.rs index 74526261c8..952170b12a 100644 --- a/beacon_node/beacon_chain/tests/block_verification.rs +++ b/beacon_node/beacon_chain/tests/block_verification.rs @@ -2219,7 +2219,6 @@ async fn rpc_block_allows_construction_past_da_boundary() { let da_boundary = harness .chain .data_availability_checker - .v1() .data_availability_boundary() .expect("DA boundary should be set"); assert!( diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index 03ce894485..96c8b7748b 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -3182,7 +3182,6 @@ async fn weak_subjectivity_sync_test( harness .chain .data_availability_checker - .v1() .verify_kzg_for_available_block(&available_block) .expect("should verify kzg"); available_blocks.push(available_block); 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 ec15f2b639..d38089c6da 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -1055,7 +1055,7 @@ impl NetworkBeaconProcessor { ); // If a block is in the da_checker, sync maybe awaiting for an event when block is finally - // imported. A block can become imported both after processing a block or data column. If + // imported. A block can become imported both after processing a block or data column. If // importing a block results in `Imported`, notify. Do not notify of data column errors. self.send_sync_message(SyncMessage::GossipBlockProcessResult { block_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 7d09464f4d..4095ed847f 100644 --- a/beacon_node/network/src/network_beacon_processor/rpc_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/rpc_methods.rs @@ -306,7 +306,6 @@ impl NetworkBeaconProcessor { let block_root = blob_id.block_root; self.chain .data_availability_checker - .v1() .get_cached_block(&block_root) .and_then(|status| match status { BlockProcessStatus::NotValidated(block, _source) => Some(block), @@ -334,7 +333,7 @@ impl NetworkBeaconProcessor { } // First attempt to get the blobs from the RPC cache. - if let Ok(Some(blob)) = self.chain.data_availability_checker.v1().get_blob(id) { + if let Ok(Some(blob)) = self.chain.data_availability_checker.get_blob(id) { self.send_response( peer_id, inbound_request_id, diff --git a/beacon_node/network/src/network_beacon_processor/sync_methods.rs b/beacon_node/network/src/network_beacon_processor/sync_methods.rs index 6f33344b94..8bc5b22be5 100644 --- a/beacon_node/network/src/network_beacon_processor/sync_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/sync_methods.rs @@ -739,7 +739,6 @@ impl NetworkBeaconProcessor { match self .chain .data_availability_checker - .v1() .batch_verify_kzg_for_available_blocks(&available_blocks) { Ok(()) => {} diff --git a/beacon_node/network/src/sync/block_sidecar_coupling.rs b/beacon_node/network/src/sync/block_sidecar_coupling.rs index 88ac863482..a287771854 100644 --- a/beacon_node/network/src/sync/block_sidecar_coupling.rs +++ b/beacon_node/network/src/sync/block_sidecar_coupling.rs @@ -490,7 +490,6 @@ mod tests { use super::RangeBlockComponentsRequest; use beacon_chain::custody_context::NodeCustodyType; - use beacon_chain::data_availability_router::AvailabilityCache; use beacon_chain::test_utils::{ NumBlobs, generate_rand_block_and_blobs, generate_rand_block_and_data_columns, test_da_checker, test_spec, diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index bdeff68855..22730fcff3 100644 --- a/beacon_node/network/src/sync/network_context.rs +++ b/beacon_node/network/src/sync/network_context.rs @@ -967,7 +967,6 @@ impl SyncNetworkContext { let imported_blob_indexes = self .chain .data_availability_checker - .v1() .cached_blob_indexes(&block_root) .unwrap_or_default(); // Include only the blob indexes not yet imported (received through gossip) @@ -1371,14 +1370,12 @@ impl SyncNetworkContext { if self .chain .data_availability_checker - .v1() .data_columns_required_for_epoch(epoch) { ByRangeRequestType::BlocksAndColumns } else if self .chain .data_availability_checker - .v1() .blobs_required_for_epoch(epoch) { ByRangeRequestType::BlocksAndBlobs diff --git a/beacon_node/network/src/sync/tests/lookups.rs b/beacon_node/network/src/sync/tests/lookups.rs index 12fd2035da..63024bd2aa 100644 --- a/beacon_node/network/src/sync/tests/lookups.rs +++ b/beacon_node/network/src/sync/tests/lookups.rs @@ -1082,7 +1082,6 @@ impl TestRig { .harness .chain .data_availability_checker - .v1() .put_executed_block(executed_block) .unwrap() { @@ -1098,7 +1097,6 @@ impl TestRig { .harness .chain .data_availability_checker - .v1() .put_gossip_verified_blobs( blob.block_root(), std::iter::once(GossipVerifiedBlob::<_, Observe>::__assumed_valid( @@ -1118,7 +1116,6 @@ impl TestRig { self.harness .chain .data_availability_checker - .v1() .put_pre_execution_block(block.canonical_root(), block, BlockImportSource::Gossip) .unwrap(); } @@ -1127,7 +1124,6 @@ impl TestRig { self.harness .chain .data_availability_checker - .v1() .remove_block_on_execution_error(&block_root); self.send_sync_message(SyncMessage::GossipBlockProcessResult { From 23a7dc561fc65b22ee2f697aabc7b2a1ef3670de Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Tue, 10 Feb 2026 21:13:40 -0800 Subject: [PATCH 026/118] Fix --- .../src/data_availability_checker.rs | 230 +++++++++--------- 1 file changed, 110 insertions(+), 120 deletions(-) diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index 75b0760b4e..f6daf386a9 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -136,6 +136,10 @@ impl DataAvailabilityChecker { }) } + pub fn custody_context(&self) -> &Arc> { + &self.custody_context + } + /// Checks if the block root is currently in the availability cache awaiting import because /// of missing components. /// @@ -159,6 +163,30 @@ impl DataAvailabilityChecker { }) } + /// Return the set of cached custody column indexes for `block_root`. Returns None if there is + /// no block component for `block_root`. + pub fn cached_data_column_indexes(&self, block_root: &Hash256) -> Option> { + self.availability_cache + .peek_pending_components(block_root, |components| { + components.map(|components| components.get_cached_data_columns_indices()) + }) + } + + /// Check if the exact data column is in the availability cache. + pub fn is_data_column_cached( + &self, + block_root: &Hash256, + data_column: &DataColumnSidecar, + ) -> bool { + self.availability_cache + .peek_pending_components(block_root, |components| { + components.is_some_and(|components| { + let cached_column_opt = components.get_cached_data_column(*data_column.index()); + cached_column_opt.is_some_and(|cached| *cached == *data_column) + }) + }) + } + /// Get a blob from the availability cache. pub fn get_blob( &self, @@ -167,6 +195,14 @@ impl DataAvailabilityChecker { self.availability_cache.peek_blob(blob_id) } + /// Get data columns for a block from the availability cache. + pub fn get_data_columns( + &self, + block_root: Hash256, + ) -> Option> { + self.availability_cache.peek_data_columns(block_root) + } + /// Put a list of blobs received via RPC into the availability cache. This performs KZG /// verification on the blobs in the list. #[instrument(skip_all, level = "trace")] @@ -194,6 +230,39 @@ impl DataAvailabilityChecker { .put_kzg_verified_blobs(block_root, verified_blobs) } + /// Put a list of custody columns received via RPC into the availability cache. This performs KZG + /// verification on the blobs in the list. + #[allow(clippy::type_complexity)] + #[instrument(skip_all, level = "trace")] + pub fn put_rpc_custody_columns( + &self, + block_root: Hash256, + slot: Slot, + custody_columns: DataColumnSidecarList, + ) -> Result, AvailabilityCheckError> { + // Attributes fault to the specific peer that sent an invalid column + let kzg_verified_columns = + KzgVerifiedDataColumn::from_batch_with_scoring(custody_columns, &self.kzg) + .map_err(AvailabilityCheckError::InvalidColumn)?; + + // Filter out columns that aren't required for custody for this slot + // This is required because `data_columns_by_root` requests the **latest** CGC that _may_ + // not be yet effective for data availability check, as CGC changes are only effecive from + // a new epoch. + let epoch = slot.epoch(T::EthSpec::slots_per_epoch()); + let sampling_columns = self + .custody_context + .sampling_columns_for_epoch(epoch, &self.spec); + let verified_custody_columns = kzg_verified_columns + .into_iter() + .filter(|col| sampling_columns.contains(&col.index())) + .map(KzgVerifiedCustodyDataColumn::from_asserted_custody) + .collect::>(); + + self.availability_cache + .put_kzg_verified_data_columns(block_root, verified_custody_columns) + } + /// Check if we've cached other blobs for this block. If it completes a set and we also /// have a block cached, return the `Availability` variant triggering block import. /// Otherwise cache the blob sidecar. @@ -222,6 +291,47 @@ impl DataAvailabilityChecker { .put_kzg_verified_blobs(block_root, blobs) } + /// Check if we've cached other data columns for this block. If it satisfies the custody requirement and we also + /// have a block cached, return the `Availability` variant triggering block import. + /// Otherwise cache the data column sidecar. + /// + /// This should only accept gossip verified data columns, so we should not have to worry about dupes. + #[instrument(skip_all, level = "trace")] + pub fn put_gossip_verified_data_columns< + O: ObservationStrategy, + I: IntoIterator>, + >( + &self, + block_root: Hash256, + slot: Slot, + data_columns: I, + ) -> Result, AvailabilityCheckError> { + let epoch = slot.epoch(T::EthSpec::slots_per_epoch()); + let sampling_columns = self + .custody_context + .sampling_columns_for_epoch(epoch, &self.spec); + let custody_columns = data_columns + .into_iter() + .filter(|col| sampling_columns.contains(&col.index())) + .map(|c| KzgVerifiedCustodyDataColumn::from_asserted_custody(c.into_inner())) + .collect::>(); + + self.availability_cache + .put_kzg_verified_data_columns(block_root, custody_columns) + } + + #[instrument(skip_all, level = "trace")] + pub fn put_kzg_verified_custody_data_columns< + I: IntoIterator>, + >( + &self, + block_root: Hash256, + custody_columns: I, + ) -> Result, AvailabilityCheckError> { + self.availability_cache + .put_kzg_verified_data_columns(block_root, custody_columns) + } + /// Check if we have all the blobs for a block. Returns `Availability` which has information /// about whether all components have been received or more are required. pub fn put_executed_block( @@ -356,113 +466,6 @@ impl DataAvailabilityChecker { block_cache_size: self.availability_cache.block_cache_size(), } } -} - -impl DataAvailabilityChecker { - pub fn custody_context(&self) -> &Arc> { - &self.custody_context - } - - /// Get data columns for a block from the availability cache. - pub fn get_data_columns( - &self, - block_root: Hash256, - ) -> Option> { - self.availability_cache.peek_data_columns(block_root) - } - - /// Return the set of cached custody column indices for `block_root`. Returns None if there is - /// no block component for `block_root`. - pub fn cached_data_column_indexes(&self, block_root: &Hash256) -> Option> { - self.availability_cache - .peek_pending_components(block_root, |components| { - components.map(|components| components.get_cached_data_columns_indices()) - }) - } - - /// Check if the exact data column is in the availability cache. - pub fn is_data_column_cached( - &self, - block_root: &Hash256, - data_column: &DataColumnSidecar, - ) -> bool { - self.availability_cache - .peek_pending_components(block_root, |components| { - components.is_some_and(|components| { - let cached_column_opt = components.get_cached_data_column(*data_column.index()); - cached_column_opt.is_some_and(|cached| *cached == *data_column) - }) - }) - } - - /// Put a list of custody columns received via RPC into the availability cache. This performs KZG - /// verification on the blobs in the list. - #[allow(clippy::type_complexity)] - #[instrument(skip_all, level = "trace")] - pub fn put_rpc_custody_columns( - &self, - block_root: Hash256, - slot: Slot, - custody_columns: DataColumnSidecarList, - ) -> Result, AvailabilityCheckError> { - // Attributes fault to the specific peer that sent an invalid column - let kzg_verified_columns = - KzgVerifiedDataColumn::from_batch_with_scoring(custody_columns, &self.kzg) - .map_err(AvailabilityCheckError::InvalidColumn)?; - - // Filter out columns that aren't required for custody for this slot - // This is required because `data_columns_by_root` requests the **latest** CGC that _may_ - // not be yet effective for data availability check, as CGC changes are only effecive from - // a new epoch. - let epoch = slot.epoch(T::EthSpec::slots_per_epoch()); - let sampling_columns = self - .custody_context - .sampling_columns_for_epoch(epoch, &self.spec); - let verified_custody_columns = kzg_verified_columns - .into_iter() - .filter(|col| sampling_columns.contains(&col.index())) - .map(KzgVerifiedCustodyDataColumn::from_asserted_custody) - .collect::>(); - - self.availability_cache - .put_kzg_verified_data_columns(block_root, verified_custody_columns) - } - - /// Check if we've cached other data columns for this block. If it satisfies the custody requirement and we also - /// have a block cached, return the `Availability` variant triggering block import. - /// Otherwise cache the data column sidecar. - /// - /// This should only accept gossip verified data columns, so we should not have to worry about dupes. - #[instrument(skip_all, level = "trace")] - pub fn put_gossip_verified_data_columns( - &self, - block_root: Hash256, - slot: Slot, - data_columns: Vec>, - ) -> Result, AvailabilityCheckError> { - let epoch = slot.epoch(T::EthSpec::slots_per_epoch()); - let sampling_columns = self - .custody_context - .sampling_columns_for_epoch(epoch, &self.spec); - let custody_columns = data_columns - .into_iter() - .filter(|col| sampling_columns.contains(&col.index())) - .map(|c| KzgVerifiedCustodyDataColumn::from_asserted_custody(c.into_inner())) - .collect::>(); - - self.availability_cache - .put_kzg_verified_data_columns(block_root, custody_columns) - } - - #[instrument(skip_all, level = "trace")] - pub fn put_kzg_verified_custody_data_columns( - &self, - block_root: Hash256, - custody_columns: Vec>, - ) -> Result, AvailabilityCheckError> { - self.availability_cache - .put_kzg_verified_data_columns(block_root, custody_columns) - } #[instrument(skip_all, level = "debug")] pub fn reconstruct_data_columns( @@ -551,18 +554,6 @@ impl DataAvailabilityChecker { )) }) } - - /// Verifies KZG commitments for data columns. - pub fn verify_kzg_for_data_columns( - &self, - data_columns: &DataColumnSidecarList, - ) -> Result<(), AvailabilityCheckError> { - if !data_columns.is_empty() { - verify_kzg_for_data_column_list(data_columns.iter(), &self.kzg) - .map_err(AvailabilityCheckError::InvalidColumn)?; - } - Ok(()) - } } /// Helper struct to group data availability checker metrics. @@ -590,7 +581,6 @@ pub fn start_availability_cache_maintenance_service( } } -// TODO(gloas) we can shut down this service once we reach the gloas fork epoch async fn availability_cache_maintenance_service( chain: Arc>, overflow_cache: Arc>, From 3778e5071007bcb7571ac25a96561d64a4e1015a Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Tue, 10 Feb 2026 21:38:19 -0800 Subject: [PATCH 027/118] Fix --- .../src/data_availability_router.rs | 8 ++--- beacon_node/beacon_chain/src/test_utils.rs | 18 +++++------ .../beacon_chain/tests/block_verification.rs | 32 +++++++++---------- .../tests/payload_invalidation.rs | 8 ++--- beacon_node/beacon_chain/tests/store_tests.rs | 6 ++-- beacon_node/http_api/src/publish_blocks.rs | 2 +- .../src/network_beacon_processor/tests.rs | 4 +-- .../network/src/sync/network_context.rs | 2 +- beacon_node/network/src/sync/tests/lookups.rs | 2 +- beacon_node/network/src/sync/tests/range.rs | 6 ++-- testing/ef_tests/src/cases/fork_choice.rs | 4 +-- 11 files changed, 46 insertions(+), 46 deletions(-) diff --git a/beacon_node/beacon_chain/src/data_availability_router.rs b/beacon_node/beacon_chain/src/data_availability_router.rs index b480cce5b0..1524e4a5ae 100644 --- a/beacon_node/beacon_chain/src/data_availability_router.rs +++ b/beacon_node/beacon_chain/src/data_availability_router.rs @@ -388,14 +388,14 @@ impl DataAvailabilityRouter { /// Direct access to v1 checker for block execution/availability checks. /// /// Use this for operations that are specific to the legacy DA checker, - pub fn v1(&self) -> Arc> { - self.v1.clone() + pub fn v1(&self) -> &Arc> { + &self.v1 } /// Direct access to v2 checker for payload availability checks. /// /// Use this for operations that are specific to the Gloas DA checker, - pub fn v2(&self) -> Arc> { - self.v2.clone() + pub fn v2(&self) -> &Arc> { + &self.v2 } } diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index 2bf04a8401..e738be7692 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -2490,7 +2490,7 @@ where return RpcBlock::new( block, Some(AvailableBlockData::NoData), - &self.chain.data_availability_checker.v1(), + self.chain.data_availability_checker.v1(), self.chain.spec.clone(), ) .unwrap(); @@ -2509,7 +2509,7 @@ where RpcBlock::new( block, Some(block_data), - &self.chain.data_availability_checker.v1(), + self.chain.data_availability_checker.v1(), self.chain.spec.clone(), ) .unwrap() @@ -2524,7 +2524,7 @@ where RpcBlock::new( block, Some(block_data), - &self.chain.data_availability_checker.v1(), + self.chain.data_availability_checker.v1(), self.chain.spec.clone(), ) .unwrap() @@ -2555,14 +2555,14 @@ where RpcBlock::new( block, Some(block_data), - &self.chain.data_availability_checker.v1(), + self.chain.data_availability_checker.v1(), self.chain.spec.clone(), )? } else { RpcBlock::new( block, None, - &self.chain.data_availability_checker.v1(), + self.chain.data_availability_checker.v1(), self.chain.spec.clone(), )? } @@ -2570,14 +2570,14 @@ where RpcBlock::new( block, Some(AvailableBlockData::NoData), - &self.chain.data_availability_checker.v1(), + self.chain.data_availability_checker.v1(), self.chain.spec.clone(), )? } else { RpcBlock::new( block, None, - &self.chain.data_availability_checker.v1(), + self.chain.data_availability_checker.v1(), self.chain.spec.clone(), )? } @@ -2598,14 +2598,14 @@ where RpcBlock::new( block, Some(block_data), - &self.chain.data_availability_checker.v1(), + self.chain.data_availability_checker.v1(), self.chain.spec.clone(), )? } else { RpcBlock::new( block, None, - &self.chain.data_availability_checker.v1(), + self.chain.data_availability_checker.v1(), self.chain.spec.clone(), )? } diff --git a/beacon_node/beacon_chain/tests/block_verification.rs b/beacon_node/beacon_chain/tests/block_verification.rs index 952170b12a..b6cac025a0 100644 --- a/beacon_node/beacon_chain/tests/block_verification.rs +++ b/beacon_node/beacon_chain/tests/block_verification.rs @@ -165,7 +165,7 @@ where RpcBlock::new( block, Some(block_data), - &chain.data_availability_checker.v1(), + chain.data_availability_checker.v1(), chain.spec.clone(), ) .unwrap() @@ -180,7 +180,7 @@ where RpcBlock::new( block, Some(block_data), - &chain.data_availability_checker.v1(), + chain.data_availability_checker.v1(), chain.spec.clone(), ) .unwrap() @@ -188,7 +188,7 @@ where None => RpcBlock::new( block, Some(AvailableBlockData::NoData), - &chain.data_availability_checker.v1(), + chain.data_availability_checker.v1(), chain.spec.clone(), ) .unwrap(), @@ -417,7 +417,7 @@ async fn chain_segment_non_linear_parent_roots() { blocks[3] = RpcBlock::new( Arc::new(SignedBeaconBlock::from_block(block, signature)), blocks[3].block_data().cloned(), - &harness.chain.data_availability_checker.v1(), + harness.chain.data_availability_checker.v1(), harness.spec.clone(), ) .unwrap(); @@ -457,7 +457,7 @@ async fn chain_segment_non_linear_slots() { blocks[3] = RpcBlock::new( Arc::new(SignedBeaconBlock::from_block(block, signature)), blocks[3].block_data().cloned(), - &harness.chain.data_availability_checker.v1(), + harness.chain.data_availability_checker.v1(), harness.spec.clone(), ) .unwrap(); @@ -487,7 +487,7 @@ async fn chain_segment_non_linear_slots() { blocks[3] = RpcBlock::new( Arc::new(SignedBeaconBlock::from_block(block, signature)), blocks[3].block_data().cloned(), - &harness.chain.data_availability_checker.v1(), + harness.chain.data_availability_checker.v1(), harness.chain.spec.clone(), ) .unwrap(); @@ -634,7 +634,7 @@ async fn invalid_signature_gossip_block() { let rpc_block = RpcBlock::new( Arc::new(signed_block), None, - &harness.chain.data_availability_checker.v1(), + harness.chain.data_availability_checker.v1(), harness.spec.clone(), ) .unwrap(); @@ -1645,7 +1645,7 @@ async fn add_base_block_to_altair_chain() { let base_rpc_block = RpcBlock::new( Arc::new(base_block.clone()), None, - &harness.chain.data_availability_checker.v1(), + harness.chain.data_availability_checker.v1(), harness.spec.clone(), ) .unwrap(); @@ -1676,7 +1676,7 @@ async fn add_base_block_to_altair_chain() { RpcBlock::new( Arc::new(base_block), None, - &harness.chain.data_availability_checker.v1(), + harness.chain.data_availability_checker.v1(), harness.spec.clone() ) .unwrap() @@ -1796,7 +1796,7 @@ async fn add_altair_block_to_base_chain() { let altair_rpc_block = RpcBlock::new( Arc::new(altair_block.clone()), None, - &harness.chain.data_availability_checker.v1(), + harness.chain.data_availability_checker.v1(), harness.spec.clone(), ) .unwrap(); @@ -1827,7 +1827,7 @@ async fn add_altair_block_to_base_chain() { RpcBlock::new( Arc::new(altair_block), None, - &harness.chain.data_availability_checker.v1(), + harness.chain.data_availability_checker.v1(), harness.spec.clone() ) .unwrap() @@ -1897,7 +1897,7 @@ async fn import_duplicate_block_unrealized_justification() { let rpc_block = RpcBlock::new( block.clone(), Some(AvailableBlockData::NoData), - &harness.chain.data_availability_checker.v1(), + harness.chain.data_availability_checker.v1(), harness.spec.clone(), ) .unwrap(); @@ -2000,7 +2000,7 @@ async fn signature_verify_mixed_rpc_block_variants() { RpcBlock::new( block, None, - &harness.chain.data_availability_checker.v1(), + harness.chain.data_availability_checker.v1(), harness.chain.spec.clone(), ) .unwrap() @@ -2070,7 +2070,7 @@ async fn rpc_block_construction_fails_with_wrong_blob_count() { let result = RpcBlock::new( Arc::new(block), Some(block_data), - &harness.chain.data_availability_checker.v1(), + harness.chain.data_availability_checker.v1(), harness.chain.spec.clone(), ); @@ -2145,7 +2145,7 @@ async fn rpc_block_rejects_missing_custody_columns() { let result = RpcBlock::new( Arc::new(block), Some(block_data), - &harness.chain.data_availability_checker.v1(), + harness.chain.data_availability_checker.v1(), harness.chain.spec.clone(), ); @@ -2233,7 +2233,7 @@ async fn rpc_block_allows_construction_past_da_boundary() { let result = RpcBlock::new( Arc::new(block), Some(AvailableBlockData::NoData), - &harness.chain.data_availability_checker.v1(), + harness.chain.data_availability_checker.v1(), harness.chain.spec.clone(), ); diff --git a/beacon_node/beacon_chain/tests/payload_invalidation.rs b/beacon_node/beacon_chain/tests/payload_invalidation.rs index b02e832eae..b3a8b80e9b 100644 --- a/beacon_node/beacon_chain/tests/payload_invalidation.rs +++ b/beacon_node/beacon_chain/tests/payload_invalidation.rs @@ -688,7 +688,7 @@ async fn invalidates_all_descendants() { let fork_rpc_block = RpcBlock::new( fork_block.clone(), None, - &rig.harness.chain.data_availability_checker.v1(), + rig.harness.chain.data_availability_checker.v1(), rig.harness.chain.spec.clone(), ) .unwrap(); @@ -796,7 +796,7 @@ async fn switches_heads() { let fork_rpc_block = RpcBlock::new( fork_block.clone(), None, - &rig.harness.chain.data_availability_checker.v1(), + rig.harness.chain.data_availability_checker.v1(), rig.harness.chain.spec.clone(), ) .unwrap(); @@ -1074,7 +1074,7 @@ async fn invalid_parent() { let rpc_block = RpcBlock::new( block.clone(), None, - &rig.harness.chain.data_availability_checker.v1(), + rig.harness.chain.data_availability_checker.v1(), rig.harness.chain.spec.clone(), ) .unwrap(); @@ -1405,7 +1405,7 @@ async fn recover_from_invalid_head_by_importing_blocks() { let fork_rpc_block = RpcBlock::new( fork_block.clone(), None, - &rig.harness.chain.data_availability_checker.v1(), + rig.harness.chain.data_availability_checker.v1(), rig.harness.chain.spec.clone(), ) .unwrap(); diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index 96c8b7748b..8fbba81038 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -3201,7 +3201,7 @@ async fn weak_subjectivity_sync_test( AvailableBlock::new( Arc::new(corrupt_block), data, - &beacon_chain.data_availability_checker.v1(), + beacon_chain.data_availability_checker.v1(), Arc::new(spec), ) .expect("available block") @@ -3751,7 +3751,7 @@ async fn process_blocks_and_attestations_for_unaligned_checkpoint() { let invalid_fork_rpc_block = RpcBlock::new( invalid_fork_block.clone(), None, - &harness.chain.data_availability_checker.v1(), + harness.chain.data_availability_checker.v1(), harness.spec.clone(), ) .unwrap(); @@ -3773,7 +3773,7 @@ async fn process_blocks_and_attestations_for_unaligned_checkpoint() { let valid_fork_rpc_block = RpcBlock::new( valid_fork_block.clone(), None, - &harness.chain.data_availability_checker.v1(), + harness.chain.data_availability_checker.v1(), harness.spec.clone(), ) .unwrap(); diff --git a/beacon_node/http_api/src/publish_blocks.rs b/beacon_node/http_api/src/publish_blocks.rs index 9f1da8111a..2d22dda66a 100644 --- a/beacon_node/http_api/src/publish_blocks.rs +++ b/beacon_node/http_api/src/publish_blocks.rs @@ -316,7 +316,7 @@ pub async fn publish_block>( let Ok(rpc_block) = RpcBlock::new( block.clone(), None, - &chain.data_availability_checker.v1(), + chain.data_availability_checker.v1(), chain.spec.clone(), ) else { return Err(warp_utils::reject::custom_bad_request( diff --git a/beacon_node/network/src/network_beacon_processor/tests.rs b/beacon_node/network/src/network_beacon_processor/tests.rs index 05297015e3..4764e77377 100644 --- a/beacon_node/network/src/network_beacon_processor/tests.rs +++ b/beacon_node/network/src/network_beacon_processor/tests.rs @@ -404,7 +404,7 @@ impl TestRig { RpcBlock::new( self.next_block.clone(), None, - &self._harness.chain.data_availability_checker.v1(), + self._harness.chain.data_availability_checker.v1(), self._harness.spec.clone(), ) .unwrap(), @@ -422,7 +422,7 @@ impl TestRig { RpcBlock::new( self.next_block.clone(), None, - &self._harness.chain.data_availability_checker.v1(), + self._harness.chain.data_availability_checker.v1(), self._harness.spec.clone(), ) .unwrap(), diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index 22730fcff3..416d455736 100644 --- a/beacon_node/network/src/sync/network_context.rs +++ b/beacon_node/network/src/sync/network_context.rs @@ -1612,7 +1612,7 @@ impl SyncNetworkContext { let block = RpcBlock::new( block, None, - &self.chain.data_availability_checker.v1(), + self.chain.data_availability_checker.v1(), self.chain.spec.clone(), ) .map_err(|_| SendErrorProcessor::SendError)?; diff --git a/beacon_node/network/src/sync/tests/lookups.rs b/beacon_node/network/src/sync/tests/lookups.rs index 63024bd2aa..e31570eaab 100644 --- a/beacon_node/network/src/sync/tests/lookups.rs +++ b/beacon_node/network/src/sync/tests/lookups.rs @@ -2290,7 +2290,7 @@ mod deneb_only { let block = RpcBlock::new( block, None, - &self.rig.harness.chain.data_availability_checker.v1(), + self.rig.harness.chain.data_availability_checker.v1(), self.rig.harness.chain.spec.clone(), ) .unwrap(); diff --git a/beacon_node/network/src/sync/tests/range.rs b/beacon_node/network/src/sync/tests/range.rs index 86c3652742..9283387ebd 100644 --- a/beacon_node/network/src/sync/tests/range.rs +++ b/beacon_node/network/src/sync/tests/range.rs @@ -454,7 +454,7 @@ fn build_rpc_block( RpcBlock::new( block, Some(block_data), - &chain.data_availability_checker.v1(), + chain.data_availability_checker.v1(), chain.spec.clone(), ) .unwrap() @@ -469,7 +469,7 @@ fn build_rpc_block( RpcBlock::new( block, Some(block_data), - &chain.data_availability_checker.v1(), + chain.data_availability_checker.v1(), chain.spec.clone(), ) .unwrap() @@ -478,7 +478,7 @@ fn build_rpc_block( None => RpcBlock::new( block, Some(AvailableBlockData::NoData), - &chain.data_availability_checker.v1(), + chain.data_availability_checker.v1(), chain.spec.clone(), ) .unwrap(), diff --git a/testing/ef_tests/src/cases/fork_choice.rs b/testing/ef_tests/src/cases/fork_choice.rs index b9f8e5ce5b..c62a2593ae 100644 --- a/testing/ef_tests/src/cases/fork_choice.rs +++ b/testing/ef_tests/src/cases/fork_choice.rs @@ -567,7 +567,7 @@ impl Tester { RpcBlock::new( block.clone(), None, - &self.harness.chain.data_availability_checker.v1(), + self.harness.chain.data_availability_checker.v1(), self.harness.chain.spec.clone(), ) .map_err(|e| Error::InternalError(format!("{:?}", e)))?, @@ -665,7 +665,7 @@ impl Tester { RpcBlock::new( block.clone(), None, - &self.harness.chain.data_availability_checker.v1(), + self.harness.chain.data_availability_checker.v1(), self.harness.chain.spec.clone(), ) .map_err(|e| Error::InternalError(format!("{:?}", e)))?, From 9cfe66233fec28632536ea3125d52f60a0268baa Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Mon, 16 Mar 2026 16:34:52 -0700 Subject: [PATCH 028/118] add payload stuff --- beacon_node/beacon_chain/src/beacon_chain.rs | 23 +-- .../src/data_availability_checker_v2/mod.rs | 7 +- .../payload_envelope_cache.rs | 1 + .../pending_components_cache.rs | 145 ++++++++++++------ .../src/data_availability_router.rs | 4 +- .../src/payload_envelope_verification/mod.rs | 37 ++++- 6 files changed, 151 insertions(+), 66 deletions(-) create mode 100644 beacon_node/beacon_chain/src/data_availability_checker_v2/payload_envelope_cache.rs diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 23fecb7d5a..8575089c78 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -3799,14 +3799,18 @@ impl BeaconChain { } } AvailabilityOutcome::Payload(availability) => match availability { - PayloadAvailability::Available(available_payload_data) => { + PayloadAvailability::Available(available_envelope) => { // TODO(gloas) execution publish_fn // publish_fn()?; - // Payload data is fully available - let (block_root, data_columns) = *available_payload_data; - self.import_available_payload_data(block_root, data_columns) + // Payload envelope is fully available + let res = self + .import_available_execution_payload_envelope(available_envelope) .await + .unwrap(); + + // TODO(gloas) unwrap + Ok(res) } PayloadAvailability::MissingComponents(block_root) => Ok( AvailabilityProcessingStatus::MissingComponents(slot, block_root), @@ -3815,17 +3819,6 @@ impl BeaconChain { } } - #[instrument(skip_all)] - pub async fn import_available_payload_data( - self: &Arc, - block_root: Hash256, - _data_columns: Vec>>, - ) -> Result { - // TODO(gloas) this is just a stub implementation - // this function should mark payload data as available somehow - Ok(AvailabilityProcessingStatus::Imported(block_root)) - } - #[instrument(skip_all)] pub async fn import_available_block( self: &Arc, diff --git a/beacon_node/beacon_chain/src/data_availability_checker_v2/mod.rs b/beacon_node/beacon_chain/src/data_availability_checker_v2/mod.rs index cad8070a65..448d87cfcd 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker_v2/mod.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker_v2/mod.rs @@ -3,6 +3,7 @@ use crate::data_availability_checker_v2::pending_components_cache::{ }; use crate::data_availability_checker::AvailabilityCheckError; +use crate::payload_envelope_verification::AvailableExecutedEnvelope; use crate::{BeaconChain, BeaconChainTypes, CustodyContext, metrics}; use kzg::Kzg; use slot_clock::SlotClock; @@ -17,6 +18,7 @@ use types::{ SignedExecutionPayloadBid, Slot, }; +mod payload_envelope_cache; mod pending_components_cache; use crate::data_column_verification::{ @@ -45,7 +47,7 @@ pub type AvailableData = (Hash256, DataColumnSidecarList); /// Indicates if the payloads data is fully `Available` or if we need more columns. pub enum Availability { MissingComponents(Hash256), - Available(Box>), + Available(Box>), } impl Debug for Availability { @@ -54,7 +56,8 @@ impl Debug for Availability { Self::MissingComponents(block_root) => { write!(f, "MissingComponents({})", block_root) } - Self::Available(data) => write!(f, "Available({}, {} columns)", data.0, data.1.len()), + // TODO(gloas) fix success case + Self::Available(data) => todo!(), } } } diff --git a/beacon_node/beacon_chain/src/data_availability_checker_v2/payload_envelope_cache.rs b/beacon_node/beacon_chain/src/data_availability_checker_v2/payload_envelope_cache.rs new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/beacon_node/beacon_chain/src/data_availability_checker_v2/payload_envelope_cache.rs @@ -0,0 +1 @@ + diff --git a/beacon_node/beacon_chain/src/data_availability_checker_v2/pending_components_cache.rs b/beacon_node/beacon_chain/src/data_availability_checker_v2/pending_components_cache.rs index 51a19554dd..1ccf953036 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker_v2/pending_components_cache.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker_v2/pending_components_cache.rs @@ -3,17 +3,26 @@ use crate::CustodyContext; use crate::data_availability_checker::AvailabilityCheckError; use crate::data_availability_checker_v2::Availability; use crate::data_column_verification::KzgVerifiedCustodyDataColumn; +use crate::payload_envelope_verification::AvailabilityPendingExecutedEnvelope; +use crate::payload_envelope_verification::AvailableEnvelope; +use crate::payload_envelope_verification::AvailableExecutedEnvelope; use lru::LruCache; use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard, RwLockWriteGuard}; use std::cmp::Ordering; use std::num::NonZeroUsize; use std::sync::Arc; use tracing::{Span, debug, debug_span}; +use types::BlockImportSource; use types::{ ChainSpec, ColumnIndex, DataColumnSidecar, DataColumnSidecarList, Epoch, EthSpec, Hash256, - SignedExecutionPayloadBid, + SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope, }; +pub enum CachedPayloadEnvelope { + PreExecution(Arc>, BlockImportSource), + Executed(Box>), +} + /// This represents the components of a payload pending data availability. /// /// The columns are all gossip and kzg verified. @@ -24,9 +33,12 @@ pub struct PendingComponents { pub block_root: Hash256, /// The execution payload bid containing blob_kzg_commitments. pub bid: Option>>, + /// a cached pre or post executed payload envelope + pub envelope: Option>, pub verified_data_columns: Vec>, pub reconstruction_started: bool, span: Span, + spec: Arc, } impl PendingComponents { @@ -68,6 +80,19 @@ impl PendingComponents { self.bid = Some(bid); } + pub fn insert_pending_executed_envelope( + &mut self, + envelope: Arc>, + import_source: BlockImportSource, + ) { + self.envelope = Some(CachedPayloadEnvelope::PreExecution(envelope, import_source)) + } + + /// Inserts an executed payload envelope into the cache. + pub fn insert_executed_envelope(&mut self, envelope: AvailabilityPendingExecutedEnvelope) { + self.envelope = Some(CachedPayloadEnvelope::Executed(Box::new(envelope))) + } + /// Returns the number of blobs expected by reading the bid's kzg commitments. /// Returns an error if the bid is not cached. This function should only be called /// after ensuring that the bid has been cached. @@ -80,66 +105,92 @@ impl PendingComponents { Ok(bid.message.blob_kzg_commitments.len()) } - /// Returns `Some` if the bid and all required data columns have been received. + /// Returns `Some` if the envelope and all required data columns have been received. pub fn make_available( &self, num_expected_columns: usize, - ) -> Result>, AvailabilityCheckError> { - // Check if we have a bid - if not, still waiting + ) -> Result>, AvailabilityCheckError> { + // If no bid has been received, we can start verifying the columns if self.bid.is_none() { return Ok(None); } + // Check if the payload has been received and executed + let Some(CachedPayloadEnvelope::Executed(envelope)) = self.envelope.as_ref() else { + return Ok(None); + }; + + let AvailabilityPendingExecutedEnvelope { + envelope, + import_data, + payload_verification_outcome, + } = envelope.as_ref(); + // Get the number of blobs expected from the bid let num_expected_blobs = self.num_blobs_expected()?; - if num_expected_blobs == 0 { - // No blobs expected, data is available (empty) + let columns = if num_expected_blobs == 0 { self.span.in_scope(|| { debug!("Bid has no blobs, data is available"); }); - return Ok(Some(vec![])); - } + vec![] + } else { + let num_received_columns = self.verified_data_columns.len(); + match num_received_columns.cmp(&num_expected_columns) { + Ordering::Greater => { + // Should never happen + return Err(AvailabilityCheckError::Unexpected(format!( + "too many columns got {num_received_columns} expected {num_expected_columns}" + ))); + } + Ordering::Equal => { + // We have enough columns + let data_columns = self + .verified_data_columns + .iter() + .map(|d| d.clone().into_inner()) + .collect::>(); - let num_received_columns = self.verified_data_columns.len(); - match num_received_columns.cmp(&num_expected_columns) { - Ordering::Greater => { - // Should never happen - Err(AvailabilityCheckError::Unexpected(format!( - "too many columns got {num_received_columns} expected {num_expected_columns}" - ))) - } - Ordering::Equal => { - // We have enough columns - let data_columns = self - .verified_data_columns - .iter() - .map(|d| d.clone().into_inner()) - .collect::>(); + self.span.in_scope(|| { + debug!("All data columns received, data is available"); + }); - self.span.in_scope(|| { - debug!("All data columns received, data is available"); - }); + data_columns + } + Ordering::Less => { + // Not enough data columns received yet + return Ok(None); + } + } + }; - Ok(Some(data_columns)) - } - Ordering::Less => { - // Not enough data columns received yet - Ok(None) - } - } + let available_envelope = AvailableEnvelope { + execution_block_hash: envelope.block_hash(), + envelope: envelope.clone(), + columns, + columns_available_timestamp: None, + spec: self.spec.clone(), + }; + + Ok(Some(AvailableExecutedEnvelope { + envelope: available_envelope, + import_data: import_data.clone(), + payload_verification_outcome: payload_verification_outcome.clone(), + })) } /// Returns an empty `PendingComponents` object with the given block root. - pub fn empty(block_root: Hash256) -> Self { + pub fn empty(block_root: Hash256, spec: Arc) -> Self { let span = debug_span!(parent: None, "lh_pending_components", %block_root); let _guard = span.clone().entered(); Self { block_root, bid: None, + envelope: None, verified_data_columns: vec![], reconstruction_started: false, span, + spec, } } @@ -294,7 +345,7 @@ impl DataAvailabilityCheckerInner { pending_components: MappedRwLockReadGuard<'_, PendingComponents>, num_expected_columns: usize, ) -> Result, AvailabilityCheckError> { - if let Some(columns) = pending_components.make_available(num_expected_columns)? { + if let Some(available_envelope) = pending_components.make_available(num_expected_columns)? { // Explicitly drop read lock before acquiring write lock drop(pending_components); if let Some(components) = self.critical.write().get_mut(&block_root) { @@ -308,7 +359,7 @@ impl DataAvailabilityCheckerInner { // imported, but re-inserted immediately, causing partial pending components to be // stored and served to peers. // Components are only removed via LRU eviction as finality advances. - Ok(Availability::Available(Box::new((block_root, columns)))) + Ok(Availability::Available(Box::new(available_envelope))) } else { Ok(Availability::MissingComponents(block_root)) } @@ -330,8 +381,9 @@ impl DataAvailabilityCheckerInner { let mut write_lock = self.critical.write(); { - let pending_components = - write_lock.get_or_insert_mut(block_root, || PendingComponents::empty(block_root)); + let pending_components = write_lock.get_or_insert_mut(block_root, || { + PendingComponents::empty(block_root, self.spec.clone()) + }); update_fn(pending_components)? } @@ -433,6 +485,8 @@ impl DataAvailabilityCheckerInner { #[cfg(test)] mod pending_components_tests { + use crate::test_utils::test_spec; + use super::*; use types::MinimalEthSpec; @@ -440,8 +494,9 @@ mod pending_components_tests { #[test] fn test_empty_pending_components() { + let spec = Arc::new(test_spec::()); let block_root = Hash256::random(); - let components = PendingComponents::::empty(block_root); + let components = PendingComponents::::empty(block_root, spec); assert_eq!(components.block_root, block_root); assert!(components.bid.is_none()); @@ -452,8 +507,9 @@ mod pending_components_tests { #[test] fn test_get_cached_data_columns_indices_empty() { + let spec = Arc::new(test_spec::()); let block_root = Hash256::random(); - let components = PendingComponents::::empty(block_root); + let components = PendingComponents::::empty(block_root, spec); let indices = components.get_cached_data_columns_indices(); assert!(indices.is_empty()); @@ -461,8 +517,9 @@ mod pending_components_tests { #[test] fn test_status_str_no_bid() { + let spec = Arc::new(test_spec::()); let block_root = Hash256::random(); - let components = PendingComponents::::empty(block_root); + let components = PendingComponents::::empty(block_root, spec); let status = components.status_str(10); assert_eq!(status, "data_columns 0/10"); @@ -470,8 +527,9 @@ mod pending_components_tests { #[test] fn test_num_blobs_expected_no_bid() { + let spec = Arc::new(test_spec::()); let block_root = Hash256::random(); - let components = PendingComponents::::empty(block_root); + let components = PendingComponents::::empty(block_root, spec); let result = components.num_blobs_expected(); assert!(result.is_err()); @@ -484,8 +542,9 @@ mod pending_components_tests { #[test] fn test_make_available_no_bid_returns_none() { + let spec = Arc::new(test_spec::()); let block_root = Hash256::random(); - let components = PendingComponents::::empty(block_root); + let components = PendingComponents::::empty(block_root, spec); // Without a bid, make_available should return Ok(None) let result = components.make_available(10); diff --git a/beacon_node/beacon_chain/src/data_availability_router.rs b/beacon_node/beacon_chain/src/data_availability_router.rs index 1524e4a5ae..78de0d8935 100644 --- a/beacon_node/beacon_chain/src/data_availability_router.rs +++ b/beacon_node/beacon_chain/src/data_availability_router.rs @@ -64,7 +64,9 @@ impl AvailabilityOutcome { Self::Block(BlockAvailability::Available(block)) => block.import_data.block_root, Self::Block(BlockAvailability::MissingComponents(root)) => *root, // For payload availability, the first element of the tuple is the block root - Self::Payload(PayloadAvailability::Available(available_data)) => available_data.0, + Self::Payload(PayloadAvailability::Available(available_data)) => { + available_data.envelope.message().beacon_block_root + } Self::Payload(PayloadAvailability::MissingComponents(root)) => *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 c707d62dc7..d9503a0272 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs @@ -41,7 +41,7 @@ mod payload_notifier; pub use execution_pending_envelope::ExecutionPendingEnvelope; -#[derive(PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub struct EnvelopeImportData { pub block_root: Hash256, pub post_state: Box>, @@ -50,11 +50,11 @@ pub struct EnvelopeImportData { #[derive(Debug)] #[allow(dead_code)] pub struct AvailableEnvelope { - execution_block_hash: ExecutionBlockHash, - envelope: Arc>, - columns: DataColumnSidecarList, + pub execution_block_hash: ExecutionBlockHash, + pub envelope: Arc>, + pub columns: DataColumnSidecarList, /// Timestamp at which this envelope first became available (UNIX timestamp, time since 1970). - columns_available_timestamp: Option, + pub columns_available_timestamp: Option, pub spec: Arc, } @@ -132,6 +132,33 @@ impl ExecutedEnvelope { } } +/// A payload ernvelope that has completed all envelope procesing checks, verification +/// by an EL client but does not have all requisite columns to get imported into +/// fork choice. +pub struct AvailabilityPendingExecutedEnvelope { + pub envelope: Arc>, + pub import_data: EnvelopeImportData, + pub payload_verification_outcome: PayloadVerificationOutcome, +} + +impl AvailabilityPendingExecutedEnvelope { + pub fn new( + envelope: Arc>, + import_data: EnvelopeImportData, + payload_verification_outcome: PayloadVerificationOutcome, + ) -> Self { + Self { + envelope, + import_data, + payload_verification_outcome, + } + } + + pub fn as_envelope(&self) -> &SignedExecutionPayloadEnvelope { + &self.envelope + } +} + /// A payload envelope that has completed all payload processing checks including verification /// by an EL client **and** has all requisite blob data to be imported into fork choice. pub struct AvailableExecutedEnvelope { From c6b12990732b81a3d5895e7f84c3c8954c0f3c8e Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Wed, 18 Mar 2026 00:08:26 -0700 Subject: [PATCH 029/118] temp chhanges --- .../src/data_availability_checker_v2/mod.rs | 23 +++++++- .../pending_components_cache.rs | 59 ++++++++++++++++++- .../payload_envelope_verification/import.rs | 20 ++++--- .../src/payload_envelope_verification/mod.rs | 11 +++- 4 files changed, 102 insertions(+), 11 deletions(-) diff --git a/beacon_node/beacon_chain/src/data_availability_checker_v2/mod.rs b/beacon_node/beacon_chain/src/data_availability_checker_v2/mod.rs index 448d87cfcd..2d006ea4c6 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker_v2/mod.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker_v2/mod.rs @@ -14,8 +14,8 @@ use std::sync::Arc; use task_executor::TaskExecutor; use tracing::{debug, error, instrument}; use types::{ - ChainSpec, ColumnIndex, DataColumnSidecar, DataColumnSidecarList, EthSpec, Hash256, - SignedExecutionPayloadBid, Slot, + BlockImportSource, ChainSpec, ColumnIndex, DataColumnSidecar, DataColumnSidecarList, EthSpec, + Hash256, SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope, Slot, }; mod payload_envelope_cache; @@ -50,6 +50,16 @@ pub enum Availability { Available(Box>), } +pub enum PayloadEnvelopeProcessingStatus { + /// Envelope is not in any pre-import cache. Envelope may be in the data-base or in the fork-choice. + Unknown, + /// Envelope is currently processing but not yet validated. + NotValidated(Arc>, BlockImportSource), + /// Envelope is fully valid, but not yet imported. It's cached in the da_checker while awaiting + /// missing envelope components. + ExecutionValidated(Arc>), +} + impl Debug for Availability { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { @@ -148,6 +158,15 @@ impl DataAvailabilityChecker { }) } + pub fn put_pre_executed_payload_envelope( + &self, + envelope: Arc>, + source: BlockImportSource, + ) -> Result<(), AvailabilityCheckError> { + self.availability_cache + .put_pre_executed_payload_envelope(envelope, source) + } + /// Insert RPC custody columns and check if the payload becomes available. #[instrument(skip_all, level = "trace")] pub fn put_rpc_custody_columns( diff --git a/beacon_node/beacon_chain/src/data_availability_checker_v2/pending_components_cache.rs b/beacon_node/beacon_chain/src/data_availability_checker_v2/pending_components_cache.rs index 1ccf953036..de353c32b0 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker_v2/pending_components_cache.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker_v2/pending_components_cache.rs @@ -2,6 +2,7 @@ use crate::BeaconChainTypes; use crate::CustodyContext; use crate::data_availability_checker::AvailabilityCheckError; use crate::data_availability_checker_v2::Availability; +use crate::data_availability_checker_v2::PayloadEnvelopeProcessingStatus; use crate::data_column_verification::KzgVerifiedCustodyDataColumn; use crate::payload_envelope_verification::AvailabilityPendingExecutedEnvelope; use crate::payload_envelope_verification::AvailableEnvelope; @@ -42,6 +43,7 @@ pub struct PendingComponents { } impl PendingComponents { + /// Returns an immutable reference to the cached data column. pub fn get_cached_data_column( &self, @@ -80,7 +82,7 @@ impl PendingComponents { self.bid = Some(bid); } - pub fn insert_pending_executed_envelope( + pub fn insert_pre_executed_envelope( &mut self, envelope: Arc>, import_source: BlockImportSource, @@ -249,6 +251,24 @@ impl DataAvailabilityCheckerInner { }) } + /// Returns the envelope processing status for the given `block_root`. A `None` response indicates that + /// the envelope has not yet been inserted into the cache. + pub fn get_envelope_processing_status(&self, block_root: &Hash256) -> Option> { + self.critical + .read() + .peek(block_root) + .and_then(|pending_components| { + pending_components.envelope.as_ref().map(|envelope| match envelope { + CachedPayloadEnvelope::PreExecution(e, source) => { + PayloadEnvelopeProcessingStatus::NotValidated(e.clone(), *source) + } + CachedPayloadEnvelope::Executed(e) => { + PayloadEnvelopeProcessingStatus::ExecutionValidated(e.envelope.clone()) + } + }) + }) + } + /// Fetch data columns of a given `block_root` from the cache without affecting the LRU ordering pub fn peek_data_columns( &self, @@ -301,6 +321,43 @@ impl DataAvailabilityCheckerInner { self.check_availability(block_root, pending_components, num_expected_columns) } + pub fn put_pre_executed_payload_envelope( + &self, + envelope: Arc>, + source: BlockImportSource, + ) -> Result<(), AvailabilityCheckError> { + let epoch = envelope.epoch(); + let beacon_block_root = envelope.beacon_block_root(); + let pending_components = + self.update_or_insert_pending_components(beacon_block_root, |pending_components| { + pending_components.insert_pre_executed_envelope(envelope, source); + Ok(()) + })?; + + let num_expected_columns_opt = self.get_num_expected_columns(epoch); + + pending_components.span.in_scope(|| { + debug!( + component = "pre executed payload envelope", + status = pending_components.status_str(num_expected_columns_opt), + "Component added to data availability checker" + ); + }); + + Ok(()) + } + + /// Removes a pre-executed envelope from the cache. + /// This does NOT remove an existing executed envelope. + pub fn remove_pre_executed_envelope(&self, block_root: &Hash256) { + // The read lock is immediately dropped so we can safely remove the envelope from the cache. + if let Some(PayloadEnvelopeProcessingStatus::NotValidated(_, _)) = self.get_envelope_processing_status(block_root) { + // If the envelope is execution invalid, this status is permanent and idempotent to this + // block_root. We drop its components (e.g. columns) because they will never be useful. + self.critical.write().pop(block_root); + } + } + #[allow(clippy::type_complexity)] pub fn put_kzg_verified_data_columns< I: IntoIterator>, 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 2ee315e559..4f754abd2c 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs @@ -27,13 +27,13 @@ impl BeaconChain { /// /// Returns an `Err` if the given payload envelope was invalid, or an error was encountered during /// verification. - #[instrument(skip_all, fields(block_root = ?block_root, block_source = %block_source))] + #[instrument(skip_all, fields(block_root = ?block_root, envelope_source = %envelope_source))] pub async fn process_execution_payload_envelope( self: &Arc, block_root: Hash256, unverified_envelope: GossipVerifiedEnvelope, notify_execution_layer: NotifyExecutionLayer, - block_source: BlockImportSource, + envelope_source: BlockImportSource, publish_fn: impl FnOnce() -> Result<(), EnvelopeError>, ) -> Result { let block_slot = unverified_envelope.signed_envelope.slot(); @@ -49,7 +49,12 @@ impl BeaconChain { ); } - // TODO(gloas) insert the pre-executed envelope into some type of cache. + self.data_availability_checker + .v2() + .put_pre_executed_payload_envelope( + unverified_envelope.envelope_cloned(), + envelope_source, + )?; let _full_timer = metrics::start_timer(&metrics::ENVELOPE_PROCESSING_TIMES); @@ -79,11 +84,12 @@ impl BeaconChain { .into_executed_payload_envelope(execution_pending) .await .inspect_err(|_| { - // TODO(gloas) If the envelope fails execution for whatever reason (e.g. engine offline), + // If the envelope fails execution for whatever reason (e.g. engine offline), // and we keep it in the cache, then the node will NOT perform lookup and - // reprocess this block until the block is evicted from DA checker, causing the - // chain to get stuck temporarily if the block is canonical. Therefore we remove + // reprocess this envelope until the envelope is evicted from DA checker, causing the + // chain to get stuck temporarily if the envelope is canonical. Therefore we remove // it from the cache if execution fails. + // self.data_availability_checker.v2().remove_pre_executed_envelop(block_root); })?; // Record the time it took to wait for execution layer verification. @@ -111,7 +117,7 @@ impl BeaconChain { info!( ?block_root, %block_slot, - source = %block_source, + source = %envelope_source, "Execution payload envelope 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 d9503a0272..908da9a609 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs @@ -39,6 +39,7 @@ pub mod gossip_verified_envelope; pub mod import; mod payload_notifier; +use crate::data_availability_checker::AvailabilityCheckError; pub use execution_pending_envelope::ExecutionPendingEnvelope; #[derive(Clone, Debug, PartialEq)] @@ -221,6 +222,9 @@ pub enum EnvelopeError { ExecutionPayloadError(ExecutionPayloadError), /// An error from block-level checks reused during envelope import BlockError(BlockError), + /// The envelope satisfied all validity conditions except consistency + /// with the corresponding columns that we received over gossip/rpc. + AvailabilityCheck(AvailabilityCheckError), /// Internal error InternalError(String), } @@ -261,7 +265,12 @@ impl From for EnvelopeError { } } -/// Pull errors up from EnvelopeProcessingError to EnvelopeError +impl From for EnvelopeError { + fn from(e: AvailabilityCheckError) -> Self { + EnvelopeError::AvailabilityCheck(e) + } +} + impl From for EnvelopeError { fn from(e: EnvelopeProcessingError) -> Self { match e { From 4c56bd3bfbed77367a9ff5418c54089719f9ece6 Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Wed, 18 Mar 2026 00:13:28 -0700 Subject: [PATCH 030/118] remove pre exec payload envelope --- .../beacon_chain/src/data_availability_checker_v2/mod.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/beacon_node/beacon_chain/src/data_availability_checker_v2/mod.rs b/beacon_node/beacon_chain/src/data_availability_checker_v2/mod.rs index 2d006ea4c6..15a681ba35 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker_v2/mod.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker_v2/mod.rs @@ -167,6 +167,14 @@ impl DataAvailabilityChecker { .put_pre_executed_payload_envelope(envelope, source) } + pub fn remove_pre_executed_payload_envelope( + &self, + block_root: &Hash256, + ) { + self.availability_cache + .remove_pre_executed_envelope(block_root); + } + /// Insert RPC custody columns and check if the payload becomes available. #[instrument(skip_all, level = "trace")] pub fn put_rpc_custody_columns( From 2acbd2ef4507377079b119b54ebd4a6e0e7f52bc Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Wed, 18 Mar 2026 02:10:11 -0700 Subject: [PATCH 031/118] Add payload to cache --- beacon_node/beacon_chain/src/beacon_chain.rs | 12 +-- .../beacon_chain/src/block_verification.rs | 13 ++++ .../src/data_availability_checker_v2/mod.rs | 21 ++++-- .../pending_components_cache.rs | 74 +++++++++++++++---- .../payload_envelope_verification/import.rs | 38 ++++++++-- .../src/payload_envelope_verification/mod.rs | 12 +-- 6 files changed, 130 insertions(+), 40 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 8575089c78..2b380c0520 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -3779,7 +3779,7 @@ impl BeaconChain { /// /// An error is returned if the block was unable to be imported. It may be partially imported /// (i.e., this function is not atomic). - async fn process_availability( + pub(crate) async fn process_availability( self: &Arc, slot: Slot, availability: AvailabilityOutcome, @@ -3801,16 +3801,12 @@ impl BeaconChain { AvailabilityOutcome::Payload(availability) => match availability { PayloadAvailability::Available(available_envelope) => { // TODO(gloas) execution publish_fn - // publish_fn()?; + publish_fn()?; // Payload envelope is fully available - let res = self - .import_available_execution_payload_envelope(available_envelope) + self.import_available_execution_payload_envelope(available_envelope) .await - .unwrap(); - - // TODO(gloas) unwrap - Ok(res) + .map_err(BlockError::from) } PayloadAvailability::MissingComponents(block_root) => Ok( AvailabilityProcessingStatus::MissingComponents(slot, block_root), diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 244b06f475..dcc006ecd8 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -60,6 +60,7 @@ use crate::execution_payload::{ }; use crate::kzg_utils::blobs_to_data_column_sidecars; use crate::observed_block_producers::SeenBlock; +use crate::payload_envelope_verification::EnvelopeError; use crate::validator_monitor::HISTORIC_EPOCHS as VALIDATOR_MONITOR_HISTORIC_EPOCHS; use crate::validator_pubkey_cache::ValidatorPubkeyCache; use crate::{ @@ -321,6 +322,12 @@ pub enum BlockError { bid_parent_root: Hash256, block_parent_root: Hash256, }, + /// An error occurred while processing a payload envelope. + /// + /// ## Peer scoring + /// + /// Peer scoring depends on the inner `EnvelopeError`. + EnvelopeError(EnvelopeError), } /// Which specific signature(s) are invalid in a SignedBeaconBlock @@ -340,6 +347,12 @@ impl From for BlockError { } } +impl From for BlockError { + fn from(e: EnvelopeError) -> Self { + Self::EnvelopeError(e) + } +} + /// Returned when block validation failed due to some issue verifying /// the execution payload. #[derive(Debug)] diff --git a/beacon_node/beacon_chain/src/data_availability_checker_v2/mod.rs b/beacon_node/beacon_chain/src/data_availability_checker_v2/mod.rs index 15a681ba35..c45f54b467 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker_v2/mod.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker_v2/mod.rs @@ -3,7 +3,9 @@ use crate::data_availability_checker_v2::pending_components_cache::{ }; use crate::data_availability_checker::AvailabilityCheckError; -use crate::payload_envelope_verification::AvailableExecutedEnvelope; +use crate::payload_envelope_verification::{ + AvailabilityPendingExecutedEnvelope, AvailableExecutedEnvelope, +}; use crate::{BeaconChain, BeaconChainTypes, CustodyContext, metrics}; use kzg::Kzg; use slot_clock::SlotClock; @@ -12,7 +14,7 @@ use std::fmt::Debug; use std::num::NonZeroUsize; use std::sync::Arc; use task_executor::TaskExecutor; -use tracing::{debug, error, instrument}; +use tracing::{debug, error, instrument, trace}; use types::{ BlockImportSource, ChainSpec, ColumnIndex, DataColumnSidecar, DataColumnSidecarList, EthSpec, Hash256, SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope, Slot, @@ -158,6 +160,14 @@ impl DataAvailabilityChecker { }) } + pub fn put_executed_payload_envelope( + &self, + executed_envelope: AvailabilityPendingExecutedEnvelope, + ) -> Result, AvailabilityCheckError> { + self.availability_cache + .put_executed_payload_envelope(executed_envelope) + } + pub fn put_pre_executed_payload_envelope( &self, envelope: Arc>, @@ -167,10 +177,7 @@ impl DataAvailabilityChecker { .put_pre_executed_payload_envelope(envelope, source) } - pub fn remove_pre_executed_payload_envelope( - &self, - block_root: &Hash256, - ) { + pub fn remove_pre_executed_payload_envelope(&self, block_root: &Hash256) { self.availability_cache .remove_pre_executed_envelope(block_root); } @@ -373,7 +380,7 @@ pub fn start_availability_cache_maintenance_service( "availability_cache_service", ); } else { - debug!("Gloas fork not configured, not starting availability cache maintenance service"); + trace!("Gloas fork not configured, not starting availability cache maintenance service"); } } diff --git a/beacon_node/beacon_chain/src/data_availability_checker_v2/pending_components_cache.rs b/beacon_node/beacon_chain/src/data_availability_checker_v2/pending_components_cache.rs index de353c32b0..607c031694 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker_v2/pending_components_cache.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker_v2/pending_components_cache.rs @@ -43,7 +43,6 @@ pub struct PendingComponents { } impl PendingComponents { - /// Returns an immutable reference to the cached data column. pub fn get_cached_data_column( &self, @@ -82,7 +81,14 @@ impl PendingComponents { self.bid = Some(bid); } - pub fn insert_pre_executed_envelope( + pub fn insert_executed_paylaod_envelope( + &mut self, + envelope: AvailabilityPendingExecutedEnvelope, + ) { + self.envelope = Some(CachedPayloadEnvelope::Executed(Box::new(envelope))) + } + + pub fn insert_pre_executed_payload_envelope( &mut self, envelope: Arc>, import_source: BlockImportSource, @@ -91,7 +97,10 @@ impl PendingComponents { } /// Inserts an executed payload envelope into the cache. - pub fn insert_executed_envelope(&mut self, envelope: AvailabilityPendingExecutedEnvelope) { + pub fn insert_executed_payload_envelope( + &mut self, + envelope: AvailabilityPendingExecutedEnvelope, + ) { self.envelope = Some(CachedPayloadEnvelope::Executed(Box::new(envelope))) } @@ -253,19 +262,25 @@ impl DataAvailabilityCheckerInner { /// Returns the envelope processing status for the given `block_root`. A `None` response indicates that /// the envelope has not yet been inserted into the cache. - pub fn get_envelope_processing_status(&self, block_root: &Hash256) -> Option> { + pub fn get_envelope_processing_status( + &self, + block_root: &Hash256, + ) -> Option> { self.critical .read() .peek(block_root) .and_then(|pending_components| { - pending_components.envelope.as_ref().map(|envelope| match envelope { - CachedPayloadEnvelope::PreExecution(e, source) => { - PayloadEnvelopeProcessingStatus::NotValidated(e.clone(), *source) - } - CachedPayloadEnvelope::Executed(e) => { - PayloadEnvelopeProcessingStatus::ExecutionValidated(e.envelope.clone()) - } - }) + pending_components + .envelope + .as_ref() + .map(|envelope| match envelope { + CachedPayloadEnvelope::PreExecution(e, source) => { + PayloadEnvelopeProcessingStatus::NotValidated(e.clone(), *source) + } + CachedPayloadEnvelope::Executed(e) => { + PayloadEnvelopeProcessingStatus::ExecutionValidated(e.envelope.clone()) + } + }) }) } @@ -321,6 +336,35 @@ impl DataAvailabilityCheckerInner { self.check_availability(block_root, pending_components, num_expected_columns) } + pub fn put_executed_payload_envelope( + &self, + executed_envelope: AvailabilityPendingExecutedEnvelope, + ) -> Result, AvailabilityCheckError> { + let epoch = executed_envelope.envelope.epoch(); + let beacon_block_root = executed_envelope.envelope.beacon_block_root(); + let pending_components = + self.update_or_insert_pending_components(beacon_block_root, |pending_components| { + pending_components.insert_executed_payload_envelope(executed_envelope); + Ok(()) + })?; + + let num_expected_columns_opt = self.get_num_expected_columns(epoch); + + pending_components.span.in_scope(|| { + debug!( + component = "executed envelope", + status = pending_components.status_str(num_expected_columns_opt), + "Component added to data availability checker" + ); + }); + + self.check_availability( + beacon_block_root, + pending_components, + num_expected_columns_opt, + ) + } + pub fn put_pre_executed_payload_envelope( &self, envelope: Arc>, @@ -330,7 +374,7 @@ impl DataAvailabilityCheckerInner { let beacon_block_root = envelope.beacon_block_root(); let pending_components = self.update_or_insert_pending_components(beacon_block_root, |pending_components| { - pending_components.insert_pre_executed_envelope(envelope, source); + pending_components.insert_pre_executed_payload_envelope(envelope, source); Ok(()) })?; @@ -351,7 +395,9 @@ impl DataAvailabilityCheckerInner { /// This does NOT remove an existing executed envelope. pub fn remove_pre_executed_envelope(&self, block_root: &Hash256) { // The read lock is immediately dropped so we can safely remove the envelope from the cache. - if let Some(PayloadEnvelopeProcessingStatus::NotValidated(_, _)) = self.get_envelope_processing_status(block_root) { + if let Some(PayloadEnvelopeProcessingStatus::NotValidated(_, _)) = + self.get_envelope_processing_status(block_root) + { // If the envelope is execution invalid, this status is permanent and idempotent to this // block_root. We drop its components (e.g. columns) because they will never be useful. self.critical.write().pop(block_root); 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 4f754abd2c..b9b9507a6f 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs @@ -13,8 +13,14 @@ use super::{ }; use crate::{ AvailabilityProcessingStatus, BeaconChain, BeaconChainError, BeaconChainTypes, - NotifyExecutionLayer, block_verification_types::AvailableBlockData, metrics, - payload_envelope_verification::ExecutionPendingEnvelope, validator_monitor::get_slot_delay_ms, + NotifyExecutionLayer, + block_verification_types::AvailableBlockData, + data_availability_router::AvailabilityOutcome, + metrics, + payload_envelope_verification::{ + AvailabilityPendingExecutedEnvelope, ExecutionPendingEnvelope, + }, + validator_monitor::get_slot_delay_ms, }; const ENVELOPE_METRICS_CACHE_SLOT_LIMIT: u32 = 64; @@ -89,7 +95,9 @@ impl BeaconChain { // reprocess this envelope until the envelope is evicted from DA checker, causing the // chain to get stuck temporarily if the envelope is canonical. Therefore we remove // it from the cache if execution fails. - // self.data_availability_checker.v2().remove_pre_executed_envelop(block_root); + self.data_availability_checker + .v2() + .remove_pre_executed_payload_envelope(&block_root); })?; // Record the time it took to wait for execution layer verification. @@ -104,9 +112,9 @@ impl BeaconChain { self.import_available_execution_payload_envelope(Box::new(envelope)) .await } - ExecutedEnvelope::AvailabilityPending() => Err(EnvelopeError::InternalError( - "Pending payload envelope not yet implemented".to_owned(), - )), + ExecutedEnvelope::AvailabilityPending(envelope) => { + self.check_envelope_availability_and_import(envelope).await + } } }; @@ -153,6 +161,24 @@ impl BeaconChain { } } + /// Checks if the payload envelope is available, and imports immediately if so, otherwise caches the envelope + /// in the data availability checker. + #[instrument(skip_all)] + async fn check_envelope_availability_and_import( + self: &Arc, + envelope: AvailabilityPendingExecutedEnvelope, + ) -> Result { + let slot = envelope.envelope.slot(); + let availability = AvailabilityOutcome::Payload( + self.data_availability_checker + .v2() + .put_executed_payload_envelope(envelope)?, + ); + self.process_availability(slot, availability, || Ok(())) + .await + .map_err(EnvelopeError::BlockError) + } + /// Accepts a fully-verified payload envelope and awaits on its payload verification handle to /// get a fully `ExecutedEnvelope`. /// 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 908da9a609..11de97a441 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs @@ -106,8 +106,7 @@ pub struct EnvelopeProcessingSnapshot { /// fully available. pub enum ExecutedEnvelope { Available(AvailableExecutedEnvelope), - // TODO(gloas) implement availability pending - AvailabilityPending(), + AvailabilityPending(AvailabilityPendingExecutedEnvelope), } impl ExecutedEnvelope { @@ -124,11 +123,14 @@ impl ExecutedEnvelope { payload_verification_outcome, )) } - // TODO(gloas) implement availability pending MaybeAvailableEnvelope::AvailabilityPending { block_hash: _, - envelope: _, - } => Self::AvailabilityPending(), + envelope, + } => Self::AvailabilityPending(AvailabilityPendingExecutedEnvelope::new( + envelope, + import_data, + payload_verification_outcome, + )), } } } From 4d04ac1381dc4c0faa1a3cdfc9e70917bd9ba8e2 Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Wed, 18 Mar 2026 06:57:19 -0700 Subject: [PATCH 032/118] update --- beacon_node/beacon_chain/src/beacon_chain.rs | 17 ++----- .../beacon_chain/src/block_verification.rs | 12 ----- .../src/data_availability_checker_v2/mod.rs | 1 - .../payload_envelope_cache.rs | 1 - .../pending_components_cache.rs | 10 +---- .../payload_envelope_verification/import.rs | 44 +++++++++++++++---- 6 files changed, 40 insertions(+), 45 deletions(-) delete mode 100644 beacon_node/beacon_chain/src/data_availability_checker_v2/payload_envelope_cache.rs diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 2b380c0520..1250252434 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -3779,7 +3779,7 @@ impl BeaconChain { /// /// An error is returned if the block was unable to be imported. It may be partially imported /// (i.e., this function is not atomic). - pub(crate) async fn process_availability( + async fn process_availability( self: &Arc, slot: Slot, availability: AvailabilityOutcome, @@ -3798,19 +3798,8 @@ impl BeaconChain { ), } } - AvailabilityOutcome::Payload(availability) => match availability { - PayloadAvailability::Available(available_envelope) => { - // TODO(gloas) execution publish_fn - publish_fn()?; - - // Payload envelope is fully available - self.import_available_execution_payload_envelope(available_envelope) - .await - .map_err(BlockError::from) - } - PayloadAvailability::MissingComponents(block_root) => Ok( - AvailabilityProcessingStatus::MissingComponents(slot, block_root), - ), + AvailabilityOutcome::Payload(_) => { + return Err(BlockError::InternalError("Received a payload envelope availability outcome variant when a block variant was expected".to_string())) }, } } diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index dcc006ecd8..de817e35fb 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -322,12 +322,6 @@ pub enum BlockError { bid_parent_root: Hash256, block_parent_root: Hash256, }, - /// An error occurred while processing a payload envelope. - /// - /// ## Peer scoring - /// - /// Peer scoring depends on the inner `EnvelopeError`. - EnvelopeError(EnvelopeError), } /// Which specific signature(s) are invalid in a SignedBeaconBlock @@ -347,12 +341,6 @@ impl From for BlockError { } } -impl From for BlockError { - fn from(e: EnvelopeError) -> Self { - Self::EnvelopeError(e) - } -} - /// Returned when block validation failed due to some issue verifying /// the execution payload. #[derive(Debug)] diff --git a/beacon_node/beacon_chain/src/data_availability_checker_v2/mod.rs b/beacon_node/beacon_chain/src/data_availability_checker_v2/mod.rs index c45f54b467..39c235b51c 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker_v2/mod.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker_v2/mod.rs @@ -20,7 +20,6 @@ use types::{ Hash256, SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope, Slot, }; -mod payload_envelope_cache; mod pending_components_cache; use crate::data_column_verification::{ diff --git a/beacon_node/beacon_chain/src/data_availability_checker_v2/payload_envelope_cache.rs b/beacon_node/beacon_chain/src/data_availability_checker_v2/payload_envelope_cache.rs deleted file mode 100644 index 8b13789179..0000000000 --- a/beacon_node/beacon_chain/src/data_availability_checker_v2/payload_envelope_cache.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/beacon_node/beacon_chain/src/data_availability_checker_v2/pending_components_cache.rs b/beacon_node/beacon_chain/src/data_availability_checker_v2/pending_components_cache.rs index 607c031694..3666024c79 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker_v2/pending_components_cache.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker_v2/pending_components_cache.rs @@ -81,7 +81,7 @@ impl PendingComponents { self.bid = Some(bid); } - pub fn insert_executed_paylaod_envelope( + pub fn insert_executed_payload_envelope( &mut self, envelope: AvailabilityPendingExecutedEnvelope, ) { @@ -96,14 +96,6 @@ impl PendingComponents { self.envelope = Some(CachedPayloadEnvelope::PreExecution(envelope, import_source)) } - /// Inserts an executed payload envelope into the cache. - pub fn insert_executed_payload_envelope( - &mut self, - envelope: AvailabilityPendingExecutedEnvelope, - ) { - self.envelope = Some(CachedPayloadEnvelope::Executed(Box::new(envelope))) - } - /// Returns the number of blobs expected by reading the bid's kzg commitments. /// Returns an error if the bid is not cached. This function should only be called /// after ensuring that the bid has been cached. 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 b9b9507a6f..4f660362c6 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs @@ -1,16 +1,11 @@ use std::sync::Arc; use std::time::Duration; -use fork_choice::PayloadVerificationStatus; -use slot_clock::SlotClock; -use store::StoreOp; -use tracing::{debug, error, info, info_span, instrument, warn}; -use types::{BeaconState, BlockImportSource, Hash256, Slot}; - use super::{ AvailableEnvelope, AvailableExecutedEnvelope, EnvelopeError, EnvelopeImportData, ExecutedEnvelope, gossip_verified_envelope::GossipVerifiedEnvelope, }; +use crate::data_availability_checker_v2::Availability as PayloadAvailability; use crate::{ AvailabilityProcessingStatus, BeaconChain, BeaconChainError, BeaconChainTypes, NotifyExecutionLayer, @@ -22,6 +17,11 @@ use crate::{ }, validator_monitor::get_slot_delay_ms, }; +use fork_choice::PayloadVerificationStatus; +use slot_clock::SlotClock; +use store::StoreOp; +use tracing::{debug, error, info, info_span, instrument, warn}; +use types::{BeaconState, BlockImportSource, Hash256, Slot}; const ENVELOPE_METRICS_CACHE_SLOT_LIMIT: u32 = 64; @@ -161,6 +161,35 @@ impl BeaconChain { } } + /// Imports a fully available payload envelope. Otherwise, returns `AvailabilityProcessingStatus::MissingComponents` + /// + /// An error is returned if the enveope was unable to be imported. It may be partially imported + /// (i.e., this function is not atomic). + async fn process_payload_envelope_availability( + self: &Arc, + slot: Slot, + availability: AvailabilityOutcome, + publish_fn: impl FnOnce() -> Result<(), EnvelopeError>, + ) -> Result { + match availability { + AvailabilityOutcome::Block(_) => { + return Err(EnvelopeError::InternalError("Received a block availability outcome variant when a payload envelope variant was expected".to_string())) + } + AvailabilityOutcome::Payload(availability) => match availability { + PayloadAvailability::Available(available_envelope) => { + publish_fn()?; + + // Payload envelope is fully available + self.import_available_execution_payload_envelope(available_envelope) + .await + } + PayloadAvailability::MissingComponents(block_root) => Ok( + AvailabilityProcessingStatus::MissingComponents(slot, block_root), + ), + }, + } + } + /// Checks if the payload envelope is available, and imports immediately if so, otherwise caches the envelope /// in the data availability checker. #[instrument(skip_all)] @@ -174,9 +203,8 @@ impl BeaconChain { .v2() .put_executed_payload_envelope(envelope)?, ); - self.process_availability(slot, availability, || Ok(())) + self.process_payload_envelope_availability(slot, availability, || Ok(())) .await - .map_err(EnvelopeError::BlockError) } /// Accepts a fully-verified payload envelope and awaits on its payload verification handle to From 83852db4376cc09dcf8bf3d7650944be92fd978e Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Wed, 18 Mar 2026 22:02:53 -0700 Subject: [PATCH 033/118] refactor --- .../src/data_availability_checker_v2/mod.rs | 819 +++++++++++-- .../pending_components.rs | 296 +++++ .../pending_components_cache.rs | 1081 ----------------- 3 files changed, 1025 insertions(+), 1171 deletions(-) create mode 100644 beacon_node/beacon_chain/src/data_availability_checker_v2/pending_components.rs delete mode 100644 beacon_node/beacon_chain/src/data_availability_checker_v2/pending_components_cache.rs diff --git a/beacon_node/beacon_chain/src/data_availability_checker_v2/mod.rs b/beacon_node/beacon_chain/src/data_availability_checker_v2/mod.rs index 39c235b51c..fe03f2d0f4 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker_v2/mod.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker_v2/mod.rs @@ -1,35 +1,33 @@ -use crate::data_availability_checker_v2::pending_components_cache::{ - DataAvailabilityCheckerInner, ReconstructColumnsDecision, -}; - use crate::data_availability_checker::AvailabilityCheckError; use crate::payload_envelope_verification::{ AvailabilityPendingExecutedEnvelope, AvailableExecutedEnvelope, }; use crate::{BeaconChain, BeaconChainTypes, CustodyContext, metrics}; use kzg::Kzg; +use lru::LruCache; +use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard, RwLockWriteGuard}; use slot_clock::SlotClock; use std::fmt; use std::fmt::Debug; use std::num::NonZeroUsize; use std::sync::Arc; use task_executor::TaskExecutor; -use tracing::{debug, error, instrument, trace}; +use tracing::{Span, debug, error, instrument, trace}; use types::{ - BlockImportSource, ChainSpec, ColumnIndex, DataColumnSidecar, DataColumnSidecarList, EthSpec, - Hash256, SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope, Slot, + BlockImportSource, ChainSpec, ColumnIndex, DataColumnSidecar, DataColumnSidecarList, Epoch, + EthSpec, Hash256, SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope, Slot, }; -mod pending_components_cache; +mod pending_components; use crate::data_column_verification::{ GossipVerifiedDataColumn, KzgVerifiedCustodyDataColumn, KzgVerifiedDataColumn, - verify_kzg_for_data_column_list, }; use crate::metrics::{ KZG_DATA_COLUMN_RECONSTRUCTION_ATTEMPTS, KZG_DATA_COLUMN_RECONSTRUCTION_FAILURES, }; use crate::observed_data_sidecars::ObservationStrategy; +use pending_components::{PendingComponents, ReconstructColumnsDecision}; use types::new_non_zero_usize; /// The LRU Cache stores `PendingComponents`, which store the block root, the execution payload bid, and its associated column data. @@ -92,9 +90,8 @@ pub enum DataColumnReconstructionResult { /// over gossip. However, data may never become available if a malicious proposer does not /// publish its data, or there are network issues. Components are only removed via LRU eviction. pub struct DataAvailabilityChecker { - availability_cache: Arc>, - #[allow(dead_code)] - slot_clock: T::SlotClock, + /// Contains all the data we keep in memory, protected by an RwLock + availability_cache: RwLock>>, kzg: Arc, custody_context: Arc>, spec: Arc, @@ -102,19 +99,13 @@ pub struct DataAvailabilityChecker { impl DataAvailabilityChecker { pub fn new( - slot_clock: T::SlotClock, + _slot_clock: T::SlotClock, kzg: Arc, custody_context: Arc>, spec: Arc, ) -> Result { - let inner = DataAvailabilityCheckerInner::new( - OVERFLOW_LRU_CAPACITY_NON_ZERO, - custody_context.clone(), - spec.clone(), - )?; Ok(Self { - availability_cache: Arc::new(inner), - slot_clock, + availability_cache: RwLock::new(LruCache::new(OVERFLOW_LRU_CAPACITY_NON_ZERO)), kzg, custody_context, spec, @@ -131,16 +122,22 @@ impl DataAvailabilityChecker { &self, block_root: Hash256, ) -> Option> { - self.availability_cache.peek_data_columns(block_root) + self.peek_pending_components(&block_root, |components| { + components.map(|c| { + c.verified_data_columns + .iter() + .map(|col| col.clone_arc()) + .collect() + }) + }) } /// Returns the indices of cached data columns for the given block root. #[instrument(skip_all, level = "trace")] pub fn cached_data_column_indexes(&self, block_root: &Hash256) -> Option> { - self.availability_cache - .peek_pending_components(block_root, |components| { - components.map(|components| components.get_cached_data_columns_indices()) - }) + self.peek_pending_components(block_root, |components| { + components.map(|components| components.get_cached_data_columns_indices()) + }) } /// Checks if a specific data column is cached for the given block root. @@ -150,38 +147,128 @@ impl DataAvailabilityChecker { block_root: &Hash256, data_column: &DataColumnSidecar, ) -> bool { - self.availability_cache - .peek_pending_components(block_root, |components| { - components.is_some_and(|components| { - let cached_column_opt = components.get_cached_data_column(*data_column.index()); - cached_column_opt.is_some_and(|cached| *cached == *data_column) - }) + self.peek_pending_components(block_root, |components| { + components.is_some_and(|components| { + let cached_column_opt = components.get_cached_data_column(*data_column.index()); + cached_column_opt.is_some_and(|cached| *cached == *data_column) }) + }) } + /// Returns the envelope processing status for the given `block_root`. + pub fn get_envelope_processing_status( + &self, + block_root: &Hash256, + ) -> Option> { + self.peek_pending_components(block_root, |components| { + components.and_then(|c| { + c.envelope.as_ref().map(|envelope| match envelope { + pending_components::CachedPayloadEnvelope::PreExecution(e, source) => { + PayloadEnvelopeProcessingStatus::NotValidated(e.clone(), *source) + } + pending_components::CachedPayloadEnvelope::Executed(e) => { + PayloadEnvelopeProcessingStatus::ExecutionValidated(e.envelope.clone()) + } + }) + }) + }) + } + + + /// Insert an executed payload envelope into the cache and performs an availability check pub fn put_executed_payload_envelope( &self, executed_envelope: AvailabilityPendingExecutedEnvelope, ) -> Result, AvailabilityCheckError> { - self.availability_cache - .put_executed_payload_envelope(executed_envelope) + let epoch = executed_envelope.envelope.epoch(); + let beacon_block_root = executed_envelope.envelope.beacon_block_root(); + let pending_components = + self.update_or_insert_pending_components(beacon_block_root, |pending_components| { + pending_components.insert_executed_payload_envelope(executed_envelope); + Ok(()) + })?; + + let num_expected_columns = self.get_num_expected_columns(epoch); + + pending_components.span.in_scope(|| { + debug!( + component = "executed envelope", + status = pending_components.status_str(num_expected_columns), + "Component added to data availability checker" + ); + }); + + self.check_availability(beacon_block_root, pending_components, num_expected_columns) } + /// Insert a pre executed payload envelope in the cache pub fn put_pre_executed_payload_envelope( &self, envelope: Arc>, source: BlockImportSource, ) -> Result<(), AvailabilityCheckError> { - self.availability_cache - .put_pre_executed_payload_envelope(envelope, source) + let epoch = envelope.epoch(); + let beacon_block_root = envelope.beacon_block_root(); + let pending_components = + self.update_or_insert_pending_components(beacon_block_root, |pending_components| { + pending_components.insert_pre_executed_payload_envelope(envelope, source); + Ok(()) + })?; + + let num_expected_columns = self.get_num_expected_columns(epoch); + + pending_components.span.in_scope(|| { + debug!( + component = "pre executed payload envelope", + status = pending_components.status_str(num_expected_columns), + "Component added to data availability checker" + ); + }); + + Ok(()) } + /// Removes a pre-executed envelope from the cache. + /// This does NOT remove an existing executed envelope. pub fn remove_pre_executed_payload_envelope(&self, block_root: &Hash256) { - self.availability_cache - .remove_pre_executed_envelope(block_root); + if let Some(PayloadEnvelopeProcessingStatus::NotValidated(_, _)) = + self.get_envelope_processing_status(block_root) + { + // If the envelope is execution invalid, this status is permanent and idempotent to this + // block_root. We drop its components (e.g. columns) because they will never be useful. + self.availability_cache.write().pop(block_root); + } } - /// Insert RPC custody columns and check if the payload becomes available. + /// Insert an execution payload bid into the cache. + pub fn put_bid( + &self, + block_root: Hash256, + bid: Arc>, + ) -> Result, AvailabilityCheckError> { + let epoch = bid.message.slot.epoch(T::EthSpec::slots_per_epoch()); + + let pending_components = + self.update_or_insert_pending_components(block_root, |pending_components| { + pending_components.insert_bid(bid); + Ok(()) + })?; + + let num_expected_columns = self.get_num_expected_columns(epoch); + + pending_components.span.in_scope(|| { + debug!( + component = "bid", + status = pending_components.status_str(num_expected_columns), + "Component added to data availability checker" + ); + }); + + self.check_availability(block_root, pending_components, num_expected_columns) + } + + /// Perform KZG verification on RPC custody columns and insert them into the cache. + /// After insertion check if the envelope becomes available. #[instrument(skip_all, level = "trace")] pub fn put_rpc_custody_columns( &self, @@ -189,12 +276,10 @@ impl DataAvailabilityChecker { slot: Slot, custody_columns: DataColumnSidecarList, ) -> Result, AvailabilityCheckError> { - // Attributes fault to the specific peer that sent an invalid column let kzg_verified_columns = KzgVerifiedDataColumn::from_batch_with_scoring(custody_columns, &self.kzg) .map_err(AvailabilityCheckError::InvalidColumn)?; - // Filter out columns that aren't required for custody for this slot let epoch = slot.epoch(T::EthSpec::slots_per_epoch()); let sampling_columns = self .custody_context @@ -205,12 +290,11 @@ impl DataAvailabilityChecker { .map(KzgVerifiedCustodyDataColumn::from_asserted_custody) .collect::>(); - self.availability_cache - .put_kzg_verified_data_columns(block_root, verified_custody_columns) + self.put_kzg_verified_custody_data_columns(block_root, verified_custody_columns) } - /// Check if we've cached other data columns for this block root. If it satisfies the custody - /// requirement, return the `Availability::Available` variant. Otherwise cache the data column sidecar. + /// Perform KZG verification on gossip verified custody columns and insert them into the cache. + /// After insertion check if the envelope becomes available #[instrument(skip_all, level = "trace")] pub fn put_gossip_verified_data_columns( &self, @@ -228,18 +312,40 @@ impl DataAvailabilityChecker { .map(|c| KzgVerifiedCustodyDataColumn::from_asserted_custody(c.into_inner())) .collect::>(); - self.availability_cache - .put_kzg_verified_data_columns(block_root, custody_columns) + self.put_kzg_verified_custody_data_columns(block_root, custody_columns) } - #[instrument(skip_all, level = "trace")] + /// Insert KZG verified columns into the cache. + /// After insertion check if the envelope becomes available. pub fn put_kzg_verified_custody_data_columns( &self, block_root: Hash256, - custody_columns: Vec>, + kzg_verified_data_columns: Vec>, ) -> Result, AvailabilityCheckError> { - self.availability_cache - .put_kzg_verified_data_columns(block_root, custody_columns) + let mut kzg_verified_data_columns = kzg_verified_data_columns.into_iter().peekable(); + let Some(epoch) = kzg_verified_data_columns + .peek() + .map(|verified_col| verified_col.as_data_column().epoch()) + else { + return Ok(Availability::MissingComponents(block_root)); + }; + + let pending_components = self + .update_or_insert_pending_components(block_root, |pending_components| { + pending_components.merge_data_columns(kzg_verified_data_columns) + })?; + + let num_expected_columns = self.get_num_expected_columns(epoch); + + pending_components.span.in_scope(|| { + debug!( + component = "data_columns", + status = pending_components.status_str(num_expected_columns), + "Component added to data availability checker" + ); + }); + + self.check_availability(block_root, pending_components, num_expected_columns) } #[instrument(skip_all, level = "debug")] @@ -247,10 +353,7 @@ impl DataAvailabilityChecker { &self, block_root: &Hash256, ) -> Result, AvailabilityCheckError> { - let verified_data_columns = match self - .availability_cache - .check_and_set_reconstruction_started(block_root) - { + let verified_data_columns = match self.check_and_set_reconstruction_started(block_root) { ReconstructColumnsDecision::Yes(verified_data_columns) => verified_data_columns, ReconstructColumnsDecision::No(reason) => { return Ok(DataColumnReconstructionResult::NotStarted(reason)); @@ -271,13 +374,11 @@ impl DataAvailabilityChecker { error = ?e, "Error reconstructing data columns" ); - self.availability_cache - .handle_reconstruction_failure(block_root); + self.handle_reconstruction_failure(block_root); metrics::inc_counter(&KZG_DATA_COLUMN_RECONSTRUCTION_FAILURES); AvailabilityCheckError::ReconstructColumnsError(e) })?; - // Check indices from cache again to make sure we don't publish components we've already received. let Some(existing_column_indices) = self.cached_data_column_indexes(block_root) else { return Err(AvailabilityCheckError::Unexpected( "block no longer exists in the data availability checker".to_string(), @@ -294,8 +395,6 @@ impl DataAvailabilityChecker { .custody_context() .sampling_columns_for_epoch(slot.epoch(T::EthSpec::slots_per_epoch()), &self.spec); - // We only need to import and publish columns that we need to sample - // and columns that we haven't already received let data_columns_to_import_and_publish = all_data_columns .into_iter() .filter(|d| { @@ -317,8 +416,7 @@ impl DataAvailabilityChecker { "Reconstructed columns" ); - self.availability_cache - .put_kzg_verified_data_columns(*block_root, data_columns_to_import_and_publish.clone()) + self.put_kzg_verified_custody_data_columns(*block_root, data_columns_to_import_and_publish.clone()) .map(|availability| { DataColumnReconstructionResult::Success(( availability, @@ -330,33 +428,152 @@ impl DataAvailabilityChecker { }) } - /// Verifies KZG commitments for data columns. - pub fn verify_kzg_for_data_columns( - &self, - data_columns: &DataColumnSidecarList, - ) -> Result<(), AvailabilityCheckError> { - if !data_columns.is_empty() { - verify_kzg_for_data_column_list(data_columns.iter(), &self.kzg) - .map_err(AvailabilityCheckError::InvalidColumn)?; - } - Ok(()) - } - - /// Insert an execution payload bid into the cache and check if data becomes available. - pub fn put_bid( - &self, - block_root: Hash256, - bid: Arc>, - ) -> Result, AvailabilityCheckError> { - self.availability_cache.put_bid(block_root, bid) - } + // ── Metrics ── /// Collects metrics from the data availability checker. pub fn metrics(&self) -> DataAvailabilityCheckerMetrics { DataAvailabilityCheckerMetrics { - block_cache_size: self.availability_cache.block_cache_size(), + block_cache_size: self.block_cache_size(), } } + + /// Number of pending component entries in memory in the cache. + pub fn block_cache_size(&self) -> usize { + self.availability_cache.read().len() + } + + // ── Internal helpers ── + + fn check_availability( + &self, + block_root: Hash256, + pending_components: MappedRwLockReadGuard<'_, PendingComponents>, + num_expected_columns: usize, + ) -> Result, AvailabilityCheckError> { + if let Some(available_envelope) = pending_components.make_available(num_expected_columns)? { + // Explicitly drop read lock before acquiring write lock + drop(pending_components); + if let Some(components) = self.availability_cache.write().get_mut(&block_root) { + // Clean up span now that data is available + components.span = Span::none(); + } + + // We never remove the pending components manually to avoid race conditions. + // Components are only removed via LRU eviction as finality advances. + Ok(Availability::Available(Box::new(available_envelope))) + } else { + Ok(Availability::MissingComponents(block_root)) + } + } + + /// Updates or inserts a new `PendingComponents` if it doesn't exist, and then apply the + /// `update_fn` while holding the write lock. + /// + /// Once the update is complete, the write lock is downgraded and a read guard with a + /// reference of the updated `PendingComponents` is returned. + fn update_or_insert_pending_components( + &self, + block_root: Hash256, + update_fn: F, + ) -> Result>, AvailabilityCheckError> + where + F: FnOnce(&mut PendingComponents) -> Result<(), AvailabilityCheckError>, + { + let mut write_lock = self.availability_cache.write(); + + { + let pending_components = write_lock.get_or_insert_mut(block_root, || { + PendingComponents::empty(block_root, self.spec.clone()) + }); + update_fn(pending_components)? + } + + RwLockReadGuard::try_map(RwLockWriteGuard::downgrade(write_lock), |cache| { + cache.peek(&block_root) + }) + .map_err(|_| { + AvailabilityCheckError::Unexpected("pending components should exist".to_string()) + }) + } + + fn peek_pending_components>) -> R>( + &self, + block_root: &Hash256, + f: F, + ) -> R { + f(self.availability_cache.read().peek(block_root)) + } + + /// Check whether data column reconstruction should be attempted. + fn check_and_set_reconstruction_started( + &self, + block_root: &Hash256, + ) -> ReconstructColumnsDecision { + let mut write_lock = self.availability_cache.write(); + let Some(pending_components) = write_lock.get_mut(block_root) else { + return ReconstructColumnsDecision::No("block already imported"); + }; + + let Some(epoch) = pending_components + .verified_data_columns + .first() + .map(|c| c.as_data_column().epoch()) + else { + return ReconstructColumnsDecision::No("not enough columns"); + }; + + let total_column_count = T::EthSpec::number_of_columns(); + let sampling_column_count = self + .custody_context + .num_of_data_columns_to_sample(epoch, &self.spec); + let received_column_count = pending_components.verified_data_columns.len(); + + if pending_components.reconstruction_started { + return ReconstructColumnsDecision::No("already started"); + } + if received_column_count >= sampling_column_count { + return ReconstructColumnsDecision::No("all sampling columns received"); + } + if received_column_count < total_column_count / 2 { + return ReconstructColumnsDecision::No("not enough columns"); + } + + pending_components.reconstruction_started = true; + ReconstructColumnsDecision::Yes(pending_components.verified_data_columns.clone()) + } + + /// This could mean some invalid data columns made it through to the `DataAvailabilityChecker`. + /// In this case, we remove all data columns in `PendingComponents`, reset reconstruction + /// status so that we can attempt to retrieve columns from peers again. + fn handle_reconstruction_failure(&self, block_root: &Hash256) { + if let Some(pending_components_mut) = self.availability_cache.write().get_mut(block_root) { + pending_components_mut.verified_data_columns = vec![]; + pending_components_mut.reconstruction_started = false; + } + } + + fn get_num_expected_columns(&self, epoch: Epoch) -> usize { + self.custody_context + .num_of_data_columns_to_sample(epoch, &self.spec) + } + + /// Maintain the cache by removing entries older than the cutoff epoch. + pub fn do_maintenance(&self, cutoff_epoch: Epoch) -> Result<(), AvailabilityCheckError> { + let mut write_lock = self.availability_cache.write(); + let mut keys_to_remove = vec![]; + for (key, value) in write_lock.iter() { + if let Some(epoch) = value.epoch() + && epoch < cutoff_epoch + { + keys_to_remove.push(*key); + } + } + for key in keys_to_remove { + write_lock.pop(&key); + } + + Ok(()) + } } /// Helper struct to group data availability checker metrics. @@ -369,13 +586,9 @@ pub fn start_availability_cache_maintenance_service( chain: Arc>, ) { if chain.spec.gloas_fork_epoch.is_some() { - let overflow_cache = chain - .data_availability_checker - .v2() - .availability_cache - .clone(); + let da_checker = chain.data_availability_checker.v2().clone(); executor.spawn( - async move { availability_cache_maintenance_service(chain, overflow_cache).await }, + async move { availability_cache_maintenance_service(chain, da_checker).await }, "availability_cache_service", ); } else { @@ -385,7 +598,7 @@ pub fn start_availability_cache_maintenance_service( async fn availability_cache_maintenance_service( chain: Arc>, - overflow_cache: Arc>, + da_checker: Arc>, ) { let epoch_duration = chain.slot_clock.slot_duration() * T::EthSpec::slots_per_epoch() as u32; loop { @@ -434,7 +647,7 @@ async fn availability_cache_maintenance_service( // any data belonging to an epoch before this should be pruned let cutoff_epoch = std::cmp::max(finalized_epoch + 1, min_epochs_for_blobs); - if let Err(e) = overflow_cache.do_maintenance(cutoff_epoch) { + if let Err(e) = da_checker.do_maintenance(cutoff_epoch) { error!(error = ?e,"Failed to maintain availability cache"); } } @@ -446,3 +659,429 @@ async fn availability_cache_maintenance_service( }; } } + +#[cfg(test)] +mod data_availability_checker_tests { + use super::*; + + use crate::data_column_verification::{KzgVerifiedCustodyDataColumn, KzgVerifiedDataColumn}; + use crate::test_utils::{ + NumBlobs, generate_data_column_indices_rand_order, generate_rand_block_and_data_columns, + test_spec, + }; + use crate::{ + custody_context::NodeCustodyType, + test_utils::{BeaconChainHarness, DiskHarnessType}, + }; + use logging::create_test_tracing_subscriber; + use rand::SeedableRng; + use rand::rngs::StdRng; + use store::{HotColdDB, StoreConfig, database::interface::BeaconNodeBackend}; + use tempfile::{TempDir, tempdir}; + use types::{ForkName, MinimalEthSpec, Slot}; + + type E = MinimalEthSpec; + + const LOW_VALIDATOR_COUNT: usize = 32; + + fn gloas_spec() -> Arc { + let mut spec = E::default_spec(); + spec.altair_fork_epoch = Some(Epoch::new(0)); + spec.bellatrix_fork_epoch = Some(Epoch::new(0)); + spec.capella_fork_epoch = Some(Epoch::new(0)); + spec.deneb_fork_epoch = Some(Epoch::new(0)); + spec.electra_fork_epoch = Some(Epoch::new(0)); + spec.fulu_fork_epoch = Some(Epoch::new(0)); + spec.gloas_fork_epoch = Some(Epoch::new(0)); + Arc::new(spec) + } + + fn get_store_with_spec( + db_path: &TempDir, + spec: Arc, + ) -> Arc, BeaconNodeBackend>> { + let hot_path = db_path.path().join("hot_db"); + let cold_path = db_path.path().join("cold_db"); + let blobs_path = db_path.path().join("blobs_db"); + let config = StoreConfig::default(); + + HotColdDB::open( + &hot_path, + &cold_path, + &blobs_path, + |_, _, _| Ok(()), + config, + spec, + ) + .expect("disk store should initialize") + } + + async fn get_gloas_chain( + db_path: &TempDir, + ) -> BeaconChainHarness> { + let spec = gloas_spec::(); + + let chain_store = get_store_with_spec::(db_path, spec.clone()); + let validators_keypairs = + types::test_utils::generate_deterministic_keypairs(LOW_VALIDATOR_COUNT); + BeaconChainHarness::builder(E::default()) + .spec(spec.clone()) + .keypairs(validators_keypairs) + .fresh_disk_store(chain_store) + .mock_execution_layer() + .build() + } + + async fn setup_harness_and_cache( + capacity: usize, + ) -> ( + BeaconChainHarness>, + Arc>, + TempDir, + ) + where + T: BeaconChainTypes< + HotStore = BeaconNodeBackend, + ColdStore = BeaconNodeBackend, + EthSpec = E, + >, + { + create_test_tracing_subscriber(); + let chain_db_path = tempdir().expect("should get temp dir"); + let harness = get_gloas_chain::(&chain_db_path).await; + let spec = harness.spec.clone(); + let custody_context = Arc::new(CustodyContext::::new( + NodeCustodyType::Fullnode, + generate_data_column_indices_rand_order::(), + &spec, + )); + + todo!() + // let cache = Arc::new( + // DataAvailabilityChecker::::new( + // harness.chain.slot_clock.clone().into(), + // harness.chain.kzg.clone().unwrap(), + // custody_context, + // spec.clone(), + // ) + // .expect("should create cache"), + // ); + // (harness, cache, chain_db_path) + } + + fn is_gloas_enabled() -> bool { + let spec = test_spec::(); + spec.fork_name_at_slot::(Slot::new(0)).gloas_enabled() + } + + #[tokio::test] + async fn test_cache_creation() { + if !is_gloas_enabled() { + return; + } + + type T = DiskHarnessType; + let capacity = 4; + let (_harness, cache, _path) = setup_harness_and_cache::(capacity).await; + assert_eq!(cache.block_cache_size(), 0); + } + + #[tokio::test] + async fn test_put_columns_creates_pending_components() { + if !is_gloas_enabled() { + return; + } + + type T = DiskHarnessType; + let capacity = 4; + let (harness, cache, _path) = setup_harness_and_cache::(capacity).await; + + let mut rng = StdRng::seed_from_u64(0xDEADBEEF); + let spec = harness.spec.clone(); + + let (_block, data_columns) = generate_rand_block_and_data_columns::( + ForkName::Gloas, + NumBlobs::Number(1), + &mut rng, + &spec, + ); + + let block_root = Hash256::random(); + + let verified_columns: Vec<_> = data_columns + .into_iter() + .take(1) // Just take one column for the test + .map(|col| { + KzgVerifiedCustodyDataColumn::from_asserted_custody( + KzgVerifiedDataColumn::__new_for_testing(col), + ) + }) + .collect(); + + // Put columns into cache + let result = cache.put_kzg_verified_custody_data_columns(block_root, verified_columns); + assert!(result.is_ok()); + + // Check that pending components were created + assert_eq!(cache.block_cache_size(), 1); + + // Verify columns are cached + let cached_indices = cache.peek_pending_components(&block_root, |components| { + components.map(|c| c.get_cached_data_columns_indices()) + }); + assert!(cached_indices.is_some()); + assert_eq!(cached_indices.unwrap().len(), 1); + } + + #[tokio::test] + async fn test_column_deduplication() { + if !is_gloas_enabled() { + return; + } + + type T = DiskHarnessType; + let capacity = 4; + let (harness, cache, _path) = setup_harness_and_cache::(capacity).await; + + let mut rng = StdRng::seed_from_u64(0xDEADBEEF); + let spec = harness.spec.clone(); + + let (_block, data_columns) = generate_rand_block_and_data_columns::( + ForkName::Gloas, + NumBlobs::Number(1), + &mut rng, + &spec, + ); + + let block_root = Hash256::random(); + + // Get the first column + let first_column = data_columns.first().cloned().expect("should have column"); + let column_index = *first_column.index(); + + let verified_column = KzgVerifiedCustodyDataColumn::from_asserted_custody( + KzgVerifiedDataColumn::__new_for_testing(first_column.clone()), + ); + + // Insert the same column twice + cache + .put_kzg_verified_custody_data_columns(block_root, vec![verified_column.clone()]) + .expect("should put column"); + + cache + .put_kzg_verified_custody_data_columns(block_root, vec![verified_column]) + .expect("should put column again"); + + // Check that we still only have one column (deduplicated) + let cached_indices = cache.peek_pending_components(&block_root, |components| { + components.map(|c| c.get_cached_data_columns_indices()) + }); + assert!(cached_indices.is_some()); + let indices = cached_indices.unwrap(); + assert_eq!(indices.len(), 1); + assert_eq!(indices[0], column_index); + } + + #[tokio::test] + async fn test_columns_without_block_not_available() { + if !is_gloas_enabled() { + return; + } + + type T = DiskHarnessType; + let capacity = 4; + let (harness, cache, _path) = setup_harness_and_cache::(capacity).await; + + let mut rng = StdRng::seed_from_u64(0xDEADBEEF); + let spec = harness.spec.clone(); + + let (_block, data_columns) = generate_rand_block_and_data_columns::( + ForkName::Gloas, + NumBlobs::Number(1), + &mut rng, + &spec, + ); + + let block_root = Hash256::random(); + + // Add all columns + let verified_columns: Vec<_> = data_columns + .into_iter() + .map(|col| { + KzgVerifiedCustodyDataColumn::from_asserted_custody( + KzgVerifiedDataColumn::__new_for_testing(col), + ) + }) + .collect(); + + let result = cache + .put_kzg_verified_custody_data_columns(block_root, verified_columns) + .expect("should put columns"); + + // Without a bid, should still be missing components + assert!(matches!(result, Availability::MissingComponents(_))); + } + + #[tokio::test] + async fn test_reconstruction_started_flag() { + if !is_gloas_enabled() { + return; + } + + type T = DiskHarnessType; + let capacity = 4; + let (harness, cache, _path) = setup_harness_and_cache::(capacity).await; + + let mut rng = StdRng::seed_from_u64(0xDEADBEEF); + let spec = harness.spec.clone(); + + let (_block, data_columns) = generate_rand_block_and_data_columns::( + ForkName::Gloas, + NumBlobs::Number(1), + &mut rng, + &spec, + ); + + let block_root = Hash256::random(); + + // Add some columns (not enough for reconstruction threshold) + let verified_columns: Vec<_> = data_columns + .into_iter() + .take(10) // Not enough for reconstruction + .map(|col| { + KzgVerifiedCustodyDataColumn::from_asserted_custody( + KzgVerifiedDataColumn::__new_for_testing(col), + ) + }) + .collect(); + + cache + .put_kzg_verified_custody_data_columns(block_root, verified_columns) + .expect("should put columns"); + + // Check reconstruction decision - should say "not enough columns" + let decision = cache.check_and_set_reconstruction_started(&block_root); + assert!(matches!(decision, ReconstructColumnsDecision::No(_))); + } + + #[tokio::test] + async fn test_handle_reconstruction_failure_clears_columns() { + if !is_gloas_enabled() { + return; + } + + type T = DiskHarnessType; + let capacity = 4; + let (harness, cache, _path) = setup_harness_and_cache::(capacity).await; + + let mut rng = StdRng::seed_from_u64(0xDEADBEEF); + let spec = harness.spec.clone(); + + let (_block, data_columns) = generate_rand_block_and_data_columns::( + ForkName::Gloas, + NumBlobs::Number(1), + &mut rng, + &spec, + ); + + let block_root = Hash256::random(); + + // Add some columns + let verified_columns: Vec<_> = data_columns + .into_iter() + .take(5) + .map(|col| { + KzgVerifiedCustodyDataColumn::from_asserted_custody( + KzgVerifiedDataColumn::__new_for_testing(col), + ) + }) + .collect(); + + cache + .put_kzg_verified_custody_data_columns(block_root, verified_columns) + .expect("should put columns"); + + // Verify columns are cached + let cached_count = cache.peek_pending_components(&block_root, |components| { + components.map(|c| c.verified_data_columns.len()) + }); + assert_eq!(cached_count, Some(5)); + + // Handle reconstruction failure + cache.handle_reconstruction_failure(&block_root); + + // Verify columns are cleared + let cached_count_after = cache.peek_pending_components(&block_root, |components| { + components.map(|c| c.verified_data_columns.len()) + }); + assert_eq!(cached_count_after, Some(0)); + } + + #[tokio::test] + async fn test_maintenance_removes_old_entries() { + if !is_gloas_enabled() { + return; + } + + type T = DiskHarnessType; + let capacity = 4; + let (_harness, cache, _path) = setup_harness_and_cache::(capacity).await; + + let block_root = Hash256::random(); + + // Run maintenance with a future cutoff epoch + let cutoff_epoch = Epoch::new(100); + cache + .do_maintenance(cutoff_epoch) + .expect("maintenance should succeed"); + + // Cache should still be empty since we didn't add anything with an epoch + assert_eq!(cache.block_cache_size(), 0); + } + + #[tokio::test] + async fn test_peek_data_columns() { + if !is_gloas_enabled() { + return; + } + + type T = DiskHarnessType; + let capacity = 4; + let (harness, cache, _path) = setup_harness_and_cache::(capacity).await; + + let mut rng = StdRng::seed_from_u64(0xDEADBEEF); + let spec = harness.spec.clone(); + + let (_block, data_columns) = generate_rand_block_and_data_columns::( + ForkName::Gloas, + NumBlobs::Number(1), + &mut rng, + &spec, + ); + + let block_root = Hash256::random(); + + // No columns yet + assert!(cache.get_data_columns(block_root).is_none()); + + // Add columns + let verified_columns: Vec<_> = data_columns + .into_iter() + .take(3) + .map(|col| { + KzgVerifiedCustodyDataColumn::from_asserted_custody( + KzgVerifiedDataColumn::__new_for_testing(col), + ) + }) + .collect(); + + cache + .put_kzg_verified_custody_data_columns(block_root, verified_columns) + .expect("should put columns"); + + // Now columns should be returned + let peeked = cache.get_data_columns(block_root); + assert!(peeked.is_some()); + assert_eq!(peeked.unwrap().len(), 3); + } +} diff --git a/beacon_node/beacon_chain/src/data_availability_checker_v2/pending_components.rs b/beacon_node/beacon_chain/src/data_availability_checker_v2/pending_components.rs new file mode 100644 index 0000000000..f6d4cc0321 --- /dev/null +++ b/beacon_node/beacon_chain/src/data_availability_checker_v2/pending_components.rs @@ -0,0 +1,296 @@ +use crate::data_availability_checker::AvailabilityCheckError; +use crate::data_column_verification::KzgVerifiedCustodyDataColumn; +use crate::payload_envelope_verification::AvailabilityPendingExecutedEnvelope; +use crate::payload_envelope_verification::AvailableEnvelope; +use crate::payload_envelope_verification::AvailableExecutedEnvelope; +use std::cmp::Ordering; +use std::sync::Arc; +use tracing::{Span, debug, debug_span}; +use types::BlockImportSource; +use types::{ + ChainSpec, ColumnIndex, DataColumnSidecar, Epoch, EthSpec, Hash256, + SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope, +}; + +pub enum CachedPayloadEnvelope { + PreExecution(Arc>, BlockImportSource), + Executed(Box>), +} + +/// This represents the components of a payload pending data availability. +/// +/// The columns are all gossip and kzg verified. +/// The payload is considered "available" when all required columns are received. +pub struct PendingComponents { + /// The block root is stored for tracing context in the span. + #[allow(dead_code)] + pub block_root: Hash256, + /// The execution payload bid containing blob_kzg_commitments. + pub bid: Option>>, + /// a cached pre or post executed payload envelope + pub envelope: Option>, + pub verified_data_columns: Vec>, + pub reconstruction_started: bool, + pub(crate) span: Span, + spec: Arc, +} + +impl PendingComponents { + /// Returns an immutable reference to the cached data column. + pub fn get_cached_data_column( + &self, + data_column_index: u64, + ) -> Option>> { + self.verified_data_columns + .iter() + .find(|d| d.index() == data_column_index) + .map(|d| d.clone_arc()) + } + + /// Returns the indices of cached custody columns + pub fn get_cached_data_columns_indices(&self) -> Vec { + self.verified_data_columns + .iter() + .map(|d| d.index()) + .collect() + } + + /// Merges a given set of data columns into the cache. + pub(crate) fn merge_data_columns>>( + &mut self, + kzg_verified_data_columns: I, + ) -> Result<(), AvailabilityCheckError> { + for data_column in kzg_verified_data_columns { + if self.get_cached_data_column(data_column.index()).is_none() { + self.verified_data_columns.push(data_column); + } + } + + Ok(()) + } + + /// Inserts an execution payload bid into the cache. + pub fn insert_bid(&mut self, bid: Arc>) { + self.bid = Some(bid); + } + + /// Inserts an executed payload envelope into the cache. + pub fn insert_executed_payload_envelope( + &mut self, + envelope: AvailabilityPendingExecutedEnvelope, + ) { + self.envelope = Some(CachedPayloadEnvelope::Executed(Box::new(envelope))) + } + + /// Inserts a pre-executed payload envelope into the cache. + pub fn insert_pre_executed_payload_envelope( + &mut self, + envelope: Arc>, + import_source: BlockImportSource, + ) { + self.envelope = Some(CachedPayloadEnvelope::PreExecution(envelope, import_source)) + } + + /// Returns the number of blobs expected by reading the bid's kzg commitments. + /// Returns an error if the bid is not cached. This function should only be called + /// after ensuring that the bid has been cached. + pub fn num_blobs_expected(&self) -> Result { + let bid = self + .bid + .as_ref() + .ok_or_else(|| AvailabilityCheckError::Unexpected("No bid available".to_string()))?; + + Ok(bid.message.blob_kzg_commitments.len()) + } + + /// Returns `Some` if the envelope and all required data columns have been received. + pub fn make_available( + &self, + num_expected_columns: usize, + ) -> Result>, AvailabilityCheckError> { + // If no bid has been received, we can start verifying the columns + if self.bid.is_none() { + return Ok(None); + } + + // Check if the payload has been received and executed + let Some(CachedPayloadEnvelope::Executed(envelope)) = self.envelope.as_ref() else { + return Ok(None); + }; + + let AvailabilityPendingExecutedEnvelope { + envelope, + import_data, + payload_verification_outcome, + } = envelope.as_ref(); + + // Get the number of blobs expected from the bid + let num_expected_blobs = self.num_blobs_expected()?; + + let columns = if num_expected_blobs == 0 { + self.span.in_scope(|| { + debug!("Bid has no blobs, data is available"); + }); + vec![] + } else { + let num_received_columns = self.verified_data_columns.len(); + match num_received_columns.cmp(&num_expected_columns) { + Ordering::Greater => { + // Should never happen + return Err(AvailabilityCheckError::Unexpected(format!( + "too many columns got {num_received_columns} expected {num_expected_columns}" + ))); + } + Ordering::Equal => { + // We have enough columns + let data_columns = self + .verified_data_columns + .iter() + .map(|d| d.clone().into_inner()) + .collect::>(); + + self.span.in_scope(|| { + debug!("All data columns received, data is available"); + }); + + data_columns + } + Ordering::Less => { + // Not enough data columns received yet + return Ok(None); + } + } + }; + + let available_envelope = AvailableEnvelope { + execution_block_hash: envelope.block_hash(), + envelope: envelope.clone(), + columns, + columns_available_timestamp: None, + spec: self.spec.clone(), + }; + + Ok(Some(AvailableExecutedEnvelope { + envelope: available_envelope, + import_data: import_data.clone(), + payload_verification_outcome: payload_verification_outcome.clone(), + })) + } + + /// Returns an empty `PendingComponents` object with the given block root. + pub fn empty(block_root: Hash256, spec: Arc) -> Self { + let span = debug_span!(parent: None, "lh_pending_components", %block_root); + let _guard = span.clone().entered(); + Self { + block_root, + bid: None, + envelope: None, + verified_data_columns: vec![], + reconstruction_started: false, + span, + spec, + } + } + + /// Returns the epoch of the bid or first data column, if available. + pub fn epoch(&self) -> Option { + // Get epoch from bid + if let Some(bid) = &self.bid { + return Some(bid.message.slot.epoch(E::slots_per_epoch())); + } + + // Or, get epoch from first data column + if let Some(data_column) = self.verified_data_columns.first() { + return Some(data_column.as_data_column().epoch()); + } + + None + } + + pub fn status_str(&self, num_expected_columns: usize) -> String { + format!( + "data_columns {}/{}", + self.verified_data_columns.len(), + num_expected_columns + ) + } +} + +// This enum is only used internally within the crate in the reconstruction function to improve +// readability, so it's OK to not box the variant value, and it shouldn't impact memory much with +// the current usage, as it's deconstructed immediately. +#[allow(clippy::large_enum_variant)] +pub(crate) enum ReconstructColumnsDecision { + Yes(Vec>), + No(&'static str), +} + +#[cfg(test)] +mod pending_components_tests { + use crate::test_utils::test_spec; + + use super::*; + use types::MinimalEthSpec; + + type E = MinimalEthSpec; + + #[test] + fn test_empty_pending_components() { + let spec = Arc::new(test_spec::()); + let block_root = Hash256::random(); + let components = PendingComponents::::empty(block_root, spec); + + assert_eq!(components.block_root, block_root); + assert!(components.bid.is_none()); + assert!(components.verified_data_columns.is_empty()); + assert!(!components.reconstruction_started); + assert!(components.epoch().is_none()); + } + + #[test] + fn test_get_cached_data_columns_indices_empty() { + let spec = Arc::new(test_spec::()); + let block_root = Hash256::random(); + let components = PendingComponents::::empty(block_root, spec); + + let indices = components.get_cached_data_columns_indices(); + assert!(indices.is_empty()); + } + + #[test] + fn test_status_str_no_bid() { + let spec = Arc::new(test_spec::()); + let block_root = Hash256::random(); + let components = PendingComponents::::empty(block_root, spec); + + let status = components.status_str(10); + assert_eq!(status, "data_columns 0/10"); + } + + #[test] + fn test_num_blobs_expected_no_bid() { + let spec = Arc::new(test_spec::()); + let block_root = Hash256::random(); + let components = PendingComponents::::empty(block_root, spec); + + let result = components.num_blobs_expected(); + assert!(result.is_err()); + // Error should be AvailabilityCheckError::Unexpected + assert!(matches!( + result.unwrap_err(), + AvailabilityCheckError::Unexpected(_) + )); + } + + #[test] + fn test_make_available_no_bid_returns_none() { + let spec = Arc::new(test_spec::()); + let block_root = Hash256::random(); + let components = PendingComponents::::empty(block_root, spec); + + // Without a bid, make_available should return Ok(None) + let result = components.make_available(10); + assert!(result.is_ok()); + assert!(result.unwrap().is_none()); + } +} diff --git a/beacon_node/beacon_chain/src/data_availability_checker_v2/pending_components_cache.rs b/beacon_node/beacon_chain/src/data_availability_checker_v2/pending_components_cache.rs deleted file mode 100644 index 3666024c79..0000000000 --- a/beacon_node/beacon_chain/src/data_availability_checker_v2/pending_components_cache.rs +++ /dev/null @@ -1,1081 +0,0 @@ -use crate::BeaconChainTypes; -use crate::CustodyContext; -use crate::data_availability_checker::AvailabilityCheckError; -use crate::data_availability_checker_v2::Availability; -use crate::data_availability_checker_v2::PayloadEnvelopeProcessingStatus; -use crate::data_column_verification::KzgVerifiedCustodyDataColumn; -use crate::payload_envelope_verification::AvailabilityPendingExecutedEnvelope; -use crate::payload_envelope_verification::AvailableEnvelope; -use crate::payload_envelope_verification::AvailableExecutedEnvelope; -use lru::LruCache; -use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard, RwLockWriteGuard}; -use std::cmp::Ordering; -use std::num::NonZeroUsize; -use std::sync::Arc; -use tracing::{Span, debug, debug_span}; -use types::BlockImportSource; -use types::{ - ChainSpec, ColumnIndex, DataColumnSidecar, DataColumnSidecarList, Epoch, EthSpec, Hash256, - SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope, -}; - -pub enum CachedPayloadEnvelope { - PreExecution(Arc>, BlockImportSource), - Executed(Box>), -} - -/// This represents the components of a payload pending data availability. -/// -/// The columns are all gossip and kzg verified. -/// The payload is considered "available" when all required columns are received. -pub struct PendingComponents { - /// The block root is stored for tracing context in the span. - #[allow(dead_code)] - pub block_root: Hash256, - /// The execution payload bid containing blob_kzg_commitments. - pub bid: Option>>, - /// a cached pre or post executed payload envelope - pub envelope: Option>, - pub verified_data_columns: Vec>, - pub reconstruction_started: bool, - span: Span, - spec: Arc, -} - -impl PendingComponents { - /// Returns an immutable reference to the cached data column. - pub fn get_cached_data_column( - &self, - data_column_index: u64, - ) -> Option>> { - self.verified_data_columns - .iter() - .find(|d| d.index() == data_column_index) - .map(|d| d.clone_arc()) - } - - /// Returns the indices of cached custody columns - pub fn get_cached_data_columns_indices(&self) -> Vec { - self.verified_data_columns - .iter() - .map(|d| d.index()) - .collect() - } - - /// Merges a given set of data columns into the cache. - fn merge_data_columns>>( - &mut self, - kzg_verified_data_columns: I, - ) -> Result<(), AvailabilityCheckError> { - for data_column in kzg_verified_data_columns { - if self.get_cached_data_column(data_column.index()).is_none() { - self.verified_data_columns.push(data_column); - } - } - - Ok(()) - } - - /// Inserts an execution payload bid into the cache. - pub fn insert_bid(&mut self, bid: Arc>) { - self.bid = Some(bid); - } - - pub fn insert_executed_payload_envelope( - &mut self, - envelope: AvailabilityPendingExecutedEnvelope, - ) { - self.envelope = Some(CachedPayloadEnvelope::Executed(Box::new(envelope))) - } - - pub fn insert_pre_executed_payload_envelope( - &mut self, - envelope: Arc>, - import_source: BlockImportSource, - ) { - self.envelope = Some(CachedPayloadEnvelope::PreExecution(envelope, import_source)) - } - - /// Returns the number of blobs expected by reading the bid's kzg commitments. - /// Returns an error if the bid is not cached. This function should only be called - /// after ensuring that the bid has been cached. - pub fn num_blobs_expected(&self) -> Result { - let bid = self - .bid - .as_ref() - .ok_or_else(|| AvailabilityCheckError::Unexpected("No bid available".to_string()))?; - - Ok(bid.message.blob_kzg_commitments.len()) - } - - /// Returns `Some` if the envelope and all required data columns have been received. - pub fn make_available( - &self, - num_expected_columns: usize, - ) -> Result>, AvailabilityCheckError> { - // If no bid has been received, we can start verifying the columns - if self.bid.is_none() { - return Ok(None); - } - - // Check if the payload has been received and executed - let Some(CachedPayloadEnvelope::Executed(envelope)) = self.envelope.as_ref() else { - return Ok(None); - }; - - let AvailabilityPendingExecutedEnvelope { - envelope, - import_data, - payload_verification_outcome, - } = envelope.as_ref(); - - // Get the number of blobs expected from the bid - let num_expected_blobs = self.num_blobs_expected()?; - - let columns = if num_expected_blobs == 0 { - self.span.in_scope(|| { - debug!("Bid has no blobs, data is available"); - }); - vec![] - } else { - let num_received_columns = self.verified_data_columns.len(); - match num_received_columns.cmp(&num_expected_columns) { - Ordering::Greater => { - // Should never happen - return Err(AvailabilityCheckError::Unexpected(format!( - "too many columns got {num_received_columns} expected {num_expected_columns}" - ))); - } - Ordering::Equal => { - // We have enough columns - let data_columns = self - .verified_data_columns - .iter() - .map(|d| d.clone().into_inner()) - .collect::>(); - - self.span.in_scope(|| { - debug!("All data columns received, data is available"); - }); - - data_columns - } - Ordering::Less => { - // Not enough data columns received yet - return Ok(None); - } - } - }; - - let available_envelope = AvailableEnvelope { - execution_block_hash: envelope.block_hash(), - envelope: envelope.clone(), - columns, - columns_available_timestamp: None, - spec: self.spec.clone(), - }; - - Ok(Some(AvailableExecutedEnvelope { - envelope: available_envelope, - import_data: import_data.clone(), - payload_verification_outcome: payload_verification_outcome.clone(), - })) - } - - /// Returns an empty `PendingComponents` object with the given block root. - pub fn empty(block_root: Hash256, spec: Arc) -> Self { - let span = debug_span!(parent: None, "lh_pending_components", %block_root); - let _guard = span.clone().entered(); - Self { - block_root, - bid: None, - envelope: None, - verified_data_columns: vec![], - reconstruction_started: false, - span, - spec, - } - } - - /// Returns the epoch of the bid or first data column, if available. - pub fn epoch(&self) -> Option { - // Get epoch from bid - if let Some(bid) = &self.bid { - return Some(bid.message.slot.epoch(E::slots_per_epoch())); - } - - // Or, get epoch from first data column - if let Some(data_column) = self.verified_data_columns.first() { - return Some(data_column.as_data_column().epoch()); - } - - None - } - - pub fn status_str(&self, num_expected_columns: usize) -> String { - format!( - "data_columns {}/{}", - self.verified_data_columns.len(), - num_expected_columns - ) - } -} - -/// This is the main struct for this module. Outside methods should -/// interact with the cache through this. -pub struct DataAvailabilityCheckerInner { - /// Contains all the data we keep in memory, protected by an RwLock - critical: RwLock>>, - custody_context: Arc>, - spec: Arc, -} - -// This enum is only used internally within the crate in the reconstruction function to improve -// readability, so it's OK to not box the variant value, and it shouldn't impact memory much with -// the current usage, as it's deconstructed immediately. -#[allow(clippy::large_enum_variant)] -pub(crate) enum ReconstructColumnsDecision { - Yes(Vec>), - No(&'static str), -} - -impl DataAvailabilityCheckerInner { - pub fn new( - capacity: NonZeroUsize, - custody_context: Arc>, - spec: Arc, - ) -> Result { - Ok(Self { - critical: RwLock::new(LruCache::new(capacity)), - custody_context, - spec, - }) - } - - /// Returns the envelope processing status for the given `block_root`. A `None` response indicates that - /// the envelope has not yet been inserted into the cache. - pub fn get_envelope_processing_status( - &self, - block_root: &Hash256, - ) -> Option> { - self.critical - .read() - .peek(block_root) - .and_then(|pending_components| { - pending_components - .envelope - .as_ref() - .map(|envelope| match envelope { - CachedPayloadEnvelope::PreExecution(e, source) => { - PayloadEnvelopeProcessingStatus::NotValidated(e.clone(), *source) - } - CachedPayloadEnvelope::Executed(e) => { - PayloadEnvelopeProcessingStatus::ExecutionValidated(e.envelope.clone()) - } - }) - }) - } - - /// Fetch data columns of a given `block_root` from the cache without affecting the LRU ordering - pub fn peek_data_columns( - &self, - block_root: Hash256, - ) -> Option> { - self.critical - .read() - .peek(&block_root) - .map(|pending_components| { - pending_components - .verified_data_columns - .iter() - .map(|col| col.clone_arc()) - .collect() - }) - } - - pub fn peek_pending_components>) -> R>( - &self, - block_root: &Hash256, - f: F, - ) -> R { - f(self.critical.read().peek(block_root)) - } - - /// Insert an execution payload bid into the cache and check if data becomes available. - pub fn put_bid( - &self, - block_root: Hash256, - bid: Arc>, - ) -> Result, AvailabilityCheckError> { - let epoch = bid.message.slot.epoch(T::EthSpec::slots_per_epoch()); - - let pending_components = - self.update_or_insert_pending_components(block_root, |pending_components| { - pending_components.insert_bid(bid); - Ok(()) - })?; - - let num_expected_columns = self.get_num_expected_columns(epoch); - - pending_components.span.in_scope(|| { - debug!( - component = "bid", - status = pending_components.status_str(num_expected_columns), - "Component added to data availability checker" - ); - }); - - self.check_availability(block_root, pending_components, num_expected_columns) - } - - pub fn put_executed_payload_envelope( - &self, - executed_envelope: AvailabilityPendingExecutedEnvelope, - ) -> Result, AvailabilityCheckError> { - let epoch = executed_envelope.envelope.epoch(); - let beacon_block_root = executed_envelope.envelope.beacon_block_root(); - let pending_components = - self.update_or_insert_pending_components(beacon_block_root, |pending_components| { - pending_components.insert_executed_payload_envelope(executed_envelope); - Ok(()) - })?; - - let num_expected_columns_opt = self.get_num_expected_columns(epoch); - - pending_components.span.in_scope(|| { - debug!( - component = "executed envelope", - status = pending_components.status_str(num_expected_columns_opt), - "Component added to data availability checker" - ); - }); - - self.check_availability( - beacon_block_root, - pending_components, - num_expected_columns_opt, - ) - } - - pub fn put_pre_executed_payload_envelope( - &self, - envelope: Arc>, - source: BlockImportSource, - ) -> Result<(), AvailabilityCheckError> { - let epoch = envelope.epoch(); - let beacon_block_root = envelope.beacon_block_root(); - let pending_components = - self.update_or_insert_pending_components(beacon_block_root, |pending_components| { - pending_components.insert_pre_executed_payload_envelope(envelope, source); - Ok(()) - })?; - - let num_expected_columns_opt = self.get_num_expected_columns(epoch); - - pending_components.span.in_scope(|| { - debug!( - component = "pre executed payload envelope", - status = pending_components.status_str(num_expected_columns_opt), - "Component added to data availability checker" - ); - }); - - Ok(()) - } - - /// Removes a pre-executed envelope from the cache. - /// This does NOT remove an existing executed envelope. - pub fn remove_pre_executed_envelope(&self, block_root: &Hash256) { - // The read lock is immediately dropped so we can safely remove the envelope from the cache. - if let Some(PayloadEnvelopeProcessingStatus::NotValidated(_, _)) = - self.get_envelope_processing_status(block_root) - { - // If the envelope is execution invalid, this status is permanent and idempotent to this - // block_root. We drop its components (e.g. columns) because they will never be useful. - self.critical.write().pop(block_root); - } - } - - #[allow(clippy::type_complexity)] - pub fn put_kzg_verified_data_columns< - I: IntoIterator>, - >( - &self, - block_root: Hash256, - kzg_verified_data_columns: I, - ) -> Result, AvailabilityCheckError> { - let mut kzg_verified_data_columns = kzg_verified_data_columns.into_iter().peekable(); - let Some(epoch) = kzg_verified_data_columns - .peek() - .map(|verified_col| verified_col.as_data_column().epoch()) - else { - // No columns are processed. This can occur if all received columns were filtered out - // before this point, e.g. due to a CGC change that caused extra columns to be downloaded - // before the new CGC took effect. - // Return `Ok` without marking the block as available. - return Ok(Availability::MissingComponents(block_root)); - }; - - let pending_components = self - .update_or_insert_pending_components(block_root, |pending_components| { - pending_components.merge_data_columns(kzg_verified_data_columns) - })?; - - let num_expected_columns = self.get_num_expected_columns(epoch); - - pending_components.span.in_scope(|| { - debug!( - component = "data_columns", - status = pending_components.status_str(num_expected_columns), - "Component added to data availability checker" - ); - }); - - self.check_availability(block_root, pending_components, num_expected_columns) - } - - fn check_availability( - &self, - block_root: Hash256, - pending_components: MappedRwLockReadGuard<'_, PendingComponents>, - num_expected_columns: usize, - ) -> Result, AvailabilityCheckError> { - if let Some(available_envelope) = pending_components.make_available(num_expected_columns)? { - // Explicitly drop read lock before acquiring write lock - drop(pending_components); - if let Some(components) = self.critical.write().get_mut(&block_root) { - // Clean up span now that data is available - components.span = Span::none(); - } - - // We never remove the pending components manually to avoid race conditions. - // This ensures components remain available during and right after payload import, - // preventing a race condition where a component was removed after the payload was - // imported, but re-inserted immediately, causing partial pending components to be - // stored and served to peers. - // Components are only removed via LRU eviction as finality advances. - Ok(Availability::Available(Box::new(available_envelope))) - } else { - Ok(Availability::MissingComponents(block_root)) - } - } - - /// Updates or inserts a new `PendingComponents` if it doesn't exist, and then apply the - /// `update_fn` while holding the write lock. - /// - /// Once the update is complete, the write lock is downgraded and a read guard with a - /// reference of the updated `PendingComponents` is returned. - fn update_or_insert_pending_components( - &self, - block_root: Hash256, - update_fn: F, - ) -> Result>, AvailabilityCheckError> - where - F: FnOnce(&mut PendingComponents) -> Result<(), AvailabilityCheckError>, - { - let mut write_lock = self.critical.write(); - - { - let pending_components = write_lock.get_or_insert_mut(block_root, || { - PendingComponents::empty(block_root, self.spec.clone()) - }); - update_fn(pending_components)? - } - - RwLockReadGuard::try_map(RwLockWriteGuard::downgrade(write_lock), |cache| { - cache.peek(&block_root) - }) - .map_err(|_| { - AvailabilityCheckError::Unexpected("pending components should exist".to_string()) - }) - } - - /// Check whether data column reconstruction should be attempted. - /// - /// Potentially trigger reconstruction if all the following satisfy: - /// - Our custody requirement is more than 50% of total columns, - /// - We haven't received all required columns - /// - Reconstruction hasn't been started for the block - /// - /// If reconstruction is required, returns `PendingComponents` which contains the - /// components to be used as inputs to reconstruction, otherwise returns a `reason`. - pub fn check_and_set_reconstruction_started( - &self, - block_root: &Hash256, - ) -> ReconstructColumnsDecision { - let mut write_lock = self.critical.write(); - let Some(pending_components) = write_lock.get_mut(block_root) else { - // Block may have been imported as it does not exist in availability cache. - return ReconstructColumnsDecision::No("block already imported"); - }; - - let Some(epoch) = pending_components - .verified_data_columns - .first() - .map(|c| c.as_data_column().epoch()) - else { - return ReconstructColumnsDecision::No("not enough columns"); - }; - - let total_column_count = T::EthSpec::number_of_columns(); - let sampling_column_count = self - .custody_context - .num_of_data_columns_to_sample(epoch, &self.spec); - let received_column_count = pending_components.verified_data_columns.len(); - - if pending_components.reconstruction_started { - return ReconstructColumnsDecision::No("already started"); - } - if received_column_count >= sampling_column_count { - return ReconstructColumnsDecision::No("all sampling columns received"); - } - if received_column_count < total_column_count / 2 { - return ReconstructColumnsDecision::No("not enough columns"); - } - - pending_components.reconstruction_started = true; - ReconstructColumnsDecision::Yes(pending_components.verified_data_columns.clone()) - } - - /// This could mean some invalid data columns made it through to the `DataAvailabilityChecker`. - /// In this case, we remove all data columns in `PendingComponents`, reset reconstruction - /// status so that we can attempt to retrieve columns from peers again. - pub fn handle_reconstruction_failure(&self, block_root: &Hash256) { - if let Some(pending_components_mut) = self.critical.write().get_mut(block_root) { - pending_components_mut.verified_data_columns = vec![]; - pending_components_mut.reconstruction_started = false; - } - } - - fn get_num_expected_columns(&self, epoch: Epoch) -> usize { - self.custody_context - .num_of_data_columns_to_sample(epoch, &self.spec) - } - - /// maintain the cache - pub fn do_maintenance(&self, cutoff_epoch: Epoch) -> Result<(), AvailabilityCheckError> { - // Collect keys of pending blocks from a previous epoch to cutoff - let mut write_lock = self.critical.write(); - let mut keys_to_remove = vec![]; - for (key, value) in write_lock.iter() { - if let Some(epoch) = value.epoch() - && epoch < cutoff_epoch - { - keys_to_remove.push(*key); - } - } - // Now remove keys - for key in keys_to_remove { - write_lock.pop(&key); - } - - Ok(()) - } - - /// Number of pending component entries in memory in the cache. - pub fn block_cache_size(&self) -> usize { - self.critical.read().len() - } -} - -#[cfg(test)] -mod pending_components_tests { - use crate::test_utils::test_spec; - - use super::*; - use types::MinimalEthSpec; - - type E = MinimalEthSpec; - - #[test] - fn test_empty_pending_components() { - let spec = Arc::new(test_spec::()); - let block_root = Hash256::random(); - let components = PendingComponents::::empty(block_root, spec); - - assert_eq!(components.block_root, block_root); - assert!(components.bid.is_none()); - assert!(components.verified_data_columns.is_empty()); - assert!(!components.reconstruction_started); - assert!(components.epoch().is_none()); - } - - #[test] - fn test_get_cached_data_columns_indices_empty() { - let spec = Arc::new(test_spec::()); - let block_root = Hash256::random(); - let components = PendingComponents::::empty(block_root, spec); - - let indices = components.get_cached_data_columns_indices(); - assert!(indices.is_empty()); - } - - #[test] - fn test_status_str_no_bid() { - let spec = Arc::new(test_spec::()); - let block_root = Hash256::random(); - let components = PendingComponents::::empty(block_root, spec); - - let status = components.status_str(10); - assert_eq!(status, "data_columns 0/10"); - } - - #[test] - fn test_num_blobs_expected_no_bid() { - let spec = Arc::new(test_spec::()); - let block_root = Hash256::random(); - let components = PendingComponents::::empty(block_root, spec); - - let result = components.num_blobs_expected(); - assert!(result.is_err()); - // Error should be AvailabilityCheckError::Unexpected - assert!(matches!( - result.unwrap_err(), - AvailabilityCheckError::Unexpected(_) - )); - } - - #[test] - fn test_make_available_no_bid_returns_none() { - let spec = Arc::new(test_spec::()); - let block_root = Hash256::random(); - let components = PendingComponents::::empty(block_root, spec); - - // Without a bid, make_available should return Ok(None) - let result = components.make_available(10); - assert!(result.is_ok()); - assert!(result.unwrap().is_none()); - } -} - -#[cfg(test)] -mod data_availability_checker_tests { - use super::*; - - use crate::data_column_verification::{KzgVerifiedCustodyDataColumn, KzgVerifiedDataColumn}; - use crate::test_utils::{ - NumBlobs, generate_data_column_indices_rand_order, generate_rand_block_and_data_columns, - test_spec, - }; - use crate::{ - custody_context::NodeCustodyType, - test_utils::{BeaconChainHarness, DiskHarnessType}, - }; - use logging::create_test_tracing_subscriber; - use rand::SeedableRng; - use rand::rngs::StdRng; - use store::{HotColdDB, StoreConfig, database::interface::BeaconNodeBackend}; - use tempfile::{TempDir, tempdir}; - use types::new_non_zero_usize; - use types::{ForkName, MinimalEthSpec, Slot}; - - type E = MinimalEthSpec; - - const LOW_VALIDATOR_COUNT: usize = 32; - - fn gloas_spec() -> Arc { - let mut spec = E::default_spec(); - spec.altair_fork_epoch = Some(Epoch::new(0)); - spec.bellatrix_fork_epoch = Some(Epoch::new(0)); - spec.capella_fork_epoch = Some(Epoch::new(0)); - spec.deneb_fork_epoch = Some(Epoch::new(0)); - spec.electra_fork_epoch = Some(Epoch::new(0)); - spec.fulu_fork_epoch = Some(Epoch::new(0)); - spec.gloas_fork_epoch = Some(Epoch::new(0)); - Arc::new(spec) - } - - fn get_store_with_spec( - db_path: &TempDir, - spec: Arc, - ) -> Arc, BeaconNodeBackend>> { - let hot_path = db_path.path().join("hot_db"); - let cold_path = db_path.path().join("cold_db"); - let blobs_path = db_path.path().join("blobs_db"); - let config = StoreConfig::default(); - - HotColdDB::open( - &hot_path, - &cold_path, - &blobs_path, - |_, _, _| Ok(()), - config, - spec, - ) - .expect("disk store should initialize") - } - - async fn get_gloas_chain( - db_path: &TempDir, - ) -> BeaconChainHarness> { - let spec = gloas_spec::(); - - let chain_store = get_store_with_spec::(db_path, spec.clone()); - let validators_keypairs = - types::test_utils::generate_deterministic_keypairs(LOW_VALIDATOR_COUNT); - BeaconChainHarness::builder(E::default()) - .spec(spec.clone()) - .keypairs(validators_keypairs) - .fresh_disk_store(chain_store) - .mock_execution_layer() - .build() - } - - async fn setup_harness_and_cache( - capacity: usize, - ) -> ( - BeaconChainHarness>, - Arc>, - TempDir, - ) - where - T: BeaconChainTypes< - HotStore = BeaconNodeBackend, - ColdStore = BeaconNodeBackend, - EthSpec = E, - >, - { - create_test_tracing_subscriber(); - let chain_db_path = tempdir().expect("should get temp dir"); - let harness = get_gloas_chain(&chain_db_path).await; - let spec = harness.spec.clone(); - let capacity_non_zero = new_non_zero_usize(capacity); - let custody_context = Arc::new(CustodyContext::new( - NodeCustodyType::Fullnode, - generate_data_column_indices_rand_order::(), - &spec, - )); - let cache = Arc::new( - DataAvailabilityCheckerInner::::new( - capacity_non_zero, - custody_context, - spec.clone(), - ) - .expect("should create cache"), - ); - (harness, cache, chain_db_path) - } - - fn is_gloas_enabled() -> bool { - let spec = test_spec::(); - spec.fork_name_at_slot::(Slot::new(0)).gloas_enabled() - } - - #[tokio::test] - async fn test_cache_creation() { - if !is_gloas_enabled() { - return; - } - - type T = DiskHarnessType; - let capacity = 4; - let (_harness, cache, _path) = setup_harness_and_cache::(capacity).await; - assert_eq!(cache.block_cache_size(), 0); - } - - #[tokio::test] - async fn test_put_columns_creates_pending_components() { - if !is_gloas_enabled() { - return; - } - - type T = DiskHarnessType; - let capacity = 4; - let (harness, cache, _path) = setup_harness_and_cache::(capacity).await; - - let mut rng = StdRng::seed_from_u64(0xDEADBEEF); - let spec = harness.spec.clone(); - - let (_block, data_columns) = generate_rand_block_and_data_columns::( - ForkName::Gloas, - NumBlobs::Number(1), - &mut rng, - &spec, - ); - - let block_root = Hash256::random(); - - let verified_columns: Vec<_> = data_columns - .into_iter() - .take(1) // Just take one column for the test - .map(|col| { - KzgVerifiedCustodyDataColumn::from_asserted_custody( - KzgVerifiedDataColumn::__new_for_testing(col), - ) - }) - .collect(); - - // Put columns into cache - let result = cache.put_kzg_verified_data_columns(block_root, verified_columns); - assert!(result.is_ok()); - - // Check that pending components were created - assert_eq!(cache.block_cache_size(), 1); - - // Verify columns are cached - let cached_indices = cache.peek_pending_components(&block_root, |components| { - components.map(|c| c.get_cached_data_columns_indices()) - }); - assert!(cached_indices.is_some()); - assert_eq!(cached_indices.unwrap().len(), 1); - } - - #[tokio::test] - async fn test_column_deduplication() { - if !is_gloas_enabled() { - return; - } - - type T = DiskHarnessType; - let capacity = 4; - let (harness, cache, _path) = setup_harness_and_cache::(capacity).await; - - let mut rng = StdRng::seed_from_u64(0xDEADBEEF); - let spec = harness.spec.clone(); - - let (_block, data_columns) = generate_rand_block_and_data_columns::( - ForkName::Gloas, - NumBlobs::Number(1), - &mut rng, - &spec, - ); - - let block_root = Hash256::random(); - - // Get the first column - let first_column = data_columns.first().cloned().expect("should have column"); - let column_index = *first_column.index(); - - let verified_column = KzgVerifiedCustodyDataColumn::from_asserted_custody( - KzgVerifiedDataColumn::__new_for_testing(first_column.clone()), - ); - - // Insert the same column twice - cache - .put_kzg_verified_data_columns(block_root, vec![verified_column.clone()]) - .expect("should put column"); - - cache - .put_kzg_verified_data_columns(block_root, vec![verified_column]) - .expect("should put column again"); - - // Check that we still only have one column (deduplicated) - let cached_indices = cache.peek_pending_components(&block_root, |components| { - components.map(|c| c.get_cached_data_columns_indices()) - }); - assert!(cached_indices.is_some()); - let indices = cached_indices.unwrap(); - assert_eq!(indices.len(), 1); - assert_eq!(indices[0], column_index); - } - - #[tokio::test] - async fn test_columns_without_block_not_available() { - if !is_gloas_enabled() { - return; - } - - type T = DiskHarnessType; - let capacity = 4; - let (harness, cache, _path) = setup_harness_and_cache::(capacity).await; - - let mut rng = StdRng::seed_from_u64(0xDEADBEEF); - let spec = harness.spec.clone(); - - let (_block, data_columns) = generate_rand_block_and_data_columns::( - ForkName::Gloas, - NumBlobs::Number(1), - &mut rng, - &spec, - ); - - let block_root = Hash256::random(); - - // Add all columns - let verified_columns: Vec<_> = data_columns - .into_iter() - .map(|col| { - KzgVerifiedCustodyDataColumn::from_asserted_custody( - KzgVerifiedDataColumn::__new_for_testing(col), - ) - }) - .collect(); - - let result = cache - .put_kzg_verified_data_columns(block_root, verified_columns) - .expect("should put columns"); - - // Without a bid, should still be missing components - assert!(matches!(result, Availability::MissingComponents(_))); - } - - #[tokio::test] - async fn test_reconstruction_started_flag() { - if !is_gloas_enabled() { - return; - } - - type T = DiskHarnessType; - let capacity = 4; - let (harness, cache, _path) = setup_harness_and_cache::(capacity).await; - - let mut rng = StdRng::seed_from_u64(0xDEADBEEF); - let spec = harness.spec.clone(); - - let (_block, data_columns) = generate_rand_block_and_data_columns::( - ForkName::Gloas, - NumBlobs::Number(1), - &mut rng, - &spec, - ); - - let block_root = Hash256::random(); - - // Add some columns (not enough for reconstruction threshold) - let verified_columns: Vec<_> = data_columns - .into_iter() - .take(10) // Not enough for reconstruction - .map(|col| { - KzgVerifiedCustodyDataColumn::from_asserted_custody( - KzgVerifiedDataColumn::__new_for_testing(col), - ) - }) - .collect(); - - cache - .put_kzg_verified_data_columns(block_root, verified_columns) - .expect("should put columns"); - - // Check reconstruction decision - should say "not enough columns" - let decision = cache.check_and_set_reconstruction_started(&block_root); - assert!(matches!(decision, ReconstructColumnsDecision::No(_))); - } - - #[tokio::test] - async fn test_handle_reconstruction_failure_clears_columns() { - if !is_gloas_enabled() { - return; - } - - type T = DiskHarnessType; - let capacity = 4; - let (harness, cache, _path) = setup_harness_and_cache::(capacity).await; - - let mut rng = StdRng::seed_from_u64(0xDEADBEEF); - let spec = harness.spec.clone(); - - let (_block, data_columns) = generate_rand_block_and_data_columns::( - ForkName::Gloas, - NumBlobs::Number(1), - &mut rng, - &spec, - ); - - let block_root = Hash256::random(); - - // Add some columns - let verified_columns: Vec<_> = data_columns - .into_iter() - .take(5) - .map(|col| { - KzgVerifiedCustodyDataColumn::from_asserted_custody( - KzgVerifiedDataColumn::__new_for_testing(col), - ) - }) - .collect(); - - cache - .put_kzg_verified_data_columns(block_root, verified_columns) - .expect("should put columns"); - - // Verify columns are cached - let cached_count = cache.peek_pending_components(&block_root, |components| { - components.map(|c| c.verified_data_columns.len()) - }); - assert_eq!(cached_count, Some(5)); - - // Handle reconstruction failure - cache.handle_reconstruction_failure(&block_root); - - // Verify columns are cleared - let cached_count_after = cache.peek_pending_components(&block_root, |components| { - components.map(|c| c.verified_data_columns.len()) - }); - assert_eq!(cached_count_after, Some(0)); - } - - #[tokio::test] - async fn test_maintenance_removes_old_entries() { - if !is_gloas_enabled() { - return; - } - - type T = DiskHarnessType; - let capacity = 4; - let (_harness, cache, _path) = setup_harness_and_cache::(capacity).await; - - let block_root = Hash256::random(); - - // Create an empty entry in the cache - cache.peek_pending_components(&block_root, |_| {}); - - // Manually insert a pending component by putting empty columns - // This will create an entry but it won't have an epoch - // For this test, we need an entry with a known epoch - - // Run maintenance with a future cutoff epoch - let cutoff_epoch = Epoch::new(100); - cache - .do_maintenance(cutoff_epoch) - .expect("maintenance should succeed"); - - // Cache should still be empty since we didn't add anything with an epoch - assert_eq!(cache.block_cache_size(), 0); - } - - #[tokio::test] - async fn test_peek_data_columns() { - if !is_gloas_enabled() { - return; - } - - type T = DiskHarnessType; - let capacity = 4; - let (harness, cache, _path) = setup_harness_and_cache::(capacity).await; - - let mut rng = StdRng::seed_from_u64(0xDEADBEEF); - let spec = harness.spec.clone(); - - let (_block, data_columns) = generate_rand_block_and_data_columns::( - ForkName::Gloas, - NumBlobs::Number(1), - &mut rng, - &spec, - ); - - let block_root = Hash256::random(); - - // No columns yet - assert!(cache.peek_data_columns(block_root).is_none()); - - // Add columns - let verified_columns: Vec<_> = data_columns - .into_iter() - .take(3) - .map(|col| { - KzgVerifiedCustodyDataColumn::from_asserted_custody( - KzgVerifiedDataColumn::__new_for_testing(col), - ) - }) - .collect(); - - cache - .put_kzg_verified_data_columns(block_root, verified_columns) - .expect("should put columns"); - - // Now columns should be returned - let peeked = cache.peek_data_columns(block_root); - assert!(peeked.is_some()); - assert_eq!(peeked.unwrap().len(), 3); - } -} From 285b7ebae910cc72af8427dc68d7ac0b58f68ffe Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Thu, 19 Mar 2026 08:28:45 -0700 Subject: [PATCH 034/118] Refactor --- beacon_node/beacon_chain/src/beacon_chain.rs | 3 +- .../beacon_chain/src/block_verification.rs | 1 - beacon_node/beacon_chain/src/builder.rs | 1 - .../src/data_availability_checker_v2/mod.rs | 625 ++++++++++++++++-- .../pending_components.rs | 9 +- .../src/data_availability_router.rs | 43 +- beacon_node/beacon_chain/src/metrics.rs | 11 +- .../payload_envelope_verification/import.rs | 2 +- beacon_node/client/src/builder.rs | 7 +- common/eth2/src/types.rs | 5 - 10 files changed, 628 insertions(+), 79 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 1250252434..8f2c7caaa7 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -22,7 +22,6 @@ use crate::data_availability_checker::{ Availability as BlockAvailability, AvailabilityCheckError, AvailableBlock, AvailableBlockData, DataColumnReconstructionResult, }; -use crate::data_availability_checker_v2::Availability as PayloadAvailability; use crate::data_availability_router::{ AvailabilityOutcome, DataAvailabilityRouter, ReconstructionOutcome, }; @@ -3799,7 +3798,7 @@ impl BeaconChain { } } AvailabilityOutcome::Payload(_) => { - return Err(BlockError::InternalError("Received a payload envelope availability outcome variant when a block variant was expected".to_string())) + Err(BlockError::InternalError("Received a payload envelope availability outcome variant when a block variant was expected".to_string())) }, } } diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index de817e35fb..244b06f475 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -60,7 +60,6 @@ use crate::execution_payload::{ }; use crate::kzg_utils::blobs_to_data_column_sidecars; use crate::observed_block_producers::SeenBlock; -use crate::payload_envelope_verification::EnvelopeError; use crate::validator_monitor::HISTORIC_EPOCHS as VALIDATOR_MONITOR_HISTORIC_EPOCHS; use crate::validator_pubkey_cache::ValidatorPubkeyCache; use crate::{ diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index 5a97bea063..a60cc614e8 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -994,7 +994,6 @@ where let da_checker_v2 = Arc::new( DataAvailabilityCheckerV2::new( - slot_clock.clone(), self.kzg.clone(), custody_context.clone(), self.spec.clone(), diff --git a/beacon_node/beacon_chain/src/data_availability_checker_v2/mod.rs b/beacon_node/beacon_chain/src/data_availability_checker_v2/mod.rs index fe03f2d0f4..87d0dfdd5e 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker_v2/mod.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker_v2/mod.rs @@ -1,3 +1,42 @@ +//! This module builds out the data availability cache for Gloas. When a beacon block is received +//! over gossip/p2p we insert its payload into this cache, keyed by block root. As soon as the bid +//! is received we can begin using it to verify data columns. +//! +//! When a payload envelope is received over gossip/p2p we first insert it as a pre-executed envelope. A separate +//! thread eventually executes the payload envelope against the EL. Assuming the payload is executed succesfully +//! the envelope is updated in the cache from `PreExecuted` -> `Executed`. Once all required custody columns +//! have been kzg verified and the envelope has been executed we can import the envelope into fork choice and store it to disk. +//! +//! Note that the block must have arrived before the envelope for the envelope to pass upstream verification checks and reach this cache. +//! However data columns can potentially arrive before the block. +//! +//! +//! SignedBeaconBlock +//! | +//! | -> SignedExecutionPayloadBid +//! +//! +//! DataColumnSidecarList +//! | +//! | -> Perform data column verification against `SignedExecutionPayloadBid` +//! │ │ +//! │ ▼ +//! | -> KzgVerifiedCustodyDataColumn +//! +//! +//! SignedExecutionPayloadEnvelope +//! │ +//! | -> CachedPayloadEnvelope::PreExecution +//! │ │ +//! │ ▼ +//! | -> AvailabilityPendingExecutedEnvelope +//! │ │ +//! │ ▼ +//! │ -> CachedPayloadEnvelope::Executed +//! │ │ +//! │ ▼ +//! | -> AvailableExecutedEnvelope (all columns present, payload executed against the EL, ready to import) + use crate::data_availability_checker::AvailabilityCheckError; use crate::payload_envelope_verification::{ AvailabilityPendingExecutedEnvelope, AvailableExecutedEnvelope, @@ -66,7 +105,9 @@ impl Debug for Availability { write!(f, "MissingComponents({})", block_root) } // TODO(gloas) fix success case - Self::Available(data) => todo!(), + Self::Available(envelope) => { + write!(f, "Available({:?})", envelope.import_data.block_root) + } } } } @@ -99,7 +140,6 @@ pub struct DataAvailabilityChecker { impl DataAvailabilityChecker { pub fn new( - _slot_clock: T::SlotClock, kzg: Arc, custody_context: Arc>, spec: Arc, @@ -174,7 +214,6 @@ impl DataAvailabilityChecker { }) } - /// Insert an executed payload envelope into the cache and performs an availability check pub fn put_executed_payload_envelope( &self, @@ -416,16 +455,19 @@ impl DataAvailabilityChecker { "Reconstructed columns" ); - self.put_kzg_verified_custody_data_columns(*block_root, data_columns_to_import_and_publish.clone()) - .map(|availability| { - DataColumnReconstructionResult::Success(( - availability, - data_columns_to_import_and_publish - .into_iter() - .map(|d| d.clone_arc()) - .collect::>(), - )) - }) + self.put_kzg_verified_custody_data_columns( + *block_root, + data_columns_to_import_and_publish.clone(), + ) + .map(|availability| { + DataColumnReconstructionResult::Success(( + availability, + data_columns_to_import_and_publish + .into_iter() + .map(|d| d.clone_arc()) + .collect::>(), + )) + }) } // ── Metrics ── @@ -664,7 +706,9 @@ async fn availability_cache_maintenance_service( mod data_availability_checker_tests { use super::*; + use crate::block_verification::PayloadVerificationOutcome; use crate::data_column_verification::{KzgVerifiedCustodyDataColumn, KzgVerifiedDataColumn}; + use crate::payload_envelope_verification::EnvelopeImportData; use crate::test_utils::{ NumBlobs, generate_data_column_indices_rand_order, generate_rand_block_and_data_columns, test_spec, @@ -673,12 +717,16 @@ mod data_availability_checker_tests { custody_context::NodeCustodyType, test_utils::{BeaconChainHarness, DiskHarnessType}, }; + use fork_choice::PayloadVerificationStatus; use logging::create_test_tracing_subscriber; use rand::SeedableRng; use rand::rngs::StdRng; use store::{HotColdDB, StoreConfig, database::interface::BeaconNodeBackend}; use tempfile::{TempDir, tempdir}; - use types::{ForkName, MinimalEthSpec, Slot}; + use types::{ + BeaconState, ExecutionPayloadEnvelope, ExecutionPayloadGloas, ExecutionRequests, ForkName, + FullPayload, MinimalEthSpec, SignedBeaconBlock, Slot, + }; type E = MinimalEthSpec; @@ -732,9 +780,7 @@ mod data_availability_checker_tests { .build() } - async fn setup_harness_and_cache( - capacity: usize, - ) -> ( + async fn setup_harness_and_cache() -> ( BeaconChainHarness>, Arc>, TempDir, @@ -756,17 +802,15 @@ mod data_availability_checker_tests { &spec, )); - todo!() - // let cache = Arc::new( - // DataAvailabilityChecker::::new( - // harness.chain.slot_clock.clone().into(), - // harness.chain.kzg.clone().unwrap(), - // custody_context, - // spec.clone(), - // ) - // .expect("should create cache"), - // ); - // (harness, cache, chain_db_path) + let cache = Arc::new( + DataAvailabilityChecker::::new( + harness.chain.kzg.clone(), + custody_context, + spec.clone(), + ) + .expect("should create cache"), + ); + (harness, cache, chain_db_path) } fn is_gloas_enabled() -> bool { @@ -781,8 +825,7 @@ mod data_availability_checker_tests { } type T = DiskHarnessType; - let capacity = 4; - let (_harness, cache, _path) = setup_harness_and_cache::(capacity).await; + let (_harness, cache, _path) = setup_harness_and_cache::().await; assert_eq!(cache.block_cache_size(), 0); } @@ -793,8 +836,7 @@ mod data_availability_checker_tests { } type T = DiskHarnessType; - let capacity = 4; - let (harness, cache, _path) = setup_harness_and_cache::(capacity).await; + let (harness, cache, _path) = setup_harness_and_cache::().await; let mut rng = StdRng::seed_from_u64(0xDEADBEEF); let spec = harness.spec.clone(); @@ -840,8 +882,7 @@ mod data_availability_checker_tests { } type T = DiskHarnessType; - let capacity = 4; - let (harness, cache, _path) = setup_harness_and_cache::(capacity).await; + let (harness, cache, _path) = setup_harness_and_cache::().await; let mut rng = StdRng::seed_from_u64(0xDEADBEEF); let spec = harness.spec.clone(); @@ -889,8 +930,7 @@ mod data_availability_checker_tests { } type T = DiskHarnessType; - let capacity = 4; - let (harness, cache, _path) = setup_harness_and_cache::(capacity).await; + let (harness, cache, _path) = setup_harness_and_cache::().await; let mut rng = StdRng::seed_from_u64(0xDEADBEEF); let spec = harness.spec.clone(); @@ -922,6 +962,319 @@ mod data_availability_checker_tests { assert!(matches!(result, Availability::MissingComponents(_))); } + /// Helper to create a test bid with the given block root and kzg commitments from a block. + fn make_test_bid( + block: &SignedBeaconBlock>, + ) -> Arc> { + let bid = block + .message() + .body() + .signed_execution_payload_bid() + .expect("gloas block should have bid") + .clone(); + Arc::new(bid) + } + + fn make_test_signed_envelope(block_root: Hash256) -> Arc> { + Arc::new(SignedExecutionPayloadEnvelope { + message: ExecutionPayloadEnvelope { + payload: ExecutionPayloadGloas::default(), + execution_requests: ExecutionRequests::default(), + builder_index: 0, + beacon_block_root: block_root, + slot: Slot::new(0), + state_root: Hash256::ZERO, + }, + signature: bls::Signature::infinity().expect("should create infinity sig"), + }) + } + + fn make_test_executed_envelope(block_root: Hash256) -> AvailabilityPendingExecutedEnvelope { + AvailabilityPendingExecutedEnvelope { + envelope: make_test_signed_envelope(block_root), + import_data: EnvelopeImportData { + block_root, + post_state: Box::new(BeaconState::new(0, Default::default(), &gloas_spec::())), + }, + payload_verification_outcome: PayloadVerificationOutcome { + payload_verification_status: PayloadVerificationStatus::Verified, + }, + } + } + + #[tokio::test] + async fn test_full_availability_flow() { + if !is_gloas_enabled() { + return; + } + + type T = DiskHarnessType; + let (harness, cache, _path) = setup_harness_and_cache::().await; + + let mut rng = StdRng::seed_from_u64(0xDEADBEEF); + let spec = harness.spec.clone(); + + let (block, data_columns) = generate_rand_block_and_data_columns::( + ForkName::Gloas, + NumBlobs::Number(1), + &mut rng, + &spec, + ); + + let block_root = Hash256::random(); + let bid = make_test_bid(&block); + + cache.put_bid(block_root, bid).expect("should put bid"); + assert!(matches!( + cache.put_bid(block_root, make_test_bid(&block)), + Ok(Availability::MissingComponents(_)) + )); + + let verified_columns: Vec<_> = data_columns + .into_iter() + .map(|col| { + KzgVerifiedCustodyDataColumn::from_asserted_custody( + KzgVerifiedDataColumn::__new_for_testing(col), + ) + }) + .collect(); + + let result = cache + .put_kzg_verified_custody_data_columns(block_root, verified_columns) + .expect("should put columns"); + + assert!(matches!(result, Availability::MissingComponents(_))); + + // Insert pre-executed envelope first + cache + .put_pre_executed_payload_envelope( + make_test_signed_envelope(block_root), + BlockImportSource::Gossip, + ) + .expect("should put pre-executed envelope"); + + let status = cache.get_envelope_processing_status(&block_root); + assert!(matches!( + status, + Some(PayloadEnvelopeProcessingStatus::NotValidated(..)) + )); + + // Upgrade to executed envelope (after EL validation) + let executed_envelope = make_test_executed_envelope(block_root); + let result = cache + .put_executed_payload_envelope(executed_envelope) + .expect("should put executed envelope"); + + assert!( + matches!(result, Availability::Available(_)), + "expected Available, got {:?}", + result + ); + } + + #[tokio::test] + async fn test_zero_blob_bid_immediately_available() { + if !is_gloas_enabled() { + return; + } + + type T = DiskHarnessType; + let (harness, cache, _path) = setup_harness_and_cache::().await; + + let mut rng = StdRng::seed_from_u64(0xDEADBEEF); + let spec = harness.spec.clone(); + + // Generate a block with 0 blobs — bid will have empty commitments + let (block, _data_columns) = generate_rand_block_and_data_columns::( + ForkName::Gloas, + NumBlobs::Number(0), + &mut rng, + &spec, + ); + + let block_root = Hash256::random(); + let bid = make_test_bid(&block); + + // Insert bid (no blobs expected) + cache.put_bid(block_root, bid).expect("should put bid"); + + // Insert executed envelope — should become available immediately (no columns needed) + let executed_envelope = make_test_executed_envelope(block_root); + let result = cache + .put_executed_payload_envelope(executed_envelope) + .expect("should put executed envelope"); + + assert!( + matches!(result, Availability::Available(_)), + "zero-blob bid should be immediately available, got {:?}", + result + ); + } + + #[tokio::test] + async fn test_columns_arrive_before_bid() { + if !is_gloas_enabled() { + return; + } + + type T = DiskHarnessType; + let (harness, cache, _path) = setup_harness_and_cache::().await; + + let mut rng = StdRng::seed_from_u64(0xDEADBEEF); + let spec = harness.spec.clone(); + + let (block, data_columns) = generate_rand_block_and_data_columns::( + ForkName::Gloas, + NumBlobs::Number(1), + &mut rng, + &spec, + ); + + let block_root = Hash256::random(); + + // Columns arrive before bid + let verified_columns: Vec<_> = data_columns + .into_iter() + .map(|col| { + KzgVerifiedCustodyDataColumn::from_asserted_custody( + KzgVerifiedDataColumn::__new_for_testing(col), + ) + }) + .collect(); + + let result = cache + .put_kzg_verified_custody_data_columns(block_root, verified_columns) + .expect("should put columns"); + assert!(matches!(result, Availability::MissingComponents(_))); + + let bid = make_test_bid(&block); + let result = cache.put_bid(block_root, bid).expect("should put bid"); + assert!(matches!(result, Availability::MissingComponents(_))); + + let executed_envelope = make_test_executed_envelope(block_root); + let result = cache + .put_executed_payload_envelope(executed_envelope) + .expect("should put executed envelope"); + + assert!( + matches!(result, Availability::Available(_)), + "expected Available after all components inserted, got {:?}", + result + ); + } + + #[tokio::test] + async fn test_pre_executed_envelope_not_available() { + if !is_gloas_enabled() { + return; + } + + type T = DiskHarnessType; + let (harness, cache, _path) = setup_harness_and_cache::().await; + + let mut rng = StdRng::seed_from_u64(0xDEADBEEF); + let spec = harness.spec.clone(); + + let (block, data_columns) = generate_rand_block_and_data_columns::( + ForkName::Gloas, + NumBlobs::Number(1), + &mut rng, + &spec, + ); + + let block_root = Hash256::random(); + + // Insert bid + all columns + cache + .put_bid(block_root, make_test_bid(&block)) + .expect("should put bid"); + + let verified_columns: Vec<_> = data_columns + .into_iter() + .map(|col| { + KzgVerifiedCustodyDataColumn::from_asserted_custody( + KzgVerifiedDataColumn::__new_for_testing(col), + ) + }) + .collect(); + cache + .put_kzg_verified_custody_data_columns(block_root, verified_columns) + .expect("should put columns"); + + // Insert pre-executed envelope (not yet validated by EL) + cache + .put_pre_executed_payload_envelope( + make_test_signed_envelope(block_root), + BlockImportSource::Gossip, + ) + .expect("should put pre-executed envelope"); + + // Should NOT be available — envelope not executed yet + let status = cache.get_envelope_processing_status(&block_root); + assert!(matches!( + status, + Some(PayloadEnvelopeProcessingStatus::NotValidated(..)) + )); + } + + #[tokio::test] + async fn test_remove_pre_executed_envelope() { + if !is_gloas_enabled() { + return; + } + + type T = DiskHarnessType; + let (_harness, cache, _path) = setup_harness_and_cache::().await; + + let block_root = Hash256::random(); + + // Insert pre-executed envelope + cache + .put_pre_executed_payload_envelope( + make_test_signed_envelope(block_root), + BlockImportSource::Gossip, + ) + .expect("should put pre-executed envelope"); + + // Verify it's there + assert!(cache.get_envelope_processing_status(&block_root).is_some()); + + // Remove it + cache.remove_pre_executed_payload_envelope(&block_root); + + // Should be gone + let status = cache.get_envelope_processing_status(&block_root); + assert!(status.is_none()); + } + + #[tokio::test] + async fn test_remove_pre_executed_does_not_remove_executed() { + if !is_gloas_enabled() { + return; + } + + type T = DiskHarnessType; + let (_harness, cache, _path) = setup_harness_and_cache::().await; + + let block_root = Hash256::random(); + + // Insert executed envelope + let executed_envelope = make_test_executed_envelope(block_root); + cache + .put_executed_payload_envelope(executed_envelope) + .expect("should put executed envelope"); + + // Try to remove as pre-executed — should be a no-op + cache.remove_pre_executed_payload_envelope(&block_root); + + // Should still be there as executed + let status = cache.get_envelope_processing_status(&block_root); + assert!(matches!( + status, + Some(PayloadEnvelopeProcessingStatus::ExecutionValidated(..)) + )); + } + #[tokio::test] async fn test_reconstruction_started_flag() { if !is_gloas_enabled() { @@ -929,8 +1282,7 @@ mod data_availability_checker_tests { } type T = DiskHarnessType; - let capacity = 4; - let (harness, cache, _path) = setup_harness_and_cache::(capacity).await; + let (harness, cache, _path) = setup_harness_and_cache::().await; let mut rng = StdRng::seed_from_u64(0xDEADBEEF); let spec = harness.spec.clone(); @@ -971,8 +1323,7 @@ mod data_availability_checker_tests { } type T = DiskHarnessType; - let capacity = 4; - let (harness, cache, _path) = setup_harness_and_cache::(capacity).await; + let (harness, cache, _path) = setup_harness_and_cache::().await; let mut rng = StdRng::seed_from_u64(0xDEADBEEF); let spec = harness.spec.clone(); @@ -1024,10 +1375,7 @@ mod data_availability_checker_tests { } type T = DiskHarnessType; - let capacity = 4; - let (_harness, cache, _path) = setup_harness_and_cache::(capacity).await; - - let block_root = Hash256::random(); + let (_harness, cache, _path) = setup_harness_and_cache::().await; // Run maintenance with a future cutoff epoch let cutoff_epoch = Epoch::new(100); @@ -1046,8 +1394,7 @@ mod data_availability_checker_tests { } type T = DiskHarnessType; - let capacity = 4; - let (harness, cache, _path) = setup_harness_and_cache::(capacity).await; + let (harness, cache, _path) = setup_harness_and_cache::().await; let mut rng = StdRng::seed_from_u64(0xDEADBEEF); let spec = harness.spec.clone(); @@ -1084,4 +1431,190 @@ mod data_availability_checker_tests { assert!(peeked.is_some()); assert_eq!(peeked.unwrap().len(), 3); } + + #[tokio::test] + async fn test_lru_eviction() { + if !is_gloas_enabled() { + return; + } + + type T = DiskHarnessType; + let (harness, cache, _path) = setup_harness_and_cache::().await; + + let mut rng = StdRng::seed_from_u64(0xDEADBEEF); + let spec = harness.spec.clone(); + + let (_block, data_columns) = generate_rand_block_and_data_columns::( + ForkName::Gloas, + NumBlobs::Number(1), + &mut rng, + &spec, + ); + + // LRU capacity is 32 (OVERFLOW_LRU_CAPACITY_NON_ZERO). Insert 33 entries. + let mut roots = Vec::new(); + for _ in 0..33 { + let block_root = Hash256::random(); + roots.push(block_root); + let col = data_columns.first().cloned().expect("should have column"); + let verified = vec![KzgVerifiedCustodyDataColumn::from_asserted_custody( + KzgVerifiedDataColumn::__new_for_testing(col), + )]; + cache + .put_kzg_verified_custody_data_columns(block_root, verified) + .expect("should put columns"); + } + + assert_eq!(cache.block_cache_size(), 32); + assert!(cache.get_data_columns(roots[0]).is_none()); + assert!(cache.get_data_columns(*roots.last().unwrap()).is_some()); + } + + #[tokio::test] + async fn test_maintenance_prunes_old_entries() { + if !is_gloas_enabled() { + return; + } + + type T = DiskHarnessType; + let (harness, cache, _path) = setup_harness_and_cache::().await; + + let mut rng = StdRng::seed_from_u64(0xDEADBEEF); + let spec = harness.spec.clone(); + + let (block, data_columns) = generate_rand_block_and_data_columns::( + ForkName::Gloas, + NumBlobs::Number(1), + &mut rng, + &spec, + ); + + let block_root = Hash256::random(); + + // Insert bid (gives the entry an epoch via the bid's slot) + cache + .put_bid(block_root, make_test_bid(&block)) + .expect("should put bid"); + + let col = data_columns.first().cloned().expect("should have column"); + let verified = vec![KzgVerifiedCustodyDataColumn::from_asserted_custody( + KzgVerifiedDataColumn::__new_for_testing(col), + )]; + cache + .put_kzg_verified_custody_data_columns(block_root, verified) + .expect("should put columns"); + + assert_eq!(cache.block_cache_size(), 1); + + // Maintenance with cutoff in the future should prune (bid slot=0 → epoch=0 < cutoff=100) + cache + .do_maintenance(Epoch::new(100)) + .expect("maintenance should succeed"); + + assert_eq!(cache.block_cache_size(), 0); + } + + #[tokio::test] + async fn test_double_reconstruction_prevented() { + if !is_gloas_enabled() { + return; + } + + type T = DiskHarnessType; + let (harness, cache, _path) = setup_harness_and_cache::().await; + + let mut rng = StdRng::seed_from_u64(0xDEADBEEF); + let spec = harness.spec.clone(); + + let (_block, data_columns) = generate_rand_block_and_data_columns::( + ForkName::Gloas, + NumBlobs::Number(1), + &mut rng, + &spec, + ); + + let block_root = Hash256::random(); + + // Insert all columns so reconstruction threshold is met + let verified_columns: Vec<_> = data_columns + .into_iter() + .map(|col| { + KzgVerifiedCustodyDataColumn::from_asserted_custody( + KzgVerifiedDataColumn::__new_for_testing(col), + ) + }) + .collect(); + + cache + .put_kzg_verified_custody_data_columns(block_root, verified_columns) + .expect("should put columns"); + + // Manually set reconstruction_started via check_and_set + // For fullnode, sampling == all columns, so this returns No("all sampling columns received") + // But we can set the flag manually to test the guard + cache + .availability_cache + .write() + .get_mut(&block_root) + .expect("should exist") + .reconstruction_started = true; + + let decision = cache.check_and_set_reconstruction_started(&block_root); + assert!( + matches!(decision, ReconstructColumnsDecision::No(reason) if reason == "already started"), + "second reconstruction attempt should be prevented" + ); + } + + #[tokio::test] + async fn test_partial_columns_missing_components() { + if !is_gloas_enabled() { + return; + } + + type T = DiskHarnessType; + let (harness, cache, _path) = setup_harness_and_cache::().await; + + let mut rng = StdRng::seed_from_u64(0xDEADBEEF); + let spec = harness.spec.clone(); + + let (block, data_columns) = generate_rand_block_and_data_columns::( + ForkName::Gloas, + NumBlobs::Number(1), + &mut rng, + &spec, + ); + + let block_root = Hash256::random(); + + // Insert bid and executed envelope + cache + .put_bid(block_root, make_test_bid(&block)) + .expect("should put bid"); + + let executed_envelope = make_test_executed_envelope(block_root); + cache + .put_executed_payload_envelope(executed_envelope) + .expect("should put executed envelope"); + + // Insert only 1 column (need 128 for fullnode) + let verified_columns: Vec<_> = data_columns + .into_iter() + .take(1) + .map(|col| { + KzgVerifiedCustodyDataColumn::from_asserted_custody( + KzgVerifiedDataColumn::__new_for_testing(col), + ) + }) + .collect(); + + let result = cache + .put_kzg_verified_custody_data_columns(block_root, verified_columns) + .expect("should put columns"); + + assert!( + matches!(result, Availability::MissingComponents(_)), + "partial columns should not trigger availability" + ); + } } diff --git a/beacon_node/beacon_chain/src/data_availability_checker_v2/pending_components.rs b/beacon_node/beacon_chain/src/data_availability_checker_v2/pending_components.rs index f6d4cc0321..3f9d7e54d0 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker_v2/pending_components.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker_v2/pending_components.rs @@ -8,8 +8,8 @@ use std::sync::Arc; use tracing::{Span, debug, debug_span}; use types::BlockImportSource; use types::{ - ChainSpec, ColumnIndex, DataColumnSidecar, Epoch, EthSpec, Hash256, - SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope, + ChainSpec, ColumnIndex, DataColumnSidecar, Epoch, EthSpec, Hash256, SignedExecutionPayloadBid, + SignedExecutionPayloadEnvelope, }; pub enum CachedPayloadEnvelope { @@ -22,9 +22,6 @@ pub enum CachedPayloadEnvelope { /// The columns are all gossip and kzg verified. /// The payload is considered "available" when all required columns are received. pub struct PendingComponents { - /// The block root is stored for tracing context in the span. - #[allow(dead_code)] - pub block_root: Hash256, /// The execution payload bid containing blob_kzg_commitments. pub bid: Option>>, /// a cached pre or post executed payload envelope @@ -182,7 +179,6 @@ impl PendingComponents { let span = debug_span!(parent: None, "lh_pending_components", %block_root); let _guard = span.clone().entered(); Self { - block_root, bid: None, envelope: None, verified_data_columns: vec![], @@ -240,7 +236,6 @@ mod pending_components_tests { let block_root = Hash256::random(); let components = PendingComponents::::empty(block_root, spec); - assert_eq!(components.block_root, block_root); assert!(components.bid.is_none()); assert!(components.verified_data_columns.is_empty()); assert!(!components.reconstruction_started); diff --git a/beacon_node/beacon_chain/src/data_availability_router.rs b/beacon_node/beacon_chain/src/data_availability_router.rs index 78de0d8935..656fce22ff 100644 --- a/beacon_node/beacon_chain/src/data_availability_router.rs +++ b/beacon_node/beacon_chain/src/data_availability_router.rs @@ -20,10 +20,12 @@ use crate::block_verification_types::AvailabilityPendingExecutedBlock; use crate::custody_context::CustodyContext; use crate::data_availability_checker::{ Availability as BlockAvailability, AvailabilityCheckError, AvailableBlock, - DataAvailabilityChecker, DataColumnReconstructionResult as BlockReconstructionResult, + DataAvailabilityChecker, DataAvailabilityCheckerMetrics as BlockMetrics, + DataColumnReconstructionResult as BlockReconstructionResult, }; use crate::data_availability_checker_v2::{ Availability as PayloadAvailability, DataAvailabilityChecker as DataAvailabilityCheckerV2, + DataAvailabilityCheckerMetrics as PayloadMetrics, DataColumnReconstructionResult as PayloadReconstructionResult, }; use crate::data_column_verification::{GossipVerifiedDataColumn, KzgVerifiedCustodyDataColumn}; @@ -387,17 +389,44 @@ impl DataAvailabilityRouter { self.v1.put_gossip_verified_blobs(block_root, blobs) } - /// Direct access to v1 checker for block execution/availability checks. - /// - /// Use this for operations that are specific to the legacy DA checker, + // ── Metrics ── + + pub fn metrics(&self) -> DataAvailabilityRouterMetrics { + DataAvailabilityRouterMetrics { + block: self.v1.metrics(), + payload: self.v2.metrics(), + } + } + + // ── Direct access ── + + /// Direct access to the block-level DA checker (pre-Gloas). + /// Used for block availability checks, range sync, and blob verification. pub fn v1(&self) -> &Arc> { &self.v1 } - /// Direct access to v2 checker for payload availability checks. - /// - /// Use this for operations that are specific to the Gloas DA checker, + /// Direct access to the envelope-level DA checker (Gloas). + /// Used for payload envelope availability checks and column verification. pub fn v2(&self) -> &Arc> { &self.v2 } } + +pub struct DataAvailabilityRouterMetrics { + pub block: BlockMetrics, + pub payload: PayloadMetrics, +} + +pub fn start_availability_cache_maintenance_service( + executor: task_executor::TaskExecutor, + chain: Arc>, +) { + crate::data_availability_checker::start_availability_cache_maintenance_service( + executor.clone(), + chain.clone(), + ); + crate::data_availability_checker_v2::start_availability_cache_maintenance_service( + executor, chain, + ); +} diff --git a/beacon_node/beacon_chain/src/metrics.rs b/beacon_node/beacon_chain/src/metrics.rs index 89421637a9..a6d9fef59c 100644 --- a/beacon_node/beacon_chain/src/metrics.rs +++ b/beacon_node/beacon_chain/src/metrics.rs @@ -1897,6 +1897,12 @@ pub static DATA_AVAILABILITY_OVERFLOW_MEMORY_BLOCK_CACHE_SIZE: LazyLock> = LazyLock::new(|| { + try_create_int_gauge( + "data_availability_payload_cache_size", + "Number of entries in the data availability payload envelope cache.", + ) +}); pub static DATA_AVAILABILITY_RECONSTRUCTION_TIME: LazyLock> = LazyLock::new(|| { try_create_histogram( @@ -1999,12 +2005,11 @@ pub fn scrape_for_metrics(beacon_chain: &BeaconChain) { beacon_chain.store.state_cache_len(), ); - // TODO(gloas) configure v2 metrics - let da_checker_metrics = beacon_chain.data_availability_checker.v1().metrics(); + let da_checker_metrics = beacon_chain.data_availability_checker.metrics(); set_gauge_by_usize( &DATA_AVAILABILITY_OVERFLOW_MEMORY_BLOCK_CACHE_SIZE, - da_checker_metrics.block_cache_size, + da_checker_metrics.block.block_cache_size, ); if let Some((size, num_lookups)) = beacon_chain.pre_finalization_block_cache.metrics() { 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 4f660362c6..84fffb9d3b 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs @@ -173,7 +173,7 @@ impl BeaconChain { ) -> Result { match availability { AvailabilityOutcome::Block(_) => { - return Err(EnvelopeError::InternalError("Received a block availability outcome variant when a payload envelope variant was expected".to_string())) + Err(EnvelopeError::InternalError("Received a block availability outcome variant when a payload envelope variant was expected".to_string())) } AvailabilityOutcome::Payload(availability) => match availability { PayloadAvailability::Available(available_envelope) => { diff --git a/beacon_node/client/src/builder.rs b/beacon_node/client/src/builder.rs index 75d0455ac3..a3ab6f80d4 100644 --- a/beacon_node/client/src/builder.rs +++ b/beacon_node/client/src/builder.rs @@ -5,8 +5,7 @@ use crate::compute_light_client_updates::{ use crate::config::{ClientGenesis, Config as ClientConfig}; use crate::notifier::spawn_notifier; use beacon_chain::attestation_simulator::start_attestation_simulator_service; -use beacon_chain::data_availability_checker::start_availability_cache_maintenance_service; -use beacon_chain::data_availability_checker_v2::start_availability_cache_maintenance_service as start_availability_cache_maintenance_service_v2; +use beacon_chain::data_availability_router::start_availability_cache_maintenance_service; use beacon_chain::graffiti_calculator::start_engine_version_cache_refresh_service; use beacon_chain::proposer_prep_service::start_proposer_prep_service; use beacon_chain::schema_change::migrate_schema; @@ -787,10 +786,6 @@ where runtime_context.executor.clone(), beacon_chain.clone(), ); - start_availability_cache_maintenance_service_v2( - runtime_context.executor.clone(), - beacon_chain.clone(), - ); start_engine_version_cache_refresh_service( beacon_chain.as_ref(), runtime_context.executor.clone(), diff --git a/common/eth2/src/types.rs b/common/eth2/src/types.rs index 4977332ccc..94dff95bc6 100644 --- a/common/eth2/src/types.rs +++ b/common/eth2/src/types.rs @@ -1867,11 +1867,6 @@ pub type SignedBlockContentsTuple = ( Option<(KzgProofs, BlobsList)>, ); -pub type SignedPayloadEnvelopeContentsTuple = ( - Arc>, - Option<(KzgProofs, BlobsList)>, -); - fn parse_required_header( headers: &HeaderMap, header_name: &str, From dacc2f0e7e8745d3ce772606310a0add3ad92807 Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Thu, 19 Mar 2026 08:59:24 -0700 Subject: [PATCH 035/118] cleanup --- beacon_node/beacon_chain/src/beacon_chain.rs | 45 ++++++++++++++++--- .../src/data_availability_checker_v2/mod.rs | 4 ++ .../src/data_availability_router.rs | 10 ++--- beacon_node/beacon_chain/src/metrics.rs | 6 --- 4 files changed, 47 insertions(+), 18 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 8f2c7caaa7..7a93760366 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -20,8 +20,10 @@ use crate::chain_config::ChainConfig; use crate::custody_context::CustodyContextSsz; use crate::data_availability_checker::{ Availability as BlockAvailability, AvailabilityCheckError, AvailableBlock, AvailableBlockData, - DataColumnReconstructionResult, + DataColumnReconstructionResult as DataColumnReconstructionResultV1, }; + +use crate::data_availability_checker_v2::DataColumnReconstructionResult as DataColumnReconstructionResultV2; use crate::data_availability_router::{ AvailabilityOutcome, DataAvailabilityRouter, ReconstructionOutcome, }; @@ -3337,7 +3339,7 @@ impl BeaconChain { match result { ReconstructionOutcome::Block(data_column_reconstruction_result) => { match data_column_reconstruction_result { - DataColumnReconstructionResult::Success(( + DataColumnReconstructionResultV1::Success(( availability, data_columns_to_publish, )) => { @@ -3356,8 +3358,8 @@ impl BeaconChain { Some((availability_processing_status, data_columns_to_publish)) }) } - DataColumnReconstructionResult::NotStarted(reason) - | DataColumnReconstructionResult::RecoveredColumnsNotImported(reason) => { + DataColumnReconstructionResultV1::NotStarted(reason) + | DataColumnReconstructionResultV1::RecoveredColumnsNotImported(reason) => { // We use metric here because logging this would be *very* noisy. metrics::inc_counter_vec( &metrics::KZG_DATA_COLUMN_RECONSTRUCTION_INCOMPLETE_TOTAL, @@ -3368,9 +3370,38 @@ impl BeaconChain { } } // TODO(gloas) handle data column reconstruction for gloas. - ReconstructionOutcome::Payload(_data_column_reconstruction_result) => Err( - BlockError::InternalError("Not yet implemented for gloas".to_owned()), - ), + ReconstructionOutcome::Payload(data_column_reconstruction_result) => { + match data_column_reconstruction_result { + DataColumnReconstructionResultV2::Success(( + availability, + data_columns_to_publish, + )) => { + let Some(slot) = data_columns_to_publish.first().map(|d| d.slot()) else { + // This should be unreachable because empty result would return `RecoveredColumnsNotImported` instead of success. + return Ok(None); + }; + + self.process_availability( + slot, + AvailabilityOutcome::Payload(availability), + || Ok(()), + ) + .await + .map(|availability_processing_status| { + Some((availability_processing_status, data_columns_to_publish)) + }) + } + DataColumnReconstructionResultV2::NotStarted(reason) + | DataColumnReconstructionResultV2::RecoveredColumnsNotImported(reason) => { + // We use metric here because logging this would be *very* noisy. + metrics::inc_counter_vec( + &metrics::KZG_DATA_COLUMN_RECONSTRUCTION_INCOMPLETE_TOTAL, + &[reason], + ); + Ok(None) + } + } + } } } diff --git a/beacon_node/beacon_chain/src/data_availability_checker_v2/mod.rs b/beacon_node/beacon_chain/src/data_availability_checker_v2/mod.rs index 87d0dfdd5e..cb97595c36 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker_v2/mod.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker_v2/mod.rs @@ -829,6 +829,10 @@ mod data_availability_checker_tests { assert_eq!(cache.block_cache_size(), 0); } + // TODO(gloas): Add tests for `put_rpc_custody_columns` and `put_gossip_verified_data_columns` + // once the Gloas harness can produce KZG-valid columns. These wrappers add KZG verification + // and custody column filtering on top of `put_kzg_verified_custody_data_columns`. + #[tokio::test] async fn test_put_columns_creates_pending_components() { if !is_gloas_enabled() { diff --git a/beacon_node/beacon_chain/src/data_availability_router.rs b/beacon_node/beacon_chain/src/data_availability_router.rs index 656fce22ff..cca5ff207d 100644 --- a/beacon_node/beacon_chain/src/data_availability_router.rs +++ b/beacon_node/beacon_chain/src/data_availability_router.rs @@ -169,7 +169,7 @@ impl DataAvailabilityRouter { self.v1.custody_context() } - /// Query data columns from the appropriate checker based on slot. + /// Query data columns from the appropriate checker based on fork. pub fn get_data_columns( &self, block_root: Hash256, @@ -208,7 +208,7 @@ impl DataAvailabilityRouter { } } - /// Insert RPC custody columns, routing to the correct checker based on fork. + /// Insert RPC custody columns, routing to the correct checker based on slot. pub fn put_rpc_custody_columns( &self, block_root: Hash256, @@ -226,7 +226,7 @@ impl DataAvailabilityRouter { } } - /// Insert gossip-verified data columns, routing to the correct checker based on fork. + /// Insert gossip-verified data columns, routing to the correct checker based on slot. pub fn put_gossip_verified_data_columns( &self, block_root: Hash256, @@ -244,7 +244,7 @@ impl DataAvailabilityRouter { } } - /// Insert KZG-verified custody data columns, routing to the correct checker based on fork. + /// Insert KZG-verified custody data columns, routing to the correct checker based on slot. pub fn put_kzg_verified_custody_data_columns( &self, block_root: Hash256, @@ -262,7 +262,7 @@ impl DataAvailabilityRouter { } } - /// Attempt to reconstruct missing data columns, routing to the correct checker based on fork. + /// Attempt to reconstruct missing data columns, routing to the correct checker based on slot. pub fn reconstruct_data_columns( &self, block_root: &Hash256, diff --git a/beacon_node/beacon_chain/src/metrics.rs b/beacon_node/beacon_chain/src/metrics.rs index a6d9fef59c..478a3e0e6d 100644 --- a/beacon_node/beacon_chain/src/metrics.rs +++ b/beacon_node/beacon_chain/src/metrics.rs @@ -1897,12 +1897,6 @@ pub static DATA_AVAILABILITY_OVERFLOW_MEMORY_BLOCK_CACHE_SIZE: LazyLock> = LazyLock::new(|| { - try_create_int_gauge( - "data_availability_payload_cache_size", - "Number of entries in the data availability payload envelope cache.", - ) -}); pub static DATA_AVAILABILITY_RECONSTRUCTION_TIME: LazyLock> = LazyLock::new(|| { try_create_histogram( From 09e9a5431498cea7ba5ebfb7e06f072fc35eaf9f Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Thu, 26 Mar 2026 23:40:35 -0700 Subject: [PATCH 036/118] When a block comes in whose parent is unkown, queue the block for processing and lookup the parent envelope --- .../beacon_chain/src/block_verification.rs | 21 ++-- beacon_node/beacon_processor/src/lib.rs | 14 ++- .../src/beacon/execution_payload_envelope.rs | 82 +++++++++---- .../src/service/api_types.rs | 2 + .../gossip_methods.rs | 21 ++++ .../src/network_beacon_processor/mod.rs | 16 +++ .../network_beacon_processor/sync_methods.rs | 80 +++++++++++- beacon_node/network/src/router.rs | 50 +++++++- .../network/src/sync/block_lookups/mod.rs | 67 ++++++++++ .../sync/block_lookups/single_block_lookup.rs | 21 +++- beacon_node/network/src/sync/manager.rs | 106 +++++++++++++++- .../network/src/sync/network_context.rs | 114 +++++++++++++++++- .../src/sync/network_context/requests.rs | 4 + .../requests/payload_envelopes_by_root.rs | 53 ++++++++ 14 files changed, 608 insertions(+), 43 deletions(-) create mode 100644 beacon_node/network/src/sync/network_context/requests/payload_envelopes_by_root.rs diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 802b090f6a..916a207e62 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -321,6 +321,13 @@ pub enum BlockError { bid_parent_root: Hash256, block_parent_root: Hash256, }, + /// The parent block is known but its execution payload envelope has not been received yet. + /// + /// ## Peer scoring + /// + /// It's unclear if this block is valid, but it cannot be fully verified without the parent's + /// execution payload envelope. + ParentEnvelopeUnknown { parent_root: Hash256 }, } /// Which specific signature(s) are invalid in a SignedBeaconBlock @@ -1939,13 +1946,13 @@ fn load_parent>( && let Ok(parent_bid_block_hash) = parent_block.payload_bid_block_hash() { if block.as_block().is_parent_block_full(parent_bid_block_hash) { - // TODO(gloas): loading the envelope here is not very efficient - // TODO(gloas): check parent payload existence prior to this point? - let envelope = chain.store.get_payload_envelope(&root)?.ok_or_else(|| { - BeaconChainError::DBInconsistent(format!( - "Missing envelope for parent block {root:?}", - )) - })?; + // If the parent's execution payload envelope hasn't arrived yet, + // return an unknown parent error so the block gets sent to the + // reprocess queue. + let envelope = chain + .store + .get_payload_envelope(&root)? + .ok_or(BlockError::ParentEnvelopeUnknown { parent_root: root })?; (StatePayloadStatus::Full, envelope.message.state_root) } else { (StatePayloadStatus::Pending, parent_block.state_root()) diff --git a/beacon_node/beacon_processor/src/lib.rs b/beacon_node/beacon_processor/src/lib.rs index 724c41cfc9..229816ba77 100644 --- a/beacon_node/beacon_processor/src/lib.rs +++ b/beacon_node/beacon_processor/src/lib.rs @@ -416,6 +416,9 @@ pub enum Work { RpcBlobs { process_fn: AsyncFn, }, + RpcPayloadEnvelope { + process_fn: AsyncFn, + }, RpcCustodyColumn(AsyncFn), ColumnReconstruction(AsyncFn), IgnoredRpcBlock { @@ -477,6 +480,7 @@ pub enum WorkType { GossipLightClientOptimisticUpdate, RpcBlock, RpcBlobs, + RpcPayloadEnvelope, RpcCustodyColumn, ColumnReconstruction, IgnoredRpcBlock, @@ -538,6 +542,7 @@ impl Work { Work::GossipProposerPreferences(_) => WorkType::GossipProposerPreferences, Work::RpcBlock { .. } => WorkType::RpcBlock, Work::RpcBlobs { .. } => WorkType::RpcBlobs, + Work::RpcPayloadEnvelope { .. } => WorkType::RpcPayloadEnvelope, Work::RpcCustodyColumn { .. } => WorkType::RpcCustodyColumn, Work::ColumnReconstruction(_) => WorkType::ColumnReconstruction, Work::IgnoredRpcBlock { .. } => WorkType::IgnoredRpcBlock, @@ -1169,7 +1174,9 @@ impl BeaconProcessor { Work::GossipLightClientOptimisticUpdate { .. } => work_queues .lc_gossip_optimistic_update_queue .push(work, work_id), - Work::RpcBlock { .. } | Work::IgnoredRpcBlock { .. } => { + Work::RpcBlock { .. } + | Work::IgnoredRpcBlock { .. } + | Work::RpcPayloadEnvelope { .. } => { work_queues.rpc_block_queue.push(work, work_id) } Work::RpcBlobs { .. } => work_queues.rpc_blob_queue.push(work, work_id), @@ -1301,7 +1308,9 @@ impl BeaconProcessor { WorkType::GossipLightClientOptimisticUpdate => { work_queues.lc_gossip_optimistic_update_queue.len() } - WorkType::RpcBlock => work_queues.rpc_block_queue.len(), + WorkType::RpcBlock | WorkType::RpcPayloadEnvelope => { + work_queues.rpc_block_queue.len() + } WorkType::RpcBlobs | WorkType::IgnoredRpcBlock => { work_queues.rpc_blob_queue.len() } @@ -1496,6 +1505,7 @@ impl BeaconProcessor { beacon_block_root: _, } | Work::RpcBlobs { process_fn } + | Work::RpcPayloadEnvelope { process_fn } | Work::RpcCustodyColumn(process_fn) | Work::ColumnReconstruction(process_fn) => task_spawner.spawn_async(process_fn), Work::IgnoredRpcBlock { process_fn } => task_spawner.spawn_blocking(process_fn), diff --git a/beacon_node/http_api/src/beacon/execution_payload_envelope.rs b/beacon_node/http_api/src/beacon/execution_payload_envelope.rs index 81f2ea41ea..584ef40009 100644 --- a/beacon_node/http_api/src/beacon/execution_payload_envelope.rs +++ b/beacon_node/http_api/src/beacon/execution_payload_envelope.rs @@ -1,6 +1,10 @@ use crate::task_spawner::{Priority, TaskSpawner}; use crate::utils::{ChainFilter, EthV1Filter, NetworkTxFilter, ResponseFilter, TaskSpawnerFilter}; -use beacon_chain::{BeaconChain, BeaconChainTypes}; +use beacon_chain::payload_envelope_verification::gossip_verified_envelope::GossipVerifiedEnvelope; +use beacon_chain::{ + BeaconChain, BeaconChainTypes, NotifyExecutionLayer, + payload_envelope_verification::EnvelopeError, +}; use bytes::Bytes; use eth2::{CONTENT_TYPE_HEADER, SSZ_CONTENT_TYPE_HEADER}; use lighthouse_network::PubsubMessage; @@ -9,8 +13,11 @@ use ssz::Decode; use std::sync::Arc; use tokio::sync::mpsc::UnboundedSender; use tracing::{info, warn}; -use types::SignedExecutionPayloadEnvelope; -use warp::{Filter, Rejection, Reply, reply::Response}; +use types::{BlockImportSource, SignedExecutionPayloadEnvelope}; +use warp::{ + Filter, Rejection, Reply, + hyper::{Body, Response}, +}; // POST beacon/execution_payload_envelope (SSZ) pub(crate) fn post_beacon_execution_payload_envelope_ssz( @@ -77,40 +84,71 @@ pub(crate) fn post_beacon_execution_payload_envelope( .boxed() } /// Publishes a signed execution payload envelope to the network. +/// TODO(gloas): Add gossip verification (BroadcastValidation::Gossip) before import. pub async fn publish_execution_payload_envelope( envelope: SignedExecutionPayloadEnvelope, chain: Arc>, network_tx: &UnboundedSender>, -) -> Result { +) -> Result, Rejection> { let slot = envelope.message.slot; let beacon_block_root = envelope.message.beacon_block_root; + let builder_index = envelope.message.builder_index; - // TODO(gloas): Replace this check once we have gossip validation. if !chain.spec.is_gloas_scheduled() { return Err(warp_utils::reject::custom_bad_request( "Execution payload envelopes are not supported before the Gloas fork".into(), )); } - // TODO(gloas): We should probably add validation here i.e. BroadcastValidation::Gossip - info!( - %slot, - %beacon_block_root, - builder_index = envelope.message.builder_index, - "Publishing signed execution payload envelope to network" - ); + let signed_envelope = Arc::new(envelope); - // Publish to the network - crate::utils::publish_pubsub_message( - network_tx, - PubsubMessage::ExecutionPayload(Box::new(envelope)), - ) - .map_err(|_| { - warn!(%slot, "Failed to publish execution payload envelope to network"); - warp_utils::reject::custom_server_error( - "Unable to publish execution payload envelope to network".into(), + // The publish_fn is called inside process_execution_payload_envelope after consensus + // verification but before the EL call. + let envelope_for_publish = signed_envelope.clone(); + let sender = network_tx.clone(); + let publish_fn = move || { + info!( + %slot, + %beacon_block_root, + builder_index, + "Publishing signed execution payload envelope to network" + ); + crate::utils::publish_pubsub_message( + &sender, + PubsubMessage::ExecutionPayload(Box::new((*envelope_for_publish).clone())), ) - })?; + .map_err(|_| { + warn!(%slot, "Failed to publish execution payload envelope to network"); + EnvelopeError::InternalError( + "Unable to publish execution payload envelope to network".to_owned(), + ) + }) + }; + + let ctx = chain.gossip_verification_context(); + let Ok(gossip_verifed_envelope) = GossipVerifiedEnvelope::new(signed_envelope, &ctx) else { + warn!(%slot, %beacon_block_root, "Execution payload envelope rejected"); + return Err(warp_utils::reject::custom_bad_request( + "execution payload envelope rejected, gossip verification".to_string(), + )); + }; + + // Import the envelope locally (runs state transition and notifies the EL). + chain + .process_execution_payload_envelope( + beacon_block_root, + gossip_verifed_envelope, + NotifyExecutionLayer::Yes, + BlockImportSource::HttpApi, + publish_fn, + ) + .await + .map_err(|e| { + warn!(%slot, %beacon_block_root, reason = ?e, "Execution payload envelope rejected"); + warp_utils::reject::custom_bad_request(format!( + "execution payload envelope rejected: {e:?}" + )) + })?; Ok(warp::reply().into_response()) } diff --git a/beacon_node/lighthouse_network/src/service/api_types.rs b/beacon_node/lighthouse_network/src/service/api_types.rs index 486a443857..a190a42a80 100644 --- a/beacon_node/lighthouse_network/src/service/api_types.rs +++ b/beacon_node/lighthouse_network/src/service/api_types.rs @@ -31,6 +31,8 @@ pub enum SyncRequestId { BlobsByRange(BlobsByRangeRequestId), /// Data columns by range request DataColumnsByRange(DataColumnsByRangeRequestId), + /// Request searching for an execution payload envelope given a block root. + SinglePayloadEnvelope { id: SingleLookupReqId }, } /// Request ID for data_columns_by_root requests. Block lookups do not issue this request directly. 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 1f55d9a878..2e04847630 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -1290,6 +1290,17 @@ impl NetworkBeaconProcessor { self.send_sync_message(SyncMessage::UnknownParentBlock(peer_id, block, block_root)); return None; } + Err(BlockError::ParentEnvelopeUnknown { parent_root }) => { + debug!( + ?block_root, + ?parent_root, + "Parent envelope not yet available for gossip block" + ); + self.send_sync_message(SyncMessage::UnknownParentEnvelope( + peer_id, block, block_root, + )); + return None; + } Err(e @ BlockError::BeaconChainError(_)) => { debug!( error = ?e, @@ -1578,6 +1589,16 @@ impl NetworkBeaconProcessor { "Block with unknown parent attempted to be processed" ); } + Err(BlockError::ParentEnvelopeUnknown { parent_root }) => { + debug!( + %block_root, + ?parent_root, + "Parent envelope not yet available, need envelope lookup" + ); + // Unlike ParentUnknown, this can legitimately happen during processing + // because the parent envelope may not have arrived yet. The lookup + // system will handle retrying via Action::ParentEnvelopeUnknown. + } Err(e @ BlockError::ExecutionPayloadError(epe)) if !epe.penalize_peer() => { debug!( error = %e, diff --git a/beacon_node/network/src/network_beacon_processor/mod.rs b/beacon_node/network/src/network_beacon_processor/mod.rs index f74e7dacfb..ca5710076b 100644 --- a/beacon_node/network/src/network_beacon_processor/mod.rs +++ b/beacon_node/network/src/network_beacon_processor/mod.rs @@ -541,6 +541,22 @@ impl NetworkBeaconProcessor { }) } + /// Create a new `Work` event for an RPC payload envelope. + pub fn send_rpc_payload_envelope( + self: &Arc, + envelope: Arc>, + seen_timestamp: Duration, + process_type: BlockProcessType, + ) -> Result<(), Error> { + let process_fn = + self.clone() + .generate_rpc_envelope_process_fn(envelope, seen_timestamp, process_type); + self.try_send(BeaconWorkEvent { + drop_during_sync: false, + work: Work::RpcPayloadEnvelope { process_fn }, + }) + } + /// Create a new `Work` event for some blobs, where the result from computation (if any) is /// sent to the other side of `result_tx`. pub fn send_rpc_blobs( diff --git a/beacon_node/network/src/network_beacon_processor/sync_methods.rs b/beacon_node/network/src/network_beacon_processor/sync_methods.rs index f7fbce8e56..b4586994e4 100644 --- a/beacon_node/network/src/network_beacon_processor/sync_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/sync_methods.rs @@ -4,7 +4,7 @@ use crate::sync::BatchProcessResult; use crate::sync::manager::CustodyBatchProcessResult; use crate::sync::{ ChainId, - manager::{BlockProcessType, SyncMessage}, + manager::{BlockProcessType, BlockProcessingResult, SyncMessage}, }; use beacon_chain::block_verification_types::LookupBlock; use beacon_chain::block_verification_types::{AsBlock, RangeSyncBlock}; @@ -28,7 +28,9 @@ use store::KzgCommitment; use tracing::{debug, debug_span, error, info, instrument, warn}; use types::data::FixedBlobSidecarList; use types::kzg_ext::format_kzg_commitments; -use types::{BlockImportSource, DataColumnSidecarList, Epoch, Hash256}; +use types::{ + BlockImportSource, DataColumnSidecarList, Epoch, Hash256, SignedExecutionPayloadEnvelope, +}; /// Id associated to a batch processing request, either a sync batch or a parent lookup. #[derive(Clone, Debug, PartialEq)] @@ -73,6 +75,80 @@ impl NetworkBeaconProcessor { Box::pin(process_fn) } + /// Returns an async closure which processes a payload envelope received via RPC. + pub fn generate_rpc_envelope_process_fn( + self: Arc, + envelope: Arc>, + seen_timestamp: Duration, + process_type: BlockProcessType, + ) -> AsyncFn { + let process_fn = async move { + self.process_rpc_envelope(envelope, seen_timestamp, process_type) + .await; + }; + Box::pin(process_fn) + } + + /// Process an execution payload envelope received via RPC. + async fn process_rpc_envelope( + self: Arc, + envelope: Arc>, + _seen_timestamp: Duration, + process_type: BlockProcessType, + ) { + let beacon_block_root = envelope.beacon_block_root(); + + // Verify the envelope using the gossip verification path (same checks apply to RPC) + let verified_envelope = match self.chain.verify_envelope_for_gossip(envelope).await { + Ok(verified) => verified, + Err(e) => { + debug!( + error = ?e, + ?beacon_block_root, + "RPC payload envelope failed verification" + ); + self.send_sync_message(SyncMessage::BlockComponentProcessed { + process_type, + result: BlockProcessingResult::Err(BlockError::InternalError(format!( + "Envelope verification failed: {e:?}" + ))), + }); + return; + } + }; + + // Process the verified envelope + let result = self + .chain + .process_execution_payload_envelope( + beacon_block_root, + verified_envelope, + NotifyExecutionLayer::Yes, + BlockImportSource::Lookup, + || Ok(()), + ) + .await; + + let processing_result = match result { + Ok(status) => BlockProcessingResult::Ok(status), + Err(e) => { + debug!( + error = ?e, + ?beacon_block_root, + "RPC payload envelope processing failed" + ); + BlockProcessingResult::Err(BlockError::InternalError(format!( + "Envelope processing failed: {e:?}" + ))) + } + }; + + self.send_sync_message(SyncMessage::BlockComponentProcessed { + process_type, + result: processing_result, + }); + } + /// Returns the `process_fn` and `ignore_fn` required when requeuing an RPC block. pub fn generate_lookup_beacon_block_fns( self: Arc, diff --git a/beacon_node/network/src/router.rs b/beacon_node/network/src/router.rs index e6982e6a84..3fb2196975 100644 --- a/beacon_node/network/src/router.rs +++ b/beacon_node/network/src/router.rs @@ -24,7 +24,10 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH}; use tokio::sync::mpsc; use tokio_stream::wrappers::UnboundedReceiverStream; use tracing::{debug, error, trace, warn}; -use types::{BlobSidecar, DataColumnSidecar, EthSpec, ForkContext, SignedBeaconBlock}; +use types::{ + BlobSidecar, DataColumnSidecar, EthSpec, ForkContext, SignedBeaconBlock, + SignedExecutionPayloadEnvelope, +}; /// Handles messages from the network and routes them to the appropriate service to be handled. pub struct Router { @@ -327,10 +330,13 @@ impl Router { Response::DataColumnsByRange(data_column) => { self.on_data_columns_by_range_response(peer_id, app_request_id, data_column); } - // TODO(EIP-7732): implement outgoing payload envelopes by range and root - // responses once sync manager requests them. - Response::PayloadEnvelopesByRoot(_) | Response::PayloadEnvelopesByRange(_) => { - debug!("Requesting envelopes by root and by range not supported yet"); + Response::PayloadEnvelopesByRoot(envelope) => { + self.on_payload_envelopes_by_root_response(peer_id, app_request_id, envelope); + } + // TODO(EIP-7732): implement outgoing payload envelopes by range responses once + // range sync requests them. + Response::PayloadEnvelopesByRange(_) => { + unreachable!() } // Light client responses should not be received Response::LightClientBootstrap(_) @@ -703,6 +709,40 @@ impl Router { }); } + /// Handle a `PayloadEnvelopesByRoot` response from the peer. + pub fn on_payload_envelopes_by_root_response( + &mut self, + peer_id: PeerId, + app_request_id: AppRequestId, + envelope: Option>>, + ) { + let sync_request_id = match app_request_id { + AppRequestId::Sync(sync_id) => match sync_id { + id @ SyncRequestId::SinglePayloadEnvelope { .. } => id, + other => { + crit!(request = ?other, "PayloadEnvelopesByRoot response on incorrect request"); + return; + } + }, + AppRequestId::Router => { + crit!(%peer_id, "All PayloadEnvelopesByRoot requests belong to sync"); + return; + } + AppRequestId::Internal => unreachable!("Handled internally"), + }; + + trace!( + %peer_id, + "Received PayloadEnvelopesByRoot Response" + ); + self.send_to_sync(SyncMessage::RpcPayloadEnvelope { + peer_id, + sync_request_id, + envelope, + seen_timestamp: timestamp_now(), + }); + } + /// Handle a `BlobsByRoot` response from the peer. pub fn on_blobs_by_root_response( &mut self, diff --git a/beacon_node/network/src/sync/block_lookups/mod.rs b/beacon_node/network/src/sync/block_lookups/mod.rs index 394f2fc37d..7b4e3ce753 100644 --- a/beacon_node/network/src/sync/block_lookups/mod.rs +++ b/beacon_node/network/src/sync/block_lookups/mod.rs @@ -109,6 +109,7 @@ pub type SingleLookupId = u32; enum Action { Retry, ParentUnknown { parent_root: Hash256 }, + ParentEnvelopeUnknown { parent_root: Hash256 }, Drop(/* reason: */ String), Continue, } @@ -559,6 +560,19 @@ impl BlockLookups { BlockProcessType::SingleCustodyColumn(id) => { self.on_processing_result_inner::>(id, result, cx) } + BlockProcessType::SinglePayloadEnvelope { id, block_root } => { + match result { + BlockProcessingResult::Ok(_) => { + self.continue_envelope_child_lookups(block_root, cx); + } + BlockProcessingResult::Err(e) => { + debug!(%id, error = ?e, "Payload envelope processing failed"); + // TODO(EIP-7732): resolve awaiting_envelope on affected lookups so they can retry + } + _ => {} + } + return; + } }; self.on_lookup_result(process_type.id(), lookup_result, "processing_result", cx); } @@ -645,6 +659,12 @@ impl BlockLookups { request_state.revert_to_awaiting_processing()?; Action::ParentUnknown { parent_root } } + BlockError::ParentEnvelopeUnknown { parent_root } => { + // The parent block is known but its execution payload envelope is missing. + // Revert to awaiting processing and fetch the envelope via RPC. + request_state.revert_to_awaiting_processing()?; + Action::ParentEnvelopeUnknown { parent_root } + } ref e @ BlockError::ExecutionPayloadError(ref epe) if !epe.penalize_peer() => { // These errors indicate that the execution layer is offline // and failed to validate the execution payload. Do not downscore peer. @@ -742,6 +762,26 @@ impl BlockLookups { ))) } } + Action::ParentEnvelopeUnknown { parent_root } => { + let peers = lookup.all_peers(); + lookup.set_awaiting_envelope(parent_root); + // Pick a peer to request the envelope from + let peer_id = peers.first().copied().ok_or_else(|| { + LookupRequestError::Failed("No peers available for envelope request".to_owned()) + })?; + match cx.envelope_lookup_request(lookup_id, peer_id, parent_root) { + Ok(_) => { + debug!( + id = lookup_id, + ?block_root, + ?parent_root, + "Requesting missing parent envelope" + ); + Ok(LookupResult::Pending) + } + Err(e) => Err(LookupRequestError::SendFailedNetwork(e)), + } + } Action::Drop(reason) => { // Drop with noop Err(LookupRequestError::Failed(reason)) @@ -809,6 +849,33 @@ impl BlockLookups { } } + /// Makes progress on lookups that were waiting for a parent envelope to be imported. + pub fn continue_envelope_child_lookups( + &mut self, + block_root: Hash256, + cx: &mut SyncNetworkContext, + ) { + let mut lookup_results = vec![]; + + for (id, lookup) in self.single_block_lookups.iter_mut() { + if lookup.awaiting_envelope() == Some(block_root) { + lookup.resolve_awaiting_envelope(); + debug!( + envelope_root = ?block_root, + id, + block_root = ?lookup.block_root(), + "Continuing lookup after envelope imported" + ); + let result = lookup.continue_requests(cx); + lookup_results.push((*id, result)); + } + } + + for (id, result) in lookup_results { + self.on_lookup_result(id, result, "continue_envelope_child_lookups", cx); + } + } + /// Drops `dropped_id` lookup and all its children recursively. Lookups awaiting a parent need /// the parent to make progress to resolve, therefore we must drop them if the parent is /// dropped. diff --git a/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs b/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs index 919526c238..51cc191056 100644 --- a/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs +++ b/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs @@ -70,6 +70,7 @@ pub struct SingleBlockLookup { peers: Arc>>, block_root: Hash256, awaiting_parent: Option, + awaiting_envelope: Option, created: Instant, pub(crate) span: Span, } @@ -104,6 +105,7 @@ impl SingleBlockLookup { peers: Arc::new(RwLock::new(HashSet::from_iter(peers.iter().copied()))), block_root: requested_block_root, awaiting_parent, + awaiting_envelope: None, created: Instant::now(), span: lookup_span, } @@ -144,6 +146,20 @@ impl SingleBlockLookup { self.awaiting_parent = None; } + pub fn awaiting_envelope(&self) -> Option { + self.awaiting_envelope + } + + /// Mark this lookup as awaiting a parent envelope to be imported before processing. + pub fn set_awaiting_envelope(&mut self, parent_root: Hash256) { + self.awaiting_envelope = Some(parent_root); + } + + /// Mark this lookup as no longer awaiting a parent envelope. + pub fn resolve_awaiting_envelope(&mut self) { + self.awaiting_envelope = None; + } + /// Returns the time elapsed since this lookup was created pub fn elapsed_since_created(&self) -> Duration { self.created.elapsed() @@ -185,6 +201,7 @@ impl SingleBlockLookup { /// Returns true if this request is expecting some event to make progress pub fn is_awaiting_event(&self) -> bool { self.awaiting_parent.is_some() + || self.awaiting_envelope.is_some() || self.block_request_state.state.is_awaiting_event() || match &self.component_requests { // If components are waiting for the block request to complete, here we should @@ -287,7 +304,7 @@ impl SingleBlockLookup { expected_blobs: usize, ) -> Result<(), LookupRequestError> { let id = self.id; - let awaiting_parent = self.awaiting_parent.is_some(); + let awaiting_event = self.awaiting_parent.is_some() || self.awaiting_envelope.is_some(); let request = R::request_state_mut(self).map_err(|e| LookupRequestError::BadState(e.to_owned()))?; @@ -331,7 +348,7 @@ impl SingleBlockLookup { // Otherwise, attempt to progress awaiting processing // If this request is awaiting a parent lookup to be processed, do not send for processing. // The request will be rejected with unknown parent error. - } else if !awaiting_parent { + } else if !awaiting_event { // maybe_start_processing returns Some if state == AwaitingProcess. This pattern is // useful to conditionally access the result data. if let Some(result) = request.get_state_mut().maybe_start_processing() { diff --git a/beacon_node/network/src/sync/manager.rs b/beacon_node/network/src/sync/manager.rs index 7e618d8980..256752d5fb 100644 --- a/beacon_node/network/src/sync/manager.rs +++ b/beacon_node/network/src/sync/manager.rs @@ -74,7 +74,8 @@ use strum::IntoStaticStr; use tokio::sync::mpsc; use tracing::{debug, error, info, trace}; use types::{ - BlobSidecar, DataColumnSidecar, EthSpec, ForkContext, Hash256, SignedBeaconBlock, Slot, + BlobSidecar, DataColumnSidecar, EthSpec, ForkContext, Hash256, SignedBeaconBlock, + SignedExecutionPayloadEnvelope, Slot, }; /// The number of slots ahead of us that is allowed before requesting a long-range (batch) Sync @@ -133,6 +134,14 @@ pub enum SyncMessage { seen_timestamp: Duration, }, + /// An execution payload envelope has been received from the RPC. + RpcPayloadEnvelope { + sync_request_id: SyncRequestId, + peer_id: PeerId, + envelope: Option>>, + seen_timestamp: Duration, + }, + /// A block with an unknown parent has been received. UnknownParentBlock(PeerId, Arc>, Hash256), @@ -142,6 +151,9 @@ pub enum SyncMessage { /// A data column with an unknown parent has been received. UnknownParentDataColumn(PeerId, Arc>), + /// A block's parent is known but its execution payload envelope has not been received yet. + UnknownParentEnvelope(PeerId, Arc>, Hash256), + /// A peer has sent an attestation that references a block that is unknown. This triggers the /// manager to attempt to find the block matching the unknown hash. UnknownBlockHashFromAttestation(PeerId, Hash256), @@ -184,6 +196,7 @@ pub enum BlockProcessType { SingleBlock { id: Id }, SingleBlob { id: Id }, SingleCustodyColumn(Id), + SinglePayloadEnvelope { id: Id, block_root: Hash256 }, } impl BlockProcessType { @@ -191,7 +204,8 @@ impl BlockProcessType { match self { BlockProcessType::SingleBlock { id } | BlockProcessType::SingleBlob { id } - | BlockProcessType::SingleCustodyColumn(id) => *id, + | BlockProcessType::SingleCustodyColumn(id) + | BlockProcessType::SinglePayloadEnvelope { id, .. } => *id, } } } @@ -505,6 +519,9 @@ impl SyncManager { SyncRequestId::DataColumnsByRange(req_id) => { self.on_data_columns_by_range_response(req_id, peer_id, RpcEvent::RPCError(error)) } + SyncRequestId::SinglePayloadEnvelope { id } => { + self.on_single_envelope_response(id, peer_id, RpcEvent::RPCError(error)) + } } } @@ -839,6 +856,17 @@ impl SyncManager { } => { self.rpc_data_column_received(sync_request_id, peer_id, data_column, seen_timestamp) } + SyncMessage::RpcPayloadEnvelope { + sync_request_id, + peer_id, + envelope, + seen_timestamp, + } => self.rpc_payload_envelope_received( + sync_request_id, + peer_id, + envelope, + seen_timestamp, + ), SyncMessage::UnknownParentBlock(peer_id, block, block_root) => { let block_slot = block.slot(); let parent_root = block.parent_root(); @@ -900,6 +928,27 @@ impl SyncManager { } } } + SyncMessage::UnknownParentEnvelope(peer_id, block, block_root) => { + let block_slot = block.slot(); + let parent_root = block.parent_root(); + debug!( + %block_root, + %parent_root, + "Parent envelope not yet available, creating lookup" + ); + self.handle_unknown_parent( + peer_id, + block_root, + parent_root, + block_slot, + BlockComponent::Block(DownloadResult { + value: block.block_cloned(), + block_root, + seen_timestamp: timestamp_now(), + peer_group: PeerGroup::from_single(peer_id), + }), + ); + } SyncMessage::UnknownBlockHashFromAttestation(peer_id, block_root) => { if !self.notified_unknown_roots.contains(&(peer_id, block_root)) { self.notified_unknown_roots.insert((peer_id, block_root)); @@ -1200,6 +1249,59 @@ impl SyncManager { } } + fn rpc_payload_envelope_received( + &mut self, + sync_request_id: SyncRequestId, + peer_id: PeerId, + envelope: Option>>, + seen_timestamp: Duration, + ) { + match sync_request_id { + SyncRequestId::SinglePayloadEnvelope { id } => self.on_single_envelope_response( + id, + peer_id, + RpcEvent::from_chunk(envelope, seen_timestamp), + ), + _ => { + crit!(%peer_id, "bad request id for payload envelope"); + } + } + } + + fn on_single_envelope_response( + &mut self, + id: SingleLookupReqId, + peer_id: PeerId, + rpc_event: RpcEvent>>, + ) { + if let Some(resp) = self + .network + .on_single_envelope_response(id, peer_id, rpc_event) + { + match resp { + Ok((envelope, seen_timestamp)) => { + let block_root = envelope.beacon_block_root(); + debug!( + ?block_root, + %id, + "Downloaded payload envelope, sending for processing" + ); + if let Err(e) = self.network.send_envelope_for_processing( + id.req_id, + envelope, + seen_timestamp, + block_root, + ) { + error!(error = ?e, "Failed to send envelope for processing"); + } + } + Err(e) => { + debug!(error = ?e, %id, "Payload envelope download failed"); + } + } + } + } + fn on_single_blob_response( &mut self, id: SingleLookupReqId, diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index ff630bb470..e9d289b777 100644 --- a/beacon_node/network/src/sync/network_context.rs +++ b/beacon_node/network/src/sync/network_context.rs @@ -37,6 +37,7 @@ pub use requests::LookupVerifyError; use requests::{ ActiveRequests, BlobsByRangeRequestItems, BlobsByRootRequestItems, BlocksByRangeRequestItems, BlocksByRootRequestItems, DataColumnsByRangeRequestItems, DataColumnsByRootRequestItems, + PayloadEnvelopesByRootRequestItems, PayloadEnvelopesByRootSingleRequest, }; #[cfg(test)] use slot_clock::SlotClock; @@ -52,7 +53,7 @@ use tracing::{Span, debug, debug_span, error, warn}; use types::data::FixedBlobSidecarList; use types::{ BlobSidecar, BlockImportSource, ColumnIndex, DataColumnSidecar, DataColumnSidecarList, EthSpec, - ForkContext, Hash256, SignedBeaconBlock, Slot, + ForkContext, Hash256, SignedBeaconBlock, SignedExecutionPayloadEnvelope, Slot, }; pub mod custody; @@ -213,6 +214,9 @@ pub struct SyncNetworkContext { /// A mapping of active DataColumnsByRange requests data_columns_by_range_requests: ActiveRequests>, + /// A mapping of active PayloadEnvelopesByRoot requests + payload_envelopes_by_root_requests: + ActiveRequests>, /// Mapping of active custody column requests for a block root custody_by_root_requests: FnvHashMap>, @@ -298,6 +302,7 @@ impl SyncNetworkContext { blocks_by_range_requests: ActiveRequests::new("blocks_by_range"), blobs_by_range_requests: ActiveRequests::new("blobs_by_range"), data_columns_by_range_requests: ActiveRequests::new("data_columns_by_range"), + payload_envelopes_by_root_requests: ActiveRequests::new("payload_envelopes_by_root"), custody_by_root_requests: <_>::default(), components_by_range_requests: FnvHashMap::default(), custody_backfill_data_column_batch_requests: FnvHashMap::default(), @@ -326,6 +331,7 @@ impl SyncNetworkContext { blocks_by_range_requests, blobs_by_range_requests, data_columns_by_range_requests, + payload_envelopes_by_root_requests, // custody_by_root_requests is a meta request of data_columns_by_root_requests custody_by_root_requests: _, // components_by_range_requests is a meta request of various _by_range requests @@ -361,12 +367,17 @@ impl SyncNetworkContext { .active_requests_of_peer(peer_id) .into_iter() .map(|req_id| SyncRequestId::DataColumnsByRange(*req_id)); + let envelope_by_root_ids = payload_envelopes_by_root_requests + .active_requests_of_peer(peer_id) + .into_iter() + .map(|id| SyncRequestId::SinglePayloadEnvelope { id: *id }); blocks_by_root_ids .chain(blobs_by_root_ids) .chain(data_column_by_root_ids) .chain(blocks_by_range_ids) .chain(blobs_by_range_ids) .chain(data_column_by_range_ids) + .chain(envelope_by_root_ids) .collect() } @@ -423,6 +434,7 @@ impl SyncNetworkContext { blocks_by_range_requests, blobs_by_range_requests, data_columns_by_range_requests, + payload_envelopes_by_root_requests, // custody_by_root_requests is a meta request of data_columns_by_root_requests custody_by_root_requests: _, // components_by_range_requests is a meta request of various _by_range requests @@ -445,6 +457,7 @@ impl SyncNetworkContext { .chain(blocks_by_range_requests.iter_request_peers()) .chain(blobs_by_range_requests.iter_request_peers()) .chain(data_columns_by_range_requests.iter_request_peers()) + .chain(payload_envelopes_by_root_requests.iter_request_peers()) { *active_request_count_by_peer.entry(peer_id).or_default() += 1; } @@ -927,6 +940,57 @@ impl SyncNetworkContext { Ok(LookupRequestResult::RequestSent(id.req_id)) } + /// Request a payload envelope for `block_root` from a peer. + pub fn envelope_lookup_request( + &mut self, + lookup_id: SingleLookupId, + peer_id: PeerId, + block_root: Hash256, + ) -> Result { + let id = SingleLookupReqId { + lookup_id, + req_id: self.next_id(), + }; + + let request = PayloadEnvelopesByRootSingleRequest(block_root); + + let network_request = RequestType::PayloadEnvelopesByRoot( + request + .into_request(&self.fork_context) + .map_err(RpcRequestSendError::InternalError)?, + ); + self.network_send + .send(NetworkMessage::SendRequest { + peer_id, + request: network_request, + app_request_id: AppRequestId::Sync(SyncRequestId::SinglePayloadEnvelope { id }), + }) + .map_err(|_| RpcRequestSendError::InternalError("network send error".to_owned()))?; + + debug!( + method = "PayloadEnvelopesByRoot", + ?block_root, + peer = %peer_id, + %id, + "Sync RPC request sent" + ); + + let request_span = debug_span!( + parent: Span::current(), + "lh_outgoing_envelope_by_root_request", + %block_root, + ); + self.payload_envelopes_by_root_requests.insert( + id, + peer_id, + true, + PayloadEnvelopesByRootRequestItems::new(request), + request_span, + ); + + Ok(id.req_id) + } + /// Request necessary blobs for `block_root`. Requests only the necessary blobs by checking: /// - If we have a downloaded but not yet processed block /// - If the da_checker has a pending block @@ -1435,6 +1499,27 @@ impl SyncNetworkContext { self.on_rpc_response_result(resp, peer_id) } + pub(crate) fn on_single_envelope_response( + &mut self, + id: SingleLookupReqId, + peer_id: PeerId, + rpc_event: RpcEvent>>, + ) -> Option>>> { + let resp = self + .payload_envelopes_by_root_requests + .on_response(id, rpc_event); + let resp = resp.map(|res| { + res.and_then(|(mut envelopes, seen_timestamp)| { + match envelopes.pop() { + Some(envelope) => Ok((envelope, seen_timestamp)), + // Should never happen, request items enforces at least 1 chunk. + None => Err(LookupVerifyError::NotEnoughResponsesReturned { actual: 0 }.into()), + } + }) + }); + self.on_rpc_response_result(resp, peer_id) + } + pub(crate) fn on_single_blob_response( &mut self, id: SingleLookupReqId, @@ -1610,6 +1695,33 @@ impl SyncNetworkContext { }) } + pub fn send_envelope_for_processing( + &self, + id: Id, + envelope: Arc>, + seen_timestamp: Duration, + block_root: Hash256, + ) -> Result<(), SendErrorProcessor> { + let beacon_processor = self + .beacon_processor_if_enabled() + .ok_or(SendErrorProcessor::ProcessorNotAvailable)?; + + debug!(?block_root, ?id, "Sending payload envelope for processing"); + beacon_processor + .send_rpc_payload_envelope( + envelope, + seen_timestamp, + BlockProcessType::SinglePayloadEnvelope { id, block_root }, + ) + .map_err(|e| { + error!( + error = ?e, + "Failed to send sync envelope to processor" + ); + SendErrorProcessor::SendError + }) + } + pub fn send_blobs_for_processing( &self, id: Id, diff --git a/beacon_node/network/src/sync/network_context/requests.rs b/beacon_node/network/src/sync/network_context/requests.rs index 8f9540693e..5b5e779d9b 100644 --- a/beacon_node/network/src/sync/network_context/requests.rs +++ b/beacon_node/network/src/sync/network_context/requests.rs @@ -16,6 +16,9 @@ pub use data_columns_by_range::DataColumnsByRangeRequestItems; pub use data_columns_by_root::{ DataColumnsByRootRequestItems, DataColumnsByRootSingleBlockRequest, }; +pub use payload_envelopes_by_root::{ + PayloadEnvelopesByRootRequestItems, PayloadEnvelopesByRootSingleRequest, +}; use crate::metrics; @@ -27,6 +30,7 @@ mod blocks_by_range; mod blocks_by_root; mod data_columns_by_range; mod data_columns_by_root; +mod payload_envelopes_by_root; #[derive(Debug, PartialEq, Eq, IntoStaticStr)] pub enum LookupVerifyError { diff --git a/beacon_node/network/src/sync/network_context/requests/payload_envelopes_by_root.rs b/beacon_node/network/src/sync/network_context/requests/payload_envelopes_by_root.rs new file mode 100644 index 0000000000..7f7097971d --- /dev/null +++ b/beacon_node/network/src/sync/network_context/requests/payload_envelopes_by_root.rs @@ -0,0 +1,53 @@ +use lighthouse_network::rpc::methods::PayloadEnvelopesByRootRequest; +use std::sync::Arc; +use types::{EthSpec, ForkContext, Hash256, SignedExecutionPayloadEnvelope}; + +use super::{ActiveRequestItems, LookupVerifyError}; + +#[derive(Debug, Copy, Clone)] +pub struct PayloadEnvelopesByRootSingleRequest(pub Hash256); + +impl PayloadEnvelopesByRootSingleRequest { + pub fn into_request( + self, + fork_context: &ForkContext, + ) -> Result { + PayloadEnvelopesByRootRequest::new(vec![self.0], fork_context) + } +} + +pub struct PayloadEnvelopesByRootRequestItems { + request: PayloadEnvelopesByRootSingleRequest, + items: Vec>>, +} + +impl PayloadEnvelopesByRootRequestItems { + pub fn new(request: PayloadEnvelopesByRootSingleRequest) -> Self { + Self { + request, + items: vec![], + } + } +} + +impl ActiveRequestItems for PayloadEnvelopesByRootRequestItems { + type Item = Arc>; + + /// Append a response to the single chunk request. If the chunk is valid, the request is + /// resolved immediately. + /// The active request SHOULD be dropped after `add_response` returns an error + fn add(&mut self, envelope: Self::Item) -> Result { + let beacon_block_root = envelope.beacon_block_root(); + if self.request.0 != beacon_block_root { + return Err(LookupVerifyError::UnrequestedBlockRoot(beacon_block_root)); + } + + self.items.push(envelope); + // Always returns true, payload envelopes by root expects a single response + Ok(true) + } + + fn consume(&mut self) -> Vec { + std::mem::take(&mut self.items) + } +} From 86ddd0d88d3f4650f56312830262a070137940ee Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Thu, 2 Apr 2026 19:09:56 -0700 Subject: [PATCH 037/118] Add EnvelopeRequestState logic --- .../network/src/sync/block_lookups/common.rs | 58 ++++++++- .../network/src/sync/block_lookups/mod.rs | 111 +++++++++++++----- .../sync/block_lookups/single_block_lookup.rs | 67 +++++++++-- beacon_node/network/src/sync/manager.rs | 30 ++--- .../network/src/sync/network_context.rs | 23 +++- 5 files changed, 221 insertions(+), 68 deletions(-) diff --git a/beacon_node/network/src/sync/block_lookups/common.rs b/beacon_node/network/src/sync/block_lookups/common.rs index edd99345b4..bb8d81cc6e 100644 --- a/beacon_node/network/src/sync/block_lookups/common.rs +++ b/beacon_node/network/src/sync/block_lookups/common.rs @@ -2,7 +2,7 @@ use crate::sync::block_lookups::single_block_lookup::{ LookupRequestError, SingleBlockLookup, SingleLookupRequestState, }; use crate::sync::block_lookups::{ - BlobRequestState, BlockRequestState, CustodyRequestState, PeerId, + BlobRequestState, BlockRequestState, CustodyRequestState, EnvelopeRequestState, PeerId, }; use crate::sync::manager::BlockProcessType; use crate::sync::network_context::{LookupRequestResult, SyncNetworkContext}; @@ -12,16 +12,17 @@ use parking_lot::RwLock; use std::collections::HashSet; use std::sync::Arc; use types::data::FixedBlobSidecarList; -use types::{DataColumnSidecarList, SignedBeaconBlock}; +use types::{DataColumnSidecarList, SignedBeaconBlock, SignedExecutionPayloadEnvelope}; use super::SingleLookupId; use super::single_block_lookup::{ComponentRequests, DownloadResult}; -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum ResponseType { Block, Blob, CustodyColumn, + Envelope, } /// This trait unifies common single block lookup functionality across blocks and blobs. This @@ -151,6 +152,7 @@ impl RequestState for BlobRequestState { ComponentRequests::WaitingForBlock => Err("waiting for block"), ComponentRequests::ActiveBlobRequest(request, _) => Ok(request), ComponentRequests::ActiveCustodyRequest { .. } => Err("expecting custody request"), + ComponentRequests::ActiveEnvelopeRequest { .. } => Err("expecting envelope request"), ComponentRequests::NotNeeded { .. } => Err("not needed"), } } @@ -205,6 +207,7 @@ impl RequestState for CustodyRequestState { ComponentRequests::WaitingForBlock => Err("waiting for block"), ComponentRequests::ActiveBlobRequest { .. } => Err("expecting blob request"), ComponentRequests::ActiveCustodyRequest(request) => Ok(request), + ComponentRequests::ActiveEnvelopeRequest { .. } => Err("expecting envelope request"), ComponentRequests::NotNeeded { .. } => Err("not needed"), } } @@ -215,3 +218,52 @@ impl RequestState for CustodyRequestState { &mut self.state } } + +impl RequestState for EnvelopeRequestState { + type VerifiedResponseType = Arc>; + + fn make_request( + &self, + id: Id, + lookup_peers: Arc>>, + _: usize, + cx: &mut SyncNetworkContext, + ) -> Result { + cx.envelope_lookup_request(id, lookup_peers, self.block_root) + .map_err(LookupRequestError::SendFailedNetwork) + } + + fn send_for_processing( + id: Id, + download_result: DownloadResult, + cx: &SyncNetworkContext, + ) -> Result<(), LookupRequestError> { + let DownloadResult { + value, + block_root, + seen_timestamp, + .. + } = download_result; + cx.send_envelope_for_processing(id, value, seen_timestamp, block_root) + .map_err(LookupRequestError::SendFailedProcessor) + } + + fn response_type() -> ResponseType { + ResponseType::Envelope + } + + fn request_state_mut(request: &mut SingleBlockLookup) -> Result<&mut Self, &'static str> { + match &mut request.component_requests { + ComponentRequests::ActiveEnvelopeRequest(request) => Ok(request), + _ => Err("expecting envelope request"), + } + } + + fn get_state(&self) -> &SingleLookupRequestState { + &self.state + } + + fn get_state_mut(&mut self) -> &mut SingleLookupRequestState { + &mut self.state + } +} diff --git a/beacon_node/network/src/sync/block_lookups/mod.rs b/beacon_node/network/src/sync/block_lookups/mod.rs index 7b4e3ce753..b33c38d147 100644 --- a/beacon_node/network/src/sync/block_lookups/mod.rs +++ b/beacon_node/network/src/sync/block_lookups/mod.rs @@ -39,7 +39,9 @@ use fnv::FnvHashMap; use lighthouse_network::service::api_types::SingleLookupReqId; use lighthouse_network::{PeerAction, PeerId}; use lru_cache::LRUTimeCache; -pub use single_block_lookup::{BlobRequestState, BlockRequestState, CustodyRequestState}; +pub use single_block_lookup::{ + BlobRequestState, BlockRequestState, CustodyRequestState, EnvelopeRequestState, +}; use std::collections::hash_map::Entry; use std::sync::Arc; use std::time::Duration; @@ -344,6 +346,57 @@ impl BlockLookups { self.new_current_lookup(block_root_to_search, None, None, peers, cx) } + /// A block triggers the search of a parent envelope. + #[must_use = "only reference the new lookup if returns true"] + pub fn search_parent_envelope_of_child( + &mut self, + parent_root: Hash256, + peers: &[PeerId], + cx: &mut SyncNetworkContext, + ) -> bool { + // Check if there's already a lookup for this root (could be a block lookup or envelope + // lookup). If so, add peers and let it handle the envelope. + if let Some((&lookup_id, _lookup)) = self + .single_block_lookups + .iter_mut() + .find(|(_, lookup)| lookup.is_for_block(parent_root)) + { + if let Err(e) = self.add_peers_to_lookup_and_ancestors(lookup_id, peers, cx) { + warn!(error = ?e, "Error adding peers to envelope lookup"); + } + return true; + } + + if self.single_block_lookups.len() >= MAX_LOOKUPS { + warn!(?parent_root, "Dropping envelope lookup reached max"); + return false; + } + + let lookup = SingleBlockLookup::new_envelope_only(parent_root, peers, cx.next_id()); + let _guard = lookup.span.clone().entered(); + + let id = lookup.id; + let lookup = match self.single_block_lookups.entry(id) { + Entry::Vacant(entry) => entry.insert(lookup), + Entry::Occupied(_) => { + warn!(id, "Lookup exists with same id"); + return false; + } + }; + + debug!( + ?peers, + ?parent_root, + id = lookup.id, + "Created envelope-only lookup" + ); + metrics::inc_counter(&metrics::SYNC_LOOKUP_CREATED); + self.metrics.created_lookups += 1; + + let result = lookup.continue_requests(cx); + self.on_lookup_result(id, result, "new_envelope_lookup", cx) + } + /// Searches for a single block hash. If the blocks parent is unknown, a chain of blocks is /// constructed. /// Returns true if the lookup is created or already exists @@ -561,17 +614,13 @@ impl BlockLookups { self.on_processing_result_inner::>(id, result, cx) } BlockProcessType::SinglePayloadEnvelope { id, block_root } => { - match result { - BlockProcessingResult::Ok(_) => { - self.continue_envelope_child_lookups(block_root, cx); - } - BlockProcessingResult::Err(e) => { - debug!(%id, error = ?e, "Payload envelope processing failed"); - // TODO(EIP-7732): resolve awaiting_envelope on affected lookups so they can retry - } - _ => {} + let result = self + .on_processing_result_inner::>(id, result, cx); + // On successful envelope import, unblock child lookups waiting for this envelope + if matches!(&result, Ok(LookupResult::Completed)) { + self.continue_envelope_child_lookups(block_root, cx); } - return; + result } }; self.on_lookup_result(process_type.id(), lookup_result, "processing_result", cx); @@ -721,6 +770,7 @@ impl BlockLookups { ResponseType::CustodyColumn => { "lookup_custody_column_processing_failure" } + ResponseType::Envelope => "lookup_envelope_processing_failure", }, ); } @@ -764,22 +814,20 @@ impl BlockLookups { } Action::ParentEnvelopeUnknown { parent_root } => { let peers = lookup.all_peers(); - lookup.set_awaiting_envelope(parent_root); - // Pick a peer to request the envelope from - let peer_id = peers.first().copied().ok_or_else(|| { - LookupRequestError::Failed("No peers available for envelope request".to_owned()) - })?; - match cx.envelope_lookup_request(lookup_id, peer_id, parent_root) { - Ok(_) => { - debug!( - id = lookup_id, - ?block_root, - ?parent_root, - "Requesting missing parent envelope" - ); - Ok(LookupResult::Pending) - } - Err(e) => Err(LookupRequestError::SendFailedNetwork(e)), + lookup.set_awaiting_parent_envelope(parent_root); + let envelope_lookup_exists = self.search_parent_envelope_of_child(parent_root, &peers, cx); + if envelope_lookup_exists { + debug!( + id = lookup_id, + ?block_root, + ?parent_root, + "Marking lookup as awaiting parent envelope" + ); + Ok(LookupResult::Pending) + } else { + Err(LookupRequestError::Failed(format!( + "Envelope lookup could not be created for {parent_root:?}" + ))) } } Action::Drop(reason) => { @@ -858,8 +906,8 @@ impl BlockLookups { let mut lookup_results = vec![]; for (id, lookup) in self.single_block_lookups.iter_mut() { - if lookup.awaiting_envelope() == Some(block_root) { - lookup.resolve_awaiting_envelope(); + if lookup.awaiting_parent_envelope() == Some(block_root) { + lookup.resolve_awaiting_parent_envelope(); debug!( envelope_root = ?block_root, id, @@ -894,7 +942,10 @@ impl BlockLookups { let child_lookups = self .single_block_lookups .iter() - .filter(|(_, lookup)| lookup.awaiting_parent() == Some(dropped_lookup.block_root())) + .filter(|(_, lookup)| { + lookup.awaiting_parent() == Some(dropped_lookup.block_root()) + || lookup.awaiting_parent_envelope() == Some(dropped_lookup.block_root()) + }) .map(|(id, _)| *id) .collect::>(); diff --git a/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs b/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs index 51cc191056..d59753b960 100644 --- a/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs +++ b/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs @@ -16,7 +16,9 @@ use store::Hash256; use strum::IntoStaticStr; use tracing::{Span, debug_span}; use types::data::FixedBlobSidecarList; -use types::{DataColumnSidecarList, EthSpec, SignedBeaconBlock, Slot}; +use types::{ + DataColumnSidecarList, EthSpec, SignedBeaconBlock, SignedExecutionPayloadEnvelope, Slot, +}; // Dedicated enum for LookupResult to force its usage #[must_use = "LookupResult must be handled with on_lookup_result"] @@ -70,7 +72,7 @@ pub struct SingleBlockLookup { peers: Arc>>, block_root: Hash256, awaiting_parent: Option, - awaiting_envelope: Option, + awaiting_parent_envelope: Option, created: Instant, pub(crate) span: Span, } @@ -80,6 +82,7 @@ pub(crate) enum ComponentRequests { WaitingForBlock, ActiveBlobRequest(BlobRequestState, usize), ActiveCustodyRequest(CustodyRequestState), + ActiveEnvelopeRequest(EnvelopeRequestState), // When printing in debug this state display the reason why it's not needed #[allow(dead_code)] NotNeeded(&'static str), @@ -105,12 +108,26 @@ impl SingleBlockLookup { peers: Arc::new(RwLock::new(HashSet::from_iter(peers.iter().copied()))), block_root: requested_block_root, awaiting_parent, - awaiting_envelope: None, + awaiting_parent_envelope: None, created: Instant::now(), span: lookup_span, } } + /// Create an envelope-only lookup. The block is already imported, we just need the envelope. + pub fn new_envelope_only(block_root: Hash256, peers: &[PeerId], id: Id) -> Self { + let mut lookup = Self::new(block_root, peers, id, None); + // Block is already imported, mark as completed + lookup + .block_request_state + .state + .on_completed_request("block already imported") + .expect("block state starts as AwaitingDownload"); + lookup.component_requests = + ComponentRequests::ActiveEnvelopeRequest(EnvelopeRequestState::new(block_root)); + lookup + } + /// Reset the status of all internal requests pub fn reset_requests(&mut self) { self.block_request_state = BlockRequestState::new(self.block_root); @@ -146,18 +163,18 @@ impl SingleBlockLookup { self.awaiting_parent = None; } - pub fn awaiting_envelope(&self) -> Option { - self.awaiting_envelope + pub fn awaiting_parent_envelope(&self) -> Option { + self.awaiting_parent_envelope } /// Mark this lookup as awaiting a parent envelope to be imported before processing. - pub fn set_awaiting_envelope(&mut self, parent_root: Hash256) { - self.awaiting_envelope = Some(parent_root); + pub fn set_awaiting_parent_envelope(&mut self, parent_root: Hash256) { + self.awaiting_parent_envelope = Some(parent_root); } /// Mark this lookup as no longer awaiting a parent envelope. - pub fn resolve_awaiting_envelope(&mut self) { - self.awaiting_envelope = None; + pub fn resolve_awaiting_parent_envelope(&mut self) { + self.awaiting_parent_envelope = None; } /// Returns the time elapsed since this lookup was created @@ -194,6 +211,7 @@ impl SingleBlockLookup { ComponentRequests::WaitingForBlock => false, ComponentRequests::ActiveBlobRequest(request, _) => request.state.is_processed(), ComponentRequests::ActiveCustodyRequest(request) => request.state.is_processed(), + ComponentRequests::ActiveEnvelopeRequest(request) => request.state.is_processed(), ComponentRequests::NotNeeded { .. } => true, } } @@ -201,7 +219,7 @@ impl SingleBlockLookup { /// Returns true if this request is expecting some event to make progress pub fn is_awaiting_event(&self) -> bool { self.awaiting_parent.is_some() - || self.awaiting_envelope.is_some() + || self.awaiting_parent_envelope.is_some() || self.block_request_state.state.is_awaiting_event() || match &self.component_requests { // If components are waiting for the block request to complete, here we should @@ -214,6 +232,9 @@ impl SingleBlockLookup { ComponentRequests::ActiveCustodyRequest(request) => { request.state.is_awaiting_event() } + ComponentRequests::ActiveEnvelopeRequest(request) => { + request.state.is_awaiting_event() + } ComponentRequests::NotNeeded { .. } => false, } } @@ -283,6 +304,9 @@ impl SingleBlockLookup { ComponentRequests::ActiveCustodyRequest(_) => { self.continue_request::>(cx, 0)? } + ComponentRequests::ActiveEnvelopeRequest(_) => { + self.continue_request::>(cx, 0)? + } ComponentRequests::NotNeeded { .. } => {} // do nothing } @@ -304,7 +328,8 @@ impl SingleBlockLookup { expected_blobs: usize, ) -> Result<(), LookupRequestError> { let id = self.id; - let awaiting_event = self.awaiting_parent.is_some() || self.awaiting_envelope.is_some(); + let awaiting_event = + self.awaiting_parent.is_some() || self.awaiting_parent_envelope.is_some(); let request = R::request_state_mut(self).map_err(|e| LookupRequestError::BadState(e.to_owned()))?; @@ -444,6 +469,26 @@ impl BlockRequestState { } } +/// The state of the envelope request component of a `SingleBlockLookup`. +/// Used for envelope-only lookups where the parent block is already imported +/// but its execution payload envelope is missing. +#[derive(Educe)] +#[educe(Debug)] +pub struct EnvelopeRequestState { + #[educe(Debug(ignore))] + pub block_root: Hash256, + pub state: SingleLookupRequestState>>, +} + +impl EnvelopeRequestState { + pub fn new(block_root: Hash256) -> Self { + Self { + block_root, + state: SingleLookupRequestState::new(), + } + } +} + #[derive(Debug, Clone)] pub struct DownloadResult { pub value: T, diff --git a/beacon_node/network/src/sync/manager.rs b/beacon_node/network/src/sync/manager.rs index 256752d5fb..2cc35081b7 100644 --- a/beacon_node/network/src/sync/manager.rs +++ b/beacon_node/network/src/sync/manager.rs @@ -45,6 +45,7 @@ use crate::service::NetworkMessage; use crate::status::ToStatusMessage; use crate::sync::block_lookups::{ BlobRequestState, BlockComponent, BlockRequestState, CustodyRequestState, DownloadResult, + EnvelopeRequestState, }; use crate::sync::custody_backfill_sync::CustodyBackFillSync; use crate::sync::network_context::{PeerGroup, RpcResponseResult}; @@ -1278,27 +1279,14 @@ impl SyncManager { .network .on_single_envelope_response(id, peer_id, rpc_event) { - match resp { - Ok((envelope, seen_timestamp)) => { - let block_root = envelope.beacon_block_root(); - debug!( - ?block_root, - %id, - "Downloaded payload envelope, sending for processing" - ); - if let Err(e) = self.network.send_envelope_for_processing( - id.req_id, - envelope, - seen_timestamp, - block_root, - ) { - error!(error = ?e, "Failed to send envelope for processing"); - } - } - Err(e) => { - debug!(error = ?e, %id, "Payload envelope download failed"); - } - } + self.block_lookups + .on_download_response::>( + id, + resp.map(|(value, seen_timestamp)| { + (value, PeerGroup::from_single(peer_id), seen_timestamp) + }), + &mut self.network, + ) } } diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index e9d289b777..328940d672 100644 --- a/beacon_node/network/src/sync/network_context.rs +++ b/beacon_node/network/src/sync/network_context.rs @@ -944,9 +944,26 @@ impl SyncNetworkContext { pub fn envelope_lookup_request( &mut self, lookup_id: SingleLookupId, - peer_id: PeerId, + lookup_peers: Arc>>, block_root: Hash256, - ) -> Result { + ) -> Result { + let active_request_count_by_peer = self.active_request_count_by_peer(); + let Some(peer_id) = lookup_peers + .read() + .iter() + .map(|peer| { + ( + active_request_count_by_peer.get(peer).copied().unwrap_or(0), + rand::random::(), + peer, + ) + }) + .min() + .map(|(_, _, peer)| *peer) + else { + return Ok(LookupRequestResult::Pending("no peers")); + }; + let id = SingleLookupReqId { lookup_id, req_id: self.next_id(), @@ -988,7 +1005,7 @@ impl SyncNetworkContext { request_span, ); - Ok(id.req_id) + Ok(LookupRequestResult::RequestSent(id.req_id)) } /// Request necessary blobs for `block_root`. Requests only the necessary blobs by checking: From 3523804515f9e05f8c1782152694d35a1951b0e5 Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Thu, 2 Apr 2026 19:30:12 -0700 Subject: [PATCH 038/118] cleanup --- .../src/beacon/execution_payload_envelope.rs | 15 ++++--- .../network/src/sync/block_lookups/mod.rs | 44 ++++++++++++++++++- beacon_node/network/src/sync/manager.rs | 38 +++++++++++++++- 3 files changed, 88 insertions(+), 9 deletions(-) diff --git a/beacon_node/http_api/src/beacon/execution_payload_envelope.rs b/beacon_node/http_api/src/beacon/execution_payload_envelope.rs index ea8c0d4b8a..7f81f7bf25 100644 --- a/beacon_node/http_api/src/beacon/execution_payload_envelope.rs +++ b/beacon_node/http_api/src/beacon/execution_payload_envelope.rs @@ -132,18 +132,21 @@ pub async fn publish_execution_payload_envelope( }; let ctx = chain.gossip_verification_context(); - let Ok(gossip_verifed_envelope) = GossipVerifiedEnvelope::new(signed_envelope, &ctx) else { - warn!(%slot, %beacon_block_root, "Execution payload envelope rejected"); - return Err(warp_utils::reject::custom_bad_request( - "execution payload envelope rejected, gossip verification".to_string(), - )); + let gossip_verified_envelope = match GossipVerifiedEnvelope::new(signed_envelope, &ctx) { + Ok(envelope) => envelope, + Err(e) => { + warn!(%slot, %beacon_block_root, error = ?e, "Execution payload envelope rejected"); + return Err(warp_utils::reject::custom_bad_request(format!( + "execution payload envelope rejected: {e:?}", + ))); + } }; // Import the envelope locally (runs state transition and notifies the EL). chain .process_execution_payload_envelope( beacon_block_root, - gossip_verifed_envelope, + gossip_verified_envelope, NotifyExecutionLayer::Yes, BlockImportSource::HttpApi, publish_fn, diff --git a/beacon_node/network/src/sync/block_lookups/mod.rs b/beacon_node/network/src/sync/block_lookups/mod.rs index b33c38d147..4d14479627 100644 --- a/beacon_node/network/src/sync/block_lookups/mod.rs +++ b/beacon_node/network/src/sync/block_lookups/mod.rs @@ -228,6 +228,47 @@ impl BlockLookups { } } + /// A child block's parent envelope is missing. Create a child lookup (with the block component) + /// that waits for the parent envelope, and an envelope-only lookup for the parent. + /// + /// Returns true if both lookups are created or already exist. + #[must_use = "only reference the new lookup if returns true"] + pub fn search_child_and_parent_envelope( + &mut self, + block_root: Hash256, + block_component: BlockComponent, + parent_root: Hash256, + peer_id: PeerId, + cx: &mut SyncNetworkContext, + ) -> bool { + let envelope_lookup_exists = + self.search_parent_envelope_of_child(parent_root, &[peer_id], cx); + if envelope_lookup_exists { + // Create child lookup that waits for the parent envelope (not parent block). + // The child block itself is available, so we pass it as a component. + let child_created = self.new_current_lookup( + block_root, + Some(block_component), + None, // not awaiting parent block + &[], + cx, + ); + // Set awaiting_parent_envelope on the child lookup + if child_created { + if let Some((_, lookup)) = self + .single_block_lookups + .iter_mut() + .find(|(_, l)| l.is_for_block(block_root)) + { + lookup.set_awaiting_parent_envelope(parent_root); + } + } + child_created + } else { + false + } + } + /// Seach a block whose parent root is unknown. /// /// Returns true if the lookup is created or already exists @@ -815,7 +856,8 @@ impl BlockLookups { Action::ParentEnvelopeUnknown { parent_root } => { let peers = lookup.all_peers(); lookup.set_awaiting_parent_envelope(parent_root); - let envelope_lookup_exists = self.search_parent_envelope_of_child(parent_root, &peers, cx); + let envelope_lookup_exists = + self.search_parent_envelope_of_child(parent_root, &peers, cx); if envelope_lookup_exists { debug!( id = lookup_id, diff --git a/beacon_node/network/src/sync/manager.rs b/beacon_node/network/src/sync/manager.rs index 2cc35081b7..1ca338ccd3 100644 --- a/beacon_node/network/src/sync/manager.rs +++ b/beacon_node/network/src/sync/manager.rs @@ -935,9 +935,9 @@ impl SyncManager { debug!( %block_root, %parent_root, - "Parent envelope not yet available, creating lookup" + "Parent envelope not yet available, creating envelope lookup" ); - self.handle_unknown_parent( + self.handle_unknown_parent_envelope( peer_id, block_root, parent_root, @@ -1055,6 +1055,40 @@ impl SyncManager { } } + /// Handle a block whose parent block is known but parent envelope is missing. + /// Creates an envelope-only lookup for the parent and a child lookup that waits for it. + fn handle_unknown_parent_envelope( + &mut self, + peer_id: PeerId, + block_root: Hash256, + parent_root: Hash256, + slot: Slot, + block_component: BlockComponent, + ) { + match self.should_search_for_block(Some(slot), &peer_id) { + Ok(_) => { + if self.block_lookups.search_child_and_parent_envelope( + block_root, + block_component, + parent_root, + peer_id, + &mut self.network, + ) { + // Lookups created + } else { + debug!( + ?block_root, + ?parent_root, + "No lookup created for child and parent envelope" + ); + } + } + Err(reason) => { + debug!(%block_root, %parent_root, reason, "Ignoring unknown parent envelope request"); + } + } + } + fn handle_unknown_block_root(&mut self, peer_id: PeerId, block_root: Hash256) { match self.should_search_for_block(None, &peer_id) { Ok(_) => { From 1cd4d57204f9283bbbceba46585476385a1c0c53 Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Thu, 2 Apr 2026 19:37:51 -0700 Subject: [PATCH 039/118] Fixes --- beacon_node/network/src/router.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beacon_node/network/src/router.rs b/beacon_node/network/src/router.rs index 3fb2196975..3d82252a0c 100644 --- a/beacon_node/network/src/router.rs +++ b/beacon_node/network/src/router.rs @@ -336,7 +336,7 @@ impl Router { // TODO(EIP-7732): implement outgoing payload envelopes by range responses once // range sync requests them. Response::PayloadEnvelopesByRange(_) => { - unreachable!() + error!(%peer_id, "Unexpected PayloadEnvelopesByRange response"); } // Light client responses should not be received Response::LightClientBootstrap(_) From 214e3ce9f0ec4a4042e085c213b267fec63342f6 Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Fri, 3 Apr 2026 00:02:24 -0700 Subject: [PATCH 040/118] Cleanup --- .../beacon_chain/src/block_verification.rs | 37 ++++++++++++++- .../src/beacon/execution_payload_envelope.rs | 1 - .../gossip_methods.rs | 4 +- .../network_beacon_processor/sync_methods.rs | 8 +--- .../network/src/sync/block_lookups/mod.rs | 47 ++++++++++++------- 5 files changed, 72 insertions(+), 25 deletions(-) diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 916a207e62..2b46843901 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -60,6 +60,7 @@ use crate::execution_payload::{ }; use crate::kzg_utils::blobs_to_data_column_sidecars; use crate::observed_block_producers::SeenBlock; +use crate::payload_envelope_verification::EnvelopeError; use crate::validator_monitor::HISTORIC_EPOCHS as VALIDATOR_MONITOR_HISTORIC_EPOCHS; use crate::validator_pubkey_cache::ValidatorPubkeyCache; use crate::{ @@ -321,13 +322,18 @@ pub enum BlockError { bid_parent_root: Hash256, block_parent_root: Hash256, }, - /// The parent block is known but its execution payload envelope has not been received yet. + /// The child block is known but its parent execution payload envelope has not been received yet. /// /// ## Peer scoring /// /// It's unclear if this block is valid, but it cannot be fully verified without the parent's /// execution payload envelope. ParentEnvelopeUnknown { parent_root: Hash256 }, + + PayloadEnvelopeError { + e: Box, + penalize_peer: bool, + }, } /// Which specific signature(s) are invalid in a SignedBeaconBlock @@ -494,6 +500,35 @@ impl From for BlockError { } } +impl From for BlockError { + fn from(e: EnvelopeError) -> Self { + let penalize_peer = match &e { + // REJECT per spec: peer sent invalid envelope data + EnvelopeError::BadSignature + | EnvelopeError::BuilderIndexMismatch { .. } + | EnvelopeError::BlockHashMismatch { .. } + | EnvelopeError::SlotMismatch { .. } + | EnvelopeError::IncorrectBlockProposer { .. } => true, + // IGNORE per spec: not the peer's fault + EnvelopeError::BlockRootUnknown { .. } + | EnvelopeError::PriorToFinalization { .. } + | EnvelopeError::UnknownValidator { .. } => false, + // Internal errors: not the peer's fault + EnvelopeError::BeaconChainError(_) + | EnvelopeError::BeaconStateError(_) + | EnvelopeError::BlockProcessingError(_) + | EnvelopeError::EnvelopeProcessingError(_) + | EnvelopeError::ExecutionPayloadError(_) + | EnvelopeError::BlockError(_) + | EnvelopeError::InternalError(_) => false, + }; + BlockError::PayloadEnvelopeError { + e: Box::new(e), + penalize_peer, + } + } +} + /// Stores information about verifying a payload against an execution engine. #[derive(Debug, PartialEq, Clone, Encode, Decode)] pub struct PayloadVerificationOutcome { diff --git a/beacon_node/http_api/src/beacon/execution_payload_envelope.rs b/beacon_node/http_api/src/beacon/execution_payload_envelope.rs index 7f81f7bf25..3479d62f6a 100644 --- a/beacon_node/http_api/src/beacon/execution_payload_envelope.rs +++ b/beacon_node/http_api/src/beacon/execution_payload_envelope.rs @@ -90,7 +90,6 @@ pub(crate) fn post_beacon_execution_payload_envelope( .boxed() } /// Publishes a signed execution payload envelope to the network. -/// TODO(gloas): Add gossip verification (BroadcastValidation::Gossip) before import. pub async fn publish_execution_payload_envelope( envelope: SignedExecutionPayloadEnvelope, chain: Arc>, 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 2e04847630..fe9e1755b6 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -1390,7 +1390,9 @@ impl NetworkBeaconProcessor { return None; } // BlobNotRequired is unreachable. Only constructed in `process_gossip_blob` - Err(e @ BlockError::InternalError(_)) | Err(e @ BlockError::BlobNotRequired(_)) => { + Err(e @ BlockError::InternalError(_)) + | Err(e @ BlockError::BlobNotRequired(_)) + | Err(e @ BlockError::PayloadEnvelopeError { .. }) => { error!(error = %e, "Internal block gossip validation error"); return None; } diff --git a/beacon_node/network/src/network_beacon_processor/sync_methods.rs b/beacon_node/network/src/network_beacon_processor/sync_methods.rs index f6d4940121..57d3d7d220 100644 --- a/beacon_node/network/src/network_beacon_processor/sync_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/sync_methods.rs @@ -109,9 +109,7 @@ impl NetworkBeaconProcessor { ); self.send_sync_message(SyncMessage::BlockComponentProcessed { process_type, - result: BlockProcessingResult::Err(BlockError::InternalError(format!( - "Envelope verification failed: {e:?}" - ))), + result: BlockProcessingResult::Err(e.into()), }); return; } @@ -138,9 +136,7 @@ impl NetworkBeaconProcessor { ?beacon_block_root, "RPC payload envelope processing failed" ); - BlockProcessingResult::Err(BlockError::InternalError(format!( - "Envelope processing failed: {e:?}" - ))) + BlockProcessingResult::Err(e.into()) } }; diff --git a/beacon_node/network/src/sync/block_lookups/mod.rs b/beacon_node/network/src/sync/block_lookups/mod.rs index 4d14479627..8a183a0b1b 100644 --- a/beacon_node/network/src/sync/block_lookups/mod.rs +++ b/beacon_node/network/src/sync/block_lookups/mod.rs @@ -217,6 +217,7 @@ impl BlockLookups { block_root, Some(block_component), Some(parent_root), + None, // On a `UnknownParentBlock` or `UnknownParentBlob` event the peer is not required // to have the rest of the block components (refer to decoupled blob gossip). Create // the lookup with zero peers to house the block components. @@ -246,30 +247,20 @@ impl BlockLookups { if envelope_lookup_exists { // Create child lookup that waits for the parent envelope (not parent block). // The child block itself is available, so we pass it as a component. - let child_created = self.new_current_lookup( + self.new_current_lookup( block_root, Some(block_component), None, // not awaiting parent block + Some(parent_root), &[], cx, - ); - // Set awaiting_parent_envelope on the child lookup - if child_created { - if let Some((_, lookup)) = self - .single_block_lookups - .iter_mut() - .find(|(_, l)| l.is_for_block(block_root)) - { - lookup.set_awaiting_parent_envelope(parent_root); - } - } - child_created + ) } else { false } } - /// Seach a block whose parent root is unknown. + /// Search a block whose parent root is unknown. /// /// Returns true if the lookup is created or already exists #[must_use = "only reference the new lookup if returns true"] @@ -279,7 +270,7 @@ impl BlockLookups { peer_source: &[PeerId], cx: &mut SyncNetworkContext, ) -> bool { - self.new_current_lookup(block_root, None, None, peer_source, cx) + self.new_current_lookup(block_root, None, None, None, peer_source, cx) } /// A block or blob triggers the search of a parent. @@ -384,7 +375,7 @@ impl BlockLookups { } // `block_root_to_search` is a failed chain check happens inside new_current_lookup - self.new_current_lookup(block_root_to_search, None, None, peers, cx) + self.new_current_lookup(block_root_to_search, None, None, None, peers, cx) } /// A block triggers the search of a parent envelope. @@ -447,6 +438,7 @@ impl BlockLookups { block_root: Hash256, block_component: Option>, awaiting_parent: Option, + awaiting_parent_envelope: Option, peers: &[PeerId], cx: &mut SyncNetworkContext, ) -> bool { @@ -501,6 +493,9 @@ impl BlockLookups { // If we know that this lookup has unknown parent (is awaiting a parent lookup to resolve), // signal here to hold processing downloaded data. let mut lookup = SingleBlockLookup::new(block_root, peers, cx.next_id(), awaiting_parent); + if let Some(parent_root) = awaiting_parent_envelope { + lookup.set_awaiting_parent_envelope(parent_root); + } let _guard = lookup.span.clone().entered(); // Add block components to the new request @@ -777,6 +772,26 @@ impl BlockLookups { // We opt to drop the lookup instead. Action::Drop(format!("{e:?}")) } + BlockError::PayloadEnvelopeError { e, penalize_peer } => { + debug!( + ?block_root, + error = ?e, + "Payload envelope processing error" + ); + if penalize_peer { + let peer_group = request_state.on_processing_failure()?; + for peer in peer_group.all() { + cx.report_peer( + *peer, + PeerAction::MidToleranceError, + "lookup_envelope_processing_failure", + ); + } + Action::Retry + } else { + Action::Drop(format!("{e:?}")) + } + } other => { debug!( ?block_root, From f897215684c8b1a91fc0d95f991f8e1aee17a96f Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Fri, 3 Apr 2026 01:02:57 -0700 Subject: [PATCH 041/118] refactor awaiting_parent field and some metrics --- .../network/src/sync/block_lookups/mod.rs | 50 +++++++------- .../src/sync/block_lookups/parent_chain.rs | 2 +- .../sync/block_lookups/single_block_lookup.rs | 66 ++++++++++++------- .../network/src/sync/network_context.rs | 4 ++ 4 files changed, 69 insertions(+), 53 deletions(-) diff --git a/beacon_node/network/src/sync/block_lookups/mod.rs b/beacon_node/network/src/sync/block_lookups/mod.rs index 8a183a0b1b..8dedcba2f4 100644 --- a/beacon_node/network/src/sync/block_lookups/mod.rs +++ b/beacon_node/network/src/sync/block_lookups/mod.rs @@ -22,7 +22,9 @@ use self::parent_chain::{NodeChain, compute_parent_chains}; pub use self::single_block_lookup::DownloadResult; -use self::single_block_lookup::{LookupRequestError, LookupResult, SingleBlockLookup}; +use self::single_block_lookup::{ + AwaitingParent, LookupRequestError, LookupResult, SingleBlockLookup, +}; use super::manager::{BlockProcessType, BlockProcessingResult, SLOT_IMPORT_TOLERANCE}; use super::network_context::{PeerGroup, RpcResponseError, SyncNetworkContext}; use crate::metrics; @@ -216,8 +218,7 @@ impl BlockLookups { self.new_current_lookup( block_root, Some(block_component), - Some(parent_root), - None, + Some(AwaitingParent::Block(parent_root)), // On a `UnknownParentBlock` or `UnknownParentBlob` event the peer is not required // to have the rest of the block components (refer to decoupled blob gossip). Create // the lookup with zero peers to house the block components. @@ -250,8 +251,7 @@ impl BlockLookups { self.new_current_lookup( block_root, Some(block_component), - None, // not awaiting parent block - Some(parent_root), + Some(AwaitingParent::Envelope(parent_root)), &[], cx, ) @@ -270,7 +270,7 @@ impl BlockLookups { peer_source: &[PeerId], cx: &mut SyncNetworkContext, ) -> bool { - self.new_current_lookup(block_root, None, None, None, peer_source, cx) + self.new_current_lookup(block_root, None, None, peer_source, cx) } /// A block or blob triggers the search of a parent. @@ -375,7 +375,7 @@ impl BlockLookups { } // `block_root_to_search` is a failed chain check happens inside new_current_lookup - self.new_current_lookup(block_root_to_search, None, None, None, peers, cx) + self.new_current_lookup(block_root_to_search, None, None, peers, cx) } /// A block triggers the search of a parent envelope. @@ -437,8 +437,7 @@ impl BlockLookups { &mut self, block_root: Hash256, block_component: Option>, - awaiting_parent: Option, - awaiting_parent_envelope: Option, + awaiting_parent: Option, peers: &[PeerId], cx: &mut SyncNetworkContext, ) -> bool { @@ -473,13 +472,14 @@ impl BlockLookups { } // Ensure that awaiting parent exists, otherwise this lookup won't be able to make progress - if let Some(awaiting_parent) = awaiting_parent + if let Some(AwaitingParent::Block(parent_root) | AwaitingParent::Envelope(parent_root)) = + awaiting_parent && !self .single_block_lookups .iter() - .any(|(_, lookup)| lookup.is_for_block(awaiting_parent)) + .any(|(_, lookup)| lookup.is_for_block(parent_root)) { - warn!(block_root = ?awaiting_parent, "Ignoring child lookup parent lookup not found"); + warn!(block_root = ?parent_root, "Ignoring child lookup parent lookup not found"); return false; } @@ -493,9 +493,6 @@ impl BlockLookups { // If we know that this lookup has unknown parent (is awaiting a parent lookup to resolve), // signal here to hold processing downloaded data. let mut lookup = SingleBlockLookup::new(block_root, peers, cx.next_id(), awaiting_parent); - if let Some(parent_root) = awaiting_parent_envelope { - lookup.set_awaiting_parent_envelope(parent_root); - } let _guard = lookup.span.clone().entered(); // Add block components to the new request @@ -516,9 +513,7 @@ impl BlockLookups { debug!( ?peers, ?block_root, - awaiting_parent = awaiting_parent - .map(|root| root.to_string()) - .unwrap_or("none".to_owned()), + ?awaiting_parent, id = lookup.id, "Created block lookup" ); @@ -936,7 +931,7 @@ impl BlockLookups { let mut lookup_results = vec![]; // < need to buffer lookup results to not re-borrow &mut self for (id, lookup) in self.single_block_lookups.iter_mut() { - if lookup.awaiting_parent() == Some(block_root) { + if lookup.awaiting_parent_block() == Some(block_root) { lookup.resolve_awaiting_parent(); debug!( parent_root = ?block_root, @@ -964,7 +959,7 @@ impl BlockLookups { for (id, lookup) in self.single_block_lookups.iter_mut() { if lookup.awaiting_parent_envelope() == Some(block_root) { - lookup.resolve_awaiting_parent_envelope(); + lookup.resolve_awaiting_parent(); debug!( envelope_root = ?block_root, id, @@ -996,12 +991,13 @@ impl BlockLookups { metrics::inc_counter_vec(&metrics::SYNC_LOOKUP_DROPPED, &[reason]); self.metrics.dropped_lookups += 1; + let dropped_root = dropped_lookup.block_root(); let child_lookups = self .single_block_lookups .iter() .filter(|(_, lookup)| { - lookup.awaiting_parent() == Some(dropped_lookup.block_root()) - || lookup.awaiting_parent_envelope() == Some(dropped_lookup.block_root()) + lookup.awaiting_parent_block() == Some(dropped_root) + || lookup.awaiting_parent_envelope() == Some(dropped_root) }) .map(|(id, _)| *id) .collect::>(); @@ -1170,17 +1166,15 @@ impl BlockLookups { &'a self, lookup: &'a SingleBlockLookup, ) -> Result<&'a SingleBlockLookup, String> { - if let Some(awaiting_parent) = lookup.awaiting_parent() { + if let Some(parent_root) = lookup.awaiting_parent_block() { if let Some(lookup) = self .single_block_lookups .values() - .find(|l| l.block_root() == awaiting_parent) + .find(|l| l.block_root() == parent_root) { self.find_oldest_ancestor_lookup(lookup) } else { - Err(format!( - "Lookup references unknown parent {awaiting_parent:?}" - )) + Err(format!("Lookup references unknown parent {parent_root:?}")) } } else { Ok(lookup) @@ -1213,7 +1207,7 @@ impl BlockLookups { } } - if let Some(parent_root) = lookup.awaiting_parent() { + if let Some(parent_root) = lookup.awaiting_parent_block() { if let Some((&child_id, _)) = self .single_block_lookups .iter() diff --git a/beacon_node/network/src/sync/block_lookups/parent_chain.rs b/beacon_node/network/src/sync/block_lookups/parent_chain.rs index 5deea1dd94..18363e9b8d 100644 --- a/beacon_node/network/src/sync/block_lookups/parent_chain.rs +++ b/beacon_node/network/src/sync/block_lookups/parent_chain.rs @@ -13,7 +13,7 @@ impl From<&SingleBlockLookup> for Node { fn from(value: &SingleBlockLookup) -> Self { Self { block_root: value.block_root(), - parent_root: value.awaiting_parent(), + parent_root: value.awaiting_parent_block(), } } } diff --git a/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs b/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs index d59753b960..6687a1ec75 100644 --- a/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs +++ b/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs @@ -58,6 +58,14 @@ pub enum LookupRequestError { }, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AwaitingParent { + /// Waiting for the parent block to be imported. + Block(Hash256), + /// The parent block is imported but its execution payload envelope is missing. + Envelope(Hash256), +} + #[derive(Educe)] #[educe(Debug(bound(T: BeaconChainTypes)))] pub struct SingleBlockLookup { @@ -71,8 +79,7 @@ pub struct SingleBlockLookup { #[educe(Debug(method(fmt_peer_set_as_len)))] peers: Arc>>, block_root: Hash256, - awaiting_parent: Option, - awaiting_parent_envelope: Option, + awaiting_parent: Option, created: Instant, pub(crate) span: Span, } @@ -93,7 +100,7 @@ impl SingleBlockLookup { requested_block_root: Hash256, peers: &[PeerId], id: Id, - awaiting_parent: Option, + awaiting_parent: Option, ) -> Self { let lookup_span = debug_span!( "lh_single_block_lookup", @@ -108,7 +115,6 @@ impl SingleBlockLookup { peers: Arc::new(RwLock::new(HashSet::from_iter(peers.iter().copied()))), block_root: requested_block_root, awaiting_parent, - awaiting_parent_envelope: None, created: Instant::now(), span: lookup_span, } @@ -131,7 +137,16 @@ impl SingleBlockLookup { /// Reset the status of all internal requests pub fn reset_requests(&mut self) { self.block_request_state = BlockRequestState::new(self.block_root); - self.component_requests = ComponentRequests::WaitingForBlock; + match &self.component_requests { + ComponentRequests::ActiveEnvelopeRequest(_) => { + self.component_requests = ComponentRequests::ActiveEnvelopeRequest( + EnvelopeRequestState::new(self.block_root), + ); + } + _ => { + self.component_requests = ComponentRequests::WaitingForBlock; + } + } } /// Return the slot of this lookup's block if it's currently cached as `AwaitingProcessing` @@ -147,34 +162,39 @@ impl SingleBlockLookup { self.block_root } - pub fn awaiting_parent(&self) -> Option { + pub fn awaiting_parent(&self) -> Option { self.awaiting_parent } - /// Mark this lookup as awaiting a parent lookup from being processed. Meanwhile don't send - /// components for processing. - pub fn set_awaiting_parent(&mut self, parent_root: Hash256) { - self.awaiting_parent = Some(parent_root) - } - - /// Mark this lookup as no longer awaiting a parent lookup. Components can be sent for - /// processing. - pub fn resolve_awaiting_parent(&mut self) { - self.awaiting_parent = None; + /// Returns the parent root if awaiting a parent block. + pub fn awaiting_parent_block(&self) -> Option { + match self.awaiting_parent { + Some(AwaitingParent::Block(root)) => Some(root), + _ => None, + } } + /// Returns the parent root if awaiting a parent envelope. pub fn awaiting_parent_envelope(&self) -> Option { - self.awaiting_parent_envelope + match self.awaiting_parent { + Some(AwaitingParent::Envelope(root)) => Some(root), + _ => None, + } + } + + /// Mark this lookup as awaiting a parent block to be imported before processing. + pub fn set_awaiting_parent(&mut self, parent_root: Hash256) { + self.awaiting_parent = Some(AwaitingParent::Block(parent_root)); } /// Mark this lookup as awaiting a parent envelope to be imported before processing. pub fn set_awaiting_parent_envelope(&mut self, parent_root: Hash256) { - self.awaiting_parent_envelope = Some(parent_root); + self.awaiting_parent = Some(AwaitingParent::Envelope(parent_root)); } - /// Mark this lookup as no longer awaiting a parent envelope. - pub fn resolve_awaiting_parent_envelope(&mut self) { - self.awaiting_parent_envelope = None; + /// Mark this lookup as no longer awaiting any parent. + pub fn resolve_awaiting_parent(&mut self) { + self.awaiting_parent = None; } /// Returns the time elapsed since this lookup was created @@ -219,7 +239,6 @@ impl SingleBlockLookup { /// Returns true if this request is expecting some event to make progress pub fn is_awaiting_event(&self) -> bool { self.awaiting_parent.is_some() - || self.awaiting_parent_envelope.is_some() || self.block_request_state.state.is_awaiting_event() || match &self.component_requests { // If components are waiting for the block request to complete, here we should @@ -328,8 +347,7 @@ impl SingleBlockLookup { expected_blobs: usize, ) -> Result<(), LookupRequestError> { let id = self.id; - let awaiting_event = - self.awaiting_parent.is_some() || self.awaiting_parent_envelope.is_some(); + let awaiting_event = self.awaiting_parent.is_some(); let request = R::request_state_mut(self).map_err(|e| LookupRequestError::BadState(e.to_owned()))?; diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index 328940d672..1176442202 100644 --- a/beacon_node/network/src/sync/network_context.rs +++ b/beacon_node/network/src/sync/network_context.rs @@ -1917,6 +1917,10 @@ impl SyncNetworkContext { "data_columns_by_range", self.data_columns_by_range_requests.len(), ), + ( + "payload_envelopes_by_root", + self.payload_envelopes_by_root_requests.len(), + ), ("custody_by_root", self.custody_by_root_requests.len()), ( "components_by_range", From b333841229c45349bf5226f7c12af77a577b50b6 Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Fri, 3 Apr 2026 01:04:34 -0700 Subject: [PATCH 042/118] update --- beacon_node/network/src/sync/block_lookups/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/beacon_node/network/src/sync/block_lookups/mod.rs b/beacon_node/network/src/sync/block_lookups/mod.rs index 8dedcba2f4..27d96de51d 100644 --- a/beacon_node/network/src/sync/block_lookups/mod.rs +++ b/beacon_node/network/src/sync/block_lookups/mod.rs @@ -246,8 +246,8 @@ impl BlockLookups { let envelope_lookup_exists = self.search_parent_envelope_of_child(parent_root, &[peer_id], cx); if envelope_lookup_exists { - // Create child lookup that waits for the parent envelope (not parent block). - // The child block itself is available, so we pass it as a component. + // Create child lookup that waits for the parent envelope. + // The child block itself has already been seen, so we pass it as a component. self.new_current_lookup( block_root, Some(block_component), From 3112792435ef80482505286ec4ad991bc08f34d2 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Fri, 3 Apr 2026 17:42:22 +0900 Subject: [PATCH 043/118] Apply suggestion from @eserilev --- beacon_node/beacon_chain/src/block_verification.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index d6cf15027c..910e966666 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -322,7 +322,7 @@ pub enum BlockError { bid_parent_root: Hash256, block_parent_root: Hash256, }, - /// The child block is known but its parent execution payload envelope has not been received yet. + /// Th block is known but its parent execution payload envelope has not been received yet. /// /// ## Peer scoring /// From 34e5f89537986ffb689e51fdbde1a7e472ab7061 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Fri, 3 Apr 2026 17:42:41 +0900 Subject: [PATCH 044/118] Apply suggestion from @eserilev --- beacon_node/beacon_chain/src/block_verification.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 910e966666..36b0b54e62 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -322,7 +322,7 @@ pub enum BlockError { bid_parent_root: Hash256, block_parent_root: Hash256, }, - /// Th block is known but its parent execution payload envelope has not been received yet. + /// The block is known but its parent execution payload envelope has not been received yet. /// /// ## Peer scoring /// From ca59cf453ebc7e12a7920089578eb55d69de4cba Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Thu, 23 Apr 2026 02:34:34 +0900 Subject: [PATCH 045/118] Merge conflicts' --- .../beacon_chain/src/block_verification.rs | 20 +++++++++++++------ .../src/beacon/execution_payload_envelope.rs | 2 +- beacon_node/network/src/router.rs | 2 +- beacon_node/network/src/sync/manager.rs | 2 +- consensus/fork_choice/src/fork_choice.rs | 5 +++++ 5 files changed, 22 insertions(+), 9 deletions(-) diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 8f723ebb11..f4d8b80c42 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -940,12 +940,20 @@ impl GossipVerifiedBlock { }); } - // TODO(gloas) The following validation can only be completed once fork choice has been implemented: - // The block's parent execution payload (defined by bid.parent_block_hash) has been seen - // (via gossip or non-gossip sources) (a client MAY queue blocks for processing - // once the parent payload is retrieved). If execution_payload verification of block's execution - // payload parent by an execution node is complete, verify the block's execution payload - // parent (defined by bid.parent_block_hash) passes all validation. + // Check that we've received the parent envelope. If not, issue a single envelope + // lookup for the parent and queue this block in the reprocess queue. + let parent_is_gloas = chain + .spec + .fork_name_at_slot::(parent_block.slot) + .gloas_enabled(); + + if parent_is_gloas + && !fork_choice_read_lock.is_payload_received(&block.message().parent_root()) + { + return Err(BlockError::ParentEnvelopeUnknown { + parent_root: block.message().parent_root(), + }); + } drop(fork_choice_read_lock); diff --git a/beacon_node/http_api/src/beacon/execution_payload_envelope.rs b/beacon_node/http_api/src/beacon/execution_payload_envelope.rs index 95acf5688d..161e091100 100644 --- a/beacon_node/http_api/src/beacon/execution_payload_envelope.rs +++ b/beacon_node/http_api/src/beacon/execution_payload_envelope.rs @@ -130,7 +130,7 @@ pub async fn publish_execution_payload_envelope( }) }; - let ctx = chain.gossip_verification_context(); + let ctx = chain.payload_envelope_gossip_verification_context(); let gossip_verified_envelope = match GossipVerifiedEnvelope::new(signed_envelope, &ctx) { Ok(envelope) => envelope, Err(e) => { diff --git a/beacon_node/network/src/router.rs b/beacon_node/network/src/router.rs index e8838283e9..ae3aa65ff3 100644 --- a/beacon_node/network/src/router.rs +++ b/beacon_node/network/src/router.rs @@ -19,7 +19,7 @@ use lighthouse_network::{ }; use logging::TimeLatch; use logging::crit; -use slot_clock::SlotClock; +use slot_clock::{SlotClock, timestamp_now}; use std::sync::Arc; use tokio::sync::mpsc; use tokio_stream::wrappers::UnboundedReceiverStream; diff --git a/beacon_node/network/src/sync/manager.rs b/beacon_node/network/src/sync/manager.rs index 9c75d11214..4463b5b41d 100644 --- a/beacon_node/network/src/sync/manager.rs +++ b/beacon_node/network/src/sync/manager.rs @@ -66,7 +66,7 @@ use lighthouse_network::types::{NetworkGlobals, SyncState}; use lighthouse_network::{PeerAction, PeerId}; use logging::crit; use lru_cache::LRUTimeCache; -use slot_clock::SlotClock; +use slot_clock::{SlotClock, timestamp_now}; use std::ops::Sub; use std::sync::Arc; use std::time::Duration; diff --git a/consensus/fork_choice/src/fork_choice.rs b/consensus/fork_choice/src/fork_choice.rs index 21415e478a..122640b48b 100644 --- a/consensus/fork_choice/src/fork_choice.rs +++ b/consensus/fork_choice/src/fork_choice.rs @@ -1506,6 +1506,11 @@ where } } + /// Returns whether the payload envelope has been received for the given block. + pub fn is_payload_received(&self, block_root: &Hash256) -> bool { + self.proto_array.is_payload_received(block_root) + } + /// Returns whether the proposer should extend the execution payload chain of the given block. pub fn should_extend_payload(&self, block_root: &Hash256) -> Result> { let proposer_boost_root = self.fc_store.proposer_boost_root(); From aaf3f1d5f999b7d51cc991a5b7b8c3c882c83518 Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Mon, 27 Apr 2026 09:16:22 +0200 Subject: [PATCH 046/118] Fix beacon-chain and network test failures under FORK_NAME=gloas/fulu - block_verification: skip ParentEnvelopeUnknown check when parent is the proto-array anchor. The anchor's `payload_received` is intentionally false per spec (never added to `store.payloads`), but no envelope is expected for it; without this exception the check rejects every post-anchor gloas block. - network tests: disable `engineGetBlobs` in the TestRig harness. Under real crypto the mock EL's blob fetch raced the gossip path, importing via a spawned task that the test didn't await -- leaving `head_root()` unchanged when the assertion ran. The tests are designed to exercise the gossip + data-column path; the engine fetch was incidental. - network tests: relax `data_column_reconstruction_at_deadline` to allow trailing duplicate reconstruction work items. The reprocess queue removes its dedup entry on dispatch, so a column processed during an in-flight reconstruction can dispatch a second one. The second is a no-op via `reconstruction_started`, so accept >= 1 trailing event. --- .../beacon_chain/src/block_verification.rs | 6 ++ .../src/network_beacon_processor/tests.rs | 72 +++++++++++++++++-- 2 files changed, 74 insertions(+), 4 deletions(-) diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index f4d8b80c42..84df30e75f 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -942,12 +942,18 @@ impl GossipVerifiedBlock { // Check that we've received the parent envelope. If not, issue a single envelope // lookup for the parent and queue this block in the reprocess queue. + // + // The anchor block (proto-array root) is implicitly considered to have its payload + // received: there is no envelope to fetch for the anchor (per spec, the anchor is + // never added to `store.payloads`), and the anchor is trusted by definition. let parent_is_gloas = chain .spec .fork_name_at_slot::(parent_block.slot) .gloas_enabled(); + let parent_is_anchor = parent_block.parent_root.is_none(); if parent_is_gloas + && !parent_is_anchor && !fork_choice_read_lock.is_payload_received(&block.message().parent_root()) { return Err(BlockError::ParentEnvelopeUnknown { diff --git a/beacon_node/network/src/network_beacon_processor/tests.rs b/beacon_node/network/src/network_beacon_processor/tests.rs index 76c6ba812d..367fc3ccb3 100644 --- a/beacon_node/network/src/network_beacon_processor/tests.rs +++ b/beacon_node/network/src/network_beacon_processor/tests.rs @@ -9,6 +9,7 @@ use crate::{ sync::{SyncMessage, manager::BlockProcessType}, }; use beacon_chain::block_verification_types::LookupBlock; +use beacon_chain::chain_config::ChainConfig; use beacon_chain::custody_context::NodeCustodyType; use beacon_chain::data_column_verification::validate_data_column_sidecar_for_gossip_fulu; use beacon_chain::kzg_utils::blobs_to_data_column_sidecars; @@ -134,7 +135,10 @@ impl TestRig { .fresh_ephemeral_store() .mock_execution_layer() .node_custody_type(NodeCustodyType::Fullnode) - .chain_config(<_>::default()) + .chain_config(ChainConfig { + disable_get_blobs: true, + ..ChainConfig::default() + }) .build(); harness.advance_slot(); @@ -169,7 +173,10 @@ impl TestRig { .fresh_ephemeral_store() .mock_execution_layer() .node_custody_type(node_custody_type) - .chain_config(<_>::default()) + .chain_config(ChainConfig { + disable_get_blobs: true, + ..ChainConfig::default() + }) .build(); harness.advance_slot(); @@ -649,6 +656,60 @@ impl TestRig { /// /// Given the described logic, `expected` must not contain `WORKER_FREED` or `NOTHING_TO_DO` /// events. + /// Like [`Self::assert_event_journal_contains_ordered`], but tolerant of extra trailing + /// repetitions of the final expected event. Useful for events the reprocess queue can + /// dispatch redundantly under timing pressure (e.g. reconstruction). + pub async fn assert_event_journal_contains_at_least_ordered(&mut self, expected: &[WorkType]) { + let expected_strs = expected + .iter() + .map(|ev| ev.into()) + .collect::>(); + + let mut events = Vec::with_capacity(expected_strs.len()); + let mut worker_freed_remaining = expected_strs.len(); + + let drain_future = async { + loop { + match self.work_journal_rx.recv().await { + Some(event) if event == WORKER_FREED => { + worker_freed_remaining = worker_freed_remaining.saturating_sub(1); + if worker_freed_remaining == 0 { + break; + } + } + Some(event) if event == NOTHING_TO_DO => {} + Some(event) => events.push(event), + None => break, + } + } + }; + + tokio::select! { + _ = tokio::time::sleep(STANDARD_TIMEOUT) => panic!( + "Timeout ({:?}) expired waiting for events. Expected at least {:?} but got {:?} waiting for {} `WORKER_FREED` events.", + STANDARD_TIMEOUT, expected_strs, events, worker_freed_remaining, + ), + _ = drain_future => {}, + } + + // Events must start with the exact expected sequence; trailing events must all be + // repetitions of the final expected event. + assert!( + events.len() >= expected_strs.len(), + "expected at least {} events, got {}: {:?}", + expected_strs.len(), + events.len(), + events, + ); + let (head, tail) = events.split_at(expected_strs.len()); + assert_eq!(head, expected_strs.as_slice()); + let trailing = expected_strs.last().copied().unwrap_or(""); + for event in tail { + assert_eq!(*event, trailing, "unexpected trailing event {event:?}"); + } + assert_eq!(worker_freed_remaining, 0); + } + pub async fn assert_event_journal_contains_ordered(&mut self, expected: &[WorkType]) { let expected = expected .iter() @@ -1001,13 +1062,16 @@ async fn data_column_reconstruction_at_deadline() { rig.enqueue_gossip_data_columns(i); } - // Expect all gossip events + reconstruction + // Expect all gossip events followed by at least one reconstruction. Under a slow + // signature backend (real crypto) the reprocess queue can dispatch multiple + // reconstruction work items before the import completes; subsequent ones are no-ops + // via the `reconstruction_started` flag, so we just require >= 1. let mut expected_events: Vec = (0..min_columns_for_reconstruction) .map(|_| WorkType::GossipDataColumnSidecar) .collect(); expected_events.push(WorkType::ColumnReconstruction); - rig.assert_event_journal_contains_ordered(&expected_events) + rig.assert_event_journal_contains_at_least_ordered(&expected_events) .await; } From f44c9e6b84aaab31f06d3b554e72c17b0d9e5505 Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Mon, 27 Apr 2026 09:25:44 +0200 Subject: [PATCH 047/118] Simplify reconstruction test assertion Replace `assert_event_journal_contains_at_least_ordered` helper with an inline drain that just counts the gossip + reconstruction events. The helper was carrying around `WORKER_FREED` bookkeeping and a strict prefix-match for one caller; counting the two relevant work types until both thresholds are met is the same check with much less code. --- .../src/network_beacon_processor/tests.rs | 89 +++++-------------- 1 file changed, 24 insertions(+), 65 deletions(-) diff --git a/beacon_node/network/src/network_beacon_processor/tests.rs b/beacon_node/network/src/network_beacon_processor/tests.rs index 367fc3ccb3..2a7542e73b 100644 --- a/beacon_node/network/src/network_beacon_processor/tests.rs +++ b/beacon_node/network/src/network_beacon_processor/tests.rs @@ -656,60 +656,6 @@ impl TestRig { /// /// Given the described logic, `expected` must not contain `WORKER_FREED` or `NOTHING_TO_DO` /// events. - /// Like [`Self::assert_event_journal_contains_ordered`], but tolerant of extra trailing - /// repetitions of the final expected event. Useful for events the reprocess queue can - /// dispatch redundantly under timing pressure (e.g. reconstruction). - pub async fn assert_event_journal_contains_at_least_ordered(&mut self, expected: &[WorkType]) { - let expected_strs = expected - .iter() - .map(|ev| ev.into()) - .collect::>(); - - let mut events = Vec::with_capacity(expected_strs.len()); - let mut worker_freed_remaining = expected_strs.len(); - - let drain_future = async { - loop { - match self.work_journal_rx.recv().await { - Some(event) if event == WORKER_FREED => { - worker_freed_remaining = worker_freed_remaining.saturating_sub(1); - if worker_freed_remaining == 0 { - break; - } - } - Some(event) if event == NOTHING_TO_DO => {} - Some(event) => events.push(event), - None => break, - } - } - }; - - tokio::select! { - _ = tokio::time::sleep(STANDARD_TIMEOUT) => panic!( - "Timeout ({:?}) expired waiting for events. Expected at least {:?} but got {:?} waiting for {} `WORKER_FREED` events.", - STANDARD_TIMEOUT, expected_strs, events, worker_freed_remaining, - ), - _ = drain_future => {}, - } - - // Events must start with the exact expected sequence; trailing events must all be - // repetitions of the final expected event. - assert!( - events.len() >= expected_strs.len(), - "expected at least {} events, got {}: {:?}", - expected_strs.len(), - events.len(), - events, - ); - let (head, tail) = events.split_at(expected_strs.len()); - assert_eq!(head, expected_strs.as_slice()); - let trailing = expected_strs.last().copied().unwrap_or(""); - for event in tail { - assert_eq!(*event, trailing, "unexpected trailing event {event:?}"); - } - assert_eq!(worker_freed_remaining, 0); - } - pub async fn assert_event_journal_contains_ordered(&mut self, expected: &[WorkType]) { let expected = expected .iter() @@ -1062,17 +1008,30 @@ async fn data_column_reconstruction_at_deadline() { rig.enqueue_gossip_data_columns(i); } - // Expect all gossip events followed by at least one reconstruction. Under a slow - // signature backend (real crypto) the reprocess queue can dispatch multiple - // reconstruction work items before the import completes; subsequent ones are no-ops - // via the `reconstruction_started` flag, so we just require >= 1. - let mut expected_events: Vec = (0..min_columns_for_reconstruction) - .map(|_| WorkType::GossipDataColumnSidecar) - .collect(); - expected_events.push(WorkType::ColumnReconstruction); - - rig.assert_event_journal_contains_at_least_ordered(&expected_events) - .await; + // Drain the journal until we've seen all gossip events plus at least one + // reconstruction. Under real crypto the reprocess queue can dispatch the + // reconstruction work item more than once (the second is a no-op via + // `reconstruction_started`), so we don't pin the count — we just require >= 1. + let gsc: &str = WorkType::GossipDataColumnSidecar.into(); + let cr: &str = WorkType::ColumnReconstruction.into(); + let (mut gossip_seen, mut recon_seen) = (0usize, 0usize); + let drain = async { + while let Some(event) = rig.work_journal_rx.recv().await { + if event == gsc { + gossip_seen += 1; + } else if event == cr { + recon_seen += 1; + } + if gossip_seen == min_columns_for_reconstruction && recon_seen >= 1 { + break; + } + } + }; + if tokio::time::timeout(STANDARD_TIMEOUT, drain).await.is_err() { + panic!("timeout: gossip_seen={gossip_seen}, recon_seen={recon_seen}"); + } + assert_eq!(gossip_seen, min_columns_for_reconstruction); + assert!(recon_seen >= 1); } // Test the column reconstruction is delayed for columns that arrive for a previous slot. From 4dc34c6854dbc629c14c8728f42b0dd7cde037e4 Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Mon, 27 Apr 2026 10:47:27 +0200 Subject: [PATCH 048/118] Add gloas parent-envelope-unknown lookup tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wires the lookup test rig for Gloas: - Capture per-block execution payload envelopes from the external harness and serve them to peers via a new `network_envelopes_by_root` map. - Handle `RequestType::PayloadEnvelopesByRoot` in `simulate_on_request` and `Work::RpcPayloadEnvelope` in the simulator processor branch. - Allow `TestRig` callers to override the genesis validator count and bump initial balances to `max_effective_balance_electra` post-Electra, which Gloas committee-selection requires for genesis init to converge. Adds four tests for the parent-envelope-unknown flow (each verified red/green by stubbing the corresponding source path): - `creates_envelope_and_child_lookups` — `UnknownParentEnvelope` produces exactly one envelope-only lookup for the parent root and one child lookup awaiting that envelope. - `idempotent_triggers` — repeated triggers for the same parent merge into the existing envelope lookup; no duplicate lookups are created. - `issues_payload_envelopes_by_root_rpc` — the envelope-only lookup dispatches a `PayloadEnvelopesByRoot` RPC for the parent block_root. - `drops_cascade_on_rpc_error` — when the envelope RPC errors, the envelope lookup is dropped and the awaiting child cascades with it. The end-to-end happy path (envelope arrives → child unblocks → block imports → head advances) is gated on `process_execution_payload_envelope` supporting `AvailabilityPending`, which today returns `InternalError("Pending payload envelope not yet implemented")`. That gap is independent of this PR's lookup machinery. --- beacon_node/network/src/sync/tests/lookups.rs | 225 +++++++++++++++++- beacon_node/network/src/sync/tests/mod.rs | 4 +- 2 files changed, 215 insertions(+), 14 deletions(-) diff --git a/beacon_node/network/src/sync/tests/lookups.rs b/beacon_node/network/src/sync/tests/lookups.rs index a26996ec5e..9ff0bec15d 100644 --- a/beacon_node/network/src/sync/tests/lookups.rs +++ b/beacon_node/network/src/sync/tests/lookups.rs @@ -37,7 +37,7 @@ use tokio::sync::mpsc; use tracing::info; use types::{ BlobSidecar, BlockImportSource, ColumnIndex, DataColumnSidecar, EthSpec, ForkContext, ForkName, - Hash256, MinimalEthSpec as E, SignedBeaconBlock, Slot, + Hash256, MinimalEthSpec as E, SignedBeaconBlock, SignedExecutionPayloadEnvelope, Slot, test_utils::{SeedableRng, XorShiftRng}, }; @@ -209,6 +209,9 @@ pub(crate) struct TestRigConfig { fulu_test_type: FuluTestType, /// Override the node custody type derived from `fulu_test_type` node_custody_type_override: Option, + /// Override the number of validators in the harness genesis state. Defaults to 1. + /// Some forks (e.g. Gloas) cannot initialise a state with a single validator. + validator_count_override: Option, } impl TestRig { @@ -222,9 +225,9 @@ impl TestRig { ); // Initialise a new beacon chain - let harness = BeaconChainHarness::>::builder(E) + let mut builder = BeaconChainHarness::>::builder(E) .spec(spec.clone()) - .deterministic_keypairs(1) + .deterministic_keypairs(test_rig_config.validator_count_override.unwrap_or(1)) .fresh_ephemeral_store() .mock_execution_layer() .testing_slot_clock(clock.clone()) @@ -232,8 +235,17 @@ impl TestRig { test_rig_config .node_custody_type_override .unwrap_or_else(|| test_rig_config.fulu_test_type.we_node_custody_type()), - ) - .build(); + ); + // Post-Electra forks need validators with effective balance close to + // `max_effective_balance_electra` for balance-weighted committee + // selection (sync committee, PTC) to converge during genesis. + if spec.electra_fork_epoch == Some(types::Epoch::new(0)) { + let max_eb = spec.max_effective_balance_electra; + builder = builder.with_genesis_state_builder(move |b| { + b.set_initial_balance_fn(Box::new(move |_| max_eb)) + }); + } + let harness = builder.build(); let chain = harness.chain.clone(); let fork_context = Arc::new(ForkContext::new::( @@ -305,6 +317,7 @@ impl TestRig { fork_name, network_blocks_by_root: <_>::default(), network_blocks_by_slot: <_>::default(), + network_envelopes_by_root: <_>::default(), penalties: <_>::default(), seen_lookups: <_>::default(), requests: <_>::default(), @@ -319,6 +332,7 @@ impl TestRig { Self::new(TestRigConfig { fulu_test_type: FuluTestType::WeFullnodeThemSupernode, node_custody_type_override: None, + validator_count_override: None, }) } @@ -327,6 +341,7 @@ impl TestRig { Self::new(TestRigConfig { fulu_test_type: FuluTestType::WeFullnodeThemSupernode, node_custody_type_override: Some(node_custody_type), + validator_count_override: None, }) } @@ -429,9 +444,9 @@ impl TestRig { process_fn.await } } - Work::RpcBlobs { process_fn } | Work::RpcCustodyColumn(process_fn) => { - process_fn.await - } + Work::RpcBlobs { process_fn } + | Work::RpcCustodyColumn(process_fn) + | Work::RpcPayloadEnvelope { process_fn } => process_fn.await, Work::ChainSegment { process_fn, process_id: (chain_id, batch_epoch), @@ -671,6 +686,27 @@ impl TestRig { self.send_rpc_columns_response(req_id, peer_id, &columns); } + (RequestType::PayloadEnvelopesByRoot(req), AppRequestId::Sync(req_id)) => { + if self.complete_strategy.return_no_data_n_times > 0 { + self.complete_strategy.return_no_data_n_times -= 1; + return self.send_rpc_envelopes_response(req_id, peer_id, &[]); + } + + let envelopes = req + .beacon_block_roots + .iter() + .map(|block_root| { + self.network_envelopes_by_root + .get(block_root) + .unwrap_or_else(|| { + panic!("Test consumer requested unknown envelope: {block_root:?}") + }) + .clone() + }) + .collect::>(); + self.send_rpc_envelopes_response(req_id, peer_id, &envelopes); + } + (RequestType::BlocksByRange(req), AppRequestId::Sync(req_id)) => { if self.complete_strategy.skip_by_range_routes { return; @@ -894,6 +930,36 @@ impl TestRig { }); } + fn send_rpc_envelopes_response( + &mut self, + sync_request_id: SyncRequestId, + peer_id: PeerId, + envelopes: &[Arc>], + ) { + let block_roots = envelopes + .iter() + .map(|e| e.beacon_block_root()) + .collect::>(); + self.log(&format!( + "Completing request {sync_request_id:?} to {peer_id} with envelopes for {block_roots:?}" + )); + + for envelope in envelopes { + self.push_sync_message(SyncMessage::RpcPayloadEnvelope { + sync_request_id, + peer_id, + envelope: Some(envelope.clone()), + seen_timestamp: D, + }); + } + self.push_sync_message(SyncMessage::RpcPayloadEnvelope { + sync_request_id, + peer_id, + envelope: None, + seen_timestamp: D, + }); + } + fn send_rpc_columns_response( &mut self, sync_request_id: SyncRequestId, @@ -936,16 +1002,25 @@ impl TestRig { pub(super) async fn build_chain(&mut self, block_count: usize) -> Hash256 { let mut blocks = vec![]; - // Initialise a new beacon chain - let external_harness = BeaconChainHarness::>::builder(E) + // Initialise a new beacon chain. Match the local harness's validator count and + // balance hooks so post-Electra forks (where genesis-time committee selection is + // balance-weighted) can initialise. + let validator_count = self.harness.validator_keypairs.len(); + let mut builder = BeaconChainHarness::>::builder(E) .spec(self.harness.spec.clone()) - .deterministic_keypairs(1) + .deterministic_keypairs(validator_count) .fresh_ephemeral_store() .mock_execution_layer() .testing_slot_clock(self.harness.chain.slot_clock.clone()) // Make the external harness a supernode so all columns are available - .node_custody_type(NodeCustodyType::Supernode) - .build(); + .node_custody_type(NodeCustodyType::Supernode); + if self.harness.spec.electra_fork_epoch == Some(types::Epoch::new(0)) { + let max_eb = self.harness.spec.max_effective_balance_electra; + builder = builder.with_genesis_state_builder(move |b| { + b.set_initial_balance_fn(Box::new(move |_| max_eb)) + }); + } + let external_harness = builder.build(); // Ensure all blocks have data. Otherwise, the triggers for unknown blob parent and unknown // data column parent fail. external_harness @@ -974,6 +1049,14 @@ impl TestRig { self.network_blocks_by_root .insert(block_root, block.clone()); self.network_blocks_by_slot.insert(block_slot, block); + // Post-Gloas, also capture the execution payload envelope so peers can serve it. + if self.is_after_gloas() + && let Ok(Some(envelope)) = + external_harness.chain.store.get_payload_envelope(&block_root) + { + self.network_envelopes_by_root + .insert(block_root, Arc::new(envelope)); + } self.log(&format!( "Produced block {} index {i} in external harness", block_slot, @@ -1444,6 +1527,7 @@ impl TestRig { Self::new(TestRigConfig { fulu_test_type, node_custody_type_override: None, + validator_count_override: None, }) }) } @@ -1460,6 +1544,22 @@ impl TestRig { self.fork_name.fulu_enabled() } + pub fn is_after_gloas(&self) -> bool { + self.fork_name.gloas_enabled() + } + + fn new_after_gloas() -> Option { + // Gloas requires more than 1 validator to initialise the genesis state + // (committee/sampling computations fail with `InvalidIndicesCount`). + genesis_fork().gloas_enabled().then(|| { + Self::new(TestRigConfig { + fulu_test_type: FuluTestType::WeFullnodeThemSupernode, + node_custody_type_override: None, + validator_count_override: Some(1024), + }) + }) + } + fn trigger_unknown_parent_block(&mut self, peer_id: PeerId, block: Arc>) { let block_root = block.canonical_root(); self.send_sync_message(SyncMessage::UnknownParentBlock(peer_id, block, block_root)) @@ -1483,6 +1583,18 @@ impl TestRig { )); } + /// Trigger an envelope-unknown lookup for the last block in the chain. Caller is + /// expected to have already imported the parent block (via `import_blocks_up_to_slot`) + /// without registering its envelope. + fn trigger_with_last_unknown_parent_envelope(&mut self) { + let peer_id = self.new_connected_supernode_peer(); + let last_block = self.get_last_block().block_cloned(); + let block_root = last_block.canonical_root(); + self.send_sync_message(SyncMessage::UnknownParentEnvelope( + peer_id, last_block, block_root, + )); + } + fn rand_block(&mut self) -> SignedBeaconBlock { self.rand_block_and_blobs(NumBlobs::None).0 } @@ -2639,3 +2751,90 @@ async fn crypto_on_fail_with_bad_column_kzg_proof() { r.assert_penalties_of_type("lookup_custody_column_processing_failure"); } } + +// --------------------------------------------------------------------------- +// Gloas: parent envelope unknown lookup +// --------------------------------------------------------------------------- +// +// These tests exercise the lookup-sync state machine introduced in PR #9039: +// when a gossip block's parent execution payload envelope is missing, +// `SyncManager` is expected to create two single-block lookups — an envelope-only +// lookup for the parent block_root and a "child" lookup that holds the gossip +// block and waits on `AwaitingParent::Envelope(parent_root)`. The envelope-only +// lookup issues a `PayloadEnvelopesByRoot` RPC; on completion it unblocks the +// child via `continue_envelope_child_lookups`. +// +// The tests below cover lookup creation, RPC routing, and drop-cascade +// behaviour. The end-to-end happy path is gated on +// `process_execution_payload_envelope` supporting `AvailabilityPending` (today +// it returns `InternalError("Pending payload envelope not yet implemented")`), +// which is tracked separately. See `process_rpc_envelope` in `sync_methods.rs`. + +/// Builds a 2-block gloas chain in the external harness and locally imports block 1 +/// (parent) WITHOUT registering its envelope, leaving `is_payload_received(parent_root)` +/// false — the precondition for `BlockError::ParentEnvelopeUnknown`. +async fn setup_unknown_parent_envelope_scenario() -> Option { + let mut r = TestRig::new_after_gloas()?; + r.build_chain(2).await; + r.import_blocks_up_to_slot(1).await; + Some(r) +} + +fn payload_envelope_request_count(rig: &TestRig) -> usize { + rig.requests + .iter() + .filter(|(request, _)| matches!(request, RequestType::PayloadEnvelopesByRoot(_))) + .count() +} + +/// Triggering `UnknownParentEnvelope` creates exactly two lookups: an envelope-only +/// lookup for the parent and a child lookup for the gossip block awaiting that envelope. +#[tokio::test] +async fn unknown_parent_envelope_creates_envelope_and_child_lookups() { + let Some(mut r) = setup_unknown_parent_envelope_scenario().await else { + return; + }; + r.trigger_with_last_unknown_parent_envelope(); + r.assert_single_lookups_count(2); +} + +/// Repeated `UnknownParentEnvelope` triggers for the same parent must not spawn extra +/// lookups (peers are merged into the existing envelope lookup). +#[tokio::test] +async fn unknown_parent_envelope_idempotent_triggers() { + let Some(mut r) = setup_unknown_parent_envelope_scenario().await else { + return; + }; + r.trigger_with_last_unknown_parent_envelope(); + r.trigger_with_last_unknown_parent_envelope(); + r.assert_single_lookups_count(2); +} + +/// The envelope-only lookup must dispatch a `PayloadEnvelopesByRoot` RPC for the +/// parent block_root. +#[tokio::test] +async fn unknown_parent_envelope_issues_payload_envelopes_by_root_rpc() { + let Some(mut r) = setup_unknown_parent_envelope_scenario().await else { + return; + }; + r.trigger_with_last_unknown_parent_envelope(); + r.simulate(SimulateConfig::new()).await; + assert_eq!( + payload_envelope_request_count(&r), + 1, + "expected exactly one PayloadEnvelopesByRoot request" + ); +} + +/// If the envelope RPC errors out, the envelope-only lookup is dropped and the +/// drop cascades to the awaiting child lookup. +#[tokio::test] +async fn unknown_parent_envelope_drops_cascade_on_rpc_error() { + let Some(mut r) = setup_unknown_parent_envelope_scenario().await else { + return; + }; + r.trigger_with_last_unknown_parent_envelope(); + r.simulate(SimulateConfig::new().return_rpc_error(RPCError::IoError("test".into()))) + .await; + r.assert_failed_lookup_sync(); +} diff --git a/beacon_node/network/src/sync/tests/mod.rs b/beacon_node/network/src/sync/tests/mod.rs index 6e948e4726..29dd7b898e 100644 --- a/beacon_node/network/src/sync/tests/mod.rs +++ b/beacon_node/network/src/sync/tests/mod.rs @@ -22,7 +22,7 @@ use tokio::sync::mpsc; use tracing_subscriber::fmt::MakeWriter; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; -use types::{ForkName, Hash256, MinimalEthSpec as E, Slot}; +use types::{ForkName, Hash256, MinimalEthSpec as E, SignedExecutionPayloadEnvelope, Slot}; mod lookups; mod range; @@ -79,6 +79,8 @@ struct TestRig { /// Blocks that will be used in the test but may not be known to `harness` yet. network_blocks_by_root: HashMap>, network_blocks_by_slot: HashMap>, + /// Execution payload envelopes (Gloas) keyed by beacon block root, available to peers. + network_envelopes_by_root: HashMap>>, penalties: Vec, /// All seen lookups through the test run seen_lookups: HashMap, From 7e50d470820782f8decdd4057f082adad82f6c01 Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Tue, 28 Apr 2026 15:25:17 +0200 Subject: [PATCH 049/118] Add bad-peer and crypto-fail envelope-lookup tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - bad_peer_wrong_envelope_response — peer responds with a different block_root than requested; request items raise UnrequestedBlockRoot, both lookups drop. - crypto_on_fail_with_bad_envelope_signature — signature corruption rejected in gossip verification, peer scored with lookup_envelope_processing_failure. Rename the four already-landed tests to match the existing happy_path / bad_peer / envelope_* / crypto_on_fail_with_* naming. --- beacon_node/network/src/sync/tests/lookups.rs | 91 +++++++++++++++++-- 1 file changed, 85 insertions(+), 6 deletions(-) diff --git a/beacon_node/network/src/sync/tests/lookups.rs b/beacon_node/network/src/sync/tests/lookups.rs index 9ff0bec15d..4f03924eef 100644 --- a/beacon_node/network/src/sync/tests/lookups.rs +++ b/beacon_node/network/src/sync/tests/lookups.rs @@ -85,6 +85,9 @@ pub struct SimulateConfig { ee_offline_for_n_range_responses: Option, /// Disconnect all peers after this many successful BlocksByRange responses. successful_range_responses_before_disconnect: Option, + /// Number of `PayloadEnvelopesByRoot` responses that return an envelope for a + /// different block_root than requested. + return_wrong_envelopes_n_times: usize, } impl SimulateConfig { @@ -116,6 +119,11 @@ impl SimulateConfig { self } + fn return_wrong_envelope_once(mut self) -> Self { + self.return_wrong_envelopes_n_times = 1; + self + } + fn return_wrong_sidecar_for_block_once(mut self) -> Self { self.return_wrong_sidecar_for_block_n_times = 1; self @@ -692,6 +700,24 @@ impl TestRig { return self.send_rpc_envelopes_response(req_id, peer_id, &[]); } + if self.complete_strategy.return_wrong_envelopes_n_times > 0 { + self.complete_strategy.return_wrong_envelopes_n_times -= 1; + // Return any envelope that doesn't match the request, so the + // request items layer raises `UnrequestedBlockRoot`. + let requested = req + .beacon_block_roots + .iter() + .copied() + .collect::>(); + let wrong = self + .network_envelopes_by_root + .iter() + .find(|(root, _)| !requested.contains(*root)) + .map(|(_, envelope)| envelope.clone()) + .expect("test fixture must produce at least one extra envelope"); + return self.send_rpc_envelopes_response(req_id, peer_id, &[wrong]); + } + let envelopes = req .beacon_block_roots .iter() @@ -1051,8 +1077,10 @@ impl TestRig { self.network_blocks_by_slot.insert(block_slot, block); // Post-Gloas, also capture the execution payload envelope so peers can serve it. if self.is_after_gloas() - && let Ok(Some(envelope)) = - external_harness.chain.store.get_payload_envelope(&block_root) + && let Ok(Some(envelope)) = external_harness + .chain + .store + .get_payload_envelope(&block_root) { self.network_envelopes_by_root .insert(block_root, Arc::new(envelope)); @@ -1085,6 +1113,21 @@ impl TestRig { self.re_insert_block(Arc::new(block), blobs, columns); } + /// Replace the cached envelope's signature for `block_root` with one signed by an + /// unrelated key, so it fails verification against the proposer's pubkey. + fn corrupt_envelope_signature_for(&mut self, block_root: Hash256) { + let envelope = self + .network_envelopes_by_root + .get(&block_root) + .expect("no envelope cached for block_root") + .as_ref() + .clone(); + let mut envelope = envelope; + envelope.signature = self.valid_signature(); + self.network_envelopes_by_root + .insert(block_root, Arc::new(envelope)); + } + fn valid_signature(&mut self) -> bls::Signature { let keypair = bls::Keypair::random(); let msg = Hash256::random(); @@ -2790,7 +2833,7 @@ fn payload_envelope_request_count(rig: &TestRig) -> usize { /// Triggering `UnknownParentEnvelope` creates exactly two lookups: an envelope-only /// lookup for the parent and a child lookup for the gossip block awaiting that envelope. #[tokio::test] -async fn unknown_parent_envelope_creates_envelope_and_child_lookups() { +async fn unknown_parent_envelope_creates_two_lookups() { let Some(mut r) = setup_unknown_parent_envelope_scenario().await else { return; }; @@ -2801,7 +2844,7 @@ async fn unknown_parent_envelope_creates_envelope_and_child_lookups() { /// Repeated `UnknownParentEnvelope` triggers for the same parent must not spawn extra /// lookups (peers are merged into the existing envelope lookup). #[tokio::test] -async fn unknown_parent_envelope_idempotent_triggers() { +async fn happy_path_unknown_parent_envelope_multiple_triggers() { let Some(mut r) = setup_unknown_parent_envelope_scenario().await else { return; }; @@ -2813,7 +2856,7 @@ async fn unknown_parent_envelope_idempotent_triggers() { /// The envelope-only lookup must dispatch a `PayloadEnvelopesByRoot` RPC for the /// parent block_root. #[tokio::test] -async fn unknown_parent_envelope_issues_payload_envelopes_by_root_rpc() { +async fn envelope_lookup_issues_by_root_rpc() { let Some(mut r) = setup_unknown_parent_envelope_scenario().await else { return; }; @@ -2829,7 +2872,7 @@ async fn unknown_parent_envelope_issues_payload_envelopes_by_root_rpc() { /// If the envelope RPC errors out, the envelope-only lookup is dropped and the /// drop cascades to the awaiting child lookup. #[tokio::test] -async fn unknown_parent_envelope_drops_cascade_on_rpc_error() { +async fn bad_peer_envelope_rpc_failure() { let Some(mut r) = setup_unknown_parent_envelope_scenario().await else { return; }; @@ -2838,3 +2881,39 @@ async fn unknown_parent_envelope_drops_cascade_on_rpc_error() { .await; r.assert_failed_lookup_sync(); } + +/// Peer responds with an envelope for a different block_root than was requested. +/// The request-items layer must reject as `UnrequestedBlockRoot`; both lookups drop. +#[tokio::test] +async fn bad_peer_wrong_envelope_response() { + let Some(mut r) = setup_unknown_parent_envelope_scenario().await else { + return; + }; + r.trigger_with_last_unknown_parent_envelope(); + r.simulate(SimulateConfig::new().return_wrong_envelope_once()) + .await; + r.assert_failed_lookup_sync(); + r.assert_penalties_of_type("UnrequestedBlockRoot"); +} + +/// Peer returns the requested envelope but with a corrupted signature. Gossip +/// verification rejects it; the lookup retries (single peer → exhaust → drop) +/// and reports `lookup_envelope_processing_failure` against the peer. +#[tokio::test] +async fn crypto_on_fail_with_bad_envelope_signature() { + let Some(mut r) = setup_unknown_parent_envelope_scenario().await else { + return; + }; + let parent_root = r.get_last_block().block_cloned().parent_root(); + r.corrupt_envelope_signature_for(parent_root); + r.trigger_with_last_unknown_parent_envelope(); + r.simulate(SimulateConfig::happy_path()).await; + if cfg!(feature = "fake_crypto") { + // Under fake_crypto, signature checks are no-ops, so a "corrupted" + // signature still passes. Skip — analogous to the existing + // `crypto_on_fail_with_invalid_block_signature` test. + return; + } + r.assert_failed_lookup_sync(); + r.assert_penalties_of_type("lookup_envelope_processing_failure"); +} From 82dde267b5c38c21074f4094eb9b024df57f18cd Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Tue, 28 Apr 2026 15:39:40 +0200 Subject: [PATCH 050/118] range sync --- beacon_node/beacon_chain/src/beacon_chain.rs | 2 +- .../beacon_chain/src/block_verification.rs | 104 +++++++++++++++-- .../src/block_verification_types.rs | 106 +++++++++++++----- .../src/data_availability_checker.rs | 7 +- beacon_node/beacon_processor/src/lib.rs | 14 ++- .../src/service/api_types.rs | 18 +++ 6 files changed, 205 insertions(+), 46 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 9da64888c2..5a9b6b8c87 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -3150,7 +3150,7 @@ impl BeaconChain { }; // Import the blocks into the chain. - for signature_verified_block in signature_verified_blocks { + for (signature_verified_block, _envelope) in signature_verified_blocks { let block_slot = signature_verified_block.slot(); match self .process_block( diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 9a43147233..7a81a4465e 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -60,6 +60,7 @@ use crate::execution_payload::{ }; use crate::kzg_utils::blobs_to_data_column_sidecars; use crate::observed_block_producers::SeenBlock; +use crate::payload_envelope_verification::{AvailableEnvelope, EnvelopeError}; use crate::validator_monitor::HISTORIC_EPOCHS as VALIDATOR_MONITOR_HISTORIC_EPOCHS; use crate::validator_pubkey_cache::ValidatorPubkeyCache; use crate::{ @@ -320,6 +321,20 @@ pub enum BlockError { bid_parent_root: Hash256, block_parent_root: Hash256, }, + /// The child block is known but its parent execution payload envelope has not been received yet. + /// + /// ## Peer scoring + /// + /// It's unclear if this block is valid, but it cannot be fully verified without the parent's + /// execution payload envelope. + ParentEnvelopeUnknown { parent_root: Hash256 }, + /// An error occurred while processing the execution payload envelope during range sync. + EnvelopeError(Box), + + PayloadEnvelopeError { + e: Box, + penalize_peer: bool, + }, } /// Which specific signature(s) are invalid in a SignedBeaconBlock @@ -486,6 +501,36 @@ impl From for BlockError { } } +impl From for BlockError { + fn from(e: EnvelopeError) -> Self { + let penalize_peer = match &e { + // REJECT per spec: peer sent invalid envelope data + EnvelopeError::BadSignature + | EnvelopeError::BuilderIndexMismatch { .. } + | EnvelopeError::BlockHashMismatch { .. } + | EnvelopeError::SlotMismatch { .. } + | EnvelopeError::IncorrectBlockProposer { .. } => true, + // IGNORE per spec: not the peer's fault + EnvelopeError::BlockRootUnknown { .. } + | EnvelopeError::PriorToFinalization { .. } + | EnvelopeError::UnknownValidator { .. } => false, + // Internal errors: not the peer's fault + EnvelopeError::BeaconChainError(_) + | EnvelopeError::BeaconStateError(_) + | EnvelopeError::BlockProcessingError(_) + | EnvelopeError::EnvelopeProcessingError(_) + | EnvelopeError::ExecutionPayloadError(_) + | EnvelopeError::BlockError(_) + | EnvelopeError::InternalError(_) + | EnvelopeError::OptimisticSyncNotSupported { .. } => false, + }; + BlockError::PayloadEnvelopeError { + e: Box::new(e), + penalize_peer, + } + } +} + /// Stores information about verifying a payload against an execution engine. #[derive(Debug, PartialEq, Clone, Encode, Decode)] pub struct PayloadVerificationOutcome { @@ -583,10 +628,17 @@ pub(crate) fn process_block_slash_info( mut chain_segment: Vec<(Hash256, RangeSyncBlock)>, chain: &BeaconChain, -) -> Result>, BlockError> { +) -> Result< + Vec<( + SignatureVerifiedBlock, + Option>>, + )>, + BlockError, +> { if chain_segment.is_empty() { return Ok(vec![]); } @@ -615,14 +667,29 @@ pub fn signature_verify_chain_segment( let consensus_context = ConsensusContext::new(block.slot()).set_current_block_root(block_root); - let available_block = block.into_available_block(); + let (available_block, envelope) = match block { + RangeSyncBlock::Base(ab) => (ab, None), + RangeSyncBlock::Gloas { block, envelope } => { + let ab = AvailableBlock::new( + block, + AvailableBlockData::NoData, + &chain.data_availability_checker, + chain.spec.clone(), + ) + .map_err(BlockError::AvailabilityCheck)?; + (ab, envelope) + } + }; available_blocks.push(available_block.clone()); - signature_verified_blocks.push(SignatureVerifiedBlock { - block: MaybeAvailableBlock::Available(available_block), - block_root, - parent: None, - consensus_context, - }); + signature_verified_blocks.push(( + SignatureVerifiedBlock { + block: MaybeAvailableBlock::Available(available_block), + block_root, + parent: None, + consensus_context, + }, + envelope, + )); } chain @@ -632,7 +699,7 @@ pub fn signature_verify_chain_segment( // verify signatures let pubkey_cache = get_validator_pubkey_cache(chain)?; let mut signature_verifier = get_signature_verifier(&state, &pubkey_cache, &chain.spec); - for svb in &mut signature_verified_blocks { + for (svb, _) in &mut signature_verified_blocks { signature_verifier .include_all_signatures(svb.block.as_block(), &mut svb.consensus_context)?; } @@ -643,7 +710,7 @@ pub fn signature_verify_chain_segment( drop(pubkey_cache); - if let Some(signature_verified_block) = signature_verified_blocks.first_mut() { + if let Some((signature_verified_block, _)) = signature_verified_blocks.first_mut() { signature_verified_block.parent = Some(parent); } @@ -1191,7 +1258,7 @@ impl SignatureVerifiedBlock { let result = info_span!("signature_verify").in_scope(|| signature_verifier.verify()); match result { Ok(_) => { - // gloas blocks are always available. + // Gloas blocks are always available — data arrives via the envelope. let maybe_available = if chain .spec .fork_name_at_slot::(block.slot()) @@ -1946,6 +2013,21 @@ fn load_parent>( BlockError::from(BeaconChainError::MissingBeaconBlock(block.parent_root())) })?; + // For post-Gloas parent blocks, the execution payload arrives via the envelope. + // If the parent's execution payload envelope hasn't arrived yet, + // return an unknown parent error so the block gets sent to the + // reprocess queue. + if chain + .spec + .fork_name_at_slot::(parent_block.slot()) + .gloas_enabled() + { + let _envelope = chain + .store + .get_payload_envelope(&root)? + .ok_or(BlockError::ParentEnvelopeUnknown { parent_root: root })?; + } + // Load the parent block's state from the database, returning an error if it is not found. // It is an error because if we know the parent block we should also know the parent state. // Retrieve any state that is advanced through to at most `block.slot()`: this is diff --git a/beacon_node/beacon_chain/src/block_verification_types.rs b/beacon_node/beacon_chain/src/block_verification_types.rs index be73ef15d7..05a757e73d 100644 --- a/beacon_node/beacon_chain/src/block_verification_types.rs +++ b/beacon_node/beacon_chain/src/block_verification_types.rs @@ -2,10 +2,11 @@ use crate::data_availability_checker::{AvailabilityCheckError, DataAvailabilityC pub use crate::data_availability_checker::{ AvailableBlock, AvailableBlockData, MaybeAvailableBlock, }; +use crate::payload_envelope_verification::AvailableEnvelope; use crate::{BeaconChainTypes, PayloadVerificationOutcome}; -use educe::Educe; use state_processing::ConsensusContext; use std::fmt::{Debug, Formatter}; +use std::hash::{Hash, Hasher}; use std::sync::Arc; use types::data::BlobIdentifier; use types::{ @@ -45,38 +46,60 @@ impl LookupBlock { /// This includes any and all blobs/columns required, including zero if /// none are required. This can happen if the block is pre-deneb or if /// it's simply past the DA boundary. -#[derive(Clone, Educe)] -#[educe(Hash(bound(E: EthSpec)))] -pub struct RangeSyncBlock { - block: AvailableBlock, +pub enum RangeSyncBlock { + Base(AvailableBlock), + Gloas { + block: Arc>, + envelope: Option>>, + }, +} + +impl Hash for RangeSyncBlock { + fn hash(&self, state: &mut H) { + self.block_root().hash(state); + } } impl Debug for RangeSyncBlock { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "RpcBlock({:?})", self.block_root()) + write!(f, "RangeSyncBlock({:?})", self.block_root()) } } impl RangeSyncBlock { pub fn block_root(&self) -> Hash256 { - self.block.block_root() + match self { + RangeSyncBlock::Base(block) => block.block_root(), + RangeSyncBlock::Gloas { block, .. } => block.canonical_root(), + } } pub fn as_block(&self) -> &SignedBeaconBlock { - self.block.block() + match self { + RangeSyncBlock::Base(block) => block.block(), + RangeSyncBlock::Gloas { block, .. } => block, + } } pub fn block_cloned(&self) -> Arc> { - self.block.block_cloned() + match self { + RangeSyncBlock::Base(block) => block.block_cloned(), + RangeSyncBlock::Gloas { block, .. } => block.clone(), + } } pub fn block_data(&self) -> &AvailableBlockData { - self.block.data() + match self { + RangeSyncBlock::Base(block) => block.data(), + RangeSyncBlock::Gloas { .. } => { + unreachable!("block_data called on Gloas variant — use envelope data instead") + } + } } } impl RangeSyncBlock { - /// Constructs an `RangeSyncBlock` from a block and availability data. + /// Constructs a `RangeSyncBlock` from a block and availability data. /// /// # Errors /// @@ -94,32 +117,53 @@ impl RangeSyncBlock { T: BeaconChainTypes, { let available_block = AvailableBlock::new(block, block_data, da_checker, spec)?; - Ok(Self { - block: available_block, - }) + Ok(Self::Base(available_block)) + } + + pub fn new_gloas( + block: Arc>, + envelope: Option>>, + ) -> Self { + Self::Gloas { block, envelope } } #[allow(clippy::type_complexity)] pub fn deconstruct(self) -> (Hash256, Arc>, AvailableBlockData) { - self.block.deconstruct() + match self { + RangeSyncBlock::Base(block) => block.deconstruct(), + RangeSyncBlock::Gloas { .. } => { + unreachable!("deconstruct called on Gloas variant") + } + } } pub fn n_blobs(&self) -> usize { - match self.block_data() { - AvailableBlockData::NoData | AvailableBlockData::DataColumns(_) => 0, - AvailableBlockData::Blobs(blobs) => blobs.len(), + match self { + RangeSyncBlock::Base(block) => match block.data() { + AvailableBlockData::NoData | AvailableBlockData::DataColumns(_) => 0, + AvailableBlockData::Blobs(blobs) => blobs.len(), + }, + RangeSyncBlock::Gloas { .. } => 0, } } pub fn n_data_columns(&self) -> usize { - match self.block_data() { - AvailableBlockData::NoData | AvailableBlockData::Blobs(_) => 0, - AvailableBlockData::DataColumns(columns) => columns.len(), + match self { + RangeSyncBlock::Base(block) => match block.data() { + AvailableBlockData::NoData | AvailableBlockData::Blobs(_) => 0, + AvailableBlockData::DataColumns(columns) => columns.len(), + }, + RangeSyncBlock::Gloas { .. } => 0, } } pub fn into_available_block(self) -> AvailableBlock { - self.block + match self { + RangeSyncBlock::Base(block) => block, + RangeSyncBlock::Gloas { .. } => { + unreachable!("into_available_block called on Gloas variant") + } + } } } @@ -387,31 +431,31 @@ impl AsBlock for AvailableBlock { impl AsBlock for RangeSyncBlock { fn slot(&self) -> Slot { - self.as_block().slot() + RangeSyncBlock::as_block(self).slot() } fn epoch(&self) -> Epoch { - self.as_block().epoch() + RangeSyncBlock::as_block(self).epoch() } fn parent_root(&self) -> Hash256 { - self.as_block().parent_root() + RangeSyncBlock::as_block(self).parent_root() } fn state_root(&self) -> Hash256 { - self.as_block().state_root() + RangeSyncBlock::as_block(self).state_root() } fn signed_block_header(&self) -> SignedBeaconBlockHeader { - self.as_block().signed_block_header() + RangeSyncBlock::as_block(self).signed_block_header() } fn message(&self) -> BeaconBlockRef<'_, E> { - self.as_block().message() + RangeSyncBlock::as_block(self).message() } fn as_block(&self) -> &SignedBeaconBlock { - self.block.as_block() + RangeSyncBlock::as_block(self) } fn block_cloned(&self) -> Arc> { - self.block.block_cloned() + RangeSyncBlock::block_cloned(self) } fn canonical_root(&self) -> Hash256 { - self.block.block_root() + self.block_root() } } diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index 9d8b76aaed..e296f1b78d 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -21,7 +21,7 @@ use tracing::{debug, error, instrument}; use types::data::{BlobIdentifier, FixedBlobSidecarList, PartialDataColumn}; use types::{ BlobSidecar, BlobSidecarList, BlockImportSource, ChainSpec, DataColumnSidecar, - DataColumnSidecarList, Epoch, EthSpec, Hash256, PartialDataColumnSidecarError, + DataColumnSidecarList, Epoch, EthSpec, ForkName, Hash256, PartialDataColumnSidecarError, PartialDataColumnSidecarRef, SignedBeaconBlock, Slot, new_non_zero_usize, }; @@ -540,6 +540,11 @@ impl DataAvailabilityChecker { self.da_check_required_for_epoch(epoch) && self.spec.is_peer_das_enabled_for_epoch(epoch) } + /// Determines if execution payload envelopes are required for an epoch (Gloas and later). + pub fn envelopes_required_for_epoch(&self, epoch: Epoch) -> bool { + self.spec.fork_name_at_epoch(epoch) >= ForkName::Gloas + } + /// See `Self::blobs_required_for_epoch` fn blobs_required_for_block(&self, block: &SignedBeaconBlock) -> bool { block.num_expected_blobs() > 0 && self.blobs_required_for_epoch(block.epoch()) diff --git a/beacon_node/beacon_processor/src/lib.rs b/beacon_node/beacon_processor/src/lib.rs index ea87e9bc71..d2f7010854 100644 --- a/beacon_node/beacon_processor/src/lib.rs +++ b/beacon_node/beacon_processor/src/lib.rs @@ -417,6 +417,9 @@ pub enum Work { RpcBlobs { process_fn: AsyncFn, }, + RpcPayloadEnvelope { + process_fn: AsyncFn, + }, RpcCustodyColumn(AsyncFn), ColumnReconstruction(AsyncFn), IgnoredRpcBlock { @@ -483,6 +486,7 @@ pub enum WorkType { GossipLightClientOptimisticUpdate, RpcBlock, RpcBlobs, + RpcPayloadEnvelope, RpcCustodyColumn, ColumnReconstruction, IgnoredRpcBlock, @@ -545,6 +549,7 @@ impl Work { Work::GossipProposerPreferences(_) => WorkType::GossipProposerPreferences, Work::RpcBlock { .. } => WorkType::RpcBlock, Work::RpcBlobs { .. } => WorkType::RpcBlobs, + Work::RpcPayloadEnvelope { .. } => WorkType::RpcPayloadEnvelope, Work::RpcCustodyColumn { .. } => WorkType::RpcCustodyColumn, Work::ColumnReconstruction(_) => WorkType::ColumnReconstruction, Work::IgnoredRpcBlock { .. } => WorkType::IgnoredRpcBlock, @@ -1183,7 +1188,9 @@ impl BeaconProcessor { Work::GossipLightClientOptimisticUpdate { .. } => work_queues .lc_gossip_optimistic_update_queue .push(work, work_id), - Work::RpcBlock { .. } | Work::IgnoredRpcBlock { .. } => { + Work::RpcBlock { .. } + | Work::IgnoredRpcBlock { .. } + | Work::RpcPayloadEnvelope { .. } => { work_queues.rpc_block_queue.push(work, work_id) } Work::RpcBlobs { .. } => work_queues.rpc_blob_queue.push(work, work_id), @@ -1318,7 +1325,9 @@ impl BeaconProcessor { WorkType::GossipLightClientOptimisticUpdate => { work_queues.lc_gossip_optimistic_update_queue.len() } - WorkType::RpcBlock => work_queues.rpc_block_queue.len(), + WorkType::RpcBlock | WorkType::RpcPayloadEnvelope => { + work_queues.rpc_block_queue.len() + } WorkType::RpcBlobs | WorkType::IgnoredRpcBlock => { work_queues.rpc_blob_queue.len() } @@ -1513,6 +1522,7 @@ impl BeaconProcessor { beacon_block_root: _, } | Work::RpcBlobs { process_fn } + | Work::RpcPayloadEnvelope { process_fn } | Work::RpcCustodyColumn(process_fn) | Work::ColumnReconstruction(process_fn) => task_spawner.spawn_async(process_fn), Work::IgnoredRpcBlock { process_fn } => task_spawner.spawn_blocking(process_fn), diff --git a/beacon_node/lighthouse_network/src/service/api_types.rs b/beacon_node/lighthouse_network/src/service/api_types.rs index 486a443857..0b1d84b706 100644 --- a/beacon_node/lighthouse_network/src/service/api_types.rs +++ b/beacon_node/lighthouse_network/src/service/api_types.rs @@ -31,6 +31,10 @@ pub enum SyncRequestId { BlobsByRange(BlobsByRangeRequestId), /// Data columns by range request DataColumnsByRange(DataColumnsByRangeRequestId), + /// Request searching for an execution payload envelope given a block root. + SinglePayloadEnvelope { id: SingleLookupReqId }, + /// Payload envelopes by range request + PayloadEnvelopesByRange(PayloadEnvelopesByRangeRequestId), } /// Request ID for data_columns_by_root requests. Block lookups do not issue this request directly. @@ -76,6 +80,14 @@ pub enum DataColumnsByRangeRequester { CustodyBackfillSync(CustodyBackFillBatchRequestId), } +#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] +pub struct PayloadEnvelopesByRangeRequestId { + /// Id to identify this attempt at a payload_envelopes_by_range request for `parent_request_id` + pub id: Id, + /// The Id of the overall By Range request for block components. + pub parent_request_id: ComponentsByRangeRequestId, +} + /// Block components by range request for range sync. Includes an ID for downstream consumers to /// handle retries and tie all their sub requests together. #[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] @@ -252,6 +264,12 @@ macro_rules! impl_display { impl_display!(BlocksByRangeRequestId, "{}/{}", id, parent_request_id); impl_display!(BlobsByRangeRequestId, "{}/{}", id, parent_request_id); impl_display!(DataColumnsByRangeRequestId, "{}/{}", id, parent_request_id); +impl_display!( + PayloadEnvelopesByRangeRequestId, + "{}/{}", + id, + parent_request_id +); impl_display!(ComponentsByRangeRequestId, "{}/{}", id, requester); impl_display!(DataColumnsByRootRequestId, "{}/{}", id, requester); impl_display!(SingleLookupReqId, "{}/Lookup/{}", req_id, lookup_id); From 11684b0da0948d21cddeb531ad2b4d99f9b25d2c Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Tue, 28 Apr 2026 15:49:29 +0200 Subject: [PATCH 051/118] Complete envelope-lookup functionality and tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implementation: - payload_envelope_verification: implement the AvailabilityPending branch in the envelope import flow. Previously returned InternalError("Pending payload envelope not yet implemented") for any envelope whose data columns hadn't yet been received, blocking the end-to-end RPC import path. New `import_pending_execution_payload_envelope` marks the payload as received in fork choice and persists the envelope to the store; columns are still expected to arrive separately (gossip / engineGetBlobs / reconstruction) and persist their own ops. - sync manager: short-circuit `handle_unknown_parent_envelope` when the parent's payload was received between gossip-verification and the trigger reaching sync. No lookup is created; the trigger is treated as a no-op. - gossip→sync hook: when a Gloas envelope is imported via the gossip path, emit `SyncMessage::GossipEnvelopeImported { block_root }` so any lookups awaiting that parent envelope unblock without depending on the in-flight RPC response landing first. Closes the review-flagged race where a gossip-imported envelope left child lookups pinned. Tests (3 new): - envelope_already_received_skips_lookup — trigger after envelope already in fork choice creates zero lookups. - happy_path_unknown_parent_envelope — end-to-end RPC import path: lookups complete, head advances to the gossip block. - happy_path_unknown_parent_envelope_via_gossip — pending envelope-only lookup unblocked by a concurrent gossip import via the new sync hook. Existing tests updated: - bad_peer_envelope_rpc_failure / bad_peer_wrong_envelope_response now expect the lookup to retry and succeed (mirroring `bad_peer_*` tests for blocks/blobs/columns), reflecting the now-working import path. --- .../payload_envelope_verification/import.rs | 95 ++++++++++++++++++- .../src/payload_envelope_verification/mod.rs | 20 ++-- .../gossip_methods.rs | 9 +- beacon_node/network/src/sync/manager.rs | 27 ++++++ beacon_node/network/src/sync/tests/lookups.rs | 84 ++++++++++++++-- 5 files changed, 217 insertions(+), 18 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 5a6d3a1b7d..e40dc180b0 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs @@ -14,7 +14,8 @@ use super::{ }; use crate::{ AvailabilityProcessingStatus, BeaconChain, BeaconChainError, BeaconChainTypes, - NotifyExecutionLayer, block_verification_types::AvailableBlockData, metrics, + NotifyExecutionLayer, block_verification::PayloadVerificationOutcome, + block_verification_types::AvailableBlockData, metrics, payload_envelope_verification::ExecutionPendingEnvelope, validator_monitor::get_slot_delay_ms, }; @@ -99,9 +100,18 @@ impl BeaconChain { self.import_available_execution_payload_envelope(Box::new(envelope)) .await } - ExecutedEnvelope::AvailabilityPending() => Err(EnvelopeError::InternalError( - "Pending payload envelope not yet implemented".to_owned(), - )), + ExecutedEnvelope::AvailabilityPending { + signed_envelope, + import_data, + payload_verification_outcome, + } => { + self.import_pending_execution_payload_envelope( + signed_envelope, + import_data, + payload_verification_outcome, + ) + .await + } } }; @@ -185,6 +195,39 @@ impl BeaconChain { )) } + /// Import an envelope whose data column availability has not yet been satisfied. + /// + /// Marks the block's payload as received in fork choice and persists the envelope to the + /// store, but does not write data column ops. Columns are expected to arrive separately + /// (gossip, engineGetBlobs, or reconstruction). + #[instrument(skip_all)] + pub async fn import_pending_execution_payload_envelope( + self: &Arc, + signed_envelope: Arc>, + import_data: EnvelopeImportData, + payload_verification_outcome: PayloadVerificationOutcome, + ) -> Result { + let EnvelopeImportData { + block_root, + _phantom, + } = import_data; + let block_root = { + let chain = self.clone(); + self.spawn_blocking_handle( + move || { + chain.import_execution_payload_envelope_pending_columns( + signed_envelope, + block_root, + payload_verification_outcome.payload_verification_status, + ) + }, + "payload_verification_handle", + ) + .await?? + }; + Ok(AvailabilityProcessingStatus::Imported(block_root)) + } + #[instrument(skip_all)] pub async fn import_available_execution_payload_envelope( self: &Arc, @@ -219,6 +262,50 @@ impl BeaconChain { Ok(AvailabilityProcessingStatus::Imported(block_root)) } + /// Same as `import_execution_payload_envelope` but for envelopes whose data columns + /// have not yet been received. Marks the payload as received in fork choice and + /// persists the envelope; columns are persisted separately as they arrive. + #[instrument(skip_all)] + fn import_execution_payload_envelope_pending_columns( + &self, + signed_envelope: Arc>, + block_root: Hash256, + payload_verification_status: PayloadVerificationStatus, + ) -> Result { + let fork_choice_reader = self.canonical_head.fork_choice_upgradable_read_lock(); + if !fork_choice_reader.contains_block(&block_root) { + return Err(EnvelopeError::BlockRootUnknown { block_root }); + } + + let mut fork_choice = parking_lot::RwLockUpgradableReadGuard::upgrade(fork_choice_reader); + fork_choice + .on_valid_payload_envelope_received(block_root) + .map_err(|e| EnvelopeError::InternalError(format!("{e:?}")))?; + + let db_write_timer = metrics::start_timer(&metrics::ENVELOPE_PROCESSING_DB_WRITE); + let ops = vec![StoreOp::PutPayloadEnvelope( + block_root, + signed_envelope.clone(), + )]; + let db_span = info_span!("persist_envelope_pending_columns").entered(); + if let Err(e) = self.store.do_atomically_with_block_and_blobs_cache(ops) { + error!(error = ?e, "Database write failed for pending-columns envelope"); + return Err(e.into()); + } + drop(db_span); + drop(fork_choice); + + let envelope_time_imported = self.slot_clock.now_duration().unwrap_or(Duration::MAX); + metrics::stop_timer(db_write_timer); + self.import_envelope_update_metrics_and_events( + signed_envelope, + block_root, + payload_verification_status, + envelope_time_imported, + ); + Ok(block_root) + } + /// Accepts a fully-verified and available envelope and imports it into the chain without performing any /// additional verification. /// 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 51fc3f235d..7756e5cdbe 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs @@ -103,11 +103,16 @@ pub struct EnvelopeProcessingSnapshot { /// 1. `Available`: This envelope has been executed and also contains all data to consider it /// fully available. /// 2. `AvailabilityPending`: This envelope hasn't received all required blobs to consider it -/// fully available. +/// fully available. The envelope is still imported (fork-choice marks the block's payload +/// as received and the envelope is persisted); column persistence is handled separately +/// via gossip / engineGetBlobs as columns arrive. pub enum ExecutedEnvelope { Available(AvailableExecutedEnvelope), - // TODO(gloas) implement availability pending - AvailabilityPending(), + AvailabilityPending { + signed_envelope: Arc>, + import_data: EnvelopeImportData, + payload_verification_outcome: PayloadVerificationOutcome, + }, } impl ExecutedEnvelope { @@ -124,11 +129,14 @@ impl ExecutedEnvelope { payload_verification_outcome, )) } - // TODO(gloas) implement availability pending MaybeAvailableEnvelope::AvailabilityPending { block_hash: _, - envelope: _, - } => Self::AvailabilityPending(), + envelope: signed_envelope, + } => Self::AvailabilityPending { + signed_envelope, + import_data, + payload_verification_outcome, + }, } } } 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 fc95783975..bb67ec2beb 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -3983,8 +3983,13 @@ impl NetworkBeaconProcessor { // register_process_result_metrics(&result, metrics::BlockSource::Gossip, "envelope"); match &result { - Ok(AvailabilityProcessingStatus::Imported(_)) - | Ok(AvailabilityProcessingStatus::MissingComponents(_, _)) => { + Ok(AvailabilityProcessingStatus::Imported(block_root)) => { + // Notify sync so any pending child lookup awaiting this parent envelope unblocks. + self.send_sync_message(SyncMessage::GossipEnvelopeImported { + block_root: *block_root, + }); + } + Ok(AvailabilityProcessingStatus::MissingComponents(_, _)) => { // Nothing to do } Err(e) => match e { diff --git a/beacon_node/network/src/sync/manager.rs b/beacon_node/network/src/sync/manager.rs index f60c3949c9..869e7e32b0 100644 --- a/beacon_node/network/src/sync/manager.rs +++ b/beacon_node/network/src/sync/manager.rs @@ -154,6 +154,10 @@ pub enum SyncMessage { /// A block's parent is known but its execution payload envelope has not been received yet. UnknownParentEnvelope(PeerId, Arc>, Hash256), + /// An execution payload envelope has been imported via the local gossip path. + /// Sync uses this to unblock any child lookups that were awaiting this parent envelope. + GossipEnvelopeImported { block_root: Hash256 }, + /// A partial data column with an unknown parent has been received. UnknownParentPartialDataColumn { peer_id: PeerId, @@ -961,6 +965,14 @@ impl SyncManager { }), ); } + SyncMessage::GossipEnvelopeImported { block_root } => { + debug!( + %block_root, + "Gossip-imported envelope; unblocking awaiting child lookups" + ); + self.block_lookups + .continue_envelope_child_lookups(block_root, &mut self.network); + } SyncMessage::UnknownParentPartialDataColumn { peer_id, block_root, @@ -1096,6 +1108,21 @@ impl SyncManager { slot: Slot, block_component: BlockComponent, ) { + // Defensive: if the parent's payload envelope was already received between when + // gossip-verification raised `ParentEnvelopeUnknown` and now, no lookup is needed. + if self + .chain + .canonical_head + .fork_choice_read_lock() + .is_payload_received(&parent_root) + { + debug!( + %block_root, + %parent_root, + "Parent envelope already received, skipping envelope lookup" + ); + return; + } match self.should_search_for_block(Some(slot), &peer_id) { Ok(_) => { if self.block_lookups.search_child_and_parent_envelope( diff --git a/beacon_node/network/src/sync/tests/lookups.rs b/beacon_node/network/src/sync/tests/lookups.rs index 4f03924eef..35c45eb928 100644 --- a/beacon_node/network/src/sync/tests/lookups.rs +++ b/beacon_node/network/src/sync/tests/lookups.rs @@ -1304,6 +1304,32 @@ impl TestRig { self.harness.chain.recompute_head_at_current_slot().await; } + /// Persist a Gloas execution payload envelope into the local chain and mark the + /// block as "payload received" in fork choice. Mimics the side-effects of the + /// gossip-import path, including the `GossipEnvelopeImported` sync notification. + /// The caller is responsible for ensuring the corresponding beacon block is + /// already imported. + async fn import_envelope_for_block_root(&mut self, block_root: Hash256) { + let envelope = self + .network_envelopes_by_root + .get(&block_root) + .unwrap_or_else(|| panic!("no envelope cached for {block_root:?}")) + .as_ref() + .clone(); + self.harness + .chain + .store + .put_payload_envelope(&block_root, &envelope) + .expect("should store envelope"); + self.harness + .chain + .canonical_head + .fork_choice_write_lock() + .on_valid_payload_envelope_received(block_root) + .expect("should update fork choice with envelope"); + self.push_sync_message(SyncMessage::GossipEnvelopeImported { block_root }); + } + /// Import a block directly into the chain without going through lookup sync async fn import_block_by_root(&mut self, block_root: Hash256) { let range_sync_block = self @@ -2869,8 +2895,8 @@ async fn envelope_lookup_issues_by_root_rpc() { ); } -/// If the envelope RPC errors out, the envelope-only lookup is dropped and the -/// drop cascades to the awaiting child lookup. +/// One transient RPC error on the envelope request → lookup retries with the same peer +/// and completes successfully. Mirrors the `bad_peer_rpc_failure` shape used for blocks. #[tokio::test] async fn bad_peer_envelope_rpc_failure() { let Some(mut r) = setup_unknown_parent_envelope_scenario().await else { @@ -2879,11 +2905,13 @@ async fn bad_peer_envelope_rpc_failure() { r.trigger_with_last_unknown_parent_envelope(); r.simulate(SimulateConfig::new().return_rpc_error(RPCError::IoError("test".into()))) .await; - r.assert_failed_lookup_sync(); + r.assert_successful_lookup_sync(); + r.assert_head_slot(2); } -/// Peer responds with an envelope for a different block_root than was requested. -/// The request-items layer must reject as `UnrequestedBlockRoot`; both lookups drop. +/// Peer responds once with an envelope for a different block_root than requested. +/// The request-items layer raises `UnrequestedBlockRoot`, the peer is penalised, and +/// the lookup retries successfully on the next request. #[tokio::test] async fn bad_peer_wrong_envelope_response() { let Some(mut r) = setup_unknown_parent_envelope_scenario().await else { @@ -2892,8 +2920,52 @@ async fn bad_peer_wrong_envelope_response() { r.trigger_with_last_unknown_parent_envelope(); r.simulate(SimulateConfig::new().return_wrong_envelope_once()) .await; - r.assert_failed_lookup_sync(); r.assert_penalties_of_type("UnrequestedBlockRoot"); + r.assert_successful_lookup_sync(); + r.assert_head_slot(2); +} + +/// Trigger `UnknownParentEnvelope` when the parent's payload envelope is already +/// in fork choice. Sync should treat the trigger as a no-op and create no lookups. +#[tokio::test] +async fn envelope_already_received_skips_lookup() { + let Some(mut r) = setup_unknown_parent_envelope_scenario().await else { + return; + }; + let parent_root = r.get_last_block().block_cloned().parent_root(); + r.import_envelope_for_block_root(parent_root).await; + r.trigger_with_last_unknown_parent_envelope(); + r.assert_single_lookups_count(0); +} + +/// End-to-end: an envelope-only RPC lookup completes, the cached child block is +/// processed, and the head advances to the gossip block. +#[tokio::test] +async fn happy_path_unknown_parent_envelope() { + let Some(mut r) = setup_unknown_parent_envelope_scenario().await else { + return; + }; + r.trigger_with_last_unknown_parent_envelope(); + r.simulate(SimulateConfig::happy_path()).await; + r.assert_successful_lookup_sync(); + r.assert_head_slot(2); + r.assert_no_penalties(); +} + +/// While an envelope-only RPC lookup is pending, the same envelope is imported +/// via the gossip path. The child lookup should still unblock and import. +#[tokio::test] +async fn happy_path_unknown_parent_envelope_via_gossip() { + let Some(mut r) = setup_unknown_parent_envelope_scenario().await else { + return; + }; + let parent_root = r.get_last_block().block_cloned().parent_root(); + r.trigger_with_last_unknown_parent_envelope(); + // Import the envelope via the local gossip path before any RPC response arrives. + r.import_envelope_for_block_root(parent_root).await; + r.simulate(SimulateConfig::happy_path()).await; + r.assert_successful_lookup_sync(); + r.assert_head_slot(2); } /// Peer returns the requested envelope but with a corrupted signature. Gossip From 188f8271ec3b2464cc1f079bce731d960a83fdc0 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Tue, 28 Apr 2026 16:33:47 +0200 Subject: [PATCH 052/118] revert uneeded changes --- .../src/beacon/execution_payload_envelope.rs | 220 +++++++++++++----- 1 file changed, 163 insertions(+), 57 deletions(-) diff --git a/beacon_node/http_api/src/beacon/execution_payload_envelope.rs b/beacon_node/http_api/src/beacon/execution_payload_envelope.rs index 161e091100..06a5915c08 100644 --- a/beacon_node/http_api/src/beacon/execution_payload_envelope.rs +++ b/beacon_node/http_api/src/beacon/execution_payload_envelope.rs @@ -1,25 +1,24 @@ use crate::block_id::BlockId; +use crate::publish_blocks::publish_column_sidecars; use crate::task_spawner::{Priority, TaskSpawner}; use crate::utils::{ChainFilter, EthV1Filter, NetworkTxFilter, ResponseFilter, TaskSpawnerFilter}; use crate::version::{ ResponseIncludesVersion, add_consensus_version_header, add_ssz_content_type_header, execution_optimistic_finalized_beacon_response, }; -use beacon_chain::payload_envelope_verification::gossip_verified_envelope::GossipVerifiedEnvelope; -use beacon_chain::{ - BeaconChain, BeaconChainTypes, NotifyExecutionLayer, - payload_envelope_verification::EnvelopeError, -}; +use beacon_chain::data_column_verification::{GossipDataColumnError, GossipVerifiedDataColumn}; +use beacon_chain::{BeaconChain, BeaconChainTypes}; use bytes::Bytes; use eth2::types as api_types; use eth2::{CONTENT_TYPE_HEADER, SSZ_CONTENT_TYPE_HEADER}; use lighthouse_network::PubsubMessage; use network::NetworkMessage; use ssz::{Decode, Encode}; +use std::future::Future; use std::sync::Arc; use tokio::sync::mpsc::UnboundedSender; -use tracing::{info, warn}; -use types::{BlockImportSource, SignedExecutionPayloadEnvelope}; +use tracing::{debug, error, info, warn}; +use types::{EthSpec, SignedExecutionPayloadEnvelope}; use warp::{ Filter, Rejection, Reply, hyper::{Body, Response}, @@ -89,7 +88,9 @@ pub(crate) fn post_beacon_execution_payload_envelope( ) .boxed() } -/// Publishes a signed execution payload envelope to the network. +/// Publishes a signed execution payload envelope to the network. Implements +/// `POST /eth/v1/beacon/execution_payload_envelope` per the in-flight beacon-APIs PR +/// . pub async fn publish_execution_payload_envelope( envelope: SignedExecutionPayloadEnvelope, chain: Arc>, @@ -97,70 +98,175 @@ pub async fn publish_execution_payload_envelope( ) -> Result, Rejection> { let slot = envelope.slot(); let beacon_block_root = envelope.message.beacon_block_root; - let builder_index = envelope.message.builder_index; + // TODO(gloas): Replace this check once we have gossip validation. if !chain.spec.is_gloas_scheduled() { return Err(warp_utils::reject::custom_bad_request( "Execution payload envelopes are not supported before the Gloas fork".into(), )); } - let signed_envelope = Arc::new(envelope); + // TODO(gloas): We should probably add validation here i.e. BroadcastValidation::Gossip + info!( + %slot, + %beacon_block_root, + builder_index = envelope.message.builder_index, + "Publishing signed execution payload envelope to network" + ); - // The publish_fn is called inside process_execution_payload_envelope after consensus - // verification but before the EL call. - let envelope_for_publish = signed_envelope.clone(); - let sender = network_tx.clone(); - let publish_fn = move || { - info!( - %slot, - %beacon_block_root, - builder_index, - "Publishing signed execution payload envelope to network" - ); - crate::utils::publish_pubsub_message( - &sender, - PubsubMessage::ExecutionPayload(Box::new((*envelope_for_publish).clone())), - ) - .map_err(|_| { - warn!(%slot, "Failed to publish execution payload envelope to network"); - EnvelopeError::InternalError( - "Unable to publish execution payload envelope to network".to_owned(), - ) - }) - }; + let blobs_and_proofs = chain.pending_payload_envelopes.write().take_blobs(slot); - let ctx = chain.payload_envelope_gossip_verification_context(); - let gossip_verified_envelope = match GossipVerifiedEnvelope::new(signed_envelope, &ctx) { - Ok(envelope) => envelope, - Err(e) => { - warn!(%slot, %beacon_block_root, error = ?e, "Execution payload envelope rejected"); - return Err(warp_utils::reject::custom_bad_request(format!( - "execution payload envelope rejected: {e:?}", - ))); - } - }; - - // Import the envelope locally (runs state transition and notifies the EL). - chain - .process_execution_payload_envelope( + // Spawn the column-build task (CPU-bound KZG cell-and-proof computation) before + // publishing the envelope so it runs in parallel with envelope gossip, narrowing + // the window in which peers see envelope-without-columns. If envelope publication + // fails below, dropping this future drops the spawned `JoinHandle` (the running + // closure on the blocking pool finishes and is then discarded — no work cancellation). + let column_build_future = match blobs_and_proofs { + Some(blobs) if !blobs.is_empty() => Some(spawn_build_gloas_data_columns_task( + &chain, beacon_block_root, - gossip_verified_envelope, - NotifyExecutionLayer::Yes, - BlockImportSource::HttpApi, - publish_fn, + slot, + blobs, + )?), + _ => None, + }; + + // Publish the envelope to the network. + crate::utils::publish_pubsub_message( + network_tx, + PubsubMessage::ExecutionPayload(Box::new(envelope)), + ) + .map_err(|_| { + warn!(%slot, "Failed to publish execution payload envelope to network"); + warp_utils::reject::custom_server_error( + "Unable to publish execution payload envelope to network".into(), ) - .await - .map_err(|e| { - warn!(%slot, %beacon_block_root, reason = ?e, "Execution payload envelope rejected"); - warp_utils::reject::custom_bad_request(format!( - "execution payload envelope rejected: {e:?}" - )) - })?; + })?; + + // From here on the envelope is on the wire. `take_blobs` already consumed the cache + // entry, so a retry would not republish columns; returning Err would mislead the + // caller. Log column-build/publish failures and fall through to `Ok`. + if let Some(column_build_future) = column_build_future { + let gossip_verified_columns = match column_build_future.await { + Ok(columns) => columns, + Err(e) => { + error!( + %slot, + error = ?e, + "Failed to build data columns after envelope publication" + ); + return Ok(warp::reply().into_response()); + } + }; + + if !gossip_verified_columns.is_empty() { + if let Err(e) = publish_column_sidecars(network_tx, &gossip_verified_columns, &chain) { + error!( + %slot, + error = ?e, + "Failed to publish data column sidecars after envelope publication" + ); + return Ok(warp::reply().into_response()); + } + + let epoch = slot.epoch(T::EthSpec::slots_per_epoch()); + let sampling_column_indices = chain.sampling_columns_for_epoch(epoch); + let sampling_columns = gossip_verified_columns + .into_iter() + .filter(|col| sampling_column_indices.contains(&col.index())) + .collect::>(); + + // Local processing only — envelope already broadcast, so log and fall through. + if !sampling_columns.is_empty() + && let Err(e) = + Box::pin(chain.process_gossip_data_columns(sampling_columns, || Ok(()))).await + { + error!( + %slot, + error = ?e, + "Failed to process sampling data columns during envelope publication" + ); + } + } + } Ok(warp::reply().into_response()) } +fn spawn_build_gloas_data_columns_task( + chain: &Arc>, + beacon_block_root: types::Hash256, + slot: types::Slot, + blobs: types::BlobsList, +) -> Result>, Rejection>>, Rejection> { + let chain_for_build = chain.clone(); + let handle = chain + .task_executor + .spawn_blocking_handle( + move || build_gloas_data_columns(&chain_for_build, beacon_block_root, slot, &blobs), + "build_gloas_data_columns", + ) + .ok_or_else(|| warp_utils::reject::custom_server_error("runtime shutdown".to_string()))?; + + Ok(async move { + handle + .await + .map_err(|_| warp_utils::reject::custom_server_error("join error".to_string()))? + }) +} + +fn build_gloas_data_columns( + chain: &BeaconChain, + beacon_block_root: types::Hash256, + slot: types::Slot, + blobs: &types::BlobsList, +) -> Result>, Rejection> { + let blob_refs: Vec<_> = blobs.iter().collect(); + let data_column_sidecars = beacon_chain::kzg_utils::blobs_to_data_column_sidecars_gloas( + &blob_refs, + beacon_block_root, + slot, + &chain.kzg, + &chain.spec, + ) + .map_err(|e| { + error!( + error = ?e, + %slot, + "Failed to build data column sidecars for envelope" + ); + warp_utils::reject::custom_server_error(format!("{e:?}")) + })?; + + let gossip_verified_columns = data_column_sidecars + .into_iter() + .filter_map(|col| { + let index = *col.index(); + match GossipVerifiedDataColumn::new_for_block_publishing(col, chain) { + Ok(verified) => Some(verified), + Err(GossipDataColumnError::PriorKnownUnpublished) => None, + Err(e) => { + warn!( + %slot, + column_index = index, + error = ?e, + "Locally-built data column failed gossip verification" + ); + None + } + } + }) + .collect::>(); + + debug!( + %slot, + column_count = gossip_verified_columns.len(), + "Built data columns for envelope publication" + ); + + Ok(gossip_verified_columns) +} + // TODO(gloas): add tests for this endpoint once we support importing payloads into the db // GET beacon/execution_payload_envelope/{block_id} pub(crate) fn get_beacon_execution_payload_envelope( From 4535753c9b562ed35876e93cd45b1118f0022ae2 Mon Sep 17 00:00:00 2001 From: Daniel Knopik Date: Mon, 27 Apr 2026 11:36:09 +0200 Subject: [PATCH 053/118] starting to cell-ize --- beacon_node/beacon_chain/src/beacon_chain.rs | 16 +- .../beacon_chain/src/block_verification.rs | 2 +- beacon_node/beacon_chain/src/builder.rs | 2 +- .../src/data_availability_checker.rs | 4 +- .../src/data_availability_router.rs | 128 ++++++++------- .../src/data_column_verification.rs | 14 +- .../fetch_blobs/fetch_blobs_beacon_adapter.rs | 1 + .../beacon_chain/src/fetch_blobs/mod.rs | 2 +- beacon_node/beacon_chain/src/lib.rs | 2 +- .../payload_envelope_verification/import.rs | 8 +- .../mod.rs | 69 +++----- .../pending_payload_cache/pending_column.rs | 63 ++++++++ .../pending_components.rs | 152 +++++++----------- beacon_node/beacon_chain/src/test_utils.rs | 12 +- .../beacon_chain/tests/block_verification.rs | 46 ++++-- beacon_node/beacon_chain/tests/store_tests.rs | 2 +- .../network/src/sync/network_context.rs | 5 +- beacon_node/network/src/sync/tests/lookups.rs | 5 +- 18 files changed, 297 insertions(+), 236 deletions(-) rename beacon_node/beacon_chain/src/{data_availability_checker_v2 => pending_payload_cache}/mod.rs (96%) create mode 100644 beacon_node/beacon_chain/src/pending_payload_cache/pending_column.rs rename beacon_node/beacon_chain/src/{data_availability_checker_v2 => pending_payload_cache}/pending_components.rs (62%) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index c52220d787..e7cfe615a1 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -23,7 +23,6 @@ use crate::data_availability_checker::{ DataColumnReconstructionResult as DataColumnReconstructionResultV1, }; -use crate::data_availability_checker_v2::DataColumnReconstructionResult as DataColumnReconstructionResultV2; use crate::data_availability_router::{ AvailabilityOutcome, DataAvailabilityRouter, ReconstructionOutcome, }; @@ -68,6 +67,7 @@ use crate::partial_data_column_assembler::PartialMergeResult; use crate::payload_bid_verification::payload_bid_cache::GossipVerifiedPayloadBidCache; #[cfg(not(test))] use crate::payload_envelope_streamer::{EnvelopeRequestSource, launch_payload_envelope_stream}; +use crate::pending_payload_cache::DataColumnReconstructionResult as DataColumnReconstructionResultV2; use crate::pending_payload_envelopes::PendingPayloadEnvelopes; use crate::persisted_beacon_chain::PersistedBeaconChain; use crate::persisted_custody::persist_custody_context; @@ -2388,7 +2388,11 @@ impl BeaconChain { let _timer = metrics::start_timer( &metrics::PARTIAL_DATA_COLUMN_SIDECAR_HEADER_GOSSIP_VERIFICATION_TIMES, ); - let Some(assembler) = self.data_availability_checker.partial_assembler() else { + let Some(assembler) = self + .data_availability_checker + .pending_block_cache() + .partial_assembler() + else { return Err(GossipPartialDataColumnError::PartialColumnsDisabled); }; if let Some(cached_header) = assembler.get_header(&block_root) { @@ -3341,7 +3345,11 @@ impl BeaconChain { return Err(BlockError::DuplicateFullyImported(block_root)); } - let Some(assembler) = self.data_availability_checker.partial_assembler() else { + let Some(assembler) = self + .data_availability_checker + .pending_block_cache() + .partial_assembler() + else { // Partial messages are apparently not activated return Ok(None); }; @@ -3369,6 +3377,7 @@ impl BeaconChain { ); self.emit_sse_data_column_sidecar_events( + slot, &block_root, merge_result .full_columns @@ -3380,6 +3389,7 @@ impl BeaconChain { .data_availability_checker .put_kzg_verified_custody_data_columns( block_root, + slot, merge_result.full_columns.clone(), )?; diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index a58a76a2eb..688824d35e 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -1203,7 +1203,7 @@ impl SignatureVerifiedBlock { block, AvailableBlockData::NoData, // TODO(gloas) shouldnt matter which da checker we pass? - chain.data_availability_checker.v1(), + chain.data_availability_checker.pending_block_cache(), chain.spec.clone(), ) .map_err(BlockError::AvailabilityCheck)?, diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index 4d1964a0e4..b8aeef0700 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -6,7 +6,6 @@ use crate::beacon_chain::{ use crate::beacon_proposer_cache::BeaconProposerCache; use crate::custody_context::NodeCustodyType; use crate::data_availability_checker::DataAvailabilityChecker; -use crate::data_availability_checker_v2::DataAvailabilityChecker as DataAvailabilityCheckerV2; use crate::data_availability_router::DataAvailabilityRouter; use crate::fork_choice_signal::ForkChoiceSignalTx; use crate::graffiti_calculator::{GraffitiCalculator, GraffitiOrigin}; @@ -14,6 +13,7 @@ use crate::kzg_utils::{build_data_column_sidecars_fulu, build_data_column_sideca use crate::light_client_server_cache::LightClientServerCache; use crate::migrate::{BackgroundMigrator, MigratorConfig}; use crate::observed_data_sidecars::ObservedDataSidecars; +use crate::pending_payload_cache::PendingPayloadCache as DataAvailabilityCheckerV2; use crate::persisted_beacon_chain::PersistedBeaconChain; use crate::persisted_custody::load_custody_context; use crate::shuffling_cache::{BlockShufflingIds, ShufflingCache}; diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index 8e6bccb9b3..2150d7598b 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -689,12 +689,12 @@ pub fn start_availability_cache_maintenance_service( if chain.spec.deneb_fork_epoch.is_some() { let overflow_cache = chain .data_availability_checker - .v1() + .pending_block_cache() .availability_cache .clone(); let partial_assembler = chain .data_availability_checker - .v1() + .pending_block_cache() .partial_assembler .clone(); executor.spawn( diff --git a/beacon_node/beacon_chain/src/data_availability_router.rs b/beacon_node/beacon_chain/src/data_availability_router.rs index cca5ff207d..0e45a847d3 100644 --- a/beacon_node/beacon_chain/src/data_availability_router.rs +++ b/beacon_node/beacon_chain/src/data_availability_router.rs @@ -1,8 +1,8 @@ //! Abstraction layer for data availability operations across different DA checkers. //! //! This module provides a unified interface for availability operations that are shared -//! between the legacy `DataAvailabilityChecker` (v1, for blocks) and -//! `DataAvailabilityChecker` v2 (for payload envelopes after Gloas). +//! between the legacy `DataAvailabilityChecker` (for blocks) and +//! `DataAvailabilityCache` (for payload envelopes after Gloas). //! //! ## Design //! @@ -21,20 +21,20 @@ use crate::custody_context::CustodyContext; use crate::data_availability_checker::{ Availability as BlockAvailability, AvailabilityCheckError, AvailableBlock, DataAvailabilityChecker, DataAvailabilityCheckerMetrics as BlockMetrics, - DataColumnReconstructionResult as BlockReconstructionResult, -}; -use crate::data_availability_checker_v2::{ - Availability as PayloadAvailability, DataAvailabilityChecker as DataAvailabilityCheckerV2, - DataAvailabilityCheckerMetrics as PayloadMetrics, - DataColumnReconstructionResult as PayloadReconstructionResult, + DataColumnReconstructionResult as BlockReconstructionResult, MissingCellsError, }; use crate::data_column_verification::{GossipVerifiedDataColumn, KzgVerifiedCustodyDataColumn}; use crate::observed_data_sidecars::ObservationStrategy; +use crate::pending_payload_cache::{ + Availability as PayloadAvailability, DataAvailabilityCheckerMetrics as PayloadMetrics, + DataColumnReconstructionResult as PayloadReconstructionResult, PendingPayloadCache, +}; use std::sync::Arc; use types::data::{BlobIdentifier, FixedBlobSidecarList}; use types::{ BlobSidecar, BlockImportSource, ChainSpec, ColumnIndex, DataColumnSidecar, - DataColumnSidecarList, Epoch, EthSpec, ForkName, Hash256, SignedBeaconBlock, Slot, + DataColumnSidecarList, Epoch, EthSpec, ForkName, Hash256, PartialDataColumnSidecarRef, + SignedBeaconBlock, Slot, }; /// Unified result from operations that can come from either DA checker. @@ -65,7 +65,6 @@ impl AvailabilityOutcome { match self { Self::Block(BlockAvailability::Available(block)) => block.import_data.block_root, Self::Block(BlockAvailability::MissingComponents(root)) => *root, - // For payload availability, the first element of the tuple is the block root Self::Payload(PayloadAvailability::Available(available_data)) => { available_data.envelope.message().beacon_block_root } @@ -139,19 +138,23 @@ impl ReconstructionOutcome { /// we can use the V2 DA checker directly. pub struct DataAvailabilityRouter { /// Legacy DA checker for pre-Gloas blocks - v1: Arc>, + pending_block_cache: Arc>, /// Gloas DA checker for payload envelopes - v2: Arc>, + pending_payload_cache: Arc>, spec: Arc, } impl DataAvailabilityRouter { pub fn new( - v1: Arc>, - v2: Arc>, + pending_block_cache: Arc>, + pending_payload_cache: Arc>, spec: Arc, ) -> Self { - Self { v1, v2, spec } + Self { + pending_block_cache, + pending_payload_cache, + spec, + } } /// Returns true if the given slot is in the Gloas fork or later. @@ -166,7 +169,7 @@ impl DataAvailabilityRouter { /// Returns the custody context (same for both checkers). pub fn custody_context(&self) -> &Arc> { // Both checkers share the same custody context - self.v1.custody_context() + self.pending_block_cache.custody_context() } /// Query data columns from the appropriate checker based on fork. @@ -176,22 +179,23 @@ impl DataAvailabilityRouter { fork_name: ForkName, ) -> Option> { if fork_name.gloas_enabled() { - self.v2.get_data_columns(block_root) + self.pending_payload_cache.get_data_columns(block_root) } else { - self.v1.get_data_columns(block_root) + self.pending_block_cache.get_data_columns(block_root) } } - pub fn is_data_column_cached( - &self, + pub fn missing_cells_for_column_sidecar<'a>( + &'_ self, slot: Slot, - block_root: &Hash256, - data_column: &DataColumnSidecar, - ) -> bool { + data_column: &'a DataColumnSidecar, + ) -> Result>, MissingCellsError> { if self.is_gloas(slot) { - self.v2.is_data_column_cached(block_root, data_column) + self.pending_payload_cache + .missing_cells_for_column_sidecar(data_column) } else { - self.v1.is_data_column_cached(block_root, data_column) + self.pending_block_cache + .missing_cells_for_column_sidecar(data_column) } } @@ -202,9 +206,11 @@ impl DataAvailabilityRouter { slot: Slot, ) -> Option> { if self.is_gloas(slot) { - self.v2.cached_data_column_indexes(block_root) + self.pending_payload_cache + .cached_data_column_indexes(block_root) } else { - self.v1.cached_data_column_indexes(block_root) + self.pending_block_cache + .cached_data_column_indexes(block_root) } } @@ -216,11 +222,11 @@ impl DataAvailabilityRouter { custody_columns: DataColumnSidecarList, ) -> Result, AvailabilityCheckError> { if self.is_gloas(slot) { - self.v2 + self.pending_payload_cache .put_rpc_custody_columns(block_root, slot, custody_columns) .map(AvailabilityOutcome::Payload) } else { - self.v1 + self.pending_block_cache .put_rpc_custody_columns(block_root, slot, custody_columns) .map(AvailabilityOutcome::Block) } @@ -234,11 +240,11 @@ impl DataAvailabilityRouter { data_columns: Vec>, ) -> Result, AvailabilityCheckError> { if self.is_gloas(slot) { - self.v2 + self.pending_payload_cache .put_gossip_verified_data_columns(block_root, slot, data_columns) .map(AvailabilityOutcome::Payload) } else { - self.v1 + self.pending_block_cache .put_gossip_verified_data_columns(block_root, slot, data_columns) .map(AvailabilityOutcome::Block) } @@ -252,11 +258,11 @@ impl DataAvailabilityRouter { custody_columns: Vec>, ) -> Result, AvailabilityCheckError> { if self.is_gloas(slot) { - self.v2 + self.pending_payload_cache .put_kzg_verified_custody_data_columns(block_root, custody_columns) .map(AvailabilityOutcome::Payload) } else { - self.v1 + self.pending_block_cache .put_kzg_verified_custody_data_columns(block_root, custody_columns) .map(AvailabilityOutcome::Block) } @@ -269,11 +275,11 @@ impl DataAvailabilityRouter { slot: Slot, ) -> Result, AvailabilityCheckError> { if self.is_gloas(slot) { - self.v2 + self.pending_payload_cache .reconstruct_data_columns(block_root) .map(ReconstructionOutcome::Payload) } else { - self.v1 + self.pending_block_cache .reconstruct_data_columns(block_root) .map(ReconstructionOutcome::Block) } @@ -283,22 +289,23 @@ impl DataAvailabilityRouter { /// Returns the data availability boundary epoch (v1). pub fn data_availability_boundary(&self) -> Option { - self.v1.data_availability_boundary() + self.pending_block_cache.data_availability_boundary() } /// Returns whether a DA check is required for the given epoch (v1). pub fn da_check_required_for_epoch(&self, epoch: Epoch) -> bool { - self.v1.da_check_required_for_epoch(epoch) + self.pending_block_cache.da_check_required_for_epoch(epoch) } /// Returns whether blobs are required for the given epoch (v1). pub fn blobs_required_for_epoch(&self, epoch: Epoch) -> bool { - self.v1.blobs_required_for_epoch(epoch) + self.pending_block_cache.blobs_required_for_epoch(epoch) } /// Returns whether data columns are required for the given epoch (v1). pub fn data_columns_required_for_epoch(&self, epoch: Epoch) -> bool { - self.v1.data_columns_required_for_epoch(epoch) + self.pending_block_cache + .data_columns_required_for_epoch(epoch) } /// Verifies KZG commitments for a single available block (v1). @@ -306,7 +313,8 @@ impl DataAvailabilityRouter { &self, available_block: &AvailableBlock, ) -> Result<(), AvailabilityCheckError> { - self.v1.verify_kzg_for_available_block(available_block) + self.pending_block_cache + .verify_kzg_for_available_block(available_block) } /// Batch verifies KZG commitments for multiple available blocks (v1). @@ -314,7 +322,7 @@ impl DataAvailabilityRouter { &self, available_blocks: &[AvailableBlock], ) -> Result<(), AvailabilityCheckError> { - self.v1 + self.pending_block_cache .batch_verify_kzg_for_available_blocks(available_blocks) } @@ -323,17 +331,17 @@ impl DataAvailabilityRouter { &self, blob_id: &BlobIdentifier, ) -> Result>>, AvailabilityCheckError> { - self.v1.get_blob(blob_id) + self.pending_block_cache.get_blob(blob_id) } /// Returns the cached blob indexes for a given block root (v1). pub fn cached_blob_indexes(&self, block_root: &Hash256) -> Option> { - self.v1.cached_blob_indexes(block_root) + self.pending_block_cache.cached_blob_indexes(block_root) } /// Returns the cached block for a given block root (v1). pub fn get_cached_block(&self, block_root: &Hash256) -> Option> { - self.v1.get_cached_block(block_root) + self.pending_block_cache.get_cached_block(block_root) } /// Inserts a pre-execution block into the cache (v1). @@ -343,7 +351,8 @@ impl DataAvailabilityRouter { block: Arc>, source: BlockImportSource, ) -> Result<(), AvailabilityCheckError> { - self.v1.put_pre_execution_block(block_root, block, source) + self.pending_block_cache + .put_pre_execution_block(block_root, block, source) } /// Insert an executed block and check availability (v1). @@ -351,12 +360,13 @@ impl DataAvailabilityRouter { &self, executed_block: AvailabilityPendingExecutedBlock, ) -> Result, AvailabilityCheckError> { - self.v1.put_executed_block(executed_block) + self.pending_block_cache.put_executed_block(executed_block) } /// Removes a pre-execution block from the cache on execution error (v1). pub fn remove_block_on_execution_error(&self, block_root: &Hash256) { - self.v1.remove_block_on_execution_error(block_root) + self.pending_block_cache + .remove_block_on_execution_error(block_root) } /// Insert blobs received via RPC and check availability (v1). @@ -365,7 +375,7 @@ impl DataAvailabilityRouter { block_root: Hash256, blobs: FixedBlobSidecarList, ) -> Result, AvailabilityCheckError> { - self.v1.put_rpc_blobs(block_root, blobs) + self.pending_block_cache.put_rpc_blobs(block_root, blobs) } /// Insert KZG-verified blobs and check availability (v1). @@ -374,7 +384,8 @@ impl DataAvailabilityRouter { block_root: Hash256, blobs: I, ) -> Result, AvailabilityCheckError> { - self.v1.put_kzg_verified_blobs(block_root, blobs) + self.pending_block_cache + .put_kzg_verified_blobs(block_root, blobs) } /// Insert gossip-verified blobs into the v1 checker. @@ -386,15 +397,16 @@ impl DataAvailabilityRouter { block_root: Hash256, blobs: I, ) -> Result, AvailabilityCheckError> { - self.v1.put_gossip_verified_blobs(block_root, blobs) + self.pending_block_cache + .put_gossip_verified_blobs(block_root, blobs) } // ── Metrics ── pub fn metrics(&self) -> DataAvailabilityRouterMetrics { DataAvailabilityRouterMetrics { - block: self.v1.metrics(), - payload: self.v2.metrics(), + block: self.pending_block_cache.metrics(), + payload: self.pending_payload_cache.metrics(), } } @@ -402,14 +414,14 @@ impl DataAvailabilityRouter { /// Direct access to the block-level DA checker (pre-Gloas). /// Used for block availability checks, range sync, and blob verification. - pub fn v1(&self) -> &Arc> { - &self.v1 + pub fn pending_block_cache(&self) -> &Arc> { + &self.pending_block_cache } /// Direct access to the envelope-level DA checker (Gloas). /// Used for payload envelope availability checks and column verification. - pub fn v2(&self) -> &Arc> { - &self.v2 + pub fn pending_payload_cache(&self) -> &Arc> { + &self.pending_payload_cache } } @@ -426,7 +438,5 @@ pub fn start_availability_cache_maintenance_service( executor.clone(), chain.clone(), ); - crate::data_availability_checker_v2::start_availability_cache_maintenance_service( - executor, chain, - ); + crate::pending_payload_cache::start_availability_cache_maintenance_service(executor, chain); } diff --git a/beacon_node/beacon_chain/src/data_column_verification.rs b/beacon_node/beacon_chain/src/data_column_verification.rs index c2be03be0b..c360f90301 100644 --- a/beacon_node/beacon_chain/src/data_column_verification.rs +++ b/beacon_node/beacon_chain/src/data_column_verification.rs @@ -541,7 +541,11 @@ impl GossipVerifiedPartialDataColumnHeader { let header = Arc::new(header); // Cache the valid header - let Some(assembler) = chain.data_availability_checker.partial_assembler() else { + let Some(assembler) = chain + .data_availability_checker + .pending_block_cache() + .partial_assembler() + else { return Err(GossipPartialDataColumnError::PartialColumnsDisabled); }; let newly_cached = assembler.init(group_id, header.clone()); @@ -1005,7 +1009,11 @@ pub fn validate_partial_data_column_sidecar_for_gossip( } } } else { - let Some(assembler) = chain.data_availability_checker.partial_assembler() else { + let Some(assembler) = chain + .data_availability_checker + .pending_block_cache() + .partial_assembler() + else { return PartialColumnVerificationResult::Err( GossipPartialDataColumnError::PartialColumnsDisabled, ); @@ -1062,6 +1070,7 @@ pub fn validate_partial_data_column_sidecar_for_gossip( let column = Arc::from(column); let cells_to_kzg_verify = match chain .data_availability_checker + .pending_block_cache() .missing_cells_for_partial_column_sidecar(&column) { Ok(Some(cells_to_kzg_verify)) => cells_to_kzg_verify, @@ -1622,6 +1631,7 @@ mod test { harness .chain .data_availability_checker + .pending_block_cache() .partial_assembler() .unwrap() .init(block_root, header.clone()); diff --git a/beacon_node/beacon_chain/src/fetch_blobs/fetch_blobs_beacon_adapter.rs b/beacon_node/beacon_chain/src/fetch_blobs/fetch_blobs_beacon_adapter.rs index 7547a04e32..aaefb5cd3e 100644 --- a/beacon_node/beacon_chain/src/fetch_blobs/fetch_blobs_beacon_adapter.rs +++ b/beacon_node/beacon_chain/src/fetch_blobs/fetch_blobs_beacon_adapter.rs @@ -39,6 +39,7 @@ impl FetchBlobsBeaconAdapter { pub(crate) fn partial_assembler(&self) -> Option>> { self.chain .data_availability_checker + .pending_block_cache() .partial_assembler() .cloned() } diff --git a/beacon_node/beacon_chain/src/fetch_blobs/mod.rs b/beacon_node/beacon_chain/src/fetch_blobs/mod.rs index de65cfcc2e..e2ac20509b 100644 --- a/beacon_node/beacon_chain/src/fetch_blobs/mod.rs +++ b/beacon_node/beacon_chain/src/fetch_blobs/mod.rs @@ -445,7 +445,7 @@ async fn compute_custody_columns_to_import( // Only consider columns that are not already known to data availability. if let Some(known_columns) = - chain_adapter_cloned.cached_data_column_indexes(block.slot(), &block_root) + chain_adapter_cloned.cached_data_column_indexes(header.slot(), &block_root) { custody_columns.retain(|col| !known_columns.contains(&col.index())); if custody_columns.is_empty() { diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index 524b5ad639..4af5b1627e 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -18,7 +18,6 @@ pub mod canonical_head; pub mod chain_config; pub mod custody_context; pub mod data_availability_checker; -pub mod data_availability_checker_v2; pub mod data_availability_router; pub mod data_column_verification; mod early_attester_cache; @@ -49,6 +48,7 @@ pub mod partial_data_column_assembler; pub mod payload_bid_verification; pub mod payload_envelope_streamer; pub mod payload_envelope_verification; +pub mod pending_payload_cache; pub mod pending_payload_envelopes; pub mod persisted_beacon_chain; pub mod persisted_custody; 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 4576d9892e..37ba718111 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs @@ -12,7 +12,7 @@ use super::{ AvailableEnvelope, AvailableExecutedEnvelope, EnvelopeError, EnvelopeImportData, ExecutedEnvelope, gossip_verified_envelope::GossipVerifiedEnvelope, }; -use crate::data_availability_checker_v2::Availability as PayloadAvailability; +use crate::pending_payload_cache::Availability as PayloadAvailability; use crate::{ AvailabilityProcessingStatus, BeaconChain, BeaconChainError, BeaconChainTypes, NotifyExecutionLayer, @@ -58,7 +58,7 @@ impl BeaconChain { } self.data_availability_checker - .v2() + .pending_payload_cache() .put_pre_executed_payload_envelope( unverified_envelope.envelope_cloned(), envelope_source, @@ -98,7 +98,7 @@ impl BeaconChain { // chain to get stuck temporarily if the envelope is canonical. Therefore we remove // it from the cache if execution fails. self.data_availability_checker - .v2() + .pending_payload_cache() .remove_pre_executed_payload_envelope(&block_root); })?; @@ -202,7 +202,7 @@ impl BeaconChain { let slot = envelope.envelope.slot(); let availability = AvailabilityOutcome::Payload( self.data_availability_checker - .v2() + .pending_payload_cache() .put_executed_payload_envelope(envelope)?, ); self.process_payload_envelope_availability(slot, availability, || Ok(())) diff --git a/beacon_node/beacon_chain/src/data_availability_checker_v2/mod.rs b/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs similarity index 96% rename from beacon_node/beacon_chain/src/data_availability_checker_v2/mod.rs rename to beacon_node/beacon_chain/src/pending_payload_cache/mod.rs index c6d757cfca..fde5c98327 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker_v2/mod.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs @@ -37,7 +37,7 @@ //! │ ▼ //! | -> AvailableExecutedEnvelope (all columns present, payload executed against the EL, ready to import) -use crate::data_availability_checker::AvailabilityCheckError; +use crate::data_availability_checker::{AvailabilityCheckError, MissingCellsError}; use crate::payload_envelope_verification::{ AvailabilityPendingExecutedEnvelope, AvailableExecutedEnvelope, }; @@ -54,9 +54,11 @@ use task_executor::TaskExecutor; use tracing::{Span, debug, error, instrument, trace}; use types::{ BlockImportSource, ChainSpec, ColumnIndex, DataColumnSidecar, DataColumnSidecarList, Epoch, - EthSpec, Hash256, SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope, Slot, + EthSpec, Hash256, PartialDataColumnSidecarRef, SignedExecutionPayloadBid, + SignedExecutionPayloadEnvelope, Slot, }; +mod pending_column; mod pending_components; use crate::data_column_verification::{ @@ -130,7 +132,7 @@ pub enum DataColumnReconstructionResult { /// Usually data becomes available on its slot within a second of receiving its first component /// over gossip. However, data may never become available if a malicious proposer does not /// publish its data, or there are network issues. Components are only removed via LRU eviction. -pub struct DataAvailabilityChecker { +pub struct PendingPayloadCache { /// Contains all the data we keep in memory, protected by an RwLock availability_cache: RwLock>>, kzg: Arc, @@ -138,7 +140,7 @@ pub struct DataAvailabilityChecker { spec: Arc, } -impl DataAvailabilityChecker { +impl PendingPayloadCache { pub fn new( kzg: Arc, custody_context: Arc>, @@ -166,7 +168,9 @@ impl DataAvailabilityChecker { components.map(|c| { c.verified_data_columns .iter() - .map(|col| col.clone_arc()) + .filter_map(|(col_idx, col)| { + col.try_to_sidecar(*col_idx, c.slot, block_root, c.num_blobs_expected) + }) .collect() }) }) @@ -182,36 +186,10 @@ impl DataAvailabilityChecker { /// Checks if a specific data column is cached for the given block root. #[instrument(skip_all, level = "trace")] - pub fn is_data_column_cached( - &self, - block_root: &Hash256, - data_column: &DataColumnSidecar, - ) -> bool { - self.peek_pending_components(block_root, |components| { - components.is_some_and(|components| { - let cached_column_opt = components.get_cached_data_column(*data_column.index()); - cached_column_opt.is_some_and(|cached| *cached == *data_column) - }) - }) - } - - /// Returns the envelope processing status for the given `block_root`. - pub fn get_envelope_processing_status( - &self, - block_root: &Hash256, - ) -> Option> { - self.peek_pending_components(block_root, |components| { - components.and_then(|c| { - c.envelope.as_ref().map(|envelope| match envelope { - pending_components::CachedPayloadEnvelope::PreExecution(e, source) => { - PayloadEnvelopeProcessingStatus::NotValidated(e.clone(), *source) - } - pending_components::CachedPayloadEnvelope::Executed(e) => { - PayloadEnvelopeProcessingStatus::ExecutionValidated(e.envelope.clone()) - } - }) - }) - }) + pub fn missing_cells_for_column_sidecar<'a>( + &'_ self, + data_column: &'a DataColumnSidecar, + ) -> Result>, MissingCellsError> { } /// Insert an executed payload envelope into the cache and performs an availability check @@ -628,7 +606,10 @@ pub fn start_availability_cache_maintenance_service( chain: Arc>, ) { if chain.spec.gloas_fork_epoch.is_some() { - let da_checker = chain.data_availability_checker.v2().clone(); + let da_checker = chain + .data_availability_checker + .pending_payload_cache() + .clone(); executor.spawn( async move { availability_cache_maintenance_service(chain, da_checker).await }, "availability_cache_service", @@ -640,7 +621,7 @@ pub fn start_availability_cache_maintenance_service( async fn availability_cache_maintenance_service( chain: Arc>, - da_checker: Arc>, + da_checker: Arc>, ) { let epoch_duration = chain.slot_clock.slot_duration() * T::EthSpec::slots_per_epoch() as u32; loop { @@ -725,8 +706,8 @@ mod data_availability_checker_tests { use store::{HotColdDB, StoreConfig, database::interface::BeaconNodeBackend}; use tempfile::{TempDir, tempdir}; use types::{ - BeaconState, ExecutionPayloadEnvelope, ExecutionPayloadGloas, ExecutionRequests, ForkName, - FullPayload, MinimalEthSpec, SignedBeaconBlock, Slot, + ExecutionPayloadEnvelope, ExecutionPayloadGloas, ExecutionRequests, ForkName, FullPayload, + MinimalEthSpec, SignedBeaconBlock, Slot, }; type E = MinimalEthSpec; @@ -783,7 +764,7 @@ mod data_availability_checker_tests { async fn setup_harness_and_cache() -> ( BeaconChainHarness>, - Arc>, + Arc>, TempDir, ) where @@ -804,12 +785,8 @@ mod data_availability_checker_tests { )); let cache = Arc::new( - DataAvailabilityChecker::::new( - harness.chain.kzg.clone(), - custody_context, - spec.clone(), - ) - .expect("should create cache"), + PendingPayloadCache::::new(harness.chain.kzg.clone(), custody_context, spec.clone()) + .expect("should create cache"), ); (harness, cache, chain_db_path) } diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/pending_column.rs b/beacon_node/beacon_chain/src/pending_payload_cache/pending_column.rs new file mode 100644 index 0000000000..66cb8b6334 --- /dev/null +++ b/beacon_node/beacon_chain/src/pending_payload_cache/pending_column.rs @@ -0,0 +1,63 @@ +use kzg::KzgProof; +use ssz_types::VariableList; +use std::sync::Arc; +use types::{Cell, ColumnIndex, DataColumnSidecar, DataColumnSidecarGloas, EthSpec, Hash256, Slot}; + +pub struct PendingColumn { + cells: Vec, KzgProof)>>, +} + +impl PendingColumn { + pub fn new_with_capacity(blobs: usize) -> Self { + Self { + cells: vec![None; blobs], + } + } + + pub fn insert(&mut self, index: usize, cell: &Cell, proof: &KzgProof) { + if let Some(existing_cell) = self.cells.get_mut(index) + && existing_cell.is_none() + { + *existing_cell = Some((cell.clone(), proof.clone())); + } + } + + // TODO(gloas): insert_from_partial + + pub fn is_complete(&self, blob_count: usize) -> bool { + self.cells.len() == blob_count && self.cells.iter().all(|cell| cell.is_some()) + } + + pub fn try_to_sidecar( + &self, + index: ColumnIndex, + slot: Slot, + beacon_block_root: Hash256, + blob_count: usize, + ) -> Option>> { + if self.cells.len() != blob_count { + return None; + } + + let mut column = Vec::with_capacity(self.cells.len()); + let mut kzg_proofs = Vec::with_capacity(self.cells.len()); + + for cell in self.cells.iter() { + let Some((cell, proof)) = cell else { + return None; + }; + // TODO(gloas): we likely want to go and arc all cells + column.push(cell.clone()); + kzg_proofs.push(proof.clone()); + } + + Some(Arc::new(DataColumnSidecar::Gloas(DataColumnSidecarGloas { + index, + // TODO(gloas): this should not error, but we need to catch it + column: VariableList::try_from(column).ok()?, + kzg_proofs: VariableList::try_from(kzg_proofs).ok()?, + slot, + beacon_block_root, + }))) + } +} diff --git a/beacon_node/beacon_chain/src/data_availability_checker_v2/pending_components.rs b/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs similarity index 62% rename from beacon_node/beacon_chain/src/data_availability_checker_v2/pending_components.rs rename to beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs index 3f9d7e54d0..758a1705f3 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker_v2/pending_components.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs @@ -3,52 +3,40 @@ use crate::data_column_verification::KzgVerifiedCustodyDataColumn; use crate::payload_envelope_verification::AvailabilityPendingExecutedEnvelope; use crate::payload_envelope_verification::AvailableEnvelope; use crate::payload_envelope_verification::AvailableExecutedEnvelope; +use crate::pending_payload_cache::pending_column::PendingColumn; use std::cmp::Ordering; +use std::collections::HashMap; use std::sync::Arc; use tracing::{Span, debug, debug_span}; -use types::BlockImportSource; +use types::Slot; use types::{ - ChainSpec, ColumnIndex, DataColumnSidecar, Epoch, EthSpec, Hash256, SignedExecutionPayloadBid, + ChainSpec, ColumnIndex, DataColumnSidecar, Epoch, EthSpec, Hash256, SignedExecutionPayloadEnvelope, }; -pub enum CachedPayloadEnvelope { - PreExecution(Arc>, BlockImportSource), - Executed(Box>), -} - /// This represents the components of a payload pending data availability. /// /// The columns are all gossip and kzg verified. /// The payload is considered "available" when all required columns are received. pub struct PendingComponents { - /// The execution payload bid containing blob_kzg_commitments. - pub bid: Option>>, - /// a cached pre or post executed payload envelope - pub envelope: Option>, - pub verified_data_columns: Vec>, + pub slot: Slot, + pub num_blobs_expected: usize, + /// a cached post executed payload envelope + pub envelope: Option>, + pub verified_data_columns: HashMap>, pub reconstruction_started: bool, pub(crate) span: Span, spec: Arc, } impl PendingComponents { - /// Returns an immutable reference to the cached data column. - pub fn get_cached_data_column( - &self, - data_column_index: u64, - ) -> Option>> { - self.verified_data_columns - .iter() - .find(|d| d.index() == data_column_index) - .map(|d| d.clone_arc()) - } - /// Returns the indices of cached custody columns pub fn get_cached_data_columns_indices(&self) -> Vec { self.verified_data_columns .iter() - .map(|d| d.index()) + .filter_map(|(col_idx, col)| { + col.is_complete(self.num_blobs_expected).then_some(*col_idx) + }) .collect() } @@ -58,60 +46,49 @@ impl PendingComponents { kzg_verified_data_columns: I, ) -> Result<(), AvailabilityCheckError> { for data_column in kzg_verified_data_columns { - if self.get_cached_data_column(data_column.index()).is_none() { - self.verified_data_columns.push(data_column); + let data_column = data_column.as_data_column(); + let col = self + .verified_data_columns + .entry(*data_column.index()) + .or_insert_with(|| PendingColumn::new_with_capacity(self.num_blobs_expected)); + for (cell_idx, (cell, proof)) in data_column + .column() + .iter() + .zip(data_column.kzg_proofs().iter()) + .enumerate() + { + col.insert(cell_idx, cell, proof); } } Ok(()) } - /// Inserts an execution payload bid into the cache. - pub fn insert_bid(&mut self, bid: Arc>) { - self.bid = Some(bid); - } + // TODO(gloas): merge partial columns /// Inserts an executed payload envelope into the cache. pub fn insert_executed_payload_envelope( &mut self, envelope: AvailabilityPendingExecutedEnvelope, ) { - self.envelope = Some(CachedPayloadEnvelope::Executed(Box::new(envelope))) - } - - /// Inserts a pre-executed payload envelope into the cache. - pub fn insert_pre_executed_payload_envelope( - &mut self, - envelope: Arc>, - import_source: BlockImportSource, - ) { - self.envelope = Some(CachedPayloadEnvelope::PreExecution(envelope, import_source)) + self.envelope = Some(envelope); } /// Returns the number of blobs expected by reading the bid's kzg commitments. /// Returns an error if the bid is not cached. This function should only be called /// after ensuring that the bid has been cached. - pub fn num_blobs_expected(&self) -> Result { - let bid = self - .bid - .as_ref() - .ok_or_else(|| AvailabilityCheckError::Unexpected("No bid available".to_string()))?; - - Ok(bid.message.blob_kzg_commitments.len()) + pub fn num_blobs_expected(&self) -> usize { + self.num_blobs_expected } /// Returns `Some` if the envelope and all required data columns have been received. pub fn make_available( &self, + block_hash: Hash256, num_expected_columns: usize, ) -> Result>, AvailabilityCheckError> { - // If no bid has been received, we can start verifying the columns - if self.bid.is_none() { - return Ok(None); - } - // Check if the payload has been received and executed - let Some(CachedPayloadEnvelope::Executed(envelope)) = self.envelope.as_ref() else { + let Some(envelope) = &self.envelope else { return Ok(None); }; @@ -119,33 +96,30 @@ impl PendingComponents { envelope, import_data, payload_verification_outcome, - } = envelope.as_ref(); + } = envelope; - // Get the number of blobs expected from the bid - let num_expected_blobs = self.num_blobs_expected()?; - - let columns = if num_expected_blobs == 0 { + let columns = if self.num_blobs_expected == 0 { self.span.in_scope(|| { debug!("Bid has no blobs, data is available"); }); vec![] } else { - let num_received_columns = self.verified_data_columns.len(); - match num_received_columns.cmp(&num_expected_columns) { + let data_columns: Vec<_> = self + .verified_data_columns + .iter() + .filter_map(|(col_idx, col)| { + col.try_to_sidecar(*col_idx, self.slot, block_hash, self.num_blobs_expected) + }) + .collect(); + let num_completed_columns = data_columns.len(); + match num_completed_columns.cmp(&num_expected_columns) { Ordering::Greater => { // Should never happen return Err(AvailabilityCheckError::Unexpected(format!( - "too many columns got {num_received_columns} expected {num_expected_columns}" + "too many columns got {num_completed_columns} expected {num_expected_columns}" ))); } Ordering::Equal => { - // We have enough columns - let data_columns = self - .verified_data_columns - .iter() - .map(|d| d.clone().into_inner()) - .collect::>(); - self.span.in_scope(|| { debug!("All data columns received, data is available"); }); @@ -175,13 +149,19 @@ impl PendingComponents { } /// Returns an empty `PendingComponents` object with the given block root. - pub fn empty(block_root: Hash256, spec: Arc) -> Self { - let span = debug_span!(parent: None, "lh_pending_components", %block_root); + pub fn empty( + block_root: Hash256, + slot: Slot, + num_blobs_expected: usize, + spec: Arc, + ) -> Self { + let span = debug_span!(parent: None, "lh_pending_components", %block_root, %slot); let _guard = span.clone().entered(); Self { - bid: None, + slot, + num_blobs_expected, envelope: None, - verified_data_columns: vec![], + verified_data_columns: HashMap::new(), reconstruction_started: false, span, spec, @@ -189,18 +169,8 @@ impl PendingComponents { } /// Returns the epoch of the bid or first data column, if available. - pub fn epoch(&self) -> Option { - // Get epoch from bid - if let Some(bid) = &self.bid { - return Some(bid.message.slot.epoch(E::slots_per_epoch())); - } - - // Or, get epoch from first data column - if let Some(data_column) = self.verified_data_columns.first() { - return Some(data_column.as_data_column().epoch()); - } - - None + pub fn epoch(&self) -> Epoch { + self.slot.epoch(E::slots_per_epoch()) } pub fn status_str(&self, num_expected_columns: usize) -> String { @@ -221,6 +191,7 @@ pub(crate) enum ReconstructColumnsDecision { No(&'static str), } +/* #[cfg(test)] mod pending_components_tests { use crate::test_utils::test_spec; @@ -230,18 +201,6 @@ mod pending_components_tests { type E = MinimalEthSpec; - #[test] - fn test_empty_pending_components() { - let spec = Arc::new(test_spec::()); - let block_root = Hash256::random(); - let components = PendingComponents::::empty(block_root, spec); - - assert!(components.bid.is_none()); - assert!(components.verified_data_columns.is_empty()); - assert!(!components.reconstruction_started); - assert!(components.epoch().is_none()); - } - #[test] fn test_get_cached_data_columns_indices_empty() { let spec = Arc::new(test_spec::()); @@ -289,3 +248,4 @@ mod pending_components_tests { assert!(result.unwrap().is_none()); } } +*/ diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index 0f02d528a4..39da3ce0ae 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -2826,7 +2826,7 @@ where return RangeSyncBlock::new( block, AvailableBlockData::NoData, - self.chain.data_availability_checker.v1(), + self.chain.data_availability_checker.pending_block_cache(), self.chain.spec.clone(), ) .unwrap(); @@ -2845,7 +2845,7 @@ where RangeSyncBlock::new( block, block_data, - self.chain.data_availability_checker.v1(), + self.chain.data_availability_checker.pending_block_cache(), self.chain.spec.clone(), ) .unwrap() @@ -2860,7 +2860,7 @@ where RangeSyncBlock::new( block, block_data, - self.chain.data_availability_checker.v1(), + self.chain.data_availability_checker.pending_block_cache(), self.chain.spec.clone(), ) .unwrap() @@ -2889,14 +2889,14 @@ where RangeSyncBlock::new( block, block_data, - self.chain.data_availability_checker.v1(), + self.chain.data_availability_checker.pending_block_cache(), self.chain.spec.clone(), )? } else { RangeSyncBlock::new( block, AvailableBlockData::NoData, - self.chain.data_availability_checker.v1(), + self.chain.data_availability_checker.pending_block_cache(), self.chain.spec.clone(), )? } @@ -2916,7 +2916,7 @@ where RangeSyncBlock::new( block, block_data, - self.chain.data_availability_checker.v1(), + self.chain.data_availability_checker.pending_block_cache(), self.chain.spec.clone(), )? }) diff --git a/beacon_node/beacon_chain/tests/block_verification.rs b/beacon_node/beacon_chain/tests/block_verification.rs index e0e87dde9e..5e27985558 100644 --- a/beacon_node/beacon_chain/tests/block_verification.rs +++ b/beacon_node/beacon_chain/tests/block_verification.rs @@ -165,7 +165,7 @@ where RangeSyncBlock::new( block, block_data, - chain.data_availability_checker.v1(), + chain.data_availability_checker.pending_block_cache(), chain.spec.clone(), ) .unwrap() @@ -180,7 +180,7 @@ where RangeSyncBlock::new( block, block_data, - chain.data_availability_checker.v1(), + chain.data_availability_checker.pending_block_cache(), chain.spec.clone(), ) .unwrap() @@ -188,7 +188,7 @@ where None => RangeSyncBlock::new( block, AvailableBlockData::NoData, - chain.data_availability_checker.v1(), + chain.data_availability_checker.pending_block_cache(), chain.spec.clone(), ) .unwrap(), @@ -462,7 +462,10 @@ async fn chain_segment_non_linear_parent_roots() { blocks[3] = RangeSyncBlock::new( Arc::new(SignedBeaconBlock::from_block(block, signature)), blocks[3].block_data().clone(), - harness.chain.data_availability_checker.v1(), + harness + .chain + .data_availability_checker + .pending_block_cache(), harness.spec.clone(), ) .unwrap(); @@ -502,7 +505,10 @@ async fn chain_segment_non_linear_slots() { blocks[3] = RangeSyncBlock::new( Arc::new(SignedBeaconBlock::from_block(block, signature)), blocks[3].block_data().clone(), - harness.chain.data_availability_checker.v1(), + harness + .chain + .data_availability_checker + .pending_block_cache(), harness.spec.clone(), ) .unwrap(); @@ -532,7 +538,10 @@ async fn chain_segment_non_linear_slots() { blocks[3] = RangeSyncBlock::new( Arc::new(SignedBeaconBlock::from_block(block, signature)), blocks[3].block_data().clone(), - harness.chain.data_availability_checker.v1(), + harness + .chain + .data_availability_checker + .pending_block_cache(), harness.chain.spec.clone(), ) .unwrap(); @@ -1714,7 +1723,10 @@ async fn add_base_block_to_altair_chain() { let base_range_sync_block = RangeSyncBlock::new( Arc::new(base_block.clone()), AvailableBlockData::NoData, - harness.chain.data_availability_checker.v1(), + harness + .chain + .data_availability_checker + .pending_block_cache(), harness.spec.clone(), ) .unwrap(); @@ -1958,7 +1970,10 @@ async fn import_duplicate_block_unrealized_justification() { let range_sync_block = RangeSyncBlock::new( block.clone(), AvailableBlockData::NoData, - harness.chain.data_availability_checker.v1(), + harness + .chain + .data_availability_checker + .pending_block_cache(), harness.spec.clone(), ) .unwrap(); @@ -2092,7 +2107,10 @@ async fn range_sync_block_construction_fails_with_wrong_blob_count() { let result = RangeSyncBlock::new( Arc::new(block), block_data, - harness.chain.data_availability_checker.v1(), + harness + .chain + .data_availability_checker + .pending_block_cache(), harness.chain.spec.clone(), ); @@ -2170,7 +2188,10 @@ async fn range_sync_block_rejects_missing_custody_columns() { let result = RangeSyncBlock::new( Arc::new(block), block_data, - harness.chain.data_availability_checker.v1(), + harness + .chain + .data_availability_checker + .pending_block_cache(), harness.chain.spec.clone(), ); @@ -2261,7 +2282,10 @@ async fn rpc_block_allows_construction_past_da_boundary() { let result = RangeSyncBlock::new( Arc::new(block), AvailableBlockData::NoData, - harness.chain.data_availability_checker.v1(), + harness + .chain + .data_availability_checker + .pending_block_cache(), harness.chain.spec.clone(), ); diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index 5d6f644dad..3040f91342 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -3300,7 +3300,7 @@ async fn weak_subjectivity_sync_test( AvailableBlock::new( Arc::new(corrupt_block), data, - beacon_chain.data_availability_checker.v1(), + beacon_chain.data_availability_checker.pending_block_cache(), Arc::new(spec), ) .expect("available block") diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index 11722663ad..87addbfd8b 100644 --- a/beacon_node/network/src/sync/network_context.rs +++ b/beacon_node/network/src/sync/network_context.rs @@ -778,7 +778,10 @@ impl SyncNetworkContext { let range_req = entry.get_mut(); if let Some(blocks_result) = range_req.responses( - self.chain.data_availability_checker.v1().clone(), + self.chain + .data_availability_checker + .pending_block_cache() + .clone(), self.chain.spec.clone(), ) { if let Err(CouplingError::DataColumnPeerFailure { diff --git a/beacon_node/network/src/sync/tests/lookups.rs b/beacon_node/network/src/sync/tests/lookups.rs index 2de00bb219..9a3f0a5311 100644 --- a/beacon_node/network/src/sync/tests/lookups.rs +++ b/beacon_node/network/src/sync/tests/lookups.rs @@ -1110,7 +1110,10 @@ impl TestRig { let range_sync_block = RangeSyncBlock::new( block, block_data, - self.harness.chain.data_availability_checker.v1(), + self.harness + .chain + .data_availability_checker + .pending_block_cache(), self.harness.chain.spec.clone(), ) .unwrap(); From 4ef4c7ddd4640729f0077349455c10d40d8353f0 Mon Sep 17 00:00:00 2001 From: Daniel Knopik Date: Tue, 28 Apr 2026 17:06:45 +0200 Subject: [PATCH 054/118] some progress around reconstruction --- .../src/pending_payload_cache/mod.rs | 16 +++----- .../pending_components.rs | 40 ++++++++++++++----- 2 files changed, 36 insertions(+), 20 deletions(-) diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs b/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs index fde5c98327..44dfa0c895 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs @@ -470,7 +470,9 @@ impl PendingPayloadCache { pending_components: MappedRwLockReadGuard<'_, PendingComponents>, num_expected_columns: usize, ) -> Result, AvailabilityCheckError> { - if let Some(available_envelope) = pending_components.make_available(num_expected_columns)? { + if let Some(available_envelope) = + pending_components.make_available(block_root, num_expected_columns)? + { // Explicitly drop read lock before acquiring write lock drop(pending_components); if let Some(components) = self.availability_cache.write().get_mut(&block_root) { @@ -534,23 +536,17 @@ impl PendingPayloadCache { return ReconstructColumnsDecision::No("block already imported"); }; - let Some(epoch) = pending_components - .verified_data_columns - .first() - .map(|c| c.as_data_column().epoch()) - else { - return ReconstructColumnsDecision::No("not enough columns"); - }; + let epoch = pending_components.epoch(); let total_column_count = T::EthSpec::number_of_columns(); let sampling_column_count = self .custody_context .num_of_data_columns_to_sample(epoch, &self.spec); - let received_column_count = pending_components.verified_data_columns.len(); if pending_components.reconstruction_started { return ReconstructColumnsDecision::No("already started"); } + let received_column_count = pending_components.num_completed_columns(); if received_column_count >= sampling_column_count { return ReconstructColumnsDecision::No("all sampling columns received"); } @@ -559,7 +555,7 @@ impl PendingPayloadCache { } pending_components.reconstruction_started = true; - ReconstructColumnsDecision::Yes(pending_components.verified_data_columns.clone()) + ReconstructColumnsDecision::Yes(pending_components.get_cached_data_columns(block_root)) } /// This could mean some invalid data columns made it through to the `DataAvailabilityChecker`. diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs b/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs index 758a1705f3..3b09c10fe3 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs @@ -30,6 +30,16 @@ pub struct PendingComponents { } impl PendingComponents { + /// Returns the completed custody columns + pub fn get_cached_data_columns(&self, block_root: Hash256) -> Vec>> { + self.verified_data_columns + .iter() + .filter_map(|(col_idx, col)| { + col.try_to_sidecar(*col_idx, self.slot, block_root, self.num_blobs_expected) + }) + .collect() + } + /// Returns the indices of cached custody columns pub fn get_cached_data_columns_indices(&self) -> Vec { self.verified_data_columns @@ -81,6 +91,13 @@ impl PendingComponents { self.num_blobs_expected } + pub fn num_completed_columns(&self) -> usize { + self.verified_data_columns + .iter() + .filter_map(|(_, col)| col.is_complete(self.num_blobs_expected).then_some(())) + .count() + } + /// Returns `Some` if the envelope and all required data columns have been received. pub fn make_available( &self, @@ -104,14 +121,7 @@ impl PendingComponents { }); vec![] } else { - let data_columns: Vec<_> = self - .verified_data_columns - .iter() - .filter_map(|(col_idx, col)| { - col.try_to_sidecar(*col_idx, self.slot, block_hash, self.num_blobs_expected) - }) - .collect(); - let num_completed_columns = data_columns.len(); + let num_completed_columns = self.num_completed_columns(); match num_completed_columns.cmp(&num_expected_columns) { Ordering::Greater => { // Should never happen @@ -124,7 +134,17 @@ impl PendingComponents { debug!("All data columns received, data is available"); }); - data_columns + self.verified_data_columns + .iter() + .filter_map(|(col_idx, col)| { + col.try_to_sidecar( + *col_idx, + self.slot, + block_hash, + self.num_blobs_expected, + ) + }) + .collect() } Ordering::Less => { // Not enough data columns received yet @@ -187,7 +207,7 @@ impl PendingComponents { // the current usage, as it's deconstructed immediately. #[allow(clippy::large_enum_variant)] pub(crate) enum ReconstructColumnsDecision { - Yes(Vec>), + Yes(Vec>>), No(&'static str), } From 3772d2fa5bade487148274e71b91611d1dba96c0 Mon Sep 17 00:00:00 2001 From: Daniel Knopik Date: Tue, 28 Apr 2026 17:00:42 +0200 Subject: [PATCH 055/118] some claude progress --- .../payload_envelope_verification/import.rs | 12 - .../src/pending_payload_cache/mod.rs | 765 +++++------------- .../pending_payload_cache/pending_column.rs | 1 + .../pending_components.rs | 5 +- 4 files changed, 198 insertions(+), 585 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 37ba718111..97992a7106 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs @@ -57,13 +57,6 @@ impl BeaconChain { ); } - self.data_availability_checker - .pending_payload_cache() - .put_pre_executed_payload_envelope( - unverified_envelope.envelope_cloned(), - envelope_source, - )?; - let _full_timer = metrics::start_timer(&metrics::ENVELOPE_PROCESSING_TIMES); metrics::inc_counter(&metrics::ENVELOPE_PROCESSING_REQUESTS); @@ -95,11 +88,6 @@ impl BeaconChain { // If the envelope fails execution for whatever reason (e.g. engine offline), // and we keep it in the cache, then the node will NOT perform lookup and // reprocess this envelope until the envelope is evicted from DA checker, causing the - // chain to get stuck temporarily if the envelope is canonical. Therefore we remove - // it from the cache if execution fails. - self.data_availability_checker - .pending_payload_cache() - .remove_pre_executed_payload_envelope(&block_root); })?; // Record the time it took to wait for execution layer verification. diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs b/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs index 44dfa0c895..4f6c0f81d2 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs @@ -19,22 +19,22 @@ //! DataColumnSidecarList //! | //! | -> Perform data column verification against `SignedExecutionPayloadBid` -//! │ │ +//! │ │ //! │ ▼ //! | -> KzgVerifiedCustodyDataColumn -//! //! -//! SignedExecutionPayloadEnvelope -//! │ -//! | -> CachedPayloadEnvelope::PreExecution -//! │ │ -//! │ ▼ +//! +//! SignedExecutionPayloadEnvelope +//! │ +//! | -> CachedPayloadEnvelope::PreExecution +//! │ │ +//! │ ▼ //! | -> AvailabilityPendingExecutedEnvelope -//! │ │ -//! │ ▼ -//! │ -> CachedPayloadEnvelope::Executed -//! │ │ -//! │ ▼ +//! │ │ +//! │ ▼ +//! │ -> CachedPayloadEnvelope::Executed +//! │ │ +//! │ ▼ //! | -> AvailableExecutedEnvelope (all columns present, payload executed against the EL, ready to import) use crate::data_availability_checker::{AvailabilityCheckError, MissingCellsError}; @@ -46,6 +46,7 @@ use kzg::Kzg; use lru::LruCache; use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard, RwLockWriteGuard}; use slot_clock::SlotClock; +use std::collections::HashMap; use std::fmt; use std::fmt::Debug; use std::num::NonZeroUsize; @@ -53,9 +54,8 @@ use std::sync::Arc; use task_executor::TaskExecutor; use tracing::{Span, debug, error, instrument, trace}; use types::{ - BlockImportSource, ChainSpec, ColumnIndex, DataColumnSidecar, DataColumnSidecarList, Epoch, - EthSpec, Hash256, PartialDataColumnSidecarRef, SignedExecutionPayloadBid, - SignedExecutionPayloadEnvelope, Slot, + ChainSpec, ColumnIndex, DataColumnSidecar, DataColumnSidecarList, Epoch, EthSpec, Hash256, + PartialDataColumnSidecarRef, Slot, }; mod pending_column; @@ -90,16 +90,6 @@ pub enum Availability { Available(Box>), } -pub enum PayloadEnvelopeProcessingStatus { - /// Envelope is not in any pre-import cache. Envelope may be in the data-base or in the fork-choice. - Unknown, - /// Envelope is currently processing but not yet validated. - NotValidated(Arc>, BlockImportSource), - /// Envelope is fully valid, but not yet imported. It's cached in the da_checker while awaiting - /// missing envelope components. - ExecutionValidated(Arc>), -} - impl Debug for Availability { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { @@ -190,6 +180,8 @@ impl PendingPayloadCache { &'_ self, data_column: &'a DataColumnSidecar, ) -> Result>, MissingCellsError> { + // TODO(gloas): implement cell-level missing check + Ok(None) } /// Insert an executed payload envelope into the cache and performs an availability check @@ -200,7 +192,7 @@ impl PendingPayloadCache { let epoch = executed_envelope.envelope.epoch(); let beacon_block_root = executed_envelope.envelope.beacon_block_root(); let pending_components = - self.update_or_insert_pending_components(beacon_block_root, |pending_components| { + self.get_pending_components(beacon_block_root, |pending_components| { pending_components.insert_executed_payload_envelope(executed_envelope); Ok(()) })?; @@ -218,70 +210,14 @@ impl PendingPayloadCache { self.check_availability(beacon_block_root, pending_components, num_expected_columns) } - /// Insert a pre executed payload envelope in the cache - pub fn put_pre_executed_payload_envelope( - &self, - envelope: Arc>, - source: BlockImportSource, - ) -> Result<(), AvailabilityCheckError> { - let epoch = envelope.epoch(); - let beacon_block_root = envelope.beacon_block_root(); - let pending_components = - self.update_or_insert_pending_components(beacon_block_root, |pending_components| { - pending_components.insert_pre_executed_payload_envelope(envelope, source); - Ok(()) - })?; - - let num_expected_columns = self.get_num_expected_columns(epoch); - - pending_components.span.in_scope(|| { - debug!( - component = "pre executed payload envelope", - status = pending_components.status_str(num_expected_columns), - "Component added to data availability checker" - ); + /// Initialize pending components for a block. Called when the beacon block (containing the + /// bid) arrives. Sets up the slot and expected blob count so that subsequent column insertions + /// know how many cells to expect per column. + pub fn init_pending_block(&self, block_root: Hash256, slot: Slot, num_blobs_expected: usize) { + let mut write_lock = self.availability_cache.write(); + write_lock.get_or_insert_mut(block_root, || { + PendingComponents::empty(block_root, slot, num_blobs_expected, self.spec.clone()) }); - - Ok(()) - } - - /// Removes a pre-executed envelope from the cache. - /// This does NOT remove an existing executed envelope. - pub fn remove_pre_executed_payload_envelope(&self, block_root: &Hash256) { - if let Some(PayloadEnvelopeProcessingStatus::NotValidated(_, _)) = - self.get_envelope_processing_status(block_root) - { - // If the envelope is execution invalid, this status is permanent and idempotent to this - // block_root. We drop its components (e.g. columns) because they will never be useful. - self.availability_cache.write().pop(block_root); - } - } - - /// Insert an execution payload bid into the cache. - pub fn put_bid( - &self, - block_root: Hash256, - bid: Arc>, - ) -> Result, AvailabilityCheckError> { - let epoch = bid.message.slot.epoch(T::EthSpec::slots_per_epoch()); - - let pending_components = - self.update_or_insert_pending_components(block_root, |pending_components| { - pending_components.insert_bid(bid); - Ok(()) - })?; - - let num_expected_columns = self.get_num_expected_columns(epoch); - - pending_components.span.in_scope(|| { - debug!( - component = "bid", - status = pending_components.status_str(num_expected_columns), - "Component added to data availability checker" - ); - }); - - self.check_availability(block_root, pending_components, num_expected_columns) } /// Perform KZG verification on RPC custody columns and insert them into the cache. @@ -347,10 +283,9 @@ impl PendingPayloadCache { return Ok(Availability::MissingComponents(block_root)); }; - let pending_components = self - .update_or_insert_pending_components(block_root, |pending_components| { - pending_components.merge_data_columns(kzg_verified_data_columns) - })?; + let pending_components = self.get_pending_components(block_root, |pending_components| { + pending_components.merge_data_columns(kzg_verified_data_columns) + })?; let num_expected_columns = self.get_num_expected_columns(epoch); @@ -488,12 +423,14 @@ impl PendingPayloadCache { } } - /// Updates or inserts a new `PendingComponents` if it doesn't exist, and then apply the - /// `update_fn` while holding the write lock. + /// Gets existing `PendingComponents` and applies the `update_fn` while holding the write lock. /// /// Once the update is complete, the write lock is downgraded and a read guard with a /// reference of the updated `PendingComponents` is returned. - fn update_or_insert_pending_components( + /// + /// Returns an error if no pending components exist for the given block root (the block must + /// be initialized via `init_pending_block` first). + fn get_pending_components( &self, block_root: Hash256, update_fn: F, @@ -504,9 +441,11 @@ impl PendingPayloadCache { let mut write_lock = self.availability_cache.write(); { - let pending_components = write_lock.get_or_insert_mut(block_root, || { - PendingComponents::empty(block_root, self.spec.clone()) - }); + let pending_components = write_lock.get_mut(&block_root).ok_or_else(|| { + AvailabilityCheckError::Unexpected( + "pending components not initialized for block".to_string(), + ) + })?; update_fn(pending_components)? } @@ -527,6 +466,7 @@ impl PendingPayloadCache { } /// Check whether data column reconstruction should be attempted. + /// TODO(gloas): rethink reconstruction for the cell model fn check_and_set_reconstruction_started( &self, block_root: &Hash256, @@ -563,7 +503,7 @@ impl PendingPayloadCache { /// status so that we can attempt to retrieve columns from peers again. fn handle_reconstruction_failure(&self, block_root: &Hash256) { if let Some(pending_components_mut) = self.availability_cache.write().get_mut(block_root) { - pending_components_mut.verified_data_columns = vec![]; + pending_components_mut.verified_data_columns = HashMap::new(); pending_components_mut.reconstruction_started = false; } } @@ -578,9 +518,7 @@ impl PendingPayloadCache { let mut write_lock = self.availability_cache.write(); let mut keys_to_remove = vec![]; for (key, value) in write_lock.iter() { - if let Some(epoch) = value.epoch() - && epoch < cutoff_epoch - { + if value.epoch() < cutoff_epoch { keys_to_remove.push(*key); } } @@ -703,7 +641,7 @@ mod data_availability_checker_tests { use tempfile::{TempDir, tempdir}; use types::{ ExecutionPayloadEnvelope, ExecutionPayloadGloas, ExecutionRequests, ForkName, FullPayload, - MinimalEthSpec, SignedBeaconBlock, Slot, + MinimalEthSpec, SignedBeaconBlock, SignedExecutionPayloadEnvelope, Slot, }; type E = MinimalEthSpec; @@ -807,150 +745,15 @@ mod data_availability_checker_tests { // once the Gloas harness can produce KZG-valid columns. These wrappers add KZG verification // and custody column filtering on top of `put_kzg_verified_custody_data_columns`. - #[tokio::test] - async fn test_put_columns_creates_pending_components() { - if !is_gloas_enabled() { - return; - } - - type T = DiskHarnessType; - let (harness, cache, _path) = setup_harness_and_cache::().await; - - let mut rng = StdRng::seed_from_u64(0xDEADBEEF); - let spec = harness.spec.clone(); - - let (_block, data_columns) = generate_rand_block_and_data_columns::( - ForkName::Gloas, - NumBlobs::Number(1), - &mut rng, - &spec, - ); - - let block_root = Hash256::random(); - - let verified_columns: Vec<_> = data_columns - .into_iter() - .take(1) // Just take one column for the test - .map(|col| { - KzgVerifiedCustodyDataColumn::from_asserted_custody( - KzgVerifiedDataColumn::__new_for_testing(col), - ) - }) - .collect(); - - // Put columns into cache - let result = cache.put_kzg_verified_custody_data_columns(block_root, verified_columns); - assert!(result.is_ok()); - - // Check that pending components were created - assert_eq!(cache.block_cache_size(), 1); - - // Verify columns are cached - let cached_indices = cache.peek_pending_components(&block_root, |components| { - components.map(|c| c.get_cached_data_columns_indices()) - }); - assert!(cached_indices.is_some()); - assert_eq!(cached_indices.unwrap().len(), 1); - } - - #[tokio::test] - async fn test_column_deduplication() { - if !is_gloas_enabled() { - return; - } - - type T = DiskHarnessType; - let (harness, cache, _path) = setup_harness_and_cache::().await; - - let mut rng = StdRng::seed_from_u64(0xDEADBEEF); - let spec = harness.spec.clone(); - - let (_block, data_columns) = generate_rand_block_and_data_columns::( - ForkName::Gloas, - NumBlobs::Number(1), - &mut rng, - &spec, - ); - - let block_root = Hash256::random(); - - // Get the first column - let first_column = data_columns.first().cloned().expect("should have column"); - let column_index = *first_column.index(); - - let verified_column = KzgVerifiedCustodyDataColumn::from_asserted_custody( - KzgVerifiedDataColumn::__new_for_testing(first_column.clone()), - ); - - // Insert the same column twice - cache - .put_kzg_verified_custody_data_columns(block_root, vec![verified_column.clone()]) - .expect("should put column"); - - cache - .put_kzg_verified_custody_data_columns(block_root, vec![verified_column]) - .expect("should put column again"); - - // Check that we still only have one column (deduplicated) - let cached_indices = cache.peek_pending_components(&block_root, |components| { - components.map(|c| c.get_cached_data_columns_indices()) - }); - assert!(cached_indices.is_some()); - let indices = cached_indices.unwrap(); - assert_eq!(indices.len(), 1); - assert_eq!(indices[0], column_index); - } - - #[tokio::test] - async fn test_columns_without_block_not_available() { - if !is_gloas_enabled() { - return; - } - - type T = DiskHarnessType; - let (harness, cache, _path) = setup_harness_and_cache::().await; - - let mut rng = StdRng::seed_from_u64(0xDEADBEEF); - let spec = harness.spec.clone(); - - let (_block, data_columns) = generate_rand_block_and_data_columns::( - ForkName::Gloas, - NumBlobs::Number(1), - &mut rng, - &spec, - ); - - let block_root = Hash256::random(); - - // Add all columns - let verified_columns: Vec<_> = data_columns - .into_iter() - .map(|col| { - KzgVerifiedCustodyDataColumn::from_asserted_custody( - KzgVerifiedDataColumn::__new_for_testing(col), - ) - }) - .collect(); - - let result = cache - .put_kzg_verified_custody_data_columns(block_root, verified_columns) - .expect("should put columns"); - - // Without a bid, should still be missing components - assert!(matches!(result, Availability::MissingComponents(_))); - } - - /// Helper to create a test bid with the given block root and kzg commitments from a block. - fn make_test_bid( - block: &SignedBeaconBlock>, - ) -> Arc> { - let bid = block + fn num_blobs_in_block(block: &SignedBeaconBlock>) -> usize { + block .message() .body() .signed_execution_payload_bid() .expect("gloas block should have bid") - .clone(); - Arc::new(bid) + .message + .blob_kzg_commitments + .len() } fn make_test_signed_envelope(block_root: Hash256) -> Arc> { @@ -978,6 +781,135 @@ mod data_availability_checker_tests { } } + #[tokio::test] + async fn test_put_columns_creates_pending_components() { + if !is_gloas_enabled() { + return; + } + + type T = DiskHarnessType; + let (harness, cache, _path) = setup_harness_and_cache::().await; + + let mut rng = StdRng::seed_from_u64(0xDEADBEEF); + let spec = harness.spec.clone(); + + let (block, data_columns) = generate_rand_block_and_data_columns::( + ForkName::Gloas, + NumBlobs::Number(1), + &mut rng, + &spec, + ); + + let block_root = Hash256::random(); + cache.init_pending_block(block_root, Slot::new(0), num_blobs_in_block(&block)); + + let verified_columns: Vec<_> = data_columns + .into_iter() + .take(1) + .map(|col| { + KzgVerifiedCustodyDataColumn::from_asserted_custody( + KzgVerifiedDataColumn::__new_for_testing(col), + ) + }) + .collect(); + + let result = cache.put_kzg_verified_custody_data_columns(block_root, verified_columns); + assert!(result.is_ok()); + + assert_eq!(cache.block_cache_size(), 1); + + let cached_indices = cache.peek_pending_components(&block_root, |components| { + components.map(|c| c.get_cached_data_columns_indices()) + }); + assert!(cached_indices.is_some()); + assert_eq!(cached_indices.unwrap().len(), 1); + } + + #[tokio::test] + async fn test_column_deduplication() { + if !is_gloas_enabled() { + return; + } + + type T = DiskHarnessType; + let (harness, cache, _path) = setup_harness_and_cache::().await; + + let mut rng = StdRng::seed_from_u64(0xDEADBEEF); + let spec = harness.spec.clone(); + + let (block, data_columns) = generate_rand_block_and_data_columns::( + ForkName::Gloas, + NumBlobs::Number(1), + &mut rng, + &spec, + ); + + let block_root = Hash256::random(); + cache.init_pending_block(block_root, Slot::new(0), num_blobs_in_block(&block)); + + let first_column = data_columns.first().cloned().expect("should have column"); + let column_index = *first_column.index(); + + let verified_column = KzgVerifiedCustodyDataColumn::from_asserted_custody( + KzgVerifiedDataColumn::__new_for_testing(first_column.clone()), + ); + + cache + .put_kzg_verified_custody_data_columns(block_root, vec![verified_column.clone()]) + .expect("should put column"); + + cache + .put_kzg_verified_custody_data_columns(block_root, vec![verified_column]) + .expect("should put column again"); + + let cached_indices = cache.peek_pending_components(&block_root, |components| { + components.map(|c| c.get_cached_data_columns_indices()) + }); + assert!(cached_indices.is_some()); + let indices = cached_indices.unwrap(); + assert_eq!(indices.len(), 1); + assert_eq!(indices[0], column_index); + } + + #[tokio::test] + async fn test_columns_without_envelope_not_available() { + if !is_gloas_enabled() { + return; + } + + type T = DiskHarnessType; + let (harness, cache, _path) = setup_harness_and_cache::().await; + + let mut rng = StdRng::seed_from_u64(0xDEADBEEF); + let spec = harness.spec.clone(); + + let (block, data_columns) = generate_rand_block_and_data_columns::( + ForkName::Gloas, + NumBlobs::Number(1), + &mut rng, + &spec, + ); + + let block_root = Hash256::random(); + cache.init_pending_block(block_root, Slot::new(0), num_blobs_in_block(&block)); + + let verified_columns: Vec<_> = data_columns + .into_iter() + .map(|col| { + KzgVerifiedCustodyDataColumn::from_asserted_custody( + KzgVerifiedDataColumn::__new_for_testing(col), + ) + }) + .collect(); + + let result = cache + .put_kzg_verified_custody_data_columns(block_root, verified_columns) + .expect("should put columns"); + + // Without an executed envelope, should still be missing components + assert!(matches!(result, Availability::MissingComponents(_))); + } + #[tokio::test] async fn test_full_availability_flow() { if !is_gloas_enabled() { @@ -998,13 +930,7 @@ mod data_availability_checker_tests { ); let block_root = Hash256::random(); - let bid = make_test_bid(&block); - - cache.put_bid(block_root, bid).expect("should put bid"); - assert!(matches!( - cache.put_bid(block_root, make_test_bid(&block)), - Ok(Availability::MissingComponents(_)) - )); + cache.init_pending_block(block_root, Slot::new(0), num_blobs_in_block(&block)); let verified_columns: Vec<_> = data_columns .into_iter() @@ -1021,21 +947,6 @@ mod data_availability_checker_tests { assert!(matches!(result, Availability::MissingComponents(_))); - // Insert pre-executed envelope first - cache - .put_pre_executed_payload_envelope( - make_test_signed_envelope(block_root), - BlockImportSource::Gossip, - ) - .expect("should put pre-executed envelope"); - - let status = cache.get_envelope_processing_status(&block_root); - assert!(matches!( - status, - Some(PayloadEnvelopeProcessingStatus::NotValidated(..)) - )); - - // Upgrade to executed envelope (after EL validation) let executed_envelope = make_test_executed_envelope(block_root); let result = cache .put_executed_payload_envelope(executed_envelope) @@ -1049,152 +960,7 @@ mod data_availability_checker_tests { } #[tokio::test] - async fn test_zero_blob_bid_immediately_available() { - if !is_gloas_enabled() { - return; - } - - type T = DiskHarnessType; - let (harness, cache, _path) = setup_harness_and_cache::().await; - - let mut rng = StdRng::seed_from_u64(0xDEADBEEF); - let spec = harness.spec.clone(); - - // Generate a block with 0 blobs — bid will have empty commitments - let (block, _data_columns) = generate_rand_block_and_data_columns::( - ForkName::Gloas, - NumBlobs::Number(0), - &mut rng, - &spec, - ); - - let block_root = Hash256::random(); - let bid = make_test_bid(&block); - - // Insert bid (no blobs expected) - cache.put_bid(block_root, bid).expect("should put bid"); - - // Insert executed envelope — should become available immediately (no columns needed) - let executed_envelope = make_test_executed_envelope(block_root); - let result = cache - .put_executed_payload_envelope(executed_envelope) - .expect("should put executed envelope"); - - assert!( - matches!(result, Availability::Available(_)), - "zero-blob bid should be immediately available, got {:?}", - result - ); - } - - #[tokio::test] - async fn test_columns_arrive_before_bid() { - if !is_gloas_enabled() { - return; - } - - type T = DiskHarnessType; - let (harness, cache, _path) = setup_harness_and_cache::().await; - - let mut rng = StdRng::seed_from_u64(0xDEADBEEF); - let spec = harness.spec.clone(); - - let (block, data_columns) = generate_rand_block_and_data_columns::( - ForkName::Gloas, - NumBlobs::Number(1), - &mut rng, - &spec, - ); - - let block_root = Hash256::random(); - - // Columns arrive before bid - let verified_columns: Vec<_> = data_columns - .into_iter() - .map(|col| { - KzgVerifiedCustodyDataColumn::from_asserted_custody( - KzgVerifiedDataColumn::__new_for_testing(col), - ) - }) - .collect(); - - let result = cache - .put_kzg_verified_custody_data_columns(block_root, verified_columns) - .expect("should put columns"); - assert!(matches!(result, Availability::MissingComponents(_))); - - let bid = make_test_bid(&block); - let result = cache.put_bid(block_root, bid).expect("should put bid"); - assert!(matches!(result, Availability::MissingComponents(_))); - - let executed_envelope = make_test_executed_envelope(block_root); - let result = cache - .put_executed_payload_envelope(executed_envelope) - .expect("should put executed envelope"); - - assert!( - matches!(result, Availability::Available(_)), - "expected Available after all components inserted, got {:?}", - result - ); - } - - #[tokio::test] - async fn test_pre_executed_envelope_not_available() { - if !is_gloas_enabled() { - return; - } - - type T = DiskHarnessType; - let (harness, cache, _path) = setup_harness_and_cache::().await; - - let mut rng = StdRng::seed_from_u64(0xDEADBEEF); - let spec = harness.spec.clone(); - - let (block, data_columns) = generate_rand_block_and_data_columns::( - ForkName::Gloas, - NumBlobs::Number(1), - &mut rng, - &spec, - ); - - let block_root = Hash256::random(); - - // Insert bid + all columns - cache - .put_bid(block_root, make_test_bid(&block)) - .expect("should put bid"); - - let verified_columns: Vec<_> = data_columns - .into_iter() - .map(|col| { - KzgVerifiedCustodyDataColumn::from_asserted_custody( - KzgVerifiedDataColumn::__new_for_testing(col), - ) - }) - .collect(); - cache - .put_kzg_verified_custody_data_columns(block_root, verified_columns) - .expect("should put columns"); - - // Insert pre-executed envelope (not yet validated by EL) - cache - .put_pre_executed_payload_envelope( - make_test_signed_envelope(block_root), - BlockImportSource::Gossip, - ) - .expect("should put pre-executed envelope"); - - // Should NOT be available — envelope not executed yet - let status = cache.get_envelope_processing_status(&block_root); - assert!(matches!( - status, - Some(PayloadEnvelopeProcessingStatus::NotValidated(..)) - )); - } - - #[tokio::test] - async fn test_remove_pre_executed_envelope() { + async fn test_zero_blob_immediately_available() { if !is_gloas_enabled() { return; } @@ -1203,93 +969,18 @@ mod data_availability_checker_tests { let (_harness, cache, _path) = setup_harness_and_cache::().await; let block_root = Hash256::random(); + cache.init_pending_block(block_root, Slot::new(0), 0); - // Insert pre-executed envelope - cache - .put_pre_executed_payload_envelope( - make_test_signed_envelope(block_root), - BlockImportSource::Gossip, - ) - .expect("should put pre-executed envelope"); - - // Verify it's there - assert!(cache.get_envelope_processing_status(&block_root).is_some()); - - // Remove it - cache.remove_pre_executed_payload_envelope(&block_root); - - // Should be gone - let status = cache.get_envelope_processing_status(&block_root); - assert!(status.is_none()); - } - - #[tokio::test] - async fn test_remove_pre_executed_does_not_remove_executed() { - if !is_gloas_enabled() { - return; - } - - type T = DiskHarnessType; - let (_harness, cache, _path) = setup_harness_and_cache::().await; - - let block_root = Hash256::random(); - - // Insert executed envelope let executed_envelope = make_test_executed_envelope(block_root); - cache + let result = cache .put_executed_payload_envelope(executed_envelope) .expect("should put executed envelope"); - // Try to remove as pre-executed — should be a no-op - cache.remove_pre_executed_payload_envelope(&block_root); - - // Should still be there as executed - let status = cache.get_envelope_processing_status(&block_root); - assert!(matches!( - status, - Some(PayloadEnvelopeProcessingStatus::ExecutionValidated(..)) - )); - } - - #[tokio::test] - async fn test_reconstruction_started_flag() { - if !is_gloas_enabled() { - return; - } - - type T = DiskHarnessType; - let (harness, cache, _path) = setup_harness_and_cache::().await; - - let mut rng = StdRng::seed_from_u64(0xDEADBEEF); - let spec = harness.spec.clone(); - - let (_block, data_columns) = generate_rand_block_and_data_columns::( - ForkName::Gloas, - NumBlobs::Number(1), - &mut rng, - &spec, + assert!( + matches!(result, Availability::Available(_)), + "zero-blob block should be immediately available, got {:?}", + result ); - - let block_root = Hash256::random(); - - // Add some columns (not enough for reconstruction threshold) - let verified_columns: Vec<_> = data_columns - .into_iter() - .take(10) // Not enough for reconstruction - .map(|col| { - KzgVerifiedCustodyDataColumn::from_asserted_custody( - KzgVerifiedDataColumn::__new_for_testing(col), - ) - }) - .collect(); - - cache - .put_kzg_verified_custody_data_columns(block_root, verified_columns) - .expect("should put columns"); - - // Check reconstruction decision - should say "not enough columns" - let decision = cache.check_and_set_reconstruction_started(&block_root); - assert!(matches!(decision, ReconstructColumnsDecision::No(_))); } #[tokio::test] @@ -1304,7 +995,7 @@ mod data_availability_checker_tests { let mut rng = StdRng::seed_from_u64(0xDEADBEEF); let spec = harness.spec.clone(); - let (_block, data_columns) = generate_rand_block_and_data_columns::( + let (block, data_columns) = generate_rand_block_and_data_columns::( ForkName::Gloas, NumBlobs::Number(1), &mut rng, @@ -1312,8 +1003,8 @@ mod data_availability_checker_tests { ); let block_root = Hash256::random(); + cache.init_pending_block(block_root, Slot::new(0), num_blobs_in_block(&block)); - // Add some columns let verified_columns: Vec<_> = data_columns .into_iter() .take(5) @@ -1328,16 +1019,13 @@ mod data_availability_checker_tests { .put_kzg_verified_custody_data_columns(block_root, verified_columns) .expect("should put columns"); - // Verify columns are cached let cached_count = cache.peek_pending_components(&block_root, |components| { components.map(|c| c.verified_data_columns.len()) }); assert_eq!(cached_count, Some(5)); - // Handle reconstruction failure cache.handle_reconstruction_failure(&block_root); - // Verify columns are cleared let cached_count_after = cache.peek_pending_components(&block_root, |components| { components.map(|c| c.verified_data_columns.len()) }); @@ -1353,13 +1041,11 @@ mod data_availability_checker_tests { type T = DiskHarnessType; let (_harness, cache, _path) = setup_harness_and_cache::().await; - // Run maintenance with a future cutoff epoch let cutoff_epoch = Epoch::new(100); cache .do_maintenance(cutoff_epoch) .expect("maintenance should succeed"); - // Cache should still be empty since we didn't add anything with an epoch assert_eq!(cache.block_cache_size(), 0); } @@ -1375,7 +1061,7 @@ mod data_availability_checker_tests { let mut rng = StdRng::seed_from_u64(0xDEADBEEF); let spec = harness.spec.clone(); - let (_block, data_columns) = generate_rand_block_and_data_columns::( + let (block, data_columns) = generate_rand_block_and_data_columns::( ForkName::Gloas, NumBlobs::Number(1), &mut rng, @@ -1384,10 +1070,10 @@ mod data_availability_checker_tests { let block_root = Hash256::random(); - // No columns yet assert!(cache.get_data_columns(block_root).is_none()); - // Add columns + cache.init_pending_block(block_root, Slot::new(0), num_blobs_in_block(&block)); + let verified_columns: Vec<_> = data_columns .into_iter() .take(3) @@ -1402,7 +1088,6 @@ mod data_availability_checker_tests { .put_kzg_verified_custody_data_columns(block_root, verified_columns) .expect("should put columns"); - // Now columns should be returned let peeked = cache.get_data_columns(block_root); assert!(peeked.is_some()); assert_eq!(peeked.unwrap().len(), 3); @@ -1420,18 +1105,20 @@ mod data_availability_checker_tests { let mut rng = StdRng::seed_from_u64(0xDEADBEEF); let spec = harness.spec.clone(); - let (_block, data_columns) = generate_rand_block_and_data_columns::( + let (block, data_columns) = generate_rand_block_and_data_columns::( ForkName::Gloas, NumBlobs::Number(1), &mut rng, &spec, ); - // LRU capacity is 32 (OVERFLOW_LRU_CAPACITY_NON_ZERO). Insert 33 entries. + let num_blobs = num_blobs_in_block(&block); + let mut roots = Vec::new(); for _ in 0..33 { let block_root = Hash256::random(); roots.push(block_root); + cache.init_pending_block(block_root, Slot::new(0), num_blobs); let col = data_columns.first().cloned().expect("should have column"); let verified = vec![KzgVerifiedCustodyDataColumn::from_asserted_custody( KzgVerifiedDataColumn::__new_for_testing(col), @@ -1466,11 +1153,7 @@ mod data_availability_checker_tests { ); let block_root = Hash256::random(); - - // Insert bid (gives the entry an epoch via the bid's slot) - cache - .put_bid(block_root, make_test_bid(&block)) - .expect("should put bid"); + cache.init_pending_block(block_root, Slot::new(0), num_blobs_in_block(&block)); let col = data_columns.first().cloned().expect("should have column"); let verified = vec![KzgVerifiedCustodyDataColumn::from_asserted_custody( @@ -1482,7 +1165,7 @@ mod data_availability_checker_tests { assert_eq!(cache.block_cache_size(), 1); - // Maintenance with cutoff in the future should prune (bid slot=0 → epoch=0 < cutoff=100) + // slot=0 → epoch=0 < cutoff=100, should prune cache .do_maintenance(Epoch::new(100)) .expect("maintenance should succeed"); @@ -1490,58 +1173,6 @@ mod data_availability_checker_tests { assert_eq!(cache.block_cache_size(), 0); } - #[tokio::test] - async fn test_double_reconstruction_prevented() { - if !is_gloas_enabled() { - return; - } - - type T = DiskHarnessType; - let (harness, cache, _path) = setup_harness_and_cache::().await; - - let mut rng = StdRng::seed_from_u64(0xDEADBEEF); - let spec = harness.spec.clone(); - - let (_block, data_columns) = generate_rand_block_and_data_columns::( - ForkName::Gloas, - NumBlobs::Number(1), - &mut rng, - &spec, - ); - - let block_root = Hash256::random(); - - // Insert all columns so reconstruction threshold is met - let verified_columns: Vec<_> = data_columns - .into_iter() - .map(|col| { - KzgVerifiedCustodyDataColumn::from_asserted_custody( - KzgVerifiedDataColumn::__new_for_testing(col), - ) - }) - .collect(); - - cache - .put_kzg_verified_custody_data_columns(block_root, verified_columns) - .expect("should put columns"); - - // Manually set reconstruction_started via check_and_set - // For fullnode, sampling == all columns, so this returns No("all sampling columns received") - // But we can set the flag manually to test the guard - cache - .availability_cache - .write() - .get_mut(&block_root) - .expect("should exist") - .reconstruction_started = true; - - let decision = cache.check_and_set_reconstruction_started(&block_root); - assert!( - matches!(decision, ReconstructColumnsDecision::No(reason) if reason == "already started"), - "second reconstruction attempt should be prevented" - ); - } - #[tokio::test] async fn test_partial_columns_missing_components() { if !is_gloas_enabled() { @@ -1562,11 +1193,7 @@ mod data_availability_checker_tests { ); let block_root = Hash256::random(); - - // Insert bid and executed envelope - cache - .put_bid(block_root, make_test_bid(&block)) - .expect("should put bid"); + cache.init_pending_block(block_root, Slot::new(0), num_blobs_in_block(&block)); let executed_envelope = make_test_executed_envelope(block_root); cache diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/pending_column.rs b/beacon_node/beacon_chain/src/pending_payload_cache/pending_column.rs index 66cb8b6334..4289f17634 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/pending_column.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/pending_column.rs @@ -3,6 +3,7 @@ use ssz_types::VariableList; use std::sync::Arc; use types::{Cell, ColumnIndex, DataColumnSidecar, DataColumnSidecarGloas, EthSpec, Hash256, Slot}; +#[derive(Clone)] pub struct PendingColumn { cells: Vec, KzgProof)>>, } diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs b/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs index 3b09c10fe3..067a22701f 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs @@ -9,10 +9,7 @@ use std::collections::HashMap; use std::sync::Arc; use tracing::{Span, debug, debug_span}; use types::Slot; -use types::{ - ChainSpec, ColumnIndex, DataColumnSidecar, Epoch, EthSpec, Hash256, - SignedExecutionPayloadEnvelope, -}; +use types::{ChainSpec, ColumnIndex, Epoch, EthSpec, Hash256}; /// This represents the components of a payload pending data availability. /// From 802f77f700f86f0c446d0e5e2647ab315e4b8d5b Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Tue, 28 Apr 2026 17:25:53 +0200 Subject: [PATCH 056/118] Range sync --- .../src/block_verification_types.rs | 1 + .../src/payload_envelope_verification/mod.rs | 2 +- .../gossip_methods.rs | 15 ++ beacon_node/network/src/sync/batch.rs | 1 + .../src/sync/block_sidecar_coupling.rs | 184 ++++++++++++++++-- beacon_node/network/src/sync/manager.rs | 41 +++- .../network/src/sync/network_context.rs | 161 +++++++++++++-- .../src/sync/network_context/requests.rs | 2 + .../requests/payload_envelopes_by_range.rs | 42 ++++ 9 files changed, 414 insertions(+), 35 deletions(-) create mode 100644 beacon_node/network/src/sync/network_context/requests/payload_envelopes_by_range.rs diff --git a/beacon_node/beacon_chain/src/block_verification_types.rs b/beacon_node/beacon_chain/src/block_verification_types.rs index 05a757e73d..f97e695b29 100644 --- a/beacon_node/beacon_chain/src/block_verification_types.rs +++ b/beacon_node/beacon_chain/src/block_verification_types.rs @@ -46,6 +46,7 @@ impl LookupBlock { /// This includes any and all blobs/columns required, including zero if /// none are required. This can happen if the block is pre-deneb or if /// it's simply past the DA boundary. +#[derive(Clone)] pub enum RangeSyncBlock { Base(AvailableBlock), Gloas { 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 b153a3cd6a..c0e5ff4a7c 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs @@ -48,7 +48,7 @@ pub struct EnvelopeImportData { _phantom: PhantomData, } -#[derive(Debug)] +#[derive(Debug, Clone)] #[allow(dead_code)] pub struct AvailableEnvelope { execution_block_hash: ExecutionBlockHash, 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 29306c198d..d39d007a06 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -1831,6 +1831,21 @@ impl NetworkBeaconProcessor { error!(error = %e, "Internal block gossip validation error"); return None; } + Err(BlockError::ParentEnvelopeUnknown { .. }) => { + // Gossip validation does not check envelope availability; this should not occur. + self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); + return None; + } + Err(e @ BlockError::EnvelopeError(_)) => { + debug!(error = %e, "Gossip block envelope error"); + self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); + return None; + } + Err(e @ BlockError::PayloadEnvelopeError { .. }) => { + debug!(error = %e, "Gossip block payload envelope error"); + self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); + return None; + } }; metrics::inc_counter(&metrics::BEACON_PROCESSOR_GOSSIP_BLOCK_VERIFIED_TOTAL); diff --git a/beacon_node/network/src/sync/batch.rs b/beacon_node/network/src/sync/batch.rs index 10af1bf503..e0704e2569 100644 --- a/beacon_node/network/src/sync/batch.rs +++ b/beacon_node/network/src/sync/batch.rs @@ -33,6 +33,7 @@ pub type BatchId = Epoch; #[strum(serialize_all = "snake_case")] pub enum ByRangeRequestType { BlocksAndColumns, + BlocksAndEnvelopesAndColumns, BlocksAndBlobs, Blocks, Columns(HashSet), diff --git a/beacon_node/network/src/sync/block_sidecar_coupling.rs b/beacon_node/network/src/sync/block_sidecar_coupling.rs index 98cf3e0a1f..1ef2320062 100644 --- a/beacon_node/network/src/sync/block_sidecar_coupling.rs +++ b/beacon_node/network/src/sync/block_sidecar_coupling.rs @@ -4,11 +4,13 @@ use beacon_chain::{ data_availability_checker::DataAvailabilityChecker, data_column_verification::CustodyDataColumn, get_block_root, + payload_envelope_verification::AvailableEnvelope, }; use lighthouse_network::{ PeerId, service::api_types::{ BlobsByRangeRequestId, BlocksByRangeRequestId, DataColumnsByRangeRequestId, + PayloadEnvelopesByRangeRequestId, }, }; use ssz_types::RuntimeVariableList; @@ -16,7 +18,7 @@ use std::{collections::HashMap, sync::Arc}; use tracing::{Span, debug}; use types::{ BlobSidecar, ChainSpec, ColumnIndex, DataColumnSidecar, DataColumnSidecarList, EthSpec, - Hash256, SignedBeaconBlock, + Hash256, SignedBeaconBlock, SignedExecutionPayloadEnvelope, }; use crate::sync::network_context::MAX_COLUMN_RETRIES; @@ -35,6 +37,13 @@ use crate::sync::network_context::MAX_COLUMN_RETRIES; pub struct RangeBlockComponentsRequest { /// Blocks we have received awaiting for their corresponding sidecar. blocks_request: ByRangeRequest>>>, + /// Payload envelopes (Gloas+). None for pre-Gloas forks. + payloads_request: Option< + ByRangeRequest< + PayloadEnvelopesByRangeRequestId, + Vec>>, + >, + >, /// Sidecars we have received awaiting for their corresponding block. block_data_request: RangeBlockDataRequest, /// Span to track the range request and all children range requests. @@ -88,6 +97,7 @@ impl RangeBlockComponentsRequest { Vec<(DataColumnsByRangeRequestId, Vec)>, Vec, )>, + payloads_req_id: Option, request_span: Span, ) -> Self { let block_data_request = if let Some(blobs_req_id) = blobs_req_id { @@ -109,6 +119,7 @@ impl RangeBlockComponentsRequest { Self { blocks_request: ByRangeRequest::Active(blocks_req_id), + payloads_request: payloads_req_id.map(ByRangeRequest::Active), block_data_request, request_span, } @@ -191,6 +202,18 @@ impl RangeBlockComponentsRequest { } } + /// Adds received payload envelopes to the request. + pub fn add_payload_envelopes( + &mut self, + req_id: PayloadEnvelopesByRangeRequestId, + envelopes: Vec>>, + ) -> Result<(), String> { + match &mut self.payloads_request { + Some(req) => req.finish(req_id, envelopes), + None => Err("received payload envelopes but none expected".to_owned()), + } + } + /// Attempts to construct RPC blocks from all received components. /// /// Returns `None` if not all expected requests have completed. @@ -208,6 +231,13 @@ impl RangeBlockComponentsRequest { return None; }; + // If payloads are expected, they must also be complete before we can produce responses. + if let Some(payloads_req) = &self.payloads_request + && payloads_req.to_finished().is_none() + { + return None; + } + // Increment the attempt once this function returns the response or errors match &mut self.block_data_request { RangeBlockDataRequest::NoData => Some(Self::responses_with_blobs( @@ -254,15 +284,29 @@ impl RangeBlockComponentsRequest { } } - let resp = Self::responses_with_custody_columns( - blocks.to_vec(), - data_columns, - column_to_peer_id, - expected_custody_columns, - *attempt, - da_checker, - spec, - ); + // Gloas path: if payloads are present, produce Gloas blocks + let resp = if let Some(payloads_req) = &self.payloads_request { + let payloads = payloads_req.to_finished().expect("checked above").to_vec(); + Self::responses_gloas( + blocks.to_vec(), + payloads, + data_columns, + column_to_peer_id, + expected_custody_columns, + *attempt, + spec, + ) + } else { + Self::responses_with_custody_columns( + blocks.to_vec(), + data_columns, + column_to_peer_id, + expected_custody_columns, + *attempt, + da_checker, + spec, + ) + }; if let Err(CouplingError::DataColumnPeerFailure { error: _, @@ -460,6 +504,124 @@ impl RangeBlockComponentsRequest { Ok(range_sync_blocks) } + + /// Couples blocks with payload envelopes and custody columns for Gloas range sync. + fn responses_gloas( + blocks: Vec>>, + payloads: Vec>>, + data_columns: DataColumnSidecarList, + column_to_peer: HashMap, + expects_custody_columns: &[ColumnIndex], + attempt: usize, + spec: Arc, + ) -> Result>, CouplingError> { + let mut data_columns_by_block = + HashMap::>>>::new(); + + for column in data_columns { + let block_root = column.block_root(); + let index = *column.index(); + if data_columns_by_block + .entry(block_root) + .or_default() + .insert(index, column) + .is_some() + { + debug!(?block_root, ?index, "Repeated column for block_root"); + } + } + + let mut range_sync_blocks = Vec::with_capacity(blocks.len()); + let mut payload_iter = payloads.into_iter().peekable(); + let exceeded_retries = attempt >= MAX_COLUMN_RETRIES; + + for block in blocks { + let mut envelope_for_block = None; + if payload_iter + .peek() + .map(|e| e.message.slot() == block.slot()) + .unwrap_or(false) + { + envelope_for_block = payload_iter.next(); + } + + let block_root = get_block_root(&block); + + let available_envelope = if block.num_expected_blobs() > 0 { + let envelope = envelope_for_block.ok_or_else(|| { + CouplingError::InternalError(format!( + "Missing payload envelope for block {block_root:?} with blobs" + )) + })?; + + let Some(mut data_columns_by_index) = data_columns_by_block.remove(&block_root) + else { + let responsible_peers = column_to_peer.iter().map(|c| (*c.0, *c.1)).collect(); + return Err(CouplingError::DataColumnPeerFailure { + error: format!("No columns for block {block_root:?} with data"), + faulty_peers: responsible_peers, + exceeded_retries, + }); + }; + + let mut custody_columns = vec![]; + let mut naughty_peers = vec![]; + for index in expects_custody_columns { + if let Some(data_column) = data_columns_by_index.remove(index) { + custody_columns.push(data_column); + } else { + let Some(responsible_peer) = column_to_peer.get(index) else { + return Err(CouplingError::InternalError(format!( + "Internal error, no request made for column {index}" + ))); + }; + naughty_peers.push((*index, *responsible_peer)); + } + } + if !naughty_peers.is_empty() { + return Err(CouplingError::DataColumnPeerFailure { + error: format!( + "Peers did not return column for block_root {block_root:?} {naughty_peers:?}" + ), + faulty_peers: naughty_peers, + exceeded_retries, + }); + } + + Some(Box::new(AvailableEnvelope::new( + envelope.block_hash(), + envelope, + custody_columns, + None, + spec.clone(), + ))) + } else { + envelope_for_block.map(|envelope| { + Box::new(AvailableEnvelope::new( + envelope.block_hash(), + envelope, + vec![], + None, + spec.clone(), + )) + }) + }; + + range_sync_blocks.push(RangeSyncBlock::new_gloas(block, available_envelope)); + } + + if payload_iter.next().is_some() { + let remaining = payload_iter.count() + 1; + debug!(remaining, "Received payload envelopes that don't pair with blocks"); + } + + if !data_columns_by_block.is_empty() { + let remaining_roots = data_columns_by_block.keys().collect::>(); + debug!(?remaining_roots, "Not all columns consumed for Gloas blocks"); + } + + Ok(range_sync_blocks) + } } impl ByRangeRequest { @@ -560,7 +722,7 @@ mod tests { let blocks_req_id = blocks_id(components_id()); let mut info = - RangeBlockComponentsRequest::::new(blocks_req_id, None, None, Span::none()); + RangeBlockComponentsRequest::::new(blocks_req_id, None, None, None, Span::none()); // Send blocks and complete terminate response info.add_blocks(blocks_req_id, blocks).unwrap(); diff --git a/beacon_node/network/src/sync/manager.rs b/beacon_node/network/src/sync/manager.rs index 734295ac1d..5db0f9e921 100644 --- a/beacon_node/network/src/sync/manager.rs +++ b/beacon_node/network/src/sync/manager.rs @@ -59,7 +59,8 @@ use lighthouse_network::service::api_types::{ BlobsByRangeRequestId, BlocksByRangeRequestId, ComponentsByRangeRequestId, CustodyBackFillBatchRequestId, CustodyBackfillBatchId, CustodyRequester, DataColumnsByRangeRequestId, DataColumnsByRangeRequester, DataColumnsByRootRequestId, - DataColumnsByRootRequester, Id, SingleLookupReqId, SyncRequestId, + DataColumnsByRootRequester, Id, PayloadEnvelopesByRangeRequestId, SingleLookupReqId, + SyncRequestId, }; use lighthouse_network::types::{NetworkGlobals, SyncState}; use lighthouse_network::{PeerAction, PeerId}; @@ -73,7 +74,8 @@ use strum::IntoStaticStr; use tokio::sync::mpsc; use tracing::{debug, error, info, trace}; use types::{ - BlobSidecar, DataColumnSidecar, EthSpec, ForkContext, Hash256, SignedBeaconBlock, Slot, + BlobSidecar, DataColumnSidecar, EthSpec, ForkContext, Hash256, SignedBeaconBlock, + SignedExecutionPayloadEnvelope, Slot, }; /// The number of slots ahead of us that is allowed before requesting a long-range (batch) Sync @@ -512,6 +514,11 @@ impl SyncManager { SyncRequestId::DataColumnsByRange(req_id) => { self.on_data_columns_by_range_response(req_id, peer_id, RpcEvent::RPCError(error)) } + SyncRequestId::SinglePayloadEnvelope { id } => { + self.on_single_envelope_response(id, peer_id, RpcEvent::RPCError(error)) + } + SyncRequestId::PayloadEnvelopesByRange(req_id) => self + .on_payload_envelopes_by_range_response(req_id, peer_id, RpcEvent::RPCError(error)), } } @@ -1331,6 +1338,36 @@ impl SyncManager { } } + fn on_single_envelope_response( + &mut self, + id: SingleLookupReqId, + peer_id: PeerId, + rpc_event: RpcEvent>>, + ) { + // Placeholder: by-root envelope lookup not yet implemented for range sync. + // This is called on error injection for disconnected peers. Log and ignore. + let _ = (id, peer_id, rpc_event); + debug!("on_single_envelope_response: not yet implemented"); + } + + fn on_payload_envelopes_by_range_response( + &mut self, + id: PayloadEnvelopesByRangeRequestId, + peer_id: PeerId, + envelope: RpcEvent>>, + ) { + if let Some(resp) = self + .network + .on_payload_envelopes_by_range_response(id, peer_id, envelope) + { + self.on_range_components_response( + id.parent_request_id, + peer_id, + RangeBlockComponent::PayloadEnvelope(id, resp), + ); + } + } + fn on_custody_by_root_result( &mut self, requester: CustodyRequester, diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index b1ba87c75d..346f9c5d41 100644 --- a/beacon_node/network/src/sync/network_context.rs +++ b/beacon_node/network/src/sync/network_context.rs @@ -22,14 +22,17 @@ use beacon_chain::block_verification_types::{AsBlock, RangeSyncBlock}; use beacon_chain::{BeaconChain, BeaconChainTypes, BlockProcessStatus, EngineState}; use custody::CustodyRequestResult; use fnv::FnvHashMap; -use lighthouse_network::rpc::methods::{BlobsByRangeRequest, DataColumnsByRangeRequest}; +use lighthouse_network::rpc::methods::{ + BlobsByRangeRequest, DataColumnsByRangeRequest, PayloadEnvelopesByRangeRequest, +}; use lighthouse_network::rpc::{BlocksByRangeRequest, GoodbyeReason, RPCError, RequestType}; pub use lighthouse_network::service::api_types::RangeRequestId; use lighthouse_network::service::api_types::{ AppRequestId, BlobsByRangeRequestId, BlocksByRangeRequestId, ComponentsByRangeRequestId, CustodyBackFillBatchRequestId, CustodyBackfillBatchId, CustodyId, CustodyRequester, DataColumnsByRangeRequestId, DataColumnsByRangeRequester, DataColumnsByRootRequestId, - DataColumnsByRootRequester, Id, SingleLookupReqId, SyncRequestId, + DataColumnsByRootRequester, Id, PayloadEnvelopesByRangeRequestId, SingleLookupReqId, + SyncRequestId, }; use lighthouse_network::{Client, NetworkGlobals, PeerAction, PeerId, ReportSource}; use parking_lot::RwLock; @@ -37,6 +40,7 @@ pub use requests::LookupVerifyError; use requests::{ ActiveRequests, BlobsByRangeRequestItems, BlobsByRootRequestItems, BlocksByRangeRequestItems, BlocksByRootRequestItems, DataColumnsByRangeRequestItems, DataColumnsByRootRequestItems, + PayloadEnvelopesByRangeRequestItems, }; #[cfg(test)] use slot_clock::SlotClock; @@ -52,7 +56,7 @@ use tracing::{Span, debug, debug_span, error, warn}; use types::data::FixedBlobSidecarList; use types::{ BlobSidecar, BlockImportSource, ColumnIndex, DataColumnSidecar, DataColumnSidecarList, EthSpec, - ForkContext, Hash256, SignedBeaconBlock, Slot, + ForkContext, Hash256, SignedBeaconBlock, SignedExecutionPayloadEnvelope, Slot, }; pub mod custody; @@ -213,6 +217,11 @@ pub struct SyncNetworkContext { /// A mapping of active DataColumnsByRange requests data_columns_by_range_requests: ActiveRequests>, + /// A mapping of active PayloadEnvelopesByRange requests + payload_envelopes_by_range_requests: ActiveRequests< + PayloadEnvelopesByRangeRequestId, + PayloadEnvelopesByRangeRequestItems, + >, /// Mapping of active custody column requests for a block root custody_by_root_requests: FnvHashMap>, @@ -250,6 +259,10 @@ pub enum RangeBlockComponent { DataColumnsByRangeRequestId, RpcResponseResult>>>, ), + PayloadEnvelope( + PayloadEnvelopesByRangeRequestId, + RpcResponseResult>>>, + ), } #[cfg(test)] @@ -298,6 +311,7 @@ impl SyncNetworkContext { blocks_by_range_requests: ActiveRequests::new("blocks_by_range"), blobs_by_range_requests: ActiveRequests::new("blobs_by_range"), data_columns_by_range_requests: ActiveRequests::new("data_columns_by_range"), + payload_envelopes_by_range_requests: ActiveRequests::new("payload_envelopes_by_range"), custody_by_root_requests: <_>::default(), components_by_range_requests: FnvHashMap::default(), custody_backfill_data_column_batch_requests: FnvHashMap::default(), @@ -326,6 +340,7 @@ impl SyncNetworkContext { blocks_by_range_requests, blobs_by_range_requests, data_columns_by_range_requests, + payload_envelopes_by_range_requests, // custody_by_root_requests is a meta request of data_columns_by_root_requests custody_by_root_requests: _, // components_by_range_requests is a meta request of various _by_range requests @@ -361,12 +376,17 @@ impl SyncNetworkContext { .active_requests_of_peer(peer_id) .into_iter() .map(|req_id| SyncRequestId::DataColumnsByRange(*req_id)); + let payload_envelope_by_range_ids = payload_envelopes_by_range_requests + .active_requests_of_peer(peer_id) + .into_iter() + .map(|req_id| SyncRequestId::PayloadEnvelopesByRange(*req_id)); blocks_by_root_ids .chain(blobs_by_root_ids) .chain(data_column_by_root_ids) .chain(blocks_by_range_ids) .chain(blobs_by_range_ids) .chain(data_column_by_range_ids) + .chain(payload_envelope_by_range_ids) .collect() } @@ -423,6 +443,7 @@ impl SyncNetworkContext { blocks_by_range_requests, blobs_by_range_requests, data_columns_by_range_requests, + payload_envelopes_by_range_requests, // custody_by_root_requests is a meta request of data_columns_by_root_requests custody_by_root_requests: _, // components_by_range_requests is a meta request of various _by_range requests @@ -445,6 +466,7 @@ impl SyncNetworkContext { .chain(blocks_by_range_requests.iter_request_peers()) .chain(blobs_by_range_requests.iter_request_peers()) .chain(data_columns_by_range_requests.iter_request_peers()) + .chain(payload_envelopes_by_range_requests.iter_request_peers()) { *active_request_count_by_peer.entry(peer_id).or_default() += 1; } @@ -577,24 +599,26 @@ impl SyncNetworkContext { }; // Attempt to find all required custody peers before sending any request or creating an ID - let columns_by_range_peers_to_request = - if matches!(batch_type, ByRangeRequestType::BlocksAndColumns) { - let epoch = Slot::new(*request.start_slot()).epoch(T::EthSpec::slots_per_epoch()); - let column_indexes = self - .chain - .sampling_columns_for_epoch(epoch) - .iter() - .cloned() - .collect(); - Some(self.select_columns_by_range_peers_to_request( - &column_indexes, - column_peers, - active_request_count_by_peer, - peers_to_deprioritize, - )?) - } else { - None - }; + let columns_by_range_peers_to_request = if matches!( + batch_type, + ByRangeRequestType::BlocksAndColumns | ByRangeRequestType::BlocksAndEnvelopesAndColumns + ) { + let epoch = Slot::new(*request.start_slot()).epoch(T::EthSpec::slots_per_epoch()); + let column_indexes = self + .chain + .sampling_columns_for_epoch(epoch) + .iter() + .cloned() + .collect(); + Some(self.select_columns_by_range_peers_to_request( + &column_indexes, + column_peers, + active_request_count_by_peer, + peers_to_deprioritize, + )?) + } else { + None + }; // Create the overall components_by_range request ID before its individual components let id = ComponentsByRangeRequestId { @@ -659,6 +683,28 @@ impl SyncNetworkContext { .transpose()?; let epoch = Slot::new(*request.start_slot()).epoch(T::EthSpec::slots_per_epoch()); + + // Send envelope request for Gloas epochs + let payloads_req_id = + if matches!(batch_type, ByRangeRequestType::BlocksAndEnvelopesAndColumns) { + Some(self.send_payload_envelopes_by_range_request( + block_peer, + PayloadEnvelopesByRangeRequest { + start_slot: *request.start_slot(), + count: *request.count(), + }, + id, + new_range_request_span!( + self, + "outgoing_envelopes_by_range", + range_request_span.clone(), + block_peer + ), + )?) + } else { + None + }; + let info = RangeBlockComponentsRequest::new( blocks_req_id, blobs_req_id, @@ -668,6 +714,7 @@ impl SyncNetworkContext { self.chain.sampling_columns_for_epoch(epoch).to_vec(), ) }), + payloads_req_id, range_request_span, ); self.components_by_range_requests.insert(id, info); @@ -770,6 +817,17 @@ impl SyncNetworkContext { }) }) } + RangeBlockComponent::PayloadEnvelope(req_id, resp) => { + resp.and_then(|(envelopes, _)| { + request + .add_payload_envelopes(req_id, envelopes) + .map_err(|e| { + RpcResponseError::BlockComponentCouplingError( + CouplingError::InternalError(e), + ) + }) + }) + } } } { entry.remove(); @@ -1288,6 +1346,57 @@ impl SyncNetworkContext { Ok((id, requested_columns)) } + fn send_payload_envelopes_by_range_request( + &mut self, + peer_id: PeerId, + request: PayloadEnvelopesByRangeRequest, + parent_request_id: ComponentsByRangeRequestId, + request_span: Span, + ) -> Result { + let id = PayloadEnvelopesByRangeRequestId { + id: self.next_id(), + parent_request_id, + }; + + self.send_network_msg(NetworkMessage::SendRequest { + peer_id, + request: RequestType::PayloadEnvelopesByRange(request.clone()), + app_request_id: AppRequestId::Sync(SyncRequestId::PayloadEnvelopesByRange(id)), + }) + .map_err(|_| RpcRequestSendError::InternalError("network send error".to_owned()))?; + + debug!( + method = "PayloadEnvelopesByRange", + slots = request.count, + epoch = %Slot::new(request.start_slot).epoch(T::EthSpec::slots_per_epoch()), + peer = %peer_id, + %id, + "Sync RPC request sent" + ); + + self.payload_envelopes_by_range_requests.insert( + id, + peer_id, + false, + PayloadEnvelopesByRangeRequestItems::new(request), + request_span, + ); + Ok(id) + } + + #[allow(clippy::type_complexity)] + pub(crate) fn on_payload_envelopes_by_range_response( + &mut self, + id: PayloadEnvelopesByRangeRequestId, + peer_id: PeerId, + rpc_event: RpcEvent>>, + ) -> Option>>>> { + let resp = self + .payload_envelopes_by_range_requests + .on_response(id, rpc_event); + self.on_rpc_response_result(resp, peer_id) + } + pub fn is_execution_engine_online(&self) -> bool { self.execution_engine_state == EngineState::Online } @@ -1369,6 +1478,12 @@ impl SyncNetworkContext { ); if self + .chain + .data_availability_checker + .envelopes_required_for_epoch(epoch) + { + ByRangeRequestType::BlocksAndEnvelopesAndColumns + } else if self .chain .data_availability_checker .data_columns_required_for_epoch(epoch) @@ -1788,6 +1903,10 @@ impl SyncNetworkContext { "data_columns_by_range", self.data_columns_by_range_requests.len(), ), + ( + "payload_envelopes_by_range", + self.payload_envelopes_by_range_requests.len(), + ), ("custody_by_root", self.custody_by_root_requests.len()), ( "components_by_range", diff --git a/beacon_node/network/src/sync/network_context/requests.rs b/beacon_node/network/src/sync/network_context/requests.rs index ad60dffb45..b6361a2ed1 100644 --- a/beacon_node/network/src/sync/network_context/requests.rs +++ b/beacon_node/network/src/sync/network_context/requests.rs @@ -16,6 +16,7 @@ pub use data_columns_by_range::DataColumnsByRangeRequestItems; pub use data_columns_by_root::{ DataColumnsByRootRequestItems, DataColumnsByRootSingleBlockRequest, }; +pub use payload_envelopes_by_range::PayloadEnvelopesByRangeRequestItems; use crate::metrics; @@ -27,6 +28,7 @@ mod blocks_by_range; mod blocks_by_root; mod data_columns_by_range; mod data_columns_by_root; +mod payload_envelopes_by_range; #[derive(Debug, PartialEq, Eq, IntoStaticStr)] pub enum LookupVerifyError { diff --git a/beacon_node/network/src/sync/network_context/requests/payload_envelopes_by_range.rs b/beacon_node/network/src/sync/network_context/requests/payload_envelopes_by_range.rs new file mode 100644 index 0000000000..3d4ea8248b --- /dev/null +++ b/beacon_node/network/src/sync/network_context/requests/payload_envelopes_by_range.rs @@ -0,0 +1,42 @@ +use super::{ActiveRequestItems, LookupVerifyError}; +use lighthouse_network::rpc::methods::PayloadEnvelopesByRangeRequest; +use std::sync::Arc; +use types::{EthSpec, SignedExecutionPayloadEnvelope}; + +/// Accumulates results of a payload_envelopes_by_range request. Only returns items after +/// receiving the stream termination. +pub struct PayloadEnvelopesByRangeRequestItems { + request: PayloadEnvelopesByRangeRequest, + items: Vec>>, +} + +impl PayloadEnvelopesByRangeRequestItems { + pub fn new(request: PayloadEnvelopesByRangeRequest) -> Self { + Self { + request, + items: vec![], + } + } +} + +impl ActiveRequestItems for PayloadEnvelopesByRangeRequestItems { + type Item = Arc>; + + fn add(&mut self, envelope: Self::Item) -> Result { + let slot = envelope.slot(); + if slot < self.request.start_slot || slot >= self.request.start_slot + self.request.count { + return Err(LookupVerifyError::UnrequestedSlot(slot)); + } + if self.items.iter().any(|existing| existing.slot() == slot) { + return Err(LookupVerifyError::DuplicatedData(slot, 0)); + } + + self.items.push(envelope); + + Ok(false) + } + + fn consume(&mut self) -> Vec { + std::mem::take(&mut self.items) + } +} From d5ad1d8178fc766d3de9c70a7ced7f3c23e5fc76 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Tue, 28 Apr 2026 17:32:42 +0200 Subject: [PATCH 057/118] fix --- beacon_node/network/src/sync/block_sidecar_coupling.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/beacon_node/network/src/sync/block_sidecar_coupling.rs b/beacon_node/network/src/sync/block_sidecar_coupling.rs index 1ef2320062..6df05921c2 100644 --- a/beacon_node/network/src/sync/block_sidecar_coupling.rs +++ b/beacon_node/network/src/sync/block_sidecar_coupling.rs @@ -612,12 +612,18 @@ impl RangeBlockComponentsRequest { if payload_iter.next().is_some() { let remaining = payload_iter.count() + 1; - debug!(remaining, "Received payload envelopes that don't pair with blocks"); + debug!( + remaining, + "Received payload envelopes that don't pair with blocks" + ); } if !data_columns_by_block.is_empty() { let remaining_roots = data_columns_by_block.keys().collect::>(); - debug!(?remaining_roots, "Not all columns consumed for Gloas blocks"); + debug!( + ?remaining_roots, + "Not all columns consumed for Gloas blocks" + ); } Ok(range_sync_blocks) From 132f94c91c03f9c6b488ed59d35da99c33ec747d Mon Sep 17 00:00:00 2001 From: Daniel Knopik Date: Tue, 28 Apr 2026 17:20:31 +0200 Subject: [PATCH 058/118] clean up claude progress --- .../beacon_chain/src/data_availability_checker.rs | 5 ++++- .../beacon_chain/src/data_column_verification.rs | 12 +++--------- .../beacon_chain/src/pending_payload_cache/mod.rs | 4 ++-- .../src/pending_payload_cache/pending_column.rs | 4 ++-- .../src/pending_payload_cache/pending_components.rs | 6 +++--- 5 files changed, 14 insertions(+), 17 deletions(-) diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index 2150d7598b..9fc95f0171 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -607,7 +607,10 @@ impl DataAvailabilityChecker { let all_data_columns = KzgVerifiedCustodyDataColumn::reconstruct_columns( &self.kzg, - &verified_data_columns, + verified_data_columns + .into_iter() + .map(|c| c.into_inner()) + .collect(), &self.spec, ) .map_err(|e| { diff --git a/beacon_node/beacon_chain/src/data_column_verification.rs b/beacon_node/beacon_chain/src/data_column_verification.rs index c360f90301..24911cdc19 100644 --- a/beacon_node/beacon_chain/src/data_column_verification.rs +++ b/beacon_node/beacon_chain/src/data_column_verification.rs @@ -640,17 +640,11 @@ impl KzgVerifiedCustodyDataColumn { pub fn reconstruct_columns( kzg: &Kzg, - partial_set_of_columns: &[Self], + partial_set_of_columns: Vec>>, spec: &ChainSpec, ) -> Result>, KzgError> { - let all_data_columns = reconstruct_data_columns( - kzg, - partial_set_of_columns - .iter() - .map(|d| d.clone_arc()) - .collect::>(), - spec, - )?; + let all_data_columns = + reconstruct_data_columns(kzg, partial_set_of_columns.to_vec(), spec)?; let seen_timestamp = timestamp_now(); diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs b/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs index 4f6c0f81d2..5b46270a19 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs @@ -317,7 +317,7 @@ impl PendingPayloadCache { let all_data_columns = KzgVerifiedCustodyDataColumn::reconstruct_columns( &self.kzg, - &verified_data_columns, + verified_data_columns, &self.spec, ) .map_err(|e| { @@ -495,7 +495,7 @@ impl PendingPayloadCache { } pending_components.reconstruction_started = true; - ReconstructColumnsDecision::Yes(pending_components.get_cached_data_columns(block_root)) + ReconstructColumnsDecision::Yes(pending_components.get_cached_data_columns(*block_root)) } /// This could mean some invalid data columns made it through to the `DataAvailabilityChecker`. diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/pending_column.rs b/beacon_node/beacon_chain/src/pending_payload_cache/pending_column.rs index 4289f17634..91c0d27b8c 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/pending_column.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/pending_column.rs @@ -19,7 +19,7 @@ impl PendingColumn { if let Some(existing_cell) = self.cells.get_mut(index) && existing_cell.is_none() { - *existing_cell = Some((cell.clone(), proof.clone())); + *existing_cell = Some((cell.clone(), proof)); } } @@ -49,7 +49,7 @@ impl PendingColumn { }; // TODO(gloas): we likely want to go and arc all cells column.push(cell.clone()); - kzg_proofs.push(proof.clone()); + kzg_proofs.push(proof); } Some(Arc::new(DataColumnSidecar::Gloas(DataColumnSidecarGloas { diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs b/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs index 067a22701f..1d2899d9ee 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs @@ -8,8 +8,8 @@ use std::cmp::Ordering; use std::collections::HashMap; use std::sync::Arc; use tracing::{Span, debug, debug_span}; -use types::Slot; use types::{ChainSpec, ColumnIndex, Epoch, EthSpec, Hash256}; +use types::{DataColumnSidecar, Slot}; /// This represents the components of a payload pending data availability. /// @@ -90,8 +90,8 @@ impl PendingComponents { pub fn num_completed_columns(&self) -> usize { self.verified_data_columns - .iter() - .filter_map(|(_, col)| col.is_complete(self.num_blobs_expected).then_some(())) + .values() + .filter_map(|col| col.is_complete(self.num_blobs_expected).then_some(())) .count() } From 407fd27118e47841b5e3b4f9b03035a5fe0680e2 Mon Sep 17 00:00:00 2001 From: Daniel Knopik Date: Tue, 28 Apr 2026 17:23:37 +0200 Subject: [PATCH 059/118] impl missing_cells_for_column_sidecar --- .../src/pending_payload_cache/mod.rs | 22 ++++++++++++++++--- .../pending_payload_cache/pending_column.rs | 15 +++++++++++-- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs b/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs index 5b46270a19..616e3ead32 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs @@ -174,14 +174,30 @@ impl PendingPayloadCache { }) } - /// Checks if a specific data column is cached for the given block root. + /// Filter out cells that are already cached for the given column sidecar. + /// Returns the cells that still need KZG verification, or `None` if all cells are cached. #[instrument(skip_all, level = "trace")] pub fn missing_cells_for_column_sidecar<'a>( &'_ self, data_column: &'a DataColumnSidecar, ) -> Result>, MissingCellsError> { - // TODO(gloas): implement cell-level missing check - Ok(None) + let block_root = data_column.block_root(); + let column_index = *data_column.index(); + + self.peek_pending_components(&block_root, |components| { + let Some(cached) = components.and_then(|c| c.verified_data_columns.get(&column_index)) + else { + return data_column.try_filter_to_partial_ref(|_, _, _| Ok(true)); + }; + + data_column.try_filter_to_partial_ref(|cell_idx, cell, proof| { + match cached.cell_matches(cell_idx, cell, proof) { + None => Ok(true), + Some(true) => Ok(false), + Some(false) => Err(MissingCellsError::MismatchesCachedColumn), + } + }) + }) } /// Insert an executed payload envelope into the cache and performs an availability check diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/pending_column.rs b/beacon_node/beacon_chain/src/pending_payload_cache/pending_column.rs index 91c0d27b8c..ae2c556007 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/pending_column.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/pending_column.rs @@ -19,12 +19,23 @@ impl PendingColumn { if let Some(existing_cell) = self.cells.get_mut(index) && existing_cell.is_none() { - *existing_cell = Some((cell.clone(), proof)); + *existing_cell = Some((cell.clone(), *proof)); } } // TODO(gloas): insert_from_partial + pub fn has_cell(&self, index: usize) -> bool { + self.cells.get(index).is_some_and(|c| c.is_some()) + } + + pub fn cell_matches(&self, index: usize, cell: &Cell, proof: &KzgProof) -> Option { + self.cells + .get(index)? + .as_ref() + .map(|(c, p)| c == cell && p == proof) + } + pub fn is_complete(&self, blob_count: usize) -> bool { self.cells.len() == blob_count && self.cells.iter().all(|cell| cell.is_some()) } @@ -49,7 +60,7 @@ impl PendingColumn { }; // TODO(gloas): we likely want to go and arc all cells column.push(cell.clone()); - kzg_proofs.push(proof); + kzg_proofs.push(*proof); } Some(Arc::new(DataColumnSidecar::Gloas(DataColumnSidecarGloas { From a03906d4c0c2f274b327813209d83789e758d69d Mon Sep 17 00:00:00 2001 From: Daniel Knopik Date: Tue, 28 Apr 2026 17:38:53 +0200 Subject: [PATCH 060/118] fix remaining errors --- .../src/payload_envelope_verification/import.rs | 7 +------ .../src/network_beacon_processor/gossip_methods.rs | 12 ++++++++---- .../network/src/network_beacon_processor/mod.rs | 8 +++++++- 3 files changed, 16 insertions(+), 11 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 97992a7106..8f8ed3dd5e 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs @@ -83,12 +83,7 @@ impl BeaconChain { // about what the function actually does. let executed_envelope = chain .into_executed_payload_envelope(execution_pending) - .await - .inspect_err(|_| { - // If the envelope fails execution for whatever reason (e.g. engine offline), - // and we keep it in the cache, then the node will NOT perform lookup and - // reprocess this envelope until the envelope is evicted from DA checker, causing the - })?; + .await?; // Record the time it took to wait for execution layer verification. if let Some(timestamp) = slot_clock.now_duration() { 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 3a4f782d49..c459bd8dce 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -1320,10 +1320,12 @@ impl NetworkBeaconProcessor { let data_column_slot = verified_data_column.slot(); let data_column_index = verified_data_column.index(); + // TODO(gloas): implement partial messages if let DataColumnSidecar::Fulu(col) = verified_data_column.as_data_column() && self .chain .data_availability_checker + .pending_block_cache() .partial_assembler() .is_some_and(|a| !a.is_complete(block_root, verified_data_column.index())) { @@ -1380,7 +1382,7 @@ impl NetworkBeaconProcessor { "Processed data column, waiting for other components" ); - self.check_reconstruction_trigger(*slot, block_root).await; + self.check_reconstruction_trigger(slot, &block_root).await; } }, Err(BlockError::DuplicateFullyImported(_)) => { @@ -1570,7 +1572,7 @@ impl NetworkBeaconProcessor { slot, process_fn: Box::pin(async move { cloned_self - .attempt_data_column_reconstruction(block_root) + .attempt_data_column_reconstruction(slot, block_root) .await; }), }, @@ -3871,7 +3873,8 @@ impl NetworkBeaconProcessor { | EnvelopeError::BeaconChainError(_) | EnvelopeError::BeaconStateError(_) | EnvelopeError::BlockProcessingError(_) - | EnvelopeError::InternalError(_) => { + | EnvelopeError::InternalError(_) + | EnvelopeError::AvailabilityCheck(_) => { self.propagate_validation_result( message_id, peer_id, @@ -3985,7 +3988,8 @@ impl NetworkBeaconProcessor { | EnvelopeError::BeaconChainError(_) | EnvelopeError::BeaconStateError(_) | EnvelopeError::BlockProcessingError(_) - | EnvelopeError::InternalError(_) => {} + | EnvelopeError::InternalError(_) + | EnvelopeError::AvailabilityCheck(_) => {} }, } } diff --git a/beacon_node/network/src/network_beacon_processor/mod.rs b/beacon_node/network/src/network_beacon_processor/mod.rs index 7df2f329ef..a39923723d 100644 --- a/beacon_node/network/src/network_beacon_processor/mod.rs +++ b/beacon_node/network/src/network_beacon_processor/mod.rs @@ -998,7 +998,13 @@ impl NetworkBeaconProcessor { } // Publish partial columns without eager send - if let Some(assembler) = self.chain.data_availability_checker.partial_assembler() { + // TODO(gloas): implement + if let Some(assembler) = self + .chain + .data_availability_checker + .pending_block_cache() + .partial_assembler() + { let columns = assembler.get_partials_and_mark_as_local_fetched(block_root, &header); if !columns.is_empty() { debug!(block = %block_root, "Publishing all partials after getBlobs"); From 46c9f313e30472ce0bb5422073bf32b1744c9438 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Tue, 28 Apr 2026 21:12:22 +0200 Subject: [PATCH 061/118] tests --- .../src/sync/block_sidecar_coupling.rs | 428 ++++++++++++------ 1 file changed, 294 insertions(+), 134 deletions(-) diff --git a/beacon_node/network/src/sync/block_sidecar_coupling.rs b/beacon_node/network/src/sync/block_sidecar_coupling.rs index 6df05921c2..5de38ad9a5 100644 --- a/beacon_node/network/src/sync/block_sidecar_coupling.rs +++ b/beacon_node/network/src/sync/block_sidecar_coupling.rs @@ -287,7 +287,7 @@ impl RangeBlockComponentsRequest { // Gloas path: if payloads are present, produce Gloas blocks let resp = if let Some(payloads_req) = &self.payloads_request { let payloads = payloads_req.to_finished().expect("checked above").to_vec(); - Self::responses_gloas( + Self::responses_with_envelopes_and_columns( blocks.to_vec(), payloads, data_columns, @@ -408,97 +408,43 @@ impl RangeBlockComponentsRequest { where T: BeaconChainTypes, { - // Group data columns by block_root and index - let mut data_columns_by_block = - HashMap::>>>::new(); - - for column in data_columns { - let block_root = column.block_root(); - let index = *column.index(); - if data_columns_by_block - .entry(block_root) - .or_default() - .insert(index, column) - .is_some() - { - // `DataColumnsByRangeRequestItems` ensures that we do not request any duplicated indices across all peers - // we request the data from. - // If there are duplicated indices, its likely a peer sending us the same index multiple times. - // However we can still proceed even if there are extra columns, just log an error. - debug!(?block_root, ?index, "Repeated column for block_root"); - continue; - } - } - - // Now iterate all blocks ensuring that the block roots of each block and data column match, - // plus we have columns for our custody requirements + let mut columns_by_root = Self::group_columns_by_root(data_columns); let mut range_sync_blocks = Vec::with_capacity(blocks.len()); - let exceeded_retries = attempt >= MAX_COLUMN_RETRIES; + for block in blocks { let block_root = get_block_root(&block); range_sync_blocks.push(if block.num_expected_blobs() > 0 { - let Some(mut data_columns_by_index) = data_columns_by_block.remove(&block_root) - else { - let responsible_peers = column_to_peer.iter().map(|c| (*c.0, *c.1)).collect(); - return Err(CouplingError::DataColumnPeerFailure { - error: format!("No columns for block {block_root:?} with data"), - faulty_peers: responsible_peers, - exceeded_retries, - - }); - }; - - let mut custody_columns = vec![]; - let mut naughty_peers = vec![]; - for index in expects_custody_columns { - // Safe to convert to `CustodyDataColumn`: we have asserted that the index of - // this column is in the set of `expects_custody_columns` and with the expected - // block root, so for the expected epoch of this batch. - if let Some(data_column) = data_columns_by_index.remove(index) { - custody_columns.push(CustodyDataColumn::from_asserted_custody(data_column)); - } else { - let Some(responsible_peer) = column_to_peer.get(index) else { - return Err(CouplingError::InternalError(format!("Internal error, no request made for column {}", index))); - }; - naughty_peers.push((*index, *responsible_peer)); - } - } - if !naughty_peers.is_empty() { - return Err(CouplingError::DataColumnPeerFailure { - error: format!("Peers did not return column for block_root {block_root:?} {naughty_peers:?}"), - faulty_peers: naughty_peers, - exceeded_retries - }); - } - - // Assert that there are no columns left - if !data_columns_by_index.is_empty() { - let remaining_indices = data_columns_by_index.keys().collect::>(); - // log the error but don't return an error, we can still progress with extra columns. - debug!( - ?block_root, - ?remaining_indices, - "Not all columns consumed for block" - ); - } - - let block_data = AvailableBlockData::new_with_data_columns(custody_columns.iter().map(|c| c.as_data_column().clone()).collect::>()); - + // Safe to convert to `CustodyDataColumn`: we have asserted that the index of + // this column is in the set of `expects_custody_columns` and with the expected + // block root, so for the expected epoch of this batch. + let columns = Self::extract_custody_columns_for_root( + block_root, + &mut columns_by_root, + expects_custody_columns, + &column_to_peer, + exceeded_retries, + )?; + let custody_columns = columns + .into_iter() + .map(CustodyDataColumn::from_asserted_custody) + .collect::>(); + let block_data = AvailableBlockData::new_with_data_columns( + custody_columns + .iter() + .map(|c| c.as_data_column().clone()) + .collect::>(), + ); RangeSyncBlock::new(block, block_data, &da_checker, spec.clone()) .map_err(|e| CouplingError::InternalError(format!("{:?}", e)))? } else { - // Block has no data, expects zero columns RangeSyncBlock::new(block, AvailableBlockData::NoData, &da_checker, spec.clone()) .map_err(|e| CouplingError::InternalError(format!("{:?}", e)))? }); } - // Assert that there are no columns left for other blocks - if !data_columns_by_block.is_empty() { - let remaining_roots = data_columns_by_block.keys().collect::>(); - // log the error but don't return an error, we can still progress with responses. - // this is most likely an internal error with overrequesting or a client bug. + if !columns_by_root.is_empty() { + let remaining_roots = columns_by_root.keys().collect::>(); debug!(?remaining_roots, "Not all columns consumed for block"); } @@ -506,7 +452,7 @@ impl RangeBlockComponentsRequest { } /// Couples blocks with payload envelopes and custody columns for Gloas range sync. - fn responses_gloas( + fn responses_with_envelopes_and_columns( blocks: Vec>>, payloads: Vec>>, data_columns: DataColumnSidecarList, @@ -515,22 +461,7 @@ impl RangeBlockComponentsRequest { attempt: usize, spec: Arc, ) -> Result>, CouplingError> { - let mut data_columns_by_block = - HashMap::>>>::new(); - - for column in data_columns { - let block_root = column.block_root(); - let index = *column.index(); - if data_columns_by_block - .entry(block_root) - .or_default() - .insert(index, column) - .is_some() - { - debug!(?block_root, ?index, "Repeated column for block_root"); - } - } - + let mut columns_by_root = Self::group_columns_by_root(data_columns); let mut range_sync_blocks = Vec::with_capacity(blocks.len()); let mut payload_iter = payloads.into_iter().peekable(); let exceeded_retries = attempt >= MAX_COLUMN_RETRIES; @@ -553,41 +484,13 @@ impl RangeBlockComponentsRequest { "Missing payload envelope for block {block_root:?} with blobs" )) })?; - - let Some(mut data_columns_by_index) = data_columns_by_block.remove(&block_root) - else { - let responsible_peers = column_to_peer.iter().map(|c| (*c.0, *c.1)).collect(); - return Err(CouplingError::DataColumnPeerFailure { - error: format!("No columns for block {block_root:?} with data"), - faulty_peers: responsible_peers, - exceeded_retries, - }); - }; - - let mut custody_columns = vec![]; - let mut naughty_peers = vec![]; - for index in expects_custody_columns { - if let Some(data_column) = data_columns_by_index.remove(index) { - custody_columns.push(data_column); - } else { - let Some(responsible_peer) = column_to_peer.get(index) else { - return Err(CouplingError::InternalError(format!( - "Internal error, no request made for column {index}" - ))); - }; - naughty_peers.push((*index, *responsible_peer)); - } - } - if !naughty_peers.is_empty() { - return Err(CouplingError::DataColumnPeerFailure { - error: format!( - "Peers did not return column for block_root {block_root:?} {naughty_peers:?}" - ), - faulty_peers: naughty_peers, - exceeded_retries, - }); - } - + let custody_columns = Self::extract_custody_columns_for_root( + block_root, + &mut columns_by_root, + expects_custody_columns, + &column_to_peer, + exceeded_retries, + )?; Some(Box::new(AvailableEnvelope::new( envelope.block_hash(), envelope, @@ -618,8 +521,8 @@ impl RangeBlockComponentsRequest { ); } - if !data_columns_by_block.is_empty() { - let remaining_roots = data_columns_by_block.keys().collect::>(); + if !columns_by_root.is_empty() { + let remaining_roots = columns_by_root.keys().collect::>(); debug!( ?remaining_roots, "Not all columns consumed for Gloas blocks" @@ -628,6 +531,85 @@ impl RangeBlockComponentsRequest { Ok(range_sync_blocks) } + + /// Groups data columns by their block root, logging and skipping duplicates. + fn group_columns_by_root( + data_columns: DataColumnSidecarList, + ) -> HashMap>>> { + let mut by_root = + HashMap::>>>::new(); + for column in data_columns { + let block_root = column.block_root(); + let index = *column.index(); + if by_root + .entry(block_root) + .or_default() + .insert(index, column) + .is_some() + { + // `DataColumnsByRangeRequestItems` ensures no duplicated indices across peers. + // Duplicates are likely a peer sending the same index multiple times; log and skip. + debug!(?block_root, ?index, "Repeated column for block_root"); + } + } + by_root + } + + /// Extracts and validates custody columns for a single block root. + /// + /// Removes the matching entry from `columns_by_root`, checks all expected indices are + /// present, and logs any extras. Returns the raw columns; callers wrap them as needed. + fn extract_custody_columns_for_root( + block_root: Hash256, + columns_by_root: &mut HashMap>>>, + expects_custody_columns: &[ColumnIndex], + column_to_peer: &HashMap, + exceeded_retries: bool, + ) -> Result>>, CouplingError> { + let Some(mut by_index) = columns_by_root.remove(&block_root) else { + let responsible_peers = column_to_peer.iter().map(|c| (*c.0, *c.1)).collect(); + return Err(CouplingError::DataColumnPeerFailure { + error: format!("No columns for block {block_root:?} with data"), + faulty_peers: responsible_peers, + exceeded_retries, + }); + }; + + let mut columns = vec![]; + let mut naughty_peers = vec![]; + for index in expects_custody_columns { + if let Some(col) = by_index.remove(index) { + columns.push(col); + } else { + let Some(responsible_peer) = column_to_peer.get(index) else { + return Err(CouplingError::InternalError(format!( + "Internal error, no request made for column {index}" + ))); + }; + naughty_peers.push((*index, *responsible_peer)); + } + } + if !naughty_peers.is_empty() { + return Err(CouplingError::DataColumnPeerFailure { + error: format!( + "Peers did not return column for block_root {block_root:?} {naughty_peers:?}" + ), + faulty_peers: naughty_peers, + exceeded_retries, + }); + } + + if !by_index.is_empty() { + let remaining_indices = by_index.keys().collect::>(); + debug!( + ?block_root, + ?remaining_indices, + "Not all columns consumed for block" + ); + } + + Ok(columns) + } } impl ByRangeRequest { @@ -662,6 +644,8 @@ mod tests { NumBlobs, generate_rand_block_and_blobs, generate_rand_block_and_data_columns, test_da_checker, test_spec, }; + use bls::Signature; + use lighthouse_network::service::api_types::PayloadEnvelopesByRangeRequestId; use lighthouse_network::{ PeerId, service::api_types::{ @@ -672,7 +656,11 @@ mod tests { use rand::SeedableRng; use std::{collections::HashMap, sync::Arc}; use tracing::Span; - use types::{Epoch, ForkName, MinimalEthSpec as E, SignedBeaconBlock, test_utils::XorShiftRng}; + use types::{ + Epoch, EthSpec, ExecutionBlockHash, ExecutionPayloadEnvelope, ExecutionPayloadGloas, + ExecutionRequests, ForkName, Hash256, MinimalEthSpec as E, SignedBeaconBlock, + SignedExecutionPayloadEnvelope, Slot, test_utils::XorShiftRng, + }; fn components_id() -> ComponentsByRangeRequestId { ComponentsByRangeRequestId { @@ -759,6 +747,7 @@ mod tests { blocks_req_id, Some(blobs_req_id), None, + None, Span::none(), ); @@ -818,6 +807,7 @@ mod tests { blocks_req_id, None, Some((columns_req_id.clone(), expects_custody_columns.clone())), + None, Span::none(), ); // Send blocks and complete terminate response @@ -894,6 +884,7 @@ mod tests { blocks_req_id, None, Some((columns_req_id.clone(), expected_sampling_columns.clone())), + None, Span::none(), ); @@ -986,6 +977,7 @@ mod tests { blocks_req_id, None, Some((columns_req_id.clone(), expected_sampling_columns.clone())), + None, Span::none(), ); @@ -1083,6 +1075,7 @@ mod tests { blocks_req_id, None, Some((columns_req_id.clone(), expected_sampling_columns.clone())), + None, Span::none(), ); @@ -1198,6 +1191,7 @@ mod tests { blocks_req_id, None, Some((columns_req_id.clone(), expected_sampling_columns.clone())), + None, Span::none(), ); @@ -1269,4 +1263,170 @@ mod tests { panic!("Expected PeerFailure error with exceeded_retries=true"); } } + + // --- Gloas tests --- + + fn make_gloas_envelope( + slot: Slot, + rng: &mut impl rand::Rng, + ) -> Arc> { + let envelope = ExecutionPayloadEnvelope { + payload: ExecutionPayloadGloas { + slot_number: slot, + block_hash: ExecutionBlockHash::from_root(Hash256::from(rng.random::<[u8; 32]>())), + ..ExecutionPayloadGloas::default() + }, + execution_requests: ExecutionRequests::default(), + builder_index: 0, + beacon_block_root: Hash256::from(rng.random::<[u8; 32]>()), + }; + Arc::new(SignedExecutionPayloadEnvelope { + message: envelope, + signature: Signature::empty(), + }) + } + + fn envelope_id( + parent_request_id: ComponentsByRangeRequestId, + ) -> PayloadEnvelopesByRangeRequestId { + use lighthouse_network::service::api_types::PayloadEnvelopesByRangeRequestId; + PayloadEnvelopesByRangeRequestId { + id: 99, + parent_request_id, + } + } + + #[test] + fn gloas_blocks_couple_with_envelopes() { + let mut spec = test_spec::(); + spec.deneb_fork_epoch = Some(Epoch::new(0)); + spec.fulu_fork_epoch = Some(Epoch::new(0)); + spec.gloas_fork_epoch = Some(Epoch::new(0)); + let spec = Arc::new(spec); + let da_checker = Arc::new(test_da_checker(spec.clone(), NodeCustodyType::Fullnode)); + let mut rng = XorShiftRng::from_seed([42; 16]); + + let blocks = (0..4) + .map(|_| { + let (raw_block, _) = generate_rand_block_and_data_columns::( + ForkName::Gloas, + NumBlobs::None, + &mut rng, + &spec, + ); + Arc::new(raw_block) as Arc> + }) + .collect::>(); + + // Build envelopes with slots matching each block + let envelopes: Vec>> = blocks + .iter() + .map(|b| make_gloas_envelope::(b.slot(), &mut rng)) + .collect(); + + let components_id = components_id(); + let blocks_req_id = blocks_id(components_id); + let env_req_id = envelope_id(components_id); + + let mut info = RangeBlockComponentsRequest::::new( + blocks_req_id, + None, + None, + Some(env_req_id), + Span::none(), + ); + + info.add_blocks(blocks_req_id, blocks).unwrap(); + // Not finished — envelopes still pending + assert!(!is_finished(&mut info)); + + info.add_payload_envelopes(env_req_id, envelopes).unwrap(); + + let result = info.responses(da_checker, spec).unwrap(); + assert!(result.is_ok()); + assert_eq!(result.unwrap().len(), 4); + } + + #[test] + fn gloas_blocks_without_envelopes_succeed() { + // Blocks with no blobs don't require envelopes — they should couple fine with an empty envelope response. + let mut spec = test_spec::(); + spec.deneb_fork_epoch = Some(Epoch::new(0)); + spec.fulu_fork_epoch = Some(Epoch::new(0)); + spec.gloas_fork_epoch = Some(Epoch::new(0)); + let spec = Arc::new(spec); + let da_checker = Arc::new(test_da_checker(spec.clone(), NodeCustodyType::Fullnode)); + let mut rng = XorShiftRng::from_seed([42; 16]); + + let (raw_block, _) = generate_rand_block_and_data_columns::( + ForkName::Gloas, + NumBlobs::None, + &mut rng, + &spec, + ); + let block: Arc> = Arc::new(raw_block); + + let components_id = components_id(); + let blocks_req_id = blocks_id(components_id); + let env_req_id = envelope_id(components_id); + + let mut info = RangeBlockComponentsRequest::::new( + blocks_req_id, + None, + None, + Some(env_req_id), + Span::none(), + ); + + info.add_blocks(blocks_req_id, vec![block]).unwrap(); + // No envelope for this block (peer didn't send one) + info.add_payload_envelopes(env_req_id, vec![]).unwrap(); + + let result = info.responses(da_checker, spec).unwrap(); + assert!(result.is_ok(), "expected Ok, got: {:?}", result); + assert_eq!(result.unwrap().len(), 1); + } + + #[test] + fn gloas_extra_envelopes_are_ignored() { + let mut spec = test_spec::(); + spec.deneb_fork_epoch = Some(Epoch::new(0)); + spec.fulu_fork_epoch = Some(Epoch::new(0)); + spec.gloas_fork_epoch = Some(Epoch::new(0)); + let spec = Arc::new(spec); + let da_checker = Arc::new(test_da_checker(spec.clone(), NodeCustodyType::Fullnode)); + let mut rng = XorShiftRng::from_seed([99; 16]); + + let (raw_block, _) = generate_rand_block_and_data_columns::( + ForkName::Gloas, + NumBlobs::None, + &mut rng, + &spec, + ); + let block: Arc> = Arc::new(raw_block); + let slot = block.slot(); + + let components_id = components_id(); + let blocks_req_id = blocks_id(components_id); + let env_req_id = envelope_id(components_id); + + let mut info = RangeBlockComponentsRequest::::new( + blocks_req_id, + None, + None, + Some(env_req_id), + Span::none(), + ); + + info.add_blocks(blocks_req_id, vec![block]).unwrap(); + // Two envelopes: one matching, one extra at a different slot + let env1 = make_gloas_envelope::(slot, &mut rng); + let env2 = make_gloas_envelope::(Slot::new(slot.as_u64() + 10), &mut rng); + info.add_payload_envelopes(env_req_id, vec![env1, env2]) + .unwrap(); + + let result = info.responses(da_checker, spec).unwrap(); + assert!(result.is_ok()); + assert_eq!(result.unwrap().len(), 1); + } } From 215a07c22e36d77c2f64ce388ac7a895617922bc Mon Sep 17 00:00:00 2001 From: Daniel Knopik Date: Wed, 29 Apr 2026 09:34:06 +0200 Subject: [PATCH 062/118] actually - store bid --- .../src/data_availability_router.rs | 12 +++- .../src/payload_envelope_verification/mod.rs | 5 +- .../src/pending_payload_cache/mod.rs | 65 ++++++++++--------- .../pending_components.rs | 49 +++++++------- .../types/src/block/signed_beacon_block.rs | 6 ++ 5 files changed, 77 insertions(+), 60 deletions(-) diff --git a/beacon_node/beacon_chain/src/data_availability_router.rs b/beacon_node/beacon_chain/src/data_availability_router.rs index 0e45a847d3..3b46771146 100644 --- a/beacon_node/beacon_chain/src/data_availability_router.rs +++ b/beacon_node/beacon_chain/src/data_availability_router.rs @@ -344,15 +344,21 @@ impl DataAvailabilityRouter { self.pending_block_cache.get_cached_block(block_root) } - /// Inserts a pre-execution block into the cache (v1). + /// Inserts a pre-execution block into the cache. pub fn put_pre_execution_block( &self, block_root: Hash256, block: Arc>, source: BlockImportSource, ) -> Result<(), AvailabilityCheckError> { - self.pending_block_cache - .put_pre_execution_block(block_root, block, source) + if let ForkName::Gloas = block.fork_name_unchecked() { + self.pending_payload_cache + .init_pending_block(block_root, block); + Ok(()) + } else { + self.pending_block_cache + .put_pre_execution_block(block_root, block, source) + } } /// Insert an executed block and check availability (v1). 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 bfd459ed2e..fdce30ecb8 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs @@ -25,7 +25,7 @@ use state_processing::{BlockProcessingError, envelope_processing::EnvelopeProces use store::Error as DBError; use tracing::instrument; use types::{ - BeaconState, BeaconStateError, ChainSpec, DataColumnSidecarList, EthSpec, ExecutionBlockHash, + BeaconState, BeaconStateError, DataColumnSidecarList, EthSpec, ExecutionBlockHash, ExecutionPayloadEnvelope, Hash256, SignedExecutionPayloadEnvelope, Slot, }; @@ -57,7 +57,6 @@ pub struct AvailableEnvelope { pub columns: DataColumnSidecarList, /// Timestamp at which this envelope first became available (UNIX timestamp, time since 1970). pub columns_available_timestamp: Option, - pub spec: Arc, } impl AvailableEnvelope { @@ -66,14 +65,12 @@ impl AvailableEnvelope { envelope: Arc>, columns: DataColumnSidecarList, columns_available_timestamp: Option, - spec: Arc, ) -> Self { Self { execution_block_hash, envelope, columns, columns_available_timestamp, - spec, } } diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs b/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs index 616e3ead32..a2cd393a14 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs @@ -55,12 +55,13 @@ use task_executor::TaskExecutor; use tracing::{Span, debug, error, instrument, trace}; use types::{ ChainSpec, ColumnIndex, DataColumnSidecar, DataColumnSidecarList, Epoch, EthSpec, Hash256, - PartialDataColumnSidecarRef, Slot, + PartialDataColumnSidecarRef, SignedBeaconBlock, Slot, }; mod pending_column; mod pending_components; +use crate::block_verification_types::AsBlock; use crate::data_column_verification::{ GossipVerifiedDataColumn, KzgVerifiedCustodyDataColumn, KzgVerifiedDataColumn, }; @@ -159,7 +160,12 @@ impl PendingPayloadCache { c.verified_data_columns .iter() .filter_map(|(col_idx, col)| { - col.try_to_sidecar(*col_idx, c.slot, block_root, c.num_blobs_expected) + col.try_to_sidecar( + *col_idx, + c.block.slot(), + block_root, + c.num_blobs_expected(), + ) }) .collect() }) @@ -229,11 +235,13 @@ impl PendingPayloadCache { /// Initialize pending components for a block. Called when the beacon block (containing the /// bid) arrives. Sets up the slot and expected blob count so that subsequent column insertions /// know how many cells to expect per column. - pub fn init_pending_block(&self, block_root: Hash256, slot: Slot, num_blobs_expected: usize) { + pub fn init_pending_block( + &self, + block_root: Hash256, + block: Arc>, + ) { let mut write_lock = self.availability_cache.write(); - write_lock.get_or_insert_mut(block_root, || { - PendingComponents::empty(block_root, slot, num_blobs_expected, self.spec.clone()) - }); + write_lock.get_or_insert_mut(block_root, || PendingComponents::empty(block_root, block)); } /// Perform KZG verification on RPC custody columns and insert them into the cache. @@ -761,17 +769,6 @@ mod data_availability_checker_tests { // once the Gloas harness can produce KZG-valid columns. These wrappers add KZG verification // and custody column filtering on top of `put_kzg_verified_custody_data_columns`. - fn num_blobs_in_block(block: &SignedBeaconBlock>) -> usize { - block - .message() - .body() - .signed_execution_payload_bid() - .expect("gloas block should have bid") - .message - .blob_kzg_commitments - .len() - } - fn make_test_signed_envelope(block_root: Hash256) -> Arc> { Arc::new(SignedExecutionPayloadEnvelope { message: ExecutionPayloadEnvelope { @@ -817,7 +814,7 @@ mod data_availability_checker_tests { ); let block_root = Hash256::random(); - cache.init_pending_block(block_root, Slot::new(0), num_blobs_in_block(&block)); + cache.init_pending_block(block_root, Arc::new(block)); let verified_columns: Vec<_> = data_columns .into_iter() @@ -861,7 +858,7 @@ mod data_availability_checker_tests { ); let block_root = Hash256::random(); - cache.init_pending_block(block_root, Slot::new(0), num_blobs_in_block(&block)); + cache.init_pending_block(block_root, Arc::new(block)); let first_column = data_columns.first().cloned().expect("should have column"); let column_index = *first_column.index(); @@ -907,7 +904,7 @@ mod data_availability_checker_tests { ); let block_root = Hash256::random(); - cache.init_pending_block(block_root, Slot::new(0), num_blobs_in_block(&block)); + cache.init_pending_block(block_root, Arc::new(block)); let verified_columns: Vec<_> = data_columns .into_iter() @@ -946,7 +943,7 @@ mod data_availability_checker_tests { ); let block_root = Hash256::random(); - cache.init_pending_block(block_root, Slot::new(0), num_blobs_in_block(&block)); + cache.init_pending_block(block_root, Arc::new(block)); let verified_columns: Vec<_> = data_columns .into_iter() @@ -982,10 +979,20 @@ mod data_availability_checker_tests { } type T = DiskHarnessType; - let (_harness, cache, _path) = setup_harness_and_cache::().await; + let (harness, cache, _path) = setup_harness_and_cache::().await; + + let mut rng = StdRng::seed_from_u64(0xDEADBEEF); + let spec = harness.spec.clone(); + + let (block, _) = generate_rand_block_and_data_columns::( + ForkName::Gloas, + NumBlobs::Number(0), + &mut rng, + &spec, + ); let block_root = Hash256::random(); - cache.init_pending_block(block_root, Slot::new(0), 0); + cache.init_pending_block(block_root, Arc::new(block)); let executed_envelope = make_test_executed_envelope(block_root); let result = cache @@ -1019,7 +1026,7 @@ mod data_availability_checker_tests { ); let block_root = Hash256::random(); - cache.init_pending_block(block_root, Slot::new(0), num_blobs_in_block(&block)); + cache.init_pending_block(block_root, Arc::new(block)); let verified_columns: Vec<_> = data_columns .into_iter() @@ -1088,7 +1095,7 @@ mod data_availability_checker_tests { assert!(cache.get_data_columns(block_root).is_none()); - cache.init_pending_block(block_root, Slot::new(0), num_blobs_in_block(&block)); + cache.init_pending_block(block_root, Arc::new(block)); let verified_columns: Vec<_> = data_columns .into_iter() @@ -1128,13 +1135,13 @@ mod data_availability_checker_tests { &spec, ); - let num_blobs = num_blobs_in_block(&block); + let block = Arc::new(block); let mut roots = Vec::new(); for _ in 0..33 { let block_root = Hash256::random(); roots.push(block_root); - cache.init_pending_block(block_root, Slot::new(0), num_blobs); + cache.init_pending_block(block_root, block.clone()); let col = data_columns.first().cloned().expect("should have column"); let verified = vec![KzgVerifiedCustodyDataColumn::from_asserted_custody( KzgVerifiedDataColumn::__new_for_testing(col), @@ -1169,7 +1176,7 @@ mod data_availability_checker_tests { ); let block_root = Hash256::random(); - cache.init_pending_block(block_root, Slot::new(0), num_blobs_in_block(&block)); + cache.init_pending_block(block_root, Arc::new(block)); let col = data_columns.first().cloned().expect("should have column"); let verified = vec![KzgVerifiedCustodyDataColumn::from_asserted_custody( @@ -1209,7 +1216,7 @@ mod data_availability_checker_tests { ); let block_root = Hash256::random(); - cache.init_pending_block(block_root, Slot::new(0), num_blobs_in_block(&block)); + cache.init_pending_block(block_root, Arc::new(block)); let executed_envelope = make_test_executed_envelope(block_root); cache diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs b/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs index 9d16ecbdf6..b88a602772 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs @@ -8,31 +8,38 @@ use std::cmp::Ordering; use std::collections::HashMap; use std::sync::Arc; use tracing::{Span, debug, debug_span}; -use types::{ChainSpec, ColumnIndex, Epoch, EthSpec, Hash256}; -use types::{DataColumnSidecar, Slot}; +use types::DataColumnSidecar; +use types::{ColumnIndex, Epoch, EthSpec, Hash256, SignedBeaconBlock}; /// This represents the components of a payload pending data availability. /// /// The columns are all gossip and kzg verified. /// The payload is considered "available" when all required columns are received. pub struct PendingComponents { - pub slot: Slot, - pub num_blobs_expected: usize, + pub block: Arc>, /// a cached post executed payload envelope pub envelope: Option>, pub verified_data_columns: HashMap>, pub reconstruction_started: bool, pub(crate) span: Span, - spec: Arc, } impl PendingComponents { + pub fn num_blobs_expected(&self) -> usize { + self.block.num_expected_blobs() + } + /// Returns the completed custody columns pub fn get_cached_data_columns(&self, block_root: Hash256) -> Vec>> { self.verified_data_columns .iter() .filter_map(|(col_idx, col)| { - col.try_to_sidecar(*col_idx, self.slot, block_root, self.num_blobs_expected) + col.try_to_sidecar( + *col_idx, + self.block.slot(), + block_root, + self.num_blobs_expected(), + ) }) .collect() } @@ -42,7 +49,8 @@ impl PendingComponents { self.verified_data_columns .iter() .filter_map(|(col_idx, col)| { - col.is_complete(self.num_blobs_expected).then_some(*col_idx) + col.is_complete(self.num_blobs_expected()) + .then_some(*col_idx) }) .collect() } @@ -52,12 +60,13 @@ impl PendingComponents { &mut self, kzg_verified_data_columns: I, ) -> Result<(), AvailabilityCheckError> { + let num_blobs_expected = self.num_blobs_expected(); for data_column in kzg_verified_data_columns { let data_column = data_column.as_data_column(); let col = self .verified_data_columns .entry(*data_column.index()) - .or_insert_with(|| PendingColumn::new_with_capacity(self.num_blobs_expected)); + .or_insert_with(|| PendingColumn::new_with_capacity(num_blobs_expected)); for (cell_idx, (cell, proof)) in data_column .column() .iter() @@ -84,7 +93,7 @@ impl PendingComponents { pub fn num_completed_columns(&self) -> usize { self.verified_data_columns .values() - .filter_map(|col| col.is_complete(self.num_blobs_expected).then_some(())) + .filter_map(|col| col.is_complete(self.num_blobs_expected()).then_some(())) .count() } @@ -105,7 +114,7 @@ impl PendingComponents { payload_verification_outcome, } = envelope; - let columns = if self.num_blobs_expected == 0 { + let columns = if self.num_blobs_expected() == 0 { self.span.in_scope(|| { debug!("Bid has no blobs, data is available"); }); @@ -129,9 +138,9 @@ impl PendingComponents { .filter_map(|(col_idx, col)| { col.try_to_sidecar( *col_idx, - self.slot, + self.block.slot(), block_hash, - self.num_blobs_expected, + self.num_blobs_expected(), ) }) .collect() @@ -148,7 +157,6 @@ impl PendingComponents { envelope: envelope.clone(), columns, columns_available_timestamp: None, - spec: self.spec.clone(), }; Ok(Some(AvailableExecutedEnvelope { @@ -159,28 +167,21 @@ impl PendingComponents { } /// Returns an empty `PendingComponents` object with the given block root. - pub fn empty( - block_root: Hash256, - slot: Slot, - num_blobs_expected: usize, - spec: Arc, - ) -> Self { - let span = debug_span!(parent: None, "lh_pending_components", %block_root, %slot); + pub fn empty(block_root: Hash256, block: Arc>) -> Self { + let span = debug_span!(parent: None, "lh_pending_components", %block_root); let _guard = span.clone().entered(); Self { - slot, - num_blobs_expected, + block, envelope: None, verified_data_columns: HashMap::new(), reconstruction_started: false, span, - spec, } } /// Returns the epoch of the bid or first data column, if available. pub fn epoch(&self) -> Epoch { - self.slot.epoch(E::slots_per_epoch()) + self.block.slot().epoch(E::slots_per_epoch()) } pub fn status_str(&self, num_expected_columns: usize) -> String { diff --git a/consensus/types/src/block/signed_beacon_block.rs b/consensus/types/src/block/signed_beacon_block.rs index 23b01415c8..e901901599 100644 --- a/consensus/types/src/block/signed_beacon_block.rs +++ b/consensus/types/src/block/signed_beacon_block.rs @@ -354,6 +354,12 @@ impl> SignedBeaconBlock self.message() .body() .blob_kzg_commitments() + .or_else(|_| { + self.message() + .body() + .signed_execution_payload_bid() + .map(|bid| &bid.message.blob_kzg_commitments) + }) .map(|c| c.len()) .unwrap_or(0) } From ab1da0b6641c6846a67a446b928e2f396e8696d7 Mon Sep 17 00:00:00 2001 From: Daniel Knopik Date: Wed, 29 Apr 2026 10:21:56 +0200 Subject: [PATCH 063/118] get rid of unneded type --- .../execution_pending_envelope.rs | 16 +++++------ .../payload_envelope_verification/import.rs | 19 +++++-------- .../src/payload_envelope_verification/mod.rs | 27 +++++++------------ .../src/pending_payload_cache/mod.rs | 23 ++++------------ .../pending_components.rs | 7 ++--- 5 files changed, 30 insertions(+), 62 deletions(-) diff --git a/beacon_node/beacon_chain/src/payload_envelope_verification/execution_pending_envelope.rs b/beacon_node/beacon_chain/src/payload_envelope_verification/execution_pending_envelope.rs index 4b8e7347cc..17799d27d7 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/execution_pending_envelope.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/execution_pending_envelope.rs @@ -1,7 +1,7 @@ -use std::sync::Arc; - +use bls::Hash256; use slot_clock::SlotClock; use state_processing::{VerifySignatures, envelope_processing::verify_execution_payload_envelope}; +use std::sync::Arc; use types::EthSpec; use crate::{ @@ -9,15 +9,14 @@ use crate::{ PayloadVerificationOutcome, block_verification::PayloadVerificationHandle, payload_envelope_verification::{ - EnvelopeError, EnvelopeImportData, MaybeAvailableEnvelope, - gossip_verified_envelope::GossipVerifiedEnvelope, load_snapshot_from_state_root, - payload_notifier::PayloadNotifier, + EnvelopeError, MaybeAvailableEnvelope, gossip_verified_envelope::GossipVerifiedEnvelope, + load_snapshot_from_state_root, payload_notifier::PayloadNotifier, }, }; pub struct ExecutionPendingEnvelope { pub signed_envelope: MaybeAvailableEnvelope, - pub import_data: EnvelopeImportData, + pub block_root: Hash256, pub payload_verification_handle: PayloadVerificationHandle, } @@ -91,10 +90,7 @@ impl GossipVerifiedEnvelope { block_hash: payload.block_hash, envelope: signed_envelope, }, - import_data: EnvelopeImportData { - block_root, - _phantom: Default::default(), - }, + block_root, payload_verification_handle, }) } 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 fc6cd6926e..a0466e2eb5 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs @@ -9,8 +9,8 @@ use tracing::{debug, error, info, info_span, instrument, warn}; use types::{BlockImportSource, Hash256, SignedExecutionPayloadEnvelope, Slot}; use super::{ - AvailableEnvelope, AvailableExecutedEnvelope, EnvelopeError, EnvelopeImportData, - ExecutedEnvelope, gossip_verified_envelope::GossipVerifiedEnvelope, + AvailableEnvelope, AvailableExecutedEnvelope, EnvelopeError, ExecutedEnvelope, + gossip_verified_envelope::GossipVerifiedEnvelope, }; use crate::pending_payload_cache::Availability as PayloadAvailability; use crate::{ @@ -203,7 +203,7 @@ impl BeaconChain { ) -> Result, EnvelopeError> { let ExecutionPendingEnvelope { signed_envelope, - import_data, + block_root, payload_verification_handle, } = pending_envelope; @@ -217,14 +217,12 @@ impl BeaconChain { .payload_verification_status .is_optimistic() { - return Err(EnvelopeError::OptimisticSyncNotSupported { - block_root: import_data.block_root, - }); + return Err(EnvelopeError::OptimisticSyncNotSupported { block_root }); } Ok(ExecutedEnvelope::new( signed_envelope, - import_data, + block_root, payload_verification_outcome, )) } @@ -236,15 +234,10 @@ impl BeaconChain { ) -> Result { let AvailableExecutedEnvelope { envelope, - import_data, + block_root, payload_verification_outcome, } = *envelope; - let EnvelopeImportData { - block_root, - _phantom, - } = import_data; - let block_root = { let chain = self.clone(); self.spawn_blocking_handle( 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 fdce30ecb8..75c0f34363 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs @@ -18,7 +18,6 @@ //! //! ``` -use std::marker::PhantomData; use std::sync::Arc; use state_processing::{BlockProcessingError, envelope_processing::EnvelopeProcessingError}; @@ -42,15 +41,7 @@ mod payload_notifier; use crate::data_availability_checker::AvailabilityCheckError; pub use execution_pending_envelope::ExecutionPendingEnvelope; -// TODO(gloas): could remove this type completely, or remove the generic -#[derive(Clone, Debug, PartialEq)] -pub struct EnvelopeImportData { - pub block_root: Hash256, - pub _phantom: PhantomData, -} - #[derive(Debug)] -#[allow(dead_code)] pub struct AvailableEnvelope { pub execution_block_hash: ExecutionBlockHash, pub envelope: Arc>, @@ -127,14 +118,14 @@ pub enum ExecutedEnvelope { impl ExecutedEnvelope { pub fn new( envelope: MaybeAvailableEnvelope, - import_data: EnvelopeImportData, + block_root: Hash256, payload_verification_outcome: PayloadVerificationOutcome, ) -> Self { match envelope { MaybeAvailableEnvelope::Available(available_envelope) => { Self::Available(AvailableExecutedEnvelope::new( available_envelope, - import_data, + block_root, payload_verification_outcome, )) } @@ -143,7 +134,7 @@ impl ExecutedEnvelope { envelope, } => Self::AvailabilityPending(AvailabilityPendingExecutedEnvelope::new( envelope, - import_data, + block_root, payload_verification_outcome, )), } @@ -155,19 +146,19 @@ impl ExecutedEnvelope { /// fork choice. pub struct AvailabilityPendingExecutedEnvelope { pub envelope: Arc>, - pub import_data: EnvelopeImportData, + pub block_root: Hash256, pub payload_verification_outcome: PayloadVerificationOutcome, } impl AvailabilityPendingExecutedEnvelope { pub fn new( envelope: Arc>, - import_data: EnvelopeImportData, + block_root: Hash256, payload_verification_outcome: PayloadVerificationOutcome, ) -> Self { Self { envelope, - import_data, + block_root, payload_verification_outcome, } } @@ -181,19 +172,19 @@ impl AvailabilityPendingExecutedEnvelope { /// by an EL client **and** has all requisite blob data to be imported into fork choice. pub struct AvailableExecutedEnvelope { pub envelope: AvailableEnvelope, - pub import_data: EnvelopeImportData, + pub block_root: Hash256, pub payload_verification_outcome: PayloadVerificationOutcome, } impl AvailableExecutedEnvelope { pub fn new( envelope: AvailableEnvelope, - import_data: EnvelopeImportData, + block_root: Hash256, payload_verification_outcome: PayloadVerificationOutcome, ) -> Self { Self { envelope, - import_data, + block_root, payload_verification_outcome, } } diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs b/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs index a2cd393a14..976bfe8c67 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs @@ -99,7 +99,7 @@ impl Debug for Availability { } // TODO(gloas) fix success case Self::Available(envelope) => { - write!(f, "Available({:?})", envelope.import_data.block_root) + write!(f, "Available({:?})", envelope.block_root) } } } @@ -299,19 +299,11 @@ impl PendingPayloadCache { block_root: Hash256, kzg_verified_data_columns: Vec>, ) -> Result, AvailabilityCheckError> { - let mut kzg_verified_data_columns = kzg_verified_data_columns.into_iter().peekable(); - let Some(epoch) = kzg_verified_data_columns - .peek() - .map(|verified_col| verified_col.as_data_column().epoch()) - else { - return Ok(Availability::MissingComponents(block_root)); - }; - let pending_components = self.get_pending_components(block_root, |pending_components| { pending_components.merge_data_columns(kzg_verified_data_columns) })?; - let num_expected_columns = self.get_num_expected_columns(epoch); + let num_expected_columns = self.get_num_expected_columns(pending_components.epoch()); pending_components.span.in_scope(|| { debug!( @@ -644,11 +636,9 @@ async fn availability_cache_maintenance_service( #[cfg(test)] mod data_availability_checker_tests { use super::*; - use std::marker::PhantomData; use crate::block_verification::PayloadVerificationOutcome; use crate::data_column_verification::{KzgVerifiedCustodyDataColumn, KzgVerifiedDataColumn}; - use crate::payload_envelope_verification::EnvelopeImportData; use crate::test_utils::{ NumBlobs, generate_data_column_indices_rand_order, generate_rand_block_and_data_columns, test_spec, @@ -664,8 +654,8 @@ mod data_availability_checker_tests { use store::{HotColdDB, StoreConfig, database::interface::BeaconNodeBackend}; use tempfile::{TempDir, tempdir}; use types::{ - ExecutionPayloadEnvelope, ExecutionPayloadGloas, ExecutionRequests, ForkName, FullPayload, - MinimalEthSpec, SignedBeaconBlock, SignedExecutionPayloadEnvelope, Slot, + ExecutionPayloadEnvelope, ExecutionPayloadGloas, ExecutionRequests, ForkName, + MinimalEthSpec, SignedExecutionPayloadEnvelope, Slot, }; type E = MinimalEthSpec; @@ -784,10 +774,7 @@ mod data_availability_checker_tests { fn make_test_executed_envelope(block_root: Hash256) -> AvailabilityPendingExecutedEnvelope { AvailabilityPendingExecutedEnvelope { envelope: make_test_signed_envelope(block_root), - import_data: EnvelopeImportData { - block_root, - _phantom: PhantomData, - }, + block_root, payload_verification_outcome: PayloadVerificationOutcome { payload_verification_status: PayloadVerificationStatus::Verified, }, diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs b/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs index b88a602772..027fd06982 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs @@ -110,7 +110,7 @@ impl PendingComponents { let AvailabilityPendingExecutedEnvelope { envelope, - import_data, + block_root, payload_verification_outcome, } = envelope; @@ -161,7 +161,7 @@ impl PendingComponents { Ok(Some(AvailableExecutedEnvelope { envelope: available_envelope, - import_data: import_data.clone(), + block_root: *block_root, payload_verification_outcome: payload_verification_outcome.clone(), })) } @@ -186,7 +186,8 @@ impl PendingComponents { pub fn status_str(&self, num_expected_columns: usize) -> String { format!( - "data_columns {}/{}", + "envelope {}, data_columns {}/{}", + self.envelope.is_some(), self.verified_data_columns.len(), num_expected_columns ) From d7f5e24ede5f85c9f40fb4a4ce415792212a3f88 Mon Sep 17 00:00:00 2001 From: Daniel Knopik Date: Wed, 29 Apr 2026 13:01:32 +0200 Subject: [PATCH 064/118] nuke router --- beacon_node/beacon_chain/src/beacon_chain.rs | 349 ++++++++------ .../beacon_chain/src/block_verification.rs | 2 +- beacon_node/beacon_chain/src/builder.rs | 11 +- .../src/data_availability_checker.rs | 12 +- .../src/data_availability_router.rs | 448 ------------------ .../src/data_column_verification.rs | 19 +- .../fetch_blobs/fetch_blobs_beacon_adapter.rs | 5 +- beacon_node/beacon_chain/src/lib.rs | 1 - beacon_node/beacon_chain/src/metrics.rs | 2 +- .../payload_envelope_verification/import.rs | 34 +- .../src/pending_payload_cache/mod.rs | 5 +- beacon_node/beacon_chain/src/test_utils.rs | 12 +- .../beacon_chain/tests/block_verification.rs | 51 +- beacon_node/beacon_chain/tests/store_tests.rs | 2 +- beacon_node/client/src/builder.rs | 9 +- .../gossip_methods.rs | 1 - .../src/network_beacon_processor/mod.rs | 7 +- .../src/network_beacon_processor/tests.rs | 2 +- .../network/src/sync/network_context.rs | 9 +- beacon_node/network/src/sync/tests/lookups.rs | 5 +- 20 files changed, 259 insertions(+), 727 deletions(-) delete mode 100644 beacon_node/beacon_chain/src/data_availability_router.rs diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 6281688b31..ac74875398 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -23,9 +23,7 @@ use crate::data_availability_checker::{ DataColumnReconstructionResult as DataColumnReconstructionResultV1, }; -use crate::data_availability_router::{ - AvailabilityOutcome, DataAvailabilityRouter, ReconstructionOutcome, -}; +use crate::data_availability_checker::DataAvailabilityChecker; use crate::data_column_verification::{ GossipDataColumnError, GossipPartialDataColumnError, GossipVerifiedDataColumn, GossipVerifiedPartialDataColumnHeader, KzgVerifiedCustodyPartialDataColumn, @@ -70,6 +68,7 @@ use crate::payload_bid_verification::payload_bid_cache::GossipVerifiedPayloadBid #[cfg(not(test))] use crate::payload_envelope_streamer::{EnvelopeRequestSource, launch_payload_envelope_stream}; use crate::pending_payload_cache::DataColumnReconstructionResult as DataColumnReconstructionResultV2; +use crate::pending_payload_cache::{Availability as PayloadAvailability, PendingPayloadCache}; use crate::pending_payload_envelopes::PendingPayloadEnvelopes; use crate::persisted_beacon_chain::PersistedBeaconChain; use crate::persisted_custody::persist_custody_context; @@ -503,9 +502,10 @@ pub struct BeaconChain { pub validator_monitor: RwLock>, /// The slot at which blocks are downloaded back to. pub genesis_backfill_slot: Slot, - /// Provides a KZG verification and temporary storage for blocks and blobs as - /// they are collected and combined. - pub data_availability_checker: Arc>, + /// Provides KZG verification and temporary storage for pre-Gloas blocks and blobs. + pub data_availability_checker: Arc>, + /// Provides KZG verification and temporary storage for post-Gloas payload envelopes. + pub pending_payload_cache: Arc>, /// The KZG trusted setup used by this chain. pub kzg: Arc, /// RNG instance used by the chain. Currently used for shuffling column sidecars in block publishing. @@ -1183,10 +1183,12 @@ impl BeaconChain { indices: &[ColumnIndex], fork_name: ForkName, ) -> Result, Error> { - let all_cached_columns_opt = self - .data_availability_checker - .get_data_columns(block_root, fork_name) - .or_else(|| self.early_attester_cache.get_data_columns(block_root)); + let all_cached_columns_opt = if fork_name.gloas_enabled() { + self.pending_payload_cache.get_data_columns(block_root) + } else { + self.data_availability_checker.get_data_columns(block_root) + } + .or_else(|| self.early_attester_cache.get_data_columns(block_root)); if let Some(mut all_cached_columns) = all_cached_columns_opt { all_cached_columns.retain(|col| indices.contains(col.index())); @@ -2420,11 +2422,7 @@ impl BeaconChain { let _timer = metrics::start_timer( &metrics::PARTIAL_DATA_COLUMN_SIDECAR_HEADER_GOSSIP_VERIFICATION_TIMES, ); - let Some(assembler) = self - .data_availability_checker - .pending_block_cache() - .partial_assembler() - else { + let Some(assembler) = self.data_availability_checker.partial_assembler() else { return Err(GossipPartialDataColumnError::PartialColumnsDisabled); }; if let Some(cached_header) = assembler.get_header(&block_root) { @@ -3377,11 +3375,7 @@ impl BeaconChain { return Err(BlockError::DuplicateFullyImported(block_root)); } - let Some(assembler) = self - .data_availability_checker - .pending_block_cache() - .partial_assembler() - else { + let Some(assembler) = self.data_availability_checker.partial_assembler() else { // Partial messages are apparently not activated return Ok(None); }; @@ -3417,16 +3411,29 @@ impl BeaconChain { .map(|column| column.as_data_column()), ); - let availability = self - .data_availability_checker - .put_kzg_verified_custody_data_columns( - block_root, - slot, - merge_result.full_columns.clone(), - )?; - - self.process_availability(slot, availability, || Ok(())) - .await? + if self + .spec + .fork_name_at_slot::(slot) + .gloas_enabled() + { + let availability = self + .pending_payload_cache + .put_kzg_verified_custody_data_columns( + block_root, + merge_result.full_columns.clone(), + )?; + self.process_payload_availability(slot, availability, || Ok(())) + .await? + } else { + let availability = self + .data_availability_checker + .put_kzg_verified_custody_data_columns( + block_root, + merge_result.full_columns.clone(), + )?; + self.process_availability(slot, availability, || Ok(())) + .await? + } } else { AvailabilityProcessingStatus::MissingComponents(slot, block_root) }; @@ -3540,10 +3547,18 @@ impl BeaconChain { if let Some(event_handler) = self.event_handler.as_ref() && event_handler.has_data_column_sidecar_subscribers() { - let imported_data_columns = self - .data_availability_checker - .cached_data_column_indexes(block_root, slot) - .unwrap_or_default(); + let imported_data_columns = if self + .spec + .fork_name_at_slot::(slot) + .gloas_enabled() + { + self.pending_payload_cache + .cached_data_column_indexes(block_root) + } else { + self.data_availability_checker + .cached_data_column_indexes(block_root) + } + .unwrap_or_default(); let new_data_columns = data_columns_iter.filter(|b| !imported_data_columns.contains(b.index())); @@ -3636,80 +3651,73 @@ impl BeaconChain { return Ok(None); } - let data_availability_checker = self.data_availability_checker.clone(); + let is_gloas = self + .spec + .fork_name_at_slot::(slot) + .gloas_enabled(); - let result = self - .task_executor - .spawn_blocking_with_rayon_async(RayonPoolType::HighPriority, move || { - data_availability_checker.reconstruct_data_columns(&block_root, slot) - }) - .await - .map_err(|_| BeaconChainError::RuntimeShutdown)??; + if is_gloas { + let pending_payload_cache = self.pending_payload_cache.clone(); + let result = self + .task_executor + .spawn_blocking_with_rayon_async(RayonPoolType::HighPriority, move || { + pending_payload_cache.reconstruct_data_columns(&block_root) + }) + .await + .map_err(|_| BeaconChainError::RuntimeShutdown)??; - match result { - ReconstructionOutcome::Block(data_column_reconstruction_result) => { - match data_column_reconstruction_result { - DataColumnReconstructionResultV1::Success(( - availability, - data_columns_to_publish, - )) => { - let Some(slot) = data_columns_to_publish.first().map(|d| d.slot()) else { - // This should be unreachable because empty result would return `RecoveredColumnsNotImported` instead of success. - return Ok(None); - }; + match result { + DataColumnReconstructionResultV2::Success(( + availability, + data_columns_to_publish, + )) => { + let Some(slot) = data_columns_to_publish.first().map(|d| d.slot()) else { + return Ok(None); + }; - self.process_availability( - slot, - AvailabilityOutcome::Block(availability), - || Ok(()), - ) + self.process_payload_availability(slot, availability, || Ok(())) .await - .map(|availability_processing_status| { - Some((availability_processing_status, data_columns_to_publish)) - }) - } - DataColumnReconstructionResultV1::NotStarted(reason) - | DataColumnReconstructionResultV1::RecoveredColumnsNotImported(reason) => { - // We use metric here because logging this would be *very* noisy. - metrics::inc_counter_vec( - &metrics::KZG_DATA_COLUMN_RECONSTRUCTION_INCOMPLETE_TOTAL, - &[reason], - ); - Ok(None) - } + .map(|status| Some((status, data_columns_to_publish))) + } + DataColumnReconstructionResultV2::NotStarted(reason) + | DataColumnReconstructionResultV2::RecoveredColumnsNotImported(reason) => { + metrics::inc_counter_vec( + &metrics::KZG_DATA_COLUMN_RECONSTRUCTION_INCOMPLETE_TOTAL, + &[reason], + ); + Ok(None) } } - // TODO(gloas) handle data column reconstruction for gloas. - ReconstructionOutcome::Payload(data_column_reconstruction_result) => { - match data_column_reconstruction_result { - DataColumnReconstructionResultV2::Success(( - availability, - data_columns_to_publish, - )) => { - let Some(slot) = data_columns_to_publish.first().map(|d| d.slot()) else { - // This should be unreachable because empty result would return `RecoveredColumnsNotImported` instead of success. - return Ok(None); - }; + } else { + let pending_block_cache = self.data_availability_checker.clone(); + let result = self + .task_executor + .spawn_blocking_with_rayon_async(RayonPoolType::HighPriority, move || { + pending_block_cache.reconstruct_data_columns(&block_root) + }) + .await + .map_err(|_| BeaconChainError::RuntimeShutdown)??; - self.process_availability( - slot, - AvailabilityOutcome::Payload(availability), - || Ok(()), - ) + match result { + DataColumnReconstructionResultV1::Success(( + availability, + data_columns_to_publish, + )) => { + let Some(slot) = data_columns_to_publish.first().map(|d| d.slot()) else { + return Ok(None); + }; + + self.process_availability(slot, availability, || Ok(())) .await - .map(|availability_processing_status| { - Some((availability_processing_status, data_columns_to_publish)) - }) - } - DataColumnReconstructionResultV2::NotStarted(reason) - | DataColumnReconstructionResultV2::RecoveredColumnsNotImported(reason) => { - // We use metric here because logging this would be *very* noisy. - metrics::inc_counter_vec( - &metrics::KZG_DATA_COLUMN_RECONSTRUCTION_INCOMPLETE_TOTAL, - &[reason], - ); - Ok(None) - } + .map(|status| Some((status, data_columns_to_publish))) + } + DataColumnReconstructionResultV1::NotStarted(reason) + | DataColumnReconstructionResultV1::RecoveredColumnsNotImported(reason) => { + metrics::inc_counter_vec( + &metrics::KZG_DATA_COLUMN_RECONSTRUCTION_INCOMPLETE_TOTAL, + &[reason], + ); + Ok(None) } } } @@ -3912,8 +3920,7 @@ impl BeaconChain { block: AvailabilityPendingExecutedBlock, ) -> Result { let slot = block.block.slot(); - let availability = - AvailabilityOutcome::Block(self.data_availability_checker.put_executed_block(block)?); + let availability = self.data_availability_checker.put_executed_block(block)?; self.process_availability(slot, availability, || Ok(())) .await } @@ -3928,10 +3935,9 @@ impl BeaconChain { if let Some(slasher) = self.slasher.as_ref() { slasher.accept_block_header(blob.signed_block_header()); } - let availability = AvailabilityOutcome::Block( - self.data_availability_checker - .put_gossip_verified_blobs(blob.block_root(), std::iter::once(blob))?, - ); + let availability = self + .data_availability_checker + .put_gossip_verified_blobs(blob.block_root(), std::iter::once(blob))?; self.process_availability(slot, availability, || Ok(())) .await @@ -3958,12 +3964,23 @@ impl BeaconChain { } } - let availability = self - .data_availability_checker - .put_gossip_verified_data_columns(block_root, slot, data_columns)?; - - self.process_availability(slot, availability, publish_fn) - .await + if self + .spec + .fork_name_at_slot::(slot) + .gloas_enabled() + { + let availability = self + .pending_payload_cache + .put_gossip_verified_data_columns(block_root, slot, data_columns)?; + self.process_payload_availability(slot, availability, publish_fn) + .await + } else { + let availability = self + .data_availability_checker + .put_gossip_verified_data_columns(block_root, slot, data_columns)?; + self.process_availability(slot, availability, publish_fn) + .await + } } fn check_blob_header_signature_and_slashability<'a>( @@ -4008,10 +4025,9 @@ impl BeaconChain { block_root, blobs.iter().flatten().map(Arc::as_ref), )?; - let availability = AvailabilityOutcome::Block( - self.data_availability_checker - .put_rpc_blobs(block_root, blobs)?, - ); + let availability = self + .data_availability_checker + .put_rpc_blobs(block_root, blobs)?; self.process_availability(slot, availability, || Ok(())) .await @@ -4023,7 +4039,7 @@ impl BeaconChain { block_root: Hash256, engine_get_blobs_output: EngineGetBlobsOutput, ) -> Result { - let availability = match engine_get_blobs_output { + match engine_get_blobs_output { EngineGetBlobsOutput::Blobs(blobs) => { self.check_blob_header_signature_and_slashability( block_root, @@ -4033,7 +4049,8 @@ impl BeaconChain { .data_availability_checker .put_kzg_verified_blobs(block_root, blobs)?; - AvailabilityOutcome::Block(availability) + self.process_availability(slot, availability, || Ok(())) + .await } EngineGetBlobsOutput::CustodyColumns(data_columns) => { // TODO(gloas) verify that this check is no longer relevant for gloas @@ -4046,13 +4063,25 @@ impl BeaconChain { _ => None, }), )?; - self.data_availability_checker - .put_kzg_verified_custody_data_columns(block_root, slot, data_columns)? + if self + .spec + .fork_name_at_slot::(slot) + .gloas_enabled() + { + let availability = self + .pending_payload_cache + .put_kzg_verified_custody_data_columns(block_root, data_columns)?; + self.process_payload_availability(slot, availability, || Ok(())) + .await + } else { + let availability = self + .data_availability_checker + .put_kzg_verified_custody_data_columns(block_root, data_columns)?; + self.process_availability(slot, availability, || Ok(())) + .await + } } - }; - - self.process_availability(slot, availability, || Ok(())) - .await + } } /// Checks if the provided columns can make any cached blocks available, and imports immediately @@ -4072,16 +4101,27 @@ impl BeaconChain { }), )?; - // This slot value is purely informative for the consumers of - // `AvailabilityProcessingStatus::MissingComponents` to log an error with a slot. - let availability = self.data_availability_checker.put_rpc_custody_columns( - block_root, - slot, - custody_columns, - )?; - - self.process_availability(slot, availability, || Ok(())) - .await + if self + .spec + .fork_name_at_slot::(slot) + .gloas_enabled() + { + let availability = self.pending_payload_cache.put_rpc_custody_columns( + block_root, + slot, + custody_columns, + )?; + self.process_payload_availability(slot, availability, || Ok(())) + .await + } else { + let availability = self.data_availability_checker.put_rpc_custody_columns( + block_root, + slot, + custody_columns, + )?; + self.process_availability(slot, availability, || Ok(())) + .await + } } fn check_data_column_sidecar_header_signature_and_slashability<'a>( @@ -4124,25 +4164,36 @@ impl BeaconChain { async fn process_availability( self: &Arc, slot: Slot, - availability: AvailabilityOutcome, + availability: BlockAvailability, publish_fn: impl FnOnce() -> Result<(), BlockError>, ) -> Result { match availability { - AvailabilityOutcome::Block(availability) => { - match availability { - BlockAvailability::Available(block) => { - publish_fn()?; - // Block is fully available, import into fork choice - self.import_available_block(block).await - } - BlockAvailability::MissingComponents(block_root) => Ok( - AvailabilityProcessingStatus::MissingComponents(slot, block_root), - ), - } + BlockAvailability::Available(block) => { + publish_fn()?; + self.import_available_block(block).await } - AvailabilityOutcome::Payload(_) => { - Err(BlockError::InternalError("Received a payload envelope availability outcome variant when a block variant was expected".to_string())) - }, + BlockAvailability::MissingComponents(block_root) => Ok( + AvailabilityProcessingStatus::MissingComponents(slot, block_root), + ), + } + } + + async fn process_payload_availability( + self: &Arc, + slot: Slot, + availability: PayloadAvailability, + publish_fn: impl FnOnce() -> Result<(), BlockError>, + ) -> Result { + match availability { + PayloadAvailability::Available(available_envelope) => { + publish_fn()?; + self.import_available_execution_payload_envelope(available_envelope) + .await + .map_err(|e| BlockError::InternalError(e.to_string())) + } + PayloadAvailability::MissingComponents(block_root) => Ok( + AvailabilityProcessingStatus::MissingComponents(slot, block_root), + ), } } diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 688824d35e..4b02d77f1a 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -1203,7 +1203,7 @@ impl SignatureVerifiedBlock { block, AvailableBlockData::NoData, // TODO(gloas) shouldnt matter which da checker we pass? - chain.data_availability_checker.pending_block_cache(), + &chain.data_availability_checker, chain.spec.clone(), ) .map_err(BlockError::AvailabilityCheck)?, diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index 3f658f0d11..d3a1d851ea 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -6,7 +6,6 @@ use crate::beacon_chain::{ use crate::beacon_proposer_cache::BeaconProposerCache; use crate::custody_context::NodeCustodyType; use crate::data_availability_checker::DataAvailabilityChecker; -use crate::data_availability_router::DataAvailabilityRouter; use crate::fork_choice_signal::ForkChoiceSignalTx; use crate::graffiti_calculator::{GraffitiCalculator, GraffitiOrigin}; use crate::kzg_utils::{build_data_column_sidecars_fulu, build_data_column_sidecars_gloas}; @@ -1011,11 +1010,8 @@ where .map_err(|e| format!("Error initializing DataAvailabilityCheckerV2: {:?}", e))?, ); - let data_availability_checker = Arc::new(DataAvailabilityRouter::new( - da_checker_v1, - da_checker_v2, - self.spec.clone(), - )); + let pending_block_cache = da_checker_v1; + let pending_payload_cache = da_checker_v2; let beacon_chain = BeaconChain { spec: self.spec.clone(), @@ -1088,7 +1084,8 @@ where slasher: self.slasher.clone(), validator_monitor: RwLock::new(validator_monitor), genesis_backfill_slot, - data_availability_checker, + data_availability_checker: pending_block_cache, + pending_payload_cache, kzg: self.kzg.clone(), rng: Arc::new(Mutex::new(rng)), gossip_verified_payload_bid_cache: <_>::default(), diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index 9fc95f0171..b4abbb5290 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -690,16 +690,8 @@ pub fn start_availability_cache_maintenance_service( ) { // this cache only needs to be maintained if deneb is configured if chain.spec.deneb_fork_epoch.is_some() { - let overflow_cache = chain - .data_availability_checker - .pending_block_cache() - .availability_cache - .clone(); - let partial_assembler = chain - .data_availability_checker - .pending_block_cache() - .partial_assembler - .clone(); + let overflow_cache = chain.data_availability_checker.availability_cache.clone(); + let partial_assembler = chain.data_availability_checker.partial_assembler.clone(); executor.spawn( async move { availability_cache_maintenance_service(chain, overflow_cache, partial_assembler) diff --git a/beacon_node/beacon_chain/src/data_availability_router.rs b/beacon_node/beacon_chain/src/data_availability_router.rs deleted file mode 100644 index 3b46771146..0000000000 --- a/beacon_node/beacon_chain/src/data_availability_router.rs +++ /dev/null @@ -1,448 +0,0 @@ -//! Abstraction layer for data availability operations across different DA checkers. -//! -//! This module provides a unified interface for availability operations that are shared -//! between the legacy `DataAvailabilityChecker` (for blocks) and -//! `DataAvailabilityCache` (for payload envelopes after Gloas). -//! -//! ## Design -//! -//! - **Unified operations**: Shared column operations dispatched to v1 or v2 -//! - **Fork-aware routing**: `DataAvailabilityRouter` dispatches to v1 or v2 based on slot -//! - **Processing**: `BeaconChain::process_availability_outcome()` handles both result types -//! -//! After Gloas is fully activated and v1 is deprecated, this can be deleted and we can -//! use the Gloas DA checker directly. - -use crate::BeaconChainTypes; -use crate::BlockProcessStatus; -use crate::blob_verification::{GossipVerifiedBlob, KzgVerifiedBlob}; -use crate::block_verification_types::AvailabilityPendingExecutedBlock; -use crate::custody_context::CustodyContext; -use crate::data_availability_checker::{ - Availability as BlockAvailability, AvailabilityCheckError, AvailableBlock, - DataAvailabilityChecker, DataAvailabilityCheckerMetrics as BlockMetrics, - DataColumnReconstructionResult as BlockReconstructionResult, MissingCellsError, -}; -use crate::data_column_verification::{GossipVerifiedDataColumn, KzgVerifiedCustodyDataColumn}; -use crate::observed_data_sidecars::ObservationStrategy; -use crate::pending_payload_cache::{ - Availability as PayloadAvailability, DataAvailabilityCheckerMetrics as PayloadMetrics, - DataColumnReconstructionResult as PayloadReconstructionResult, PendingPayloadCache, -}; -use std::sync::Arc; -use types::data::{BlobIdentifier, FixedBlobSidecarList}; -use types::{ - BlobSidecar, BlockImportSource, ChainSpec, ColumnIndex, DataColumnSidecar, - DataColumnSidecarList, Epoch, EthSpec, ForkName, Hash256, PartialDataColumnSidecarRef, - SignedBeaconBlock, Slot, -}; - -/// Unified result from operations that can come from either DA checker. -/// -/// This enum allows callers to handle availability from both v1 (blocks) and v2 (payloads) -/// through a single type, with downstream processing handled by `BeaconChain::process_availability_outcome()`. -#[derive(Debug)] -pub enum AvailabilityOutcome { - /// Block became available (pre-Gloas, from v1 checker) - Block(BlockAvailability), - /// Payload became available (post-Gloas, from v2 checker) - Payload(PayloadAvailability), -} - -impl AvailabilityOutcome { - /// Returns `true` if data is fully available and ready for import. - pub fn is_available(&self) -> bool { - match self { - Self::Block(BlockAvailability::Available(_)) => true, - Self::Block(BlockAvailability::MissingComponents(_)) => false, - Self::Payload(PayloadAvailability::Available(_)) => true, - Self::Payload(PayloadAvailability::MissingComponents(_)) => false, - } - } - - /// Returns the block root, regardless of availability status. - pub fn block_root(&self) -> Hash256 { - match self { - Self::Block(BlockAvailability::Available(block)) => block.import_data.block_root, - Self::Block(BlockAvailability::MissingComponents(root)) => *root, - Self::Payload(PayloadAvailability::Available(available_data)) => { - available_data.envelope.message().beacon_block_root - } - Self::Payload(PayloadAvailability::MissingComponents(root)) => *root, - } - } - - /// Converts to the inner block availability if this is a block outcome. - pub fn into_block(self) -> Option> { - match self { - Self::Block(avail) => Some(avail), - Self::Payload(_) => None, - } - } - - /// Converts to the inner payload availability if this is a payload outcome. - pub fn into_payload(self) -> Option> { - match self { - Self::Block(_) => None, - Self::Payload(avail) => Some(avail), - } - } -} - -/// Unified result from reconstruction operations. -#[derive(Debug)] -pub enum ReconstructionOutcome { - /// Block reconstruction result (pre-Gloas) - Block(BlockReconstructionResult), - /// Payload reconstruction result (post-Gloas) - Payload(PayloadReconstructionResult), -} - -impl ReconstructionOutcome { - /// Returns the reconstructed columns if successful, regardless of type. - pub fn reconstructed_columns(&self) -> Option<&DataColumnSidecarList> { - match self { - Self::Block(BlockReconstructionResult::Success((_, cols))) => Some(cols), - Self::Payload(PayloadReconstructionResult::Success((_, cols))) => Some(cols), - _ => None, - } - } - - /// Returns true if reconstruction was successful. - pub fn is_success(&self) -> bool { - matches!( - self, - Self::Block(BlockReconstructionResult::Success(_)) - | Self::Payload(PayloadReconstructionResult::Success(_)) - ) - } - - /// Returns the reason if reconstruction was not started or columns not imported. - pub fn reason(&self) -> Option<&'static str> { - match self { - Self::Block(BlockReconstructionResult::NotStarted(r)) => Some(r), - Self::Block(BlockReconstructionResult::RecoveredColumnsNotImported(r)) => Some(r), - Self::Payload(PayloadReconstructionResult::NotStarted(r)) => Some(r), - Self::Payload(PayloadReconstructionResult::RecoveredColumnsNotImported(r)) => Some(r), - _ => None, - } - } -} - -/// Router that directs data availability checker operations to the appropriate version based on fork. -/// -/// This wraps both the legacy (v1) and Gloas (v2) DA checkers, providing unified operations -/// that dispatch to the correct checker based on fork. -/// -/// After Gloas is fully activated and v1 is deprecated, this router can be deleted and -/// we can use the V2 DA checker directly. -pub struct DataAvailabilityRouter { - /// Legacy DA checker for pre-Gloas blocks - pending_block_cache: Arc>, - /// Gloas DA checker for payload envelopes - pending_payload_cache: Arc>, - spec: Arc, -} - -impl DataAvailabilityRouter { - pub fn new( - pending_block_cache: Arc>, - pending_payload_cache: Arc>, - spec: Arc, - ) -> Self { - Self { - pending_block_cache, - pending_payload_cache, - spec, - } - } - - /// Returns true if the given slot is in the Gloas fork or later. - fn is_gloas(&self, slot: Slot) -> bool { - self.spec - .fork_name_at_slot::(slot) - .gloas_enabled() - } - - // ── Shared methods (dispatched to v1 or v2 based on fork) ── - - /// Returns the custody context (same for both checkers). - pub fn custody_context(&self) -> &Arc> { - // Both checkers share the same custody context - self.pending_block_cache.custody_context() - } - - /// Query data columns from the appropriate checker based on fork. - pub fn get_data_columns( - &self, - block_root: Hash256, - fork_name: ForkName, - ) -> Option> { - if fork_name.gloas_enabled() { - self.pending_payload_cache.get_data_columns(block_root) - } else { - self.pending_block_cache.get_data_columns(block_root) - } - } - - pub fn missing_cells_for_column_sidecar<'a>( - &'_ self, - slot: Slot, - data_column: &'a DataColumnSidecar, - ) -> Result>, MissingCellsError> { - if self.is_gloas(slot) { - self.pending_payload_cache - .missing_cells_for_column_sidecar(data_column) - } else { - self.pending_block_cache - .missing_cells_for_column_sidecar(data_column) - } - } - - /// Get cached column indexes from the appropriate checker based on slot. - pub fn cached_data_column_indexes( - &self, - block_root: &Hash256, - slot: Slot, - ) -> Option> { - if self.is_gloas(slot) { - self.pending_payload_cache - .cached_data_column_indexes(block_root) - } else { - self.pending_block_cache - .cached_data_column_indexes(block_root) - } - } - - /// Insert RPC custody columns, routing to the correct checker based on slot. - pub fn put_rpc_custody_columns( - &self, - block_root: Hash256, - slot: Slot, - custody_columns: DataColumnSidecarList, - ) -> Result, AvailabilityCheckError> { - if self.is_gloas(slot) { - self.pending_payload_cache - .put_rpc_custody_columns(block_root, slot, custody_columns) - .map(AvailabilityOutcome::Payload) - } else { - self.pending_block_cache - .put_rpc_custody_columns(block_root, slot, custody_columns) - .map(AvailabilityOutcome::Block) - } - } - - /// Insert gossip-verified data columns, routing to the correct checker based on slot. - pub fn put_gossip_verified_data_columns( - &self, - block_root: Hash256, - slot: Slot, - data_columns: Vec>, - ) -> Result, AvailabilityCheckError> { - if self.is_gloas(slot) { - self.pending_payload_cache - .put_gossip_verified_data_columns(block_root, slot, data_columns) - .map(AvailabilityOutcome::Payload) - } else { - self.pending_block_cache - .put_gossip_verified_data_columns(block_root, slot, data_columns) - .map(AvailabilityOutcome::Block) - } - } - - /// Insert KZG-verified custody data columns, routing to the correct checker based on slot. - pub fn put_kzg_verified_custody_data_columns( - &self, - block_root: Hash256, - slot: Slot, - custody_columns: Vec>, - ) -> Result, AvailabilityCheckError> { - if self.is_gloas(slot) { - self.pending_payload_cache - .put_kzg_verified_custody_data_columns(block_root, custody_columns) - .map(AvailabilityOutcome::Payload) - } else { - self.pending_block_cache - .put_kzg_verified_custody_data_columns(block_root, custody_columns) - .map(AvailabilityOutcome::Block) - } - } - - /// Attempt to reconstruct missing data columns, routing to the correct checker based on slot. - pub fn reconstruct_data_columns( - &self, - block_root: &Hash256, - slot: Slot, - ) -> Result, AvailabilityCheckError> { - if self.is_gloas(slot) { - self.pending_payload_cache - .reconstruct_data_columns(block_root) - .map(ReconstructionOutcome::Payload) - } else { - self.pending_block_cache - .reconstruct_data_columns(block_root) - .map(ReconstructionOutcome::Block) - } - } - - // ── V1-only methods (blobs, blocks, boundary queries) ── - - /// Returns the data availability boundary epoch (v1). - pub fn data_availability_boundary(&self) -> Option { - self.pending_block_cache.data_availability_boundary() - } - - /// Returns whether a DA check is required for the given epoch (v1). - pub fn da_check_required_for_epoch(&self, epoch: Epoch) -> bool { - self.pending_block_cache.da_check_required_for_epoch(epoch) - } - - /// Returns whether blobs are required for the given epoch (v1). - pub fn blobs_required_for_epoch(&self, epoch: Epoch) -> bool { - self.pending_block_cache.blobs_required_for_epoch(epoch) - } - - /// Returns whether data columns are required for the given epoch (v1). - pub fn data_columns_required_for_epoch(&self, epoch: Epoch) -> bool { - self.pending_block_cache - .data_columns_required_for_epoch(epoch) - } - - /// Verifies KZG commitments for a single available block (v1). - pub fn verify_kzg_for_available_block( - &self, - available_block: &AvailableBlock, - ) -> Result<(), AvailabilityCheckError> { - self.pending_block_cache - .verify_kzg_for_available_block(available_block) - } - - /// Batch verifies KZG commitments for multiple available blocks (v1). - pub fn batch_verify_kzg_for_available_blocks( - &self, - available_blocks: &[AvailableBlock], - ) -> Result<(), AvailabilityCheckError> { - self.pending_block_cache - .batch_verify_kzg_for_available_blocks(available_blocks) - } - - /// Get a blob from the availability cache (v1). - pub fn get_blob( - &self, - blob_id: &BlobIdentifier, - ) -> Result>>, AvailabilityCheckError> { - self.pending_block_cache.get_blob(blob_id) - } - - /// Returns the cached blob indexes for a given block root (v1). - pub fn cached_blob_indexes(&self, block_root: &Hash256) -> Option> { - self.pending_block_cache.cached_blob_indexes(block_root) - } - - /// Returns the cached block for a given block root (v1). - pub fn get_cached_block(&self, block_root: &Hash256) -> Option> { - self.pending_block_cache.get_cached_block(block_root) - } - - /// Inserts a pre-execution block into the cache. - pub fn put_pre_execution_block( - &self, - block_root: Hash256, - block: Arc>, - source: BlockImportSource, - ) -> Result<(), AvailabilityCheckError> { - if let ForkName::Gloas = block.fork_name_unchecked() { - self.pending_payload_cache - .init_pending_block(block_root, block); - Ok(()) - } else { - self.pending_block_cache - .put_pre_execution_block(block_root, block, source) - } - } - - /// Insert an executed block and check availability (v1). - pub fn put_executed_block( - &self, - executed_block: AvailabilityPendingExecutedBlock, - ) -> Result, AvailabilityCheckError> { - self.pending_block_cache.put_executed_block(executed_block) - } - - /// Removes a pre-execution block from the cache on execution error (v1). - pub fn remove_block_on_execution_error(&self, block_root: &Hash256) { - self.pending_block_cache - .remove_block_on_execution_error(block_root) - } - - /// Insert blobs received via RPC and check availability (v1). - pub fn put_rpc_blobs( - &self, - block_root: Hash256, - blobs: FixedBlobSidecarList, - ) -> Result, AvailabilityCheckError> { - self.pending_block_cache.put_rpc_blobs(block_root, blobs) - } - - /// Insert KZG-verified blobs and check availability (v1). - pub fn put_kzg_verified_blobs>>( - &self, - block_root: Hash256, - blobs: I, - ) -> Result, AvailabilityCheckError> { - self.pending_block_cache - .put_kzg_verified_blobs(block_root, blobs) - } - - /// Insert gossip-verified blobs into the v1 checker. - pub fn put_gossip_verified_blobs< - I: IntoIterator>, - O: ObservationStrategy, - >( - &self, - block_root: Hash256, - blobs: I, - ) -> Result, AvailabilityCheckError> { - self.pending_block_cache - .put_gossip_verified_blobs(block_root, blobs) - } - - // ── Metrics ── - - pub fn metrics(&self) -> DataAvailabilityRouterMetrics { - DataAvailabilityRouterMetrics { - block: self.pending_block_cache.metrics(), - payload: self.pending_payload_cache.metrics(), - } - } - - // ── Direct access ── - - /// Direct access to the block-level DA checker (pre-Gloas). - /// Used for block availability checks, range sync, and blob verification. - pub fn pending_block_cache(&self) -> &Arc> { - &self.pending_block_cache - } - - /// Direct access to the envelope-level DA checker (Gloas). - /// Used for payload envelope availability checks and column verification. - pub fn pending_payload_cache(&self) -> &Arc> { - &self.pending_payload_cache - } -} - -pub struct DataAvailabilityRouterMetrics { - pub block: BlockMetrics, - pub payload: PayloadMetrics, -} - -pub fn start_availability_cache_maintenance_service( - executor: task_executor::TaskExecutor, - chain: Arc>, -) { - crate::data_availability_checker::start_availability_cache_maintenance_service( - executor.clone(), - chain.clone(), - ); - crate::pending_payload_cache::start_availability_cache_maintenance_service(executor, chain); -} diff --git a/beacon_node/beacon_chain/src/data_column_verification.rs b/beacon_node/beacon_chain/src/data_column_verification.rs index 24911cdc19..b420965024 100644 --- a/beacon_node/beacon_chain/src/data_column_verification.rs +++ b/beacon_node/beacon_chain/src/data_column_verification.rs @@ -331,7 +331,6 @@ impl GossipVerifiedDataColumn column_sidecar: Arc>, chain: &BeaconChain, ) -> Result { - let slot = column_sidecar.slot(); verify_data_column_sidecar(&column_sidecar, &chain.spec)?; // Check if the data column is already in the DA checker cache. This happens when data columns @@ -343,7 +342,7 @@ impl GossipVerifiedDataColumn match chain .data_availability_checker - .missing_cells_for_column_sidecar(slot, &column_sidecar) + .missing_cells_for_column_sidecar(&column_sidecar) { Ok(Some(_)) => Ok(Self { block_root: column_sidecar.block_root(), @@ -541,11 +540,7 @@ impl GossipVerifiedPartialDataColumnHeader { let header = Arc::new(header); // Cache the valid header - let Some(assembler) = chain - .data_availability_checker - .pending_block_cache() - .partial_assembler() - else { + let Some(assembler) = chain.data_availability_checker.partial_assembler() else { return Err(GossipPartialDataColumnError::PartialColumnsDisabled); }; let newly_cached = assembler.init(group_id, header.clone()); @@ -929,7 +924,7 @@ pub fn validate_data_column_sidecar_for_gossip_fulu { GossipDataColumnError::MismatchesCachedColumn @@ -1003,11 +998,7 @@ pub fn validate_partial_data_column_sidecar_for_gossip( } } } else { - let Some(assembler) = chain - .data_availability_checker - .pending_block_cache() - .partial_assembler() - else { + let Some(assembler) = chain.data_availability_checker.partial_assembler() else { return PartialColumnVerificationResult::Err( GossipPartialDataColumnError::PartialColumnsDisabled, ); @@ -1064,7 +1055,6 @@ pub fn validate_partial_data_column_sidecar_for_gossip( let column = Arc::from(column); let cells_to_kzg_verify = match chain .data_availability_checker - .pending_block_cache() .missing_cells_for_partial_column_sidecar(&column) { Ok(Some(cells_to_kzg_verify)) => cells_to_kzg_verify, @@ -1625,7 +1615,6 @@ mod test { harness .chain .data_availability_checker - .pending_block_cache() .partial_assembler() .unwrap() .init(block_root, header.clone()); diff --git a/beacon_node/beacon_chain/src/fetch_blobs/fetch_blobs_beacon_adapter.rs b/beacon_node/beacon_chain/src/fetch_blobs/fetch_blobs_beacon_adapter.rs index aaefb5cd3e..abfcc8508f 100644 --- a/beacon_node/beacon_chain/src/fetch_blobs/fetch_blobs_beacon_adapter.rs +++ b/beacon_node/beacon_chain/src/fetch_blobs/fetch_blobs_beacon_adapter.rs @@ -39,7 +39,6 @@ impl FetchBlobsBeaconAdapter { pub(crate) fn partial_assembler(&self) -> Option>> { self.chain .data_availability_checker - .pending_block_cache() .partial_assembler() .cloned() } @@ -122,12 +121,12 @@ impl FetchBlobsBeaconAdapter { pub(crate) fn cached_data_column_indexes( &self, - slot: Slot, + _slot: Slot, block_root: &Hash256, ) -> Option> { self.chain .data_availability_checker - .cached_data_column_indexes(block_root, slot) + .cached_data_column_indexes(block_root) } pub(crate) async fn process_engine_blobs( diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index 12f3a86956..804268a613 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -18,7 +18,6 @@ pub mod canonical_head; pub mod chain_config; pub mod custody_context; pub mod data_availability_checker; -pub mod data_availability_router; pub mod data_column_verification; mod early_attester_cache; pub mod envelope_times_cache; diff --git a/beacon_node/beacon_chain/src/metrics.rs b/beacon_node/beacon_chain/src/metrics.rs index ef3b1995c3..9739038b3a 100644 --- a/beacon_node/beacon_chain/src/metrics.rs +++ b/beacon_node/beacon_chain/src/metrics.rs @@ -2149,7 +2149,7 @@ pub fn scrape_for_metrics(beacon_chain: &BeaconChain) { set_gauge_by_usize( &DATA_AVAILABILITY_OVERFLOW_MEMORY_BLOCK_CACHE_SIZE, - da_checker_metrics.block.block_cache_size, + da_checker_metrics.block_cache_size, ); if let Some((size, num_lookups)) = beacon_chain.pre_finalization_block_cache.metrics() { 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 a0466e2eb5..15cd0ee3b4 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs @@ -17,7 +17,6 @@ use crate::{ AvailabilityProcessingStatus, BeaconChain, BeaconChainError, BeaconChainTypes, NotifyExecutionLayer, block_verification_types::AvailableBlockData, - data_availability_router::AvailabilityOutcome, metrics, payload_envelope_verification::{ AvailabilityPendingExecutedEnvelope, ExecutionPendingEnvelope, @@ -153,41 +152,30 @@ impl BeaconChain { async fn process_payload_envelope_availability( self: &Arc, slot: Slot, - availability: AvailabilityOutcome, + availability: PayloadAvailability, publish_fn: impl FnOnce() -> Result<(), EnvelopeError>, ) -> Result { match availability { - AvailabilityOutcome::Block(_) => { - Err(EnvelopeError::InternalError("Received a block availability outcome variant when a payload envelope variant was expected".to_string())) + PayloadAvailability::Available(available_envelope) => { + publish_fn()?; + self.import_available_execution_payload_envelope(available_envelope) + .await } - AvailabilityOutcome::Payload(availability) => match availability { - PayloadAvailability::Available(available_envelope) => { - publish_fn()?; - - // Payload envelope is fully available - self.import_available_execution_payload_envelope(available_envelope) - .await - } - PayloadAvailability::MissingComponents(block_root) => Ok( - AvailabilityProcessingStatus::MissingComponents(slot, block_root), - ), - }, + PayloadAvailability::MissingComponents(block_root) => Ok( + AvailabilityProcessingStatus::MissingComponents(slot, block_root), + ), } } - /// Checks if the payload envelope is available, and imports immediately if so, otherwise caches the envelope - /// in the data availability checker. #[instrument(skip_all)] async fn check_envelope_availability_and_import( self: &Arc, envelope: AvailabilityPendingExecutedEnvelope, ) -> Result { let slot = envelope.envelope.slot(); - let availability = AvailabilityOutcome::Payload( - self.data_availability_checker - .pending_payload_cache() - .put_executed_payload_envelope(envelope)?, - ); + let availability = self + .pending_payload_cache + .put_executed_payload_envelope(envelope)?; self.process_payload_envelope_availability(slot, availability, || Ok(())) .await } diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs b/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs index 976bfe8c67..2337697fab 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs @@ -556,10 +556,7 @@ pub fn start_availability_cache_maintenance_service( chain: Arc>, ) { if chain.spec.gloas_fork_epoch.is_some() { - let da_checker = chain - .data_availability_checker - .pending_payload_cache() - .clone(); + let da_checker = chain.pending_payload_cache.clone(); executor.spawn( async move { availability_cache_maintenance_service(chain, da_checker).await }, "availability_cache_service", diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index 1ba6fa64d6..c3db26e95c 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -2828,7 +2828,7 @@ where return RangeSyncBlock::new( block, AvailableBlockData::NoData, - self.chain.data_availability_checker.pending_block_cache(), + &self.chain.data_availability_checker, self.chain.spec.clone(), ) .unwrap(); @@ -2847,7 +2847,7 @@ where RangeSyncBlock::new( block, block_data, - self.chain.data_availability_checker.pending_block_cache(), + &self.chain.data_availability_checker, self.chain.spec.clone(), ) .unwrap() @@ -2862,7 +2862,7 @@ where RangeSyncBlock::new( block, block_data, - self.chain.data_availability_checker.pending_block_cache(), + &self.chain.data_availability_checker, self.chain.spec.clone(), ) .unwrap() @@ -2891,14 +2891,14 @@ where RangeSyncBlock::new( block, block_data, - self.chain.data_availability_checker.pending_block_cache(), + &self.chain.data_availability_checker, self.chain.spec.clone(), )? } else { RangeSyncBlock::new( block, AvailableBlockData::NoData, - self.chain.data_availability_checker.pending_block_cache(), + &self.chain.data_availability_checker, self.chain.spec.clone(), )? } @@ -2918,7 +2918,7 @@ where RangeSyncBlock::new( block, block_data, - self.chain.data_availability_checker.pending_block_cache(), + &self.chain.data_availability_checker, self.chain.spec.clone(), )? }) diff --git a/beacon_node/beacon_chain/tests/block_verification.rs b/beacon_node/beacon_chain/tests/block_verification.rs index 5e27985558..b2db85713f 100644 --- a/beacon_node/beacon_chain/tests/block_verification.rs +++ b/beacon_node/beacon_chain/tests/block_verification.rs @@ -165,7 +165,7 @@ where RangeSyncBlock::new( block, block_data, - chain.data_availability_checker.pending_block_cache(), + &chain.data_availability_checker, chain.spec.clone(), ) .unwrap() @@ -180,7 +180,7 @@ where RangeSyncBlock::new( block, block_data, - chain.data_availability_checker.pending_block_cache(), + &chain.data_availability_checker, chain.spec.clone(), ) .unwrap() @@ -188,7 +188,7 @@ where None => RangeSyncBlock::new( block, AvailableBlockData::NoData, - chain.data_availability_checker.pending_block_cache(), + &chain.data_availability_checker, chain.spec.clone(), ) .unwrap(), @@ -462,10 +462,7 @@ async fn chain_segment_non_linear_parent_roots() { blocks[3] = RangeSyncBlock::new( Arc::new(SignedBeaconBlock::from_block(block, signature)), blocks[3].block_data().clone(), - harness - .chain - .data_availability_checker - .pending_block_cache(), + &harness.chain.data_availability_checker, harness.spec.clone(), ) .unwrap(); @@ -505,10 +502,7 @@ async fn chain_segment_non_linear_slots() { blocks[3] = RangeSyncBlock::new( Arc::new(SignedBeaconBlock::from_block(block, signature)), blocks[3].block_data().clone(), - harness - .chain - .data_availability_checker - .pending_block_cache(), + &harness.chain.data_availability_checker, harness.spec.clone(), ) .unwrap(); @@ -538,10 +532,7 @@ async fn chain_segment_non_linear_slots() { blocks[3] = RangeSyncBlock::new( Arc::new(SignedBeaconBlock::from_block(block, signature)), blocks[3].block_data().clone(), - harness - .chain - .data_availability_checker - .pending_block_cache(), + &harness.chain.data_availability_checker, harness.chain.spec.clone(), ) .unwrap(); @@ -1723,10 +1714,7 @@ async fn add_base_block_to_altair_chain() { let base_range_sync_block = RangeSyncBlock::new( Arc::new(base_block.clone()), AvailableBlockData::NoData, - harness - .chain - .data_availability_checker - .pending_block_cache(), + &harness.chain.data_availability_checker, harness.spec.clone(), ) .unwrap(); @@ -1757,7 +1745,7 @@ async fn add_base_block_to_altair_chain() { RangeSyncBlock::new( Arc::new(base_block), AvailableBlockData::NoData, - harness.chain.data_availability_checker.v1(), + &harness.chain.pending_block_cache, harness.spec.clone() ) .unwrap() @@ -1902,7 +1890,7 @@ async fn add_altair_block_to_base_chain() { RangeSyncBlock::new( Arc::new(altair_block), AvailableBlockData::NoData, - harness.chain.data_availability_checker.v1(), + &harness.chain.pending_block_cache, harness.spec.clone() ) .unwrap() @@ -1970,10 +1958,7 @@ async fn import_duplicate_block_unrealized_justification() { let range_sync_block = RangeSyncBlock::new( block.clone(), AvailableBlockData::NoData, - harness - .chain - .data_availability_checker - .pending_block_cache(), + &harness.chain.data_availability_checker, harness.spec.clone(), ) .unwrap(); @@ -2107,10 +2092,7 @@ async fn range_sync_block_construction_fails_with_wrong_blob_count() { let result = RangeSyncBlock::new( Arc::new(block), block_data, - harness - .chain - .data_availability_checker - .pending_block_cache(), + &harness.chain.data_availability_checker, harness.chain.spec.clone(), ); @@ -2188,10 +2170,7 @@ async fn range_sync_block_rejects_missing_custody_columns() { let result = RangeSyncBlock::new( Arc::new(block), block_data, - harness - .chain - .data_availability_checker - .pending_block_cache(), + &harness.chain.data_availability_checker, harness.chain.spec.clone(), ); @@ -2267,7 +2246,6 @@ async fn rpc_block_allows_construction_past_da_boundary() { // Now verify the block is past the DA boundary let da_boundary = harness .chain - .data_availability_checker .data_availability_boundary() .expect("DA boundary should be set"); assert!( @@ -2282,10 +2260,7 @@ async fn rpc_block_allows_construction_past_da_boundary() { let result = RangeSyncBlock::new( Arc::new(block), AvailableBlockData::NoData, - harness - .chain - .data_availability_checker - .pending_block_cache(), + &harness.chain.data_availability_checker, harness.chain.spec.clone(), ); diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index 3040f91342..86adf50995 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -3300,7 +3300,7 @@ async fn weak_subjectivity_sync_test( AvailableBlock::new( Arc::new(corrupt_block), data, - beacon_chain.data_availability_checker.pending_block_cache(), + &beacon_chain.data_availability_checker, Arc::new(spec), ) .expect("available block") diff --git a/beacon_node/client/src/builder.rs b/beacon_node/client/src/builder.rs index 6955e8e252..7699d25816 100644 --- a/beacon_node/client/src/builder.rs +++ b/beacon_node/client/src/builder.rs @@ -5,8 +5,9 @@ use crate::compute_light_client_updates::{ use crate::config::{ClientGenesis, Config as ClientConfig}; use crate::notifier::spawn_notifier; use beacon_chain::attestation_simulator::start_attestation_simulator_service; -use beacon_chain::data_availability_router::start_availability_cache_maintenance_service; +use beacon_chain::data_availability_checker::start_availability_cache_maintenance_service as start_block_cache_maintenance_service; use beacon_chain::graffiti_calculator::start_engine_version_cache_refresh_service; +use beacon_chain::pending_payload_cache::start_availability_cache_maintenance_service as start_payload_cache_maintenance_service; use beacon_chain::proposer_prep_service::start_proposer_prep_service; use beacon_chain::schema_change::migrate_schema; use beacon_chain::{ @@ -782,7 +783,11 @@ where } start_proposer_prep_service(runtime_context.executor.clone(), beacon_chain.clone()); - start_availability_cache_maintenance_service( + start_block_cache_maintenance_service( + runtime_context.executor.clone(), + beacon_chain.clone(), + ); + start_payload_cache_maintenance_service( runtime_context.executor.clone(), beacon_chain.clone(), ); 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 4d1f522cf0..c83fd9244d 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -1328,7 +1328,6 @@ impl NetworkBeaconProcessor { && self .chain .data_availability_checker - .pending_block_cache() .partial_assembler() .is_some_and(|a| !a.is_complete(block_root, verified_data_column.index())) { diff --git a/beacon_node/network/src/network_beacon_processor/mod.rs b/beacon_node/network/src/network_beacon_processor/mod.rs index 41867907b2..23515d5901 100644 --- a/beacon_node/network/src/network_beacon_processor/mod.rs +++ b/beacon_node/network/src/network_beacon_processor/mod.rs @@ -999,12 +999,7 @@ impl NetworkBeaconProcessor { // Publish partial columns without eager send // TODO(gloas): implement - if let Some(assembler) = self - .chain - .data_availability_checker - .pending_block_cache() - .partial_assembler() - { + if let Some(assembler) = self.chain.data_availability_checker.partial_assembler() { let columns = assembler.get_partials_and_mark_as_local_fetched(block_root, &header); if !columns.is_empty() { debug!(block = %block_root, "Publishing all partials after getBlobs"); diff --git a/beacon_node/network/src/network_beacon_processor/tests.rs b/beacon_node/network/src/network_beacon_processor/tests.rs index 76c6ba812d..0ff9200737 100644 --- a/beacon_node/network/src/network_beacon_processor/tests.rs +++ b/beacon_node/network/src/network_beacon_processor/tests.rs @@ -1196,7 +1196,7 @@ async fn accept_processed_gossip_data_columns_without_import() { let block_root = rig.next_block.canonical_root(); rig.chain - .data_availability_checker + .pending_block_cache .put_gossip_verified_data_columns(block_root, rig.next_block.slot(), verified_data_columns) .expect("should put data columns into availability cache"); diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index 87addbfd8b..ed28099b2e 100644 --- a/beacon_node/network/src/sync/network_context.rs +++ b/beacon_node/network/src/sync/network_context.rs @@ -778,10 +778,7 @@ impl SyncNetworkContext { let range_req = entry.get_mut(); if let Some(blocks_result) = range_req.responses( - self.chain - .data_availability_checker - .pending_block_cache() - .clone(), + self.chain.data_availability_checker.clone(), self.chain.spec.clone(), ) { if let Err(CouplingError::DataColumnPeerFailure { @@ -1085,14 +1082,14 @@ impl SyncNetworkContext { pub fn custody_lookup_request( &mut self, lookup_id: SingleLookupId, - slot: Slot, + _slot: Slot, block_root: Hash256, lookup_peers: Arc>>, ) -> Result { let custody_indexes_imported = self .chain .data_availability_checker - .cached_data_column_indexes(&block_root, slot) + .cached_data_column_indexes(&block_root) .unwrap_or_default(); let current_epoch = self.chain.epoch().map_err(|e| { diff --git a/beacon_node/network/src/sync/tests/lookups.rs b/beacon_node/network/src/sync/tests/lookups.rs index 9a3f0a5311..a26996ec5e 100644 --- a/beacon_node/network/src/sync/tests/lookups.rs +++ b/beacon_node/network/src/sync/tests/lookups.rs @@ -1110,10 +1110,7 @@ impl TestRig { let range_sync_block = RangeSyncBlock::new( block, block_data, - self.harness - .chain - .data_availability_checker - .pending_block_cache(), + &self.harness.chain.data_availability_checker, self.harness.chain.spec.clone(), ) .unwrap(); From 58fd3dde40bad9d97749b15f54c9f24ad98d147e Mon Sep 17 00:00:00 2001 From: Daniel Knopik Date: Wed, 29 Apr 2026 11:11:02 +0200 Subject: [PATCH 065/118] claude cont: error handling and wiring up --- beacon_node/beacon_chain/src/beacon_chain.rs | 73 +++++++++++-------- beacon_node/beacon_chain/src/errors.rs | 10 +++ .../execution_pending_envelope.rs | 12 +-- .../payload_envelope_verification/import.rs | 16 +--- 4 files changed, 59 insertions(+), 52 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index ac74875398..3e776f17fd 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -32,7 +32,7 @@ use crate::data_column_verification::{ }; use crate::early_attester_cache::EarlyAttesterCache; use crate::envelope_times_cache::EnvelopeTimesCache; -use crate::errors::{BeaconChainError as Error, BlockProductionError}; +use crate::errors::{BeaconChainError as Error, BlockOrEnvelopeError, BlockProductionError}; use crate::events::ServerSentEventHandler; use crate::execution_payload::{NotifyExecutionLayer, PreparePayloadHandle, get_execution_payload}; use crate::fetch_blobs::EngineGetBlobsOutput; @@ -67,8 +67,12 @@ 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::pending_payload_cache::DataColumnReconstructionResult as DataColumnReconstructionResultV2; -use crate::pending_payload_cache::{Availability as PayloadAvailability, PendingPayloadCache}; +use crate::payload_envelope_verification::EnvelopeError; +use crate::pending_payload_cache::PendingPayloadCache; +use crate::pending_payload_cache::{ + Availability as PayloadAvailability, + DataColumnReconstructionResult as DataColumnReconstructionResultV2, +}; use crate::pending_payload_envelopes::PendingPayloadEnvelopes; use crate::persisted_beacon_chain::PersistedBeaconChain; use crate::persisted_custody::persist_custody_context; @@ -3271,7 +3275,7 @@ impl BeaconChain { pub async fn process_gossip_blob( self: &Arc, blob: GossipVerifiedBlob, - ) -> Result { + ) -> Result { let block_root = blob.block_root(); // If this block has already been imported to forkchoice it must have been available, so @@ -3281,12 +3285,12 @@ impl BeaconChain { .fork_choice_read_lock() .contains_block(&block_root) { - return Err(BlockError::DuplicateFullyImported(blob.block_root())); + return Err(BlockError::DuplicateFullyImported(blob.block_root()).into()); } // No need to process and import blobs beyond the PeerDAS epoch. if self.spec.is_peer_das_enabled_for_epoch(blob.epoch()) { - return Err(BlockError::BlobNotRequired(blob.slot())); + return Err(BlockError::BlobNotRequired(blob.slot()).into()); } self.emit_sse_blob_sidecar_events(&block_root, std::iter::once(blob.as_blob())); @@ -3302,7 +3306,7 @@ impl BeaconChain { self: &Arc, data_columns: Vec>, publish_fn: impl FnOnce() -> Result<(), BlockError>, - ) -> Result { + ) -> Result { let Ok((slot, block_root)) = data_columns .iter() .map(|c| (c.slot(), c.block_root())) @@ -3311,7 +3315,8 @@ impl BeaconChain { else { return Err(BlockError::InternalError( "Columns should be from the same block".to_string(), - )); + ) + .into()); }; // If this block has already been imported to forkchoice it must have been available, so @@ -3321,7 +3326,7 @@ impl BeaconChain { .fork_choice_read_lock() .contains_block(&block_root) { - return Err(BlockError::DuplicateFullyImported(block_root)); + return Err(BlockError::DuplicateFullyImported(block_root).into()); } self.emit_sse_data_column_sidecar_events( @@ -3347,7 +3352,7 @@ impl BeaconChain { verified_partial: KzgVerifiedPartialDataColumn, verified_header: GossipVerifiedPartialDataColumnHeader, slot: Slot, - ) -> Result, BlockError> { + ) -> Result, BlockOrEnvelopeError> { let block_root = verified_partial.block_root(); let partial = verified_partial.as_data_column(); let index_str = partial.index.to_string(); @@ -3372,7 +3377,7 @@ impl BeaconChain { .fork_choice_read_lock() .contains_block(&block_root) { - return Err(BlockError::DuplicateFullyImported(block_root)); + return Err(BlockError::DuplicateFullyImported(block_root).into()); } let Some(assembler) = self.data_availability_checker.partial_assembler() else { @@ -3421,7 +3426,8 @@ impl BeaconChain { .put_kzg_verified_custody_data_columns( block_root, merge_result.full_columns.clone(), - )?; + ) + .map_err(EnvelopeError::from)?; self.process_payload_availability(slot, availability, || Ok(())) .await? } else { @@ -3430,7 +3436,8 @@ impl BeaconChain { .put_kzg_verified_custody_data_columns( block_root, merge_result.full_columns.clone(), - )?; + ) + .map_err(BlockError::from)?; self.process_availability(slot, availability, || Ok(())) .await? } @@ -3449,7 +3456,7 @@ impl BeaconChain { slot: Slot, block_root: Hash256, blobs: FixedBlobSidecarList, - ) -> Result { + ) -> Result { // If this block has already been imported to forkchoice it must have been available, so // we don't need to process its blobs again. if self @@ -3457,7 +3464,7 @@ impl BeaconChain { .fork_choice_read_lock() .contains_block(&block_root) { - return Err(BlockError::DuplicateFullyImported(block_root)); + return Err(BlockError::DuplicateFullyImported(block_root).into()); } // Reject RPC blobs referencing unknown parents. Otherwise we allow potentially invalid data @@ -3472,7 +3479,7 @@ impl BeaconChain { .fork_choice_read_lock() .contains_block(&parent_root) { - return Err(BlockError::ParentUnknown { parent_root }); + return Err(BlockError::ParentUnknown { parent_root }.into()); } self.emit_sse_blob_sidecar_events(&block_root, blobs.iter().flatten().map(Arc::as_ref)); @@ -3487,7 +3494,7 @@ impl BeaconChain { slot: Slot, block_root: Hash256, engine_get_blobs_output: EngineGetBlobsOutput, - ) -> Result { + ) -> Result { // If this block has already been imported to forkchoice it must have been available, so // we don't need to process its blobs again. if self @@ -3495,7 +3502,7 @@ impl BeaconChain { .fork_choice_read_lock() .contains_block(&block_root) { - return Err(BlockError::DuplicateFullyImported(block_root)); + return Err(BlockError::DuplicateFullyImported(block_root).into()); } match &engine_get_blobs_output { @@ -3576,7 +3583,7 @@ impl BeaconChain { pub async fn process_rpc_custody_columns( self: &Arc, custody_columns: DataColumnSidecarList, - ) -> Result { + ) -> Result { let Ok((slot, block_root)) = custody_columns .iter() .map(|c| (c.slot(), c.block_root())) @@ -3585,7 +3592,8 @@ impl BeaconChain { else { return Err(BlockError::InternalError( "Columns should be from the same block".to_string(), - )); + ) + .into()); }; // If this block has already been imported to forkchoice it must have been available, so @@ -3597,7 +3605,7 @@ impl BeaconChain { .fork_choice_read_lock() .contains_block(&block_root) { - return Err(BlockError::DuplicateFullyImported(block_root)); + return Err(BlockError::DuplicateFullyImported(block_root).into()); } // Reject RPC columns referencing unknown parents. Otherwise we allow potentially invalid data @@ -3616,7 +3624,7 @@ impl BeaconChain { .fork_choice_read_lock() .contains_block(&parent_root) { - return Err(BlockError::ParentUnknown { parent_root }); + return Err(BlockError::ParentUnknown { parent_root }.into()); } self.emit_sse_data_column_sidecar_events( @@ -3638,7 +3646,7 @@ impl BeaconChain { AvailabilityProcessingStatus, DataColumnSidecarList, )>, - BlockError, + BlockOrEnvelopeError, > { // As of now we only reconstruct data columns on supernodes, so if the block is already // available on a supernode, there's no need to reconstruct as the node must already have @@ -3664,7 +3672,8 @@ impl BeaconChain { pending_payload_cache.reconstruct_data_columns(&block_root) }) .await - .map_err(|_| BeaconChainError::RuntimeShutdown)??; + .map_err(|_| EnvelopeError::from(BeaconChainError::RuntimeShutdown))? + .map_err(EnvelopeError::from)?; match result { DataColumnReconstructionResultV2::Success(( @@ -3930,7 +3939,7 @@ impl BeaconChain { async fn check_gossip_blob_availability_and_import( self: &Arc, blob: GossipVerifiedBlob, - ) -> Result { + ) -> Result { let slot = blob.slot(); if let Some(slasher) = self.slasher.as_ref() { slasher.accept_block_header(blob.signed_block_header()); @@ -3953,7 +3962,7 @@ impl BeaconChain { block_root: Hash256, data_columns: Vec>, publish_fn: impl FnOnce() -> Result<(), BlockError>, - ) -> Result { + ) -> Result { if let Some(slasher) = self.slasher.as_ref() { for data_column in &data_columns { // TODO(gloas) different gossip checks in gloas @@ -4020,14 +4029,15 @@ impl BeaconChain { slot: Slot, block_root: Hash256, blobs: FixedBlobSidecarList, - ) -> Result { + ) -> Result { self.check_blob_header_signature_and_slashability( block_root, blobs.iter().flatten().map(Arc::as_ref), )?; let availability = self .data_availability_checker - .put_rpc_blobs(block_root, blobs)?; + .put_rpc_blobs(block_root, blobs) + .map_err(BlockError::from)?; self.process_availability(slot, availability, || Ok(())) .await @@ -4038,7 +4048,7 @@ impl BeaconChain { slot: Slot, block_root: Hash256, engine_get_blobs_output: EngineGetBlobsOutput, - ) -> Result { + ) -> Result { match engine_get_blobs_output { EngineGetBlobsOutput::Blobs(blobs) => { self.check_blob_header_signature_and_slashability( @@ -4091,7 +4101,7 @@ impl BeaconChain { slot: Slot, block_root: Hash256, custody_columns: DataColumnSidecarList, - ) -> Result { + ) -> Result { // TODO(gloas) ensure that this check is no longer relevant post gloas self.check_data_column_sidecar_header_signature_and_slashability( block_root, @@ -4183,13 +4193,12 @@ impl BeaconChain { slot: Slot, availability: PayloadAvailability, publish_fn: impl FnOnce() -> Result<(), BlockError>, - ) -> Result { + ) -> Result { match availability { PayloadAvailability::Available(available_envelope) => { publish_fn()?; self.import_available_execution_payload_envelope(available_envelope) .await - .map_err(|e| BlockError::InternalError(e.to_string())) } PayloadAvailability::MissingComponents(block_root) => Ok( AvailabilityProcessingStatus::MissingComponents(slot, block_root), diff --git a/beacon_node/beacon_chain/src/errors.rs b/beacon_node/beacon_chain/src/errors.rs index 9802f091e0..68c560611c 100644 --- a/beacon_node/beacon_chain/src/errors.rs +++ b/beacon_node/beacon_chain/src/errors.rs @@ -1,3 +1,4 @@ +use crate::BlockError; use crate::beacon_block_streamer::Error as BlockStreamerError; use crate::beacon_chain::ForkChoiceError; use crate::beacon_fork_choice_store::Error as ForkChoiceStoreError; @@ -9,6 +10,7 @@ use crate::observed_attesters::Error as ObservedAttestersError; use crate::observed_block_producers::Error as ObservedBlockProducersError; use crate::observed_data_sidecars::Error as ObservedDataSidecarsError; use crate::payload_envelope_streamer::Error as EnvelopeStreamerError; +use crate::payload_envelope_verification::EnvelopeError; use bls::PublicKeyBytes; use execution_layer::PayloadStatus; use fork_choice::ExecutionStatus; @@ -334,3 +336,11 @@ easy_from_to!(SlotProcessingError, BlockProductionError); easy_from_to!(StateAdvanceError, BlockProductionError); easy_from_to!(ForkChoiceError, BlockProductionError); easy_from_to!(EpochCacheError, BlockProductionError); + +pub enum BlockOrEnvelopeError { + BlockError(BlockError), + EnvelopeError(EnvelopeError), +} + +easy_from_to!(BlockError, BlockOrEnvelopeError); +easy_from_to!(EnvelopeError, BlockOrEnvelopeError); diff --git a/beacon_node/beacon_chain/src/payload_envelope_verification/execution_pending_envelope.rs b/beacon_node/beacon_chain/src/payload_envelope_verification/execution_pending_envelope.rs index 17799d27d7..b678bdbaea 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/execution_pending_envelope.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/execution_pending_envelope.rs @@ -2,20 +2,20 @@ use bls::Hash256; use slot_clock::SlotClock; use state_processing::{VerifySignatures, envelope_processing::verify_execution_payload_envelope}; use std::sync::Arc; -use types::EthSpec; +use types::{EthSpec, SignedExecutionPayloadEnvelope}; use crate::{ BeaconChain, BeaconChainError, BeaconChainTypes, NotifyExecutionLayer, PayloadVerificationOutcome, block_verification::PayloadVerificationHandle, payload_envelope_verification::{ - EnvelopeError, MaybeAvailableEnvelope, gossip_verified_envelope::GossipVerifiedEnvelope, + EnvelopeError, gossip_verified_envelope::GossipVerifiedEnvelope, load_snapshot_from_state_root, payload_notifier::PayloadNotifier, }, }; pub struct ExecutionPendingEnvelope { - pub signed_envelope: MaybeAvailableEnvelope, + pub signed_envelope: Arc>, pub block_root: Hash256, pub payload_verification_handle: PayloadVerificationHandle, } @@ -28,7 +28,6 @@ impl GossipVerifiedEnvelope { ) -> Result, EnvelopeError> { let signed_envelope = self.signed_envelope; let envelope = &signed_envelope.message; - let payload = &envelope.payload; // Define a future that will verify the execution payload with an execution engine. // @@ -86,10 +85,7 @@ impl GossipVerifiedEnvelope { )?; Ok(ExecutionPendingEnvelope { - signed_envelope: MaybeAvailableEnvelope::AvailabilityPending { - block_hash: payload.block_hash, - envelope: signed_envelope, - }, + signed_envelope, block_root, payload_verification_handle, }) 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 15cd0ee3b4..2d371a5315 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs @@ -9,7 +9,7 @@ use tracing::{debug, error, info, info_span, instrument, warn}; use types::{BlockImportSource, Hash256, SignedExecutionPayloadEnvelope, Slot}; use super::{ - AvailableEnvelope, AvailableExecutedEnvelope, EnvelopeError, ExecutedEnvelope, + AvailableEnvelope, AvailableExecutedEnvelope, EnvelopeError, gossip_verified_envelope::GossipVerifiedEnvelope, }; use crate::pending_payload_cache::Availability as PayloadAvailability; @@ -91,15 +91,7 @@ impl BeaconChain { .set_time_executed(block_root, block_slot, timestamp); } - match executed_envelope { - ExecutedEnvelope::Available(envelope) => { - self.import_available_execution_payload_envelope(Box::new(envelope)) - .await - } - ExecutedEnvelope::AvailabilityPending(envelope) => { - self.check_envelope_availability_and_import(envelope).await - } - } + self.check_envelope_availability_and_import(executed_envelope) }; // Verify and import the payload envelope. @@ -188,7 +180,7 @@ impl BeaconChain { async fn into_executed_payload_envelope( self: Arc, pending_envelope: ExecutionPendingEnvelope, - ) -> Result, EnvelopeError> { + ) -> Result, EnvelopeError> { let ExecutionPendingEnvelope { signed_envelope, block_root, @@ -208,7 +200,7 @@ impl BeaconChain { return Err(EnvelopeError::OptimisticSyncNotSupported { block_root }); } - Ok(ExecutedEnvelope::new( + Ok(AvailabilityPendingExecutedEnvelope::new( signed_envelope, block_root, payload_verification_outcome, From 2d3354551ed311a5d8043d469a99e018508a48be Mon Sep 17 00:00:00 2001 From: Daniel Knopik Date: Wed, 29 Apr 2026 15:35:31 +0200 Subject: [PATCH 066/118] error handling and wiring up --- beacon_node/beacon_chain/src/beacon_chain.rs | 101 ++++++++++-------- beacon_node/beacon_chain/src/errors.rs | 10 ++ .../fetch_blobs/fetch_blobs_beacon_adapter.rs | 2 +- .../beacon_chain/src/fetch_blobs/mod.rs | 6 +- beacon_node/beacon_chain/src/lib.rs | 2 +- .../payload_envelope_verification/import.rs | 1 + .../src/payload_envelope_verification/mod.rs | 6 +- beacon_node/http_api/src/publish_blocks.rs | 2 +- beacon_node/network/src/metrics.rs | 4 +- .../gossip_methods.rs | 8 +- .../src/network_beacon_processor/mod.rs | 13 ++- .../network_beacon_processor/sync_methods.rs | 11 +- .../network/src/sync/block_lookups/mod.rs | 21 +++- beacon_node/network/src/sync/manager.rs | 19 ++-- beacon_node/network/src/sync/tests/lookups.rs | 10 +- 15 files changed, 132 insertions(+), 84 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 3e776f17fd..78c59bf661 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -3275,7 +3275,7 @@ impl BeaconChain { pub async fn process_gossip_blob( self: &Arc, blob: GossipVerifiedBlob, - ) -> Result { + ) -> Result { let block_root = blob.block_root(); // If this block has already been imported to forkchoice it must have been available, so @@ -3285,12 +3285,12 @@ impl BeaconChain { .fork_choice_read_lock() .contains_block(&block_root) { - return Err(BlockError::DuplicateFullyImported(blob.block_root()).into()); + return Err(BlockError::DuplicateFullyImported(blob.block_root())); } // No need to process and import blobs beyond the PeerDAS epoch. if self.spec.is_peer_das_enabled_for_epoch(blob.epoch()) { - return Err(BlockError::BlobNotRequired(blob.slot()).into()); + return Err(BlockError::BlobNotRequired(blob.slot())); } self.emit_sse_blob_sidecar_events(&block_root, std::iter::once(blob.as_blob())); @@ -3456,7 +3456,7 @@ impl BeaconChain { slot: Slot, block_root: Hash256, blobs: FixedBlobSidecarList, - ) -> Result { + ) -> Result { // If this block has already been imported to forkchoice it must have been available, so // we don't need to process its blobs again. if self @@ -3464,7 +3464,7 @@ impl BeaconChain { .fork_choice_read_lock() .contains_block(&block_root) { - return Err(BlockError::DuplicateFullyImported(block_root).into()); + return Err(BlockError::DuplicateFullyImported(block_root)); } // Reject RPC blobs referencing unknown parents. Otherwise we allow potentially invalid data @@ -3479,7 +3479,7 @@ impl BeaconChain { .fork_choice_read_lock() .contains_block(&parent_root) { - return Err(BlockError::ParentUnknown { parent_root }.into()); + return Err(BlockError::ParentUnknown { parent_root }); } self.emit_sse_blob_sidecar_events(&block_root, blobs.iter().flatten().map(Arc::as_ref)); @@ -3684,9 +3684,10 @@ impl BeaconChain { return Ok(None); }; - self.process_payload_availability(slot, availability, || Ok(())) + Ok(self + .process_payload_availability(slot, availability, || Ok(())) .await - .map(|status| Some((status, data_columns_to_publish))) + .map(|status| Some((status, data_columns_to_publish)))?) } DataColumnReconstructionResultV2::NotStarted(reason) | DataColumnReconstructionResultV2::RecoveredColumnsNotImported(reason) => { @@ -3698,14 +3699,15 @@ impl BeaconChain { } } } else { - let pending_block_cache = self.data_availability_checker.clone(); + let data_availability_checker = self.data_availability_checker.clone(); let result = self .task_executor .spawn_blocking_with_rayon_async(RayonPoolType::HighPriority, move || { - pending_block_cache.reconstruct_data_columns(&block_root) + data_availability_checker.reconstruct_data_columns(&block_root) }) .await - .map_err(|_| BeaconChainError::RuntimeShutdown)??; + .map_err(|_| BlockError::from(BeaconChainError::RuntimeShutdown))? + .map_err(BlockError::from)?; match result { DataColumnReconstructionResultV1::Success(( @@ -3716,9 +3718,10 @@ impl BeaconChain { return Ok(None); }; - self.process_availability(slot, availability, || Ok(())) + Ok(self + .process_availability(slot, availability, || Ok(())) .await - .map(|status| Some((status, data_columns_to_publish))) + .map(|status| Some((status, data_columns_to_publish)))?) } DataColumnReconstructionResultV1::NotStarted(reason) | DataColumnReconstructionResultV1::RecoveredColumnsNotImported(reason) => { @@ -3939,7 +3942,7 @@ impl BeaconChain { async fn check_gossip_blob_availability_and_import( self: &Arc, blob: GossipVerifiedBlob, - ) -> Result { + ) -> Result { let slot = blob.slot(); if let Some(slasher) = self.slasher.as_ref() { slasher.accept_block_header(blob.signed_block_header()); @@ -3980,15 +3983,19 @@ impl BeaconChain { { let availability = self .pending_payload_cache - .put_gossip_verified_data_columns(block_root, slot, data_columns)?; - self.process_payload_availability(slot, availability, publish_fn) - .await + .put_gossip_verified_data_columns(block_root, slot, data_columns) + .map_err(EnvelopeError::from)?; + Ok(self + .process_payload_availability(slot, availability, publish_fn) + .await?) } else { let availability = self .data_availability_checker - .put_gossip_verified_data_columns(block_root, slot, data_columns)?; - self.process_availability(slot, availability, publish_fn) - .await + .put_gossip_verified_data_columns(block_root, slot, data_columns) + .map_err(BlockError::from)?; + Ok(self + .process_availability(slot, availability, publish_fn) + .await?) } } @@ -4029,7 +4036,7 @@ impl BeaconChain { slot: Slot, block_root: Hash256, blobs: FixedBlobSidecarList, - ) -> Result { + ) -> Result { self.check_blob_header_signature_and_slashability( block_root, blobs.iter().flatten().map(Arc::as_ref), @@ -4057,10 +4064,12 @@ impl BeaconChain { )?; let availability = self .data_availability_checker - .put_kzg_verified_blobs(block_root, blobs)?; + .put_kzg_verified_blobs(block_root, blobs) + .map_err(BlockError::from)?; - self.process_availability(slot, availability, || Ok(())) - .await + Ok(self + .process_availability(slot, availability, || Ok(())) + .await?) } EngineGetBlobsOutput::CustodyColumns(data_columns) => { // TODO(gloas) verify that this check is no longer relevant for gloas @@ -4080,15 +4089,19 @@ impl BeaconChain { { let availability = self .pending_payload_cache - .put_kzg_verified_custody_data_columns(block_root, data_columns)?; - self.process_payload_availability(slot, availability, || Ok(())) - .await + .put_kzg_verified_custody_data_columns(block_root, data_columns) + .map_err(EnvelopeError::from)?; + Ok(self + .process_payload_availability(slot, availability, || Ok(())) + .await?) } else { let availability = self .data_availability_checker - .put_kzg_verified_custody_data_columns(block_root, data_columns)?; - self.process_availability(slot, availability, || Ok(())) - .await + .put_kzg_verified_custody_data_columns(block_root, data_columns) + .map_err(BlockError::from)?; + Ok(self + .process_availability(slot, availability, || Ok(())) + .await?) } } } @@ -4116,21 +4129,21 @@ impl BeaconChain { .fork_name_at_slot::(slot) .gloas_enabled() { - let availability = self.pending_payload_cache.put_rpc_custody_columns( - block_root, - slot, - custody_columns, - )?; - self.process_payload_availability(slot, availability, || Ok(())) - .await + let availability = self + .pending_payload_cache + .put_rpc_custody_columns(block_root, slot, custody_columns) + .map_err(EnvelopeError::from)?; + Ok(self + .process_payload_availability(slot, availability, || Ok(())) + .await?) } else { - let availability = self.data_availability_checker.put_rpc_custody_columns( - block_root, - slot, - custody_columns, - )?; - self.process_availability(slot, availability, || Ok(())) - .await + let availability = self + .data_availability_checker + .put_rpc_custody_columns(block_root, slot, custody_columns) + .map_err(BlockError::from)?; + Ok(self + .process_availability(slot, availability, || Ok(())) + .await?) } } diff --git a/beacon_node/beacon_chain/src/errors.rs b/beacon_node/beacon_chain/src/errors.rs index 68c560611c..361521cea5 100644 --- a/beacon_node/beacon_chain/src/errors.rs +++ b/beacon_node/beacon_chain/src/errors.rs @@ -337,6 +337,7 @@ easy_from_to!(StateAdvanceError, BlockProductionError); easy_from_to!(ForkChoiceError, BlockProductionError); easy_from_to!(EpochCacheError, BlockProductionError); +#[derive(Debug)] pub enum BlockOrEnvelopeError { BlockError(BlockError), EnvelopeError(EnvelopeError), @@ -344,3 +345,12 @@ pub enum BlockOrEnvelopeError { easy_from_to!(BlockError, BlockOrEnvelopeError); easy_from_to!(EnvelopeError, BlockOrEnvelopeError); + +impl AsRef for BlockOrEnvelopeError { + fn as_ref(&self) -> &str { + match self { + BlockOrEnvelopeError::BlockError(e) => e.as_ref(), + BlockOrEnvelopeError::EnvelopeError(e) => e.as_ref(), + } + } +} diff --git a/beacon_node/beacon_chain/src/fetch_blobs/fetch_blobs_beacon_adapter.rs b/beacon_node/beacon_chain/src/fetch_blobs/fetch_blobs_beacon_adapter.rs index abfcc8508f..32e14ad42a 100644 --- a/beacon_node/beacon_chain/src/fetch_blobs/fetch_blobs_beacon_adapter.rs +++ b/beacon_node/beacon_chain/src/fetch_blobs/fetch_blobs_beacon_adapter.rs @@ -138,7 +138,7 @@ impl FetchBlobsBeaconAdapter { self.chain .process_engine_blobs(slot, block_root, blobs) .await - .map_err(FetchEngineBlobError::BlobProcessingError) + .map_err(|e| FetchEngineBlobError::BlobProcessingError(Box::new(e))) } pub(crate) fn fork_choice_contains_block(&self, block_root: &Hash256) -> bool { diff --git a/beacon_node/beacon_chain/src/fetch_blobs/mod.rs b/beacon_node/beacon_chain/src/fetch_blobs/mod.rs index e2ac20509b..a1251b8622 100644 --- a/beacon_node/beacon_chain/src/fetch_blobs/mod.rs +++ b/beacon_node/beacon_chain/src/fetch_blobs/mod.rs @@ -16,13 +16,13 @@ use crate::blob_verification::{GossipBlobError, KzgVerifiedBlob}; use crate::data_column_verification::{ KzgVerifiedCustodyDataColumn, KzgVerifiedCustodyPartialDataColumn, KzgVerifiedPartialDataColumn, }; +use crate::errors::BlockOrEnvelopeError; #[cfg_attr(test, double)] use crate::fetch_blobs::fetch_blobs_beacon_adapter::FetchBlobsBeaconAdapter; use crate::kzg_utils::blobs_to_partial_data_columns; use crate::observed_data_sidecars::ObservationKey; use crate::{ - AvailabilityProcessingStatus, BeaconChain, BeaconChainError, BeaconChainTypes, BlockError, - metrics, + AvailabilityProcessingStatus, BeaconChain, BeaconChainError, BeaconChainTypes, metrics, }; use execution_layer::Error as ExecutionLayerError; use execution_layer::json_structures::{BlobAndProofV1, BlobAndProofV2, BlobAndProofV3}; @@ -50,7 +50,7 @@ pub enum EngineGetBlobsOutput { pub enum FetchEngineBlobError { BeaconStateError(BeaconStateError), BeaconChainError(Box), - BlobProcessingError(BlockError), + BlobProcessingError(Box), BlobSidecarError(BlobSidecarError), DataColumnSidecarError(DataColumnSidecarError), ExecutionLayerMissing, diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index 804268a613..bd9c4a7c12 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -76,7 +76,7 @@ pub use self::beacon_chain::{ }; pub use self::beacon_snapshot::BeaconSnapshot; pub use self::chain_config::ChainConfig; -pub use self::errors::{BeaconChainError, BlockProductionError}; +pub use self::errors::{BeaconChainError, BlockOrEnvelopeError, BlockProductionError}; pub use self::historical_blocks::HistoricalBlockError; pub use attestation_verification::Error as AttestationError; pub use beacon_fork_choice_store::{ 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 2d371a5315..ccd31e94b7 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs @@ -92,6 +92,7 @@ impl BeaconChain { } self.check_envelope_availability_and_import(executed_envelope) + .await }; // Verify and import the payload envelope. 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 75c0f34363..cbb25a64f2 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs @@ -18,10 +18,10 @@ //! //! ``` -use std::sync::Arc; - use state_processing::{BlockProcessingError, envelope_processing::EnvelopeProcessingError}; +use std::sync::Arc; use store::Error as DBError; +use strum::AsRefStr; use tracing::instrument; use types::{ BeaconState, BeaconStateError, DataColumnSidecarList, EthSpec, ExecutionBlockHash, @@ -190,7 +190,7 @@ impl AvailableExecutedEnvelope { } } -#[derive(Debug)] +#[derive(Debug, AsRefStr)] pub enum EnvelopeError { /// The envelope's block root is unknown. BlockRootUnknown { block_root: Hash256 }, diff --git a/beacon_node/http_api/src/publish_blocks.rs b/beacon_node/http_api/src/publish_blocks.rs index 644ade956a..e96c86b17f 100644 --- a/beacon_node/http_api/src/publish_blocks.rs +++ b/beacon_node/http_api/src/publish_blocks.rs @@ -246,7 +246,7 @@ pub async fn publish_block>( if let Err(e) = Box::pin(chain.process_gossip_data_columns(sampling_columns, publish_fn)).await { - let msg = format!("Invalid data column: {e}"); + let msg = format!("Invalid data column: {e:?}"); return if let BroadcastValidation::Gossip = validation_level { Err(warp_utils::reject::broadcast_without_import(msg)) } else { diff --git a/beacon_node/network/src/metrics.rs b/beacon_node/network/src/metrics.rs index b09dc95db4..4b34d7bfc0 100644 --- a/beacon_node/network/src/metrics.rs +++ b/beacon_node/network/src/metrics.rs @@ -1,5 +1,5 @@ use beacon_chain::{ - AvailabilityProcessingStatus, BlockError, attestation_verification::Error as AttnError, + AvailabilityProcessingStatus, attestation_verification::Error as AttnError, light_client_finality_update_verification::Error as LightClientFinalityUpdateError, light_client_optimistic_update_verification::Error as LightClientOptimisticUpdateError, sync_committee_verification::Error as SyncCommitteeError, @@ -733,7 +733,7 @@ pub fn register_sync_committee_error(error: &SyncCommitteeError) { } pub(crate) fn register_process_result_metrics( - result: &std::result::Result, + result: &std::result::Result>, source: BlockSource, block_component: &'static str, ) { 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 c83fd9244d..5e291bd833 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -14,8 +14,8 @@ use beacon_chain::payload_bid_verification::PayloadBidError; use beacon_chain::proposer_preferences_verification::ProposerPreferencesError; use beacon_chain::store::Error; use beacon_chain::{ - AvailabilityProcessingStatus, BeaconChainError, BeaconChainTypes, BlockError, ForkChoiceError, - GossipVerifiedBlock, NotifyExecutionLayer, + AvailabilityProcessingStatus, BeaconChainError, BeaconChainTypes, BlockError, + BlockOrEnvelopeError, ForkChoiceError, GossipVerifiedBlock, NotifyExecutionLayer, attestation_verification::{self, Error as AttnError, VerifiedAttestation}, data_availability_checker::AvailabilityCheckErrorCategory, light_client_finality_update_verification::Error as LightClientFinalityUpdateError, @@ -1387,7 +1387,7 @@ impl NetworkBeaconProcessor { self.check_reconstruction_trigger(slot, &block_root).await; } }, - Err(BlockError::DuplicateFullyImported(_)) => { + Err(BlockOrEnvelopeError::BlockError(BlockError::DuplicateFullyImported(_))) => { debug!( ?block_root, data_column_index, "Ignoring gossip column already imported" @@ -1518,7 +1518,7 @@ impl NetworkBeaconProcessor { self.check_reconstruction_trigger(*slot, block_root).await; } }, - Err(BlockError::DuplicateFullyImported(_)) => { + Err(BlockOrEnvelopeError::BlockError(BlockError::DuplicateFullyImported(_))) => { debug!( ?block_root, data_column_index, "Ignoring completed gossip column already imported" diff --git a/beacon_node/network/src/network_beacon_processor/mod.rs b/beacon_node/network/src/network_beacon_processor/mod.rs index 23515d5901..7a978548a7 100644 --- a/beacon_node/network/src/network_beacon_processor/mod.rs +++ b/beacon_node/network/src/network_beacon_processor/mod.rs @@ -7,7 +7,9 @@ use beacon_chain::data_column_verification::{GossipDataColumnError, observe_goss use beacon_chain::fetch_blobs::{ EngineGetBlobsOutput, FetchEngineBlobError, fetch_and_process_engine_blobs, }; -use beacon_chain::{AvailabilityProcessingStatus, BeaconChain, BeaconChainTypes, BlockError}; +use beacon_chain::{ + AvailabilityProcessingStatus, BeaconChain, BeaconChainTypes, BlockError, BlockOrEnvelopeError, +}; use beacon_processor::{ BeaconProcessorSend, DuplicateCache, GossipAggregatePackage, GossipAttestationPackage, Work, WorkEvent as BeaconWorkEvent, @@ -980,9 +982,10 @@ impl NetworkBeaconProcessor { "Fetch blobs completed without import" ); } - Err(FetchEngineBlobError::BlobProcessingError(BlockError::DuplicateFullyImported( - .., - ))) => { + Err(FetchEngineBlobError::BlobProcessingError(e)) + if let BlockOrEnvelopeError::BlockError(BlockError::DuplicateFullyImported(..)) = + *e => + { debug!( %block_root, "Fetch blobs duplicate import" @@ -1050,7 +1053,7 @@ impl NetworkBeaconProcessor { "Reconstruction not required for block" ); } - Err(BlockError::DuplicateFullyImported(_)) => { + Err(BlockOrEnvelopeError::BlockError(BlockError::DuplicateFullyImported(_))) => { debug!("Block already imported in parallel with reconstruction"); } Err(e) => { diff --git a/beacon_node/network/src/network_beacon_processor/sync_methods.rs b/beacon_node/network/src/network_beacon_processor/sync_methods.rs index 988a68c9dd..03be073e7d 100644 --- a/beacon_node/network/src/network_beacon_processor/sync_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/sync_methods.rs @@ -11,8 +11,9 @@ use beacon_chain::block_verification_types::{AsBlock, RangeSyncBlock}; use beacon_chain::data_availability_checker::AvailabilityCheckError; use beacon_chain::historical_data_columns::HistoricalDataColumnError; use beacon_chain::{ - AvailabilityProcessingStatus, BeaconChainTypes, BlockError, ChainSegmentResult, - HistoricalBlockError, NotifyExecutionLayer, validator_monitor::get_slot_delay_ms, + AvailabilityProcessingStatus, BeaconChainTypes, BlockError, BlockOrEnvelopeError, + ChainSegmentResult, HistoricalBlockError, NotifyExecutionLayer, + validator_monitor::get_slot_delay_ms, }; use beacon_processor::{ AsyncFn, BlockingFn, DuplicateCache, @@ -234,7 +235,7 @@ impl NetworkBeaconProcessor { // Sync handles these results self.send_sync_message(SyncMessage::BlockComponentProcessed { process_type, - result: result.into(), + result: result.map_err(Into::into).into(), }); // Drop the handle to remove the entry from the cache @@ -345,7 +346,7 @@ impl NetworkBeaconProcessor { // Sync handles these results self.send_sync_message(SyncMessage::BlockComponentProcessed { process_type, - result: result.into(), + result: result.map_err(Into::into).into(), }); } @@ -410,7 +411,7 @@ impl NetworkBeaconProcessor { ); } }, - Err(BlockError::DuplicateFullyImported(_)) => { + Err(BlockOrEnvelopeError::BlockError(BlockError::DuplicateFullyImported(_))) => { debug!( block_hash = %block_root, "Custody columns have already been imported" diff --git a/beacon_node/network/src/sync/block_lookups/mod.rs b/beacon_node/network/src/sync/block_lookups/mod.rs index 3929f74aa0..ab9c7bbe38 100644 --- a/beacon_node/network/src/sync/block_lookups/mod.rs +++ b/beacon_node/network/src/sync/block_lookups/mod.rs @@ -33,7 +33,9 @@ use beacon_chain::block_verification_types::AsBlock; use beacon_chain::data_availability_checker::{ AvailabilityCheckError, AvailabilityCheckErrorCategory, }; -use beacon_chain::{AvailabilityProcessingStatus, BeaconChainTypes, BlockError}; +use beacon_chain::{ + AvailabilityProcessingStatus, BeaconChainTypes, BlockError, BlockOrEnvelopeError, +}; pub use common::RequestState; use fnv::FnvHashMap; use lighthouse_network::service::api_types::SingleLookupReqId; @@ -589,8 +591,12 @@ impl BlockLookups { let action = match result { BlockProcessingResult::Ok(AvailabilityProcessingStatus::Imported(_)) - | BlockProcessingResult::Err(BlockError::DuplicateFullyImported(..)) - | BlockProcessingResult::Err(BlockError::GenesisBlock) => { + | BlockProcessingResult::Err(BlockOrEnvelopeError::BlockError( + BlockError::DuplicateFullyImported(..), + )) + | BlockProcessingResult::Err(BlockOrEnvelopeError::BlockError( + BlockError::GenesisBlock, + )) => { // Successfully imported request_state.on_processing_success()?; Action::Continue @@ -614,7 +620,9 @@ impl BlockLookups { Action::Retry } } - BlockProcessingResult::Err(BlockError::DuplicateImportStatusUnknown(..)) => { + BlockProcessingResult::Err(BlockOrEnvelopeError::BlockError( + BlockError::DuplicateImportStatusUnknown(..), + )) => { // This is unreachable because RPC blocks do not undergo gossip verification, and // this error can *only* come from gossip verification. error!(?block_root, "Single block lookup hit unreachable condition"); @@ -630,6 +638,11 @@ impl BlockLookups { Action::Drop("Block processing ignored".to_owned()) } BlockProcessingResult::Err(e) => { + let BlockOrEnvelopeError::BlockError(e) = e else { + // TODO(gloas): handle properly + return Err(LookupRequestError::Failed(format!("{e:?}"))); + }; + match e { BlockError::BeaconChainError(e) => { // Internal error diff --git a/beacon_node/network/src/sync/manager.rs b/beacon_node/network/src/sync/manager.rs index 734295ac1d..fb31e92262 100644 --- a/beacon_node/network/src/sync/manager.rs +++ b/beacon_node/network/src/sync/manager.rs @@ -50,7 +50,8 @@ use crate::sync::custody_backfill_sync::CustodyBackFillSync; use crate::sync::network_context::{PeerGroup, RpcResponseResult}; use beacon_chain::block_verification_types::AsBlock; use beacon_chain::{ - AvailabilityProcessingStatus, BeaconChain, BeaconChainTypes, BlockError, EngineState, + AvailabilityProcessingStatus, BeaconChain, BeaconChainTypes, BlockError, BlockOrEnvelopeError, + EngineState, }; use futures::StreamExt; use lighthouse_network::SyncInfo; @@ -206,7 +207,7 @@ impl BlockProcessType { #[derive(Debug)] pub enum BlockProcessingResult { Ok(AvailabilityProcessingStatus), - Err(BlockError), + Err(BlockOrEnvelopeError), Ignored, } @@ -1449,8 +1450,8 @@ impl SyncManager { } } -impl From> for BlockProcessingResult { - fn from(result: Result) -> Self { +impl From> for BlockProcessingResult { + fn from(result: Result) -> Self { match result { Ok(status) => BlockProcessingResult::Ok(status), Err(e) => BlockProcessingResult::Err(e), @@ -1458,8 +1459,14 @@ impl From> for BlockProcessingR } } -impl From for BlockProcessingResult { - fn from(e: BlockError) -> Self { +impl From for BlockProcessingResult { + fn from(e: BlockOrEnvelopeError) -> Self { BlockProcessingResult::Err(e) } } + +impl From for BlockProcessingResult { + fn from(e: BlockError) -> Self { + BlockProcessingResult::Err(BlockOrEnvelopeError::BlockError(e)) + } +} diff --git a/beacon_node/network/src/sync/tests/lookups.rs b/beacon_node/network/src/sync/tests/lookups.rs index a26996ec5e..7b5cd74150 100644 --- a/beacon_node/network/src/sync/tests/lookups.rs +++ b/beacon_node/network/src/sync/tests/lookups.rs @@ -2089,8 +2089,7 @@ async fn too_many_processing_failures(depth: usize) { r.build_chain_and_trigger_last_block(depth).await; // Simulate that a peer always returns empty r.simulate( - SimulateConfig::new() - .with_process_result(|| BlockProcessingResult::Err(BlockError::BlockSlotLimitReached)), + SimulateConfig::new().with_process_result(|| BlockError::BlockSlotLimitReached.into()), ) .await; // We register multiple penalties, the lookup fails and sync does not progress @@ -2158,9 +2157,10 @@ async fn test_single_block_lookup_duplicate_response() { let mut r = TestRig::default(); r.build_chain_and_trigger_last_block(1).await; // Send a DuplicateFullyImported response, the lookup should complete successfully - r.simulate(SimulateConfig::new().with_process_result(|| { - BlockProcessingResult::Err(BlockError::DuplicateFullyImported(Hash256::ZERO)) - })) + r.simulate( + SimulateConfig::new() + .with_process_result(|| BlockError::DuplicateFullyImported(Hash256::ZERO).into()), + ) .await; // The block was not actually imported r.assert_head_slot(0); From 7cf76ac7aff9d5dd562bb1206ce1d9ef4d2b817d Mon Sep 17 00:00:00 2001 From: Daniel Knopik Date: Wed, 29 Apr 2026 13:06:07 +0200 Subject: [PATCH 067/118] clean up --- beacon_node/beacon_chain/src/beacon_chain.rs | 20 ++++---- .../beacon_chain/src/block_verification.rs | 1 - beacon_node/beacon_chain/src/builder.rs | 47 ++++++++----------- .../src/data_availability_checker.rs | 16 +++++-- .../fetch_blobs/fetch_blobs_beacon_adapter.rs | 11 +++-- .../beacon_chain/src/fetch_blobs/mod.rs | 2 +- .../beacon_chain/src/fetch_blobs/tests.rs | 2 +- beacon_node/beacon_chain/src/metrics.rs | 1 - .../payload_envelope_verification/import.rs | 2 + .../src/payload_envelope_verification/mod.rs | 45 ------------------ .../src/pending_payload_cache/mod.rs | 3 -- .../lighthouse_network/src/rpc/codec.rs | 2 - .../lighthouse_network/src/rpc/methods.rs | 8 +--- .../lighthouse_network/tests/rpc_tests.rs | 2 - .../network_beacon_processor/rpc_methods.rs | 1 - .../network/src/sync/block_lookups/common.rs | 2 +- .../sync/block_lookups/single_block_lookup.rs | 6 +-- .../network/src/sync/network_context.rs | 6 ++- .../requests/data_columns_by_root.rs | 1 - 19 files changed, 59 insertions(+), 119 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 78c59bf661..d8be9d6ac2 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -71,7 +71,7 @@ use crate::payload_envelope_verification::EnvelopeError; use crate::pending_payload_cache::PendingPayloadCache; use crate::pending_payload_cache::{ Availability as PayloadAvailability, - DataColumnReconstructionResult as DataColumnReconstructionResultV2, + DataColumnReconstructionResult as DataColumnReconstructionResultGloas, }; use crate::pending_payload_envelopes::PendingPayloadEnvelopes; use crate::persisted_beacon_chain::PersistedBeaconChain; @@ -1185,14 +1185,12 @@ impl BeaconChain { &self, block_root: Hash256, indices: &[ColumnIndex], - fork_name: ForkName, ) -> Result, Error> { - let all_cached_columns_opt = if fork_name.gloas_enabled() { - self.pending_payload_cache.get_data_columns(block_root) - } else { - self.data_availability_checker.get_data_columns(block_root) - } - .or_else(|| self.early_attester_cache.get_data_columns(block_root)); + let all_cached_columns_opt = self + .pending_payload_cache + .get_data_columns(block_root) + .or_else(|| self.data_availability_checker.get_data_columns(block_root)) + .or_else(|| self.early_attester_cache.get_data_columns(block_root)); if let Some(mut all_cached_columns) = all_cached_columns_opt { all_cached_columns.retain(|col| indices.contains(col.index())); @@ -3676,7 +3674,7 @@ impl BeaconChain { .map_err(EnvelopeError::from)?; match result { - DataColumnReconstructionResultV2::Success(( + DataColumnReconstructionResultGloas::Success(( availability, data_columns_to_publish, )) => { @@ -3689,8 +3687,8 @@ impl BeaconChain { .await .map(|status| Some((status, data_columns_to_publish)))?) } - DataColumnReconstructionResultV2::NotStarted(reason) - | DataColumnReconstructionResultV2::RecoveredColumnsNotImported(reason) => { + DataColumnReconstructionResultGloas::NotStarted(reason) + | DataColumnReconstructionResultGloas::RecoveredColumnsNotImported(reason) => { metrics::inc_counter_vec( &metrics::KZG_DATA_COLUMN_RECONSTRUCTION_INCOMPLETE_TOTAL, &[reason], diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 4b02d77f1a..b70730d047 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -1202,7 +1202,6 @@ impl SignatureVerifiedBlock { AvailableBlock::new( block, AvailableBlockData::NoData, - // TODO(gloas) shouldnt matter which da checker we pass? &chain.data_availability_checker, chain.spec.clone(), ) diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index d3a1d851ea..cccc965dee 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -12,7 +12,7 @@ use crate::kzg_utils::{build_data_column_sidecars_fulu, build_data_column_sideca use crate::light_client_server_cache::LightClientServerCache; use crate::migrate::{BackgroundMigrator, MigratorConfig}; use crate::observed_data_sidecars::ObservedDataSidecars; -use crate::pending_payload_cache::PendingPayloadCache as DataAvailabilityCheckerV2; +use crate::pending_payload_cache::PendingPayloadCache; use crate::persisted_beacon_chain::PersistedBeaconChain; use crate::persisted_custody::load_custody_context; use crate::shuffling_cache::{BlockShufflingIds, ShufflingCache}; @@ -987,31 +987,7 @@ where ) }; debug!(?custody_context, "Loaded persisted custody context"); - let custody_context = Arc::new(custody_context); - let da_checker_v1 = Arc::new( - DataAvailabilityChecker::new( - complete_blob_backfill, - slot_clock.clone(), - self.kzg.clone(), - custody_context.clone(), - self.spec.clone(), - enable_partial_columns, - ) - .map_err(|e| format!("Error initializing DataAvailabilityCheckerV1: {:?}", e))?, - ); - - let da_checker_v2 = Arc::new( - DataAvailabilityCheckerV2::new( - self.kzg.clone(), - custody_context.clone(), - self.spec.clone(), - ) - .map_err(|e| format!("Error initializing DataAvailabilityCheckerV2: {:?}", e))?, - ); - - let pending_block_cache = da_checker_v1; - let pending_payload_cache = da_checker_v2; let beacon_chain = BeaconChain { spec: self.spec.clone(), @@ -1084,8 +1060,25 @@ where slasher: self.slasher.clone(), validator_monitor: RwLock::new(validator_monitor), genesis_backfill_slot, - data_availability_checker: pending_block_cache, - pending_payload_cache, + data_availability_checker: Arc::new( + DataAvailabilityChecker::new( + complete_blob_backfill, + slot_clock.clone(), + self.kzg.clone(), + custody_context.clone(), + self.spec.clone(), + enable_partial_columns, + ) + .map_err(|e| format!("Error initializing DataAvailabilityChecker: {:?}", e))?, + ), + pending_payload_cache: Arc::new( + PendingPayloadCache::new( + self.kzg.clone(), + custody_context.clone(), + self.spec.clone(), + ) + .map_err(|e| format!("Error initializing PendingPayloadCache: {:?}", e))?, + ), kzg: self.kzg.clone(), rng: Arc::new(Mutex::new(rng)), gossip_verified_payload_bid_cache: <_>::default(), diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index b4abbb5290..ef5544930e 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -21,7 +21,7 @@ use tracing::{debug, error, instrument}; use types::data::{BlobIdentifier, FixedBlobSidecarList, PartialDataColumn}; use types::{ BlobSidecar, BlobSidecarList, BlockImportSource, ChainSpec, DataColumnSidecar, - DataColumnSidecarList, Epoch, EthSpec, Hash256, PartialDataColumnSidecarError, + DataColumnSidecarList, Epoch, EthSpec, ForkName, Hash256, PartialDataColumnSidecarError, PartialDataColumnSidecarRef, SignedBeaconBlock, Slot, new_non_zero_usize, }; @@ -877,10 +877,16 @@ impl AvailableBlock { match &block_data { AvailableBlockData::NoData => { - if columns_required { - return Err(AvailabilityCheckError::MissingCustodyColumns); - } else if blobs_required { - return Err(AvailabilityCheckError::MissingBlobs); + // For Gloas, DA is checked for the PayloadEnvelope, not for the block. + if block.fork_name(&spec).map_err(|_| { + AvailabilityCheckError::Unexpected("Unexpected fork mismatch".to_string()) + })? < ForkName::Gloas + { + if columns_required { + return Err(AvailabilityCheckError::MissingCustodyColumns); + } else if blobs_required { + return Err(AvailabilityCheckError::MissingBlobs); + } } } AvailableBlockData::Blobs(blobs) => { diff --git a/beacon_node/beacon_chain/src/fetch_blobs/fetch_blobs_beacon_adapter.rs b/beacon_node/beacon_chain/src/fetch_blobs/fetch_blobs_beacon_adapter.rs index 32e14ad42a..c319514b0e 100644 --- a/beacon_node/beacon_chain/src/fetch_blobs/fetch_blobs_beacon_adapter.rs +++ b/beacon_node/beacon_chain/src/fetch_blobs/fetch_blobs_beacon_adapter.rs @@ -119,14 +119,15 @@ impl FetchBlobsBeaconAdapter { .cached_blob_indexes(block_root) } - pub(crate) fn cached_data_column_indexes( - &self, - _slot: Slot, - block_root: &Hash256, - ) -> Option> { + pub(crate) fn cached_data_column_indexes(&self, block_root: &Hash256) -> Option> { self.chain .data_availability_checker .cached_data_column_indexes(block_root) + .or_else(|| { + self.chain + .pending_payload_cache + .cached_data_column_indexes(block_root) + }) } pub(crate) async fn process_engine_blobs( diff --git a/beacon_node/beacon_chain/src/fetch_blobs/mod.rs b/beacon_node/beacon_chain/src/fetch_blobs/mod.rs index a1251b8622..7d26a603c8 100644 --- a/beacon_node/beacon_chain/src/fetch_blobs/mod.rs +++ b/beacon_node/beacon_chain/src/fetch_blobs/mod.rs @@ -445,7 +445,7 @@ async fn compute_custody_columns_to_import( // Only consider columns that are not already known to data availability. if let Some(known_columns) = - chain_adapter_cloned.cached_data_column_indexes(header.slot(), &block_root) + chain_adapter_cloned.cached_data_column_indexes(&block_root) { custody_columns.retain(|col| !known_columns.contains(&col.index())); if custody_columns.is_empty() { diff --git a/beacon_node/beacon_chain/src/fetch_blobs/tests.rs b/beacon_node/beacon_chain/src/fetch_blobs/tests.rs index 37d40f3a27..ef282a3eaa 100644 --- a/beacon_node/beacon_chain/src/fetch_blobs/tests.rs +++ b/beacon_node/beacon_chain/src/fetch_blobs/tests.rs @@ -199,7 +199,7 @@ mod get_blobs_v2 { .returning(|_| None); mock_adapter .expect_cached_data_column_indexes() - .returning(|_, _| None); + .returning(|_| None); mock_process_engine_blobs_result( &mut mock_adapter, Ok(AvailabilityProcessingStatus::Imported(block_root)), diff --git a/beacon_node/beacon_chain/src/metrics.rs b/beacon_node/beacon_chain/src/metrics.rs index 9739038b3a..43c3337bc9 100644 --- a/beacon_node/beacon_chain/src/metrics.rs +++ b/beacon_node/beacon_chain/src/metrics.rs @@ -2146,7 +2146,6 @@ pub fn scrape_for_metrics(beacon_chain: &BeaconChain) { ); let da_checker_metrics = beacon_chain.data_availability_checker.metrics(); - set_gauge_by_usize( &DATA_AVAILABILITY_OVERFLOW_MEMORY_BLOCK_CACHE_SIZE, da_checker_metrics.block_cache_size, 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 ccd31e94b7..beabe0e76c 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs @@ -56,6 +56,8 @@ impl BeaconChain { ); } + // TODO(gloas) insert the pre-executed envelope into some type of cache? + let _full_timer = metrics::start_timer(&metrics::ENVELOPE_PROCESSING_TIMES); metrics::inc_counter(&metrics::ENVELOPE_PROCESSING_REQUESTS); 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 cbb25a64f2..56f2c2c22c 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs @@ -100,47 +100,6 @@ pub struct EnvelopeProcessingSnapshot { pub beacon_block_root: Hash256, } -/// A payload envelope that has gone through processing checks and execution by an EL client. -/// This envelope hasn't necessarily completed data availability checks. -/// -/// -/// It contains 2 variants: -/// 1. `Available`: This envelope has been executed and also contains all data to consider it -/// fully available. -/// 2. `AvailabilityPending`: This envelope hasn't received all required blobs to consider it -/// fully available. -#[allow(dead_code)] -pub enum ExecutedEnvelope { - Available(AvailableExecutedEnvelope), - AvailabilityPending(AvailabilityPendingExecutedEnvelope), -} - -impl ExecutedEnvelope { - pub fn new( - envelope: MaybeAvailableEnvelope, - block_root: Hash256, - payload_verification_outcome: PayloadVerificationOutcome, - ) -> Self { - match envelope { - MaybeAvailableEnvelope::Available(available_envelope) => { - Self::Available(AvailableExecutedEnvelope::new( - available_envelope, - block_root, - payload_verification_outcome, - )) - } - MaybeAvailableEnvelope::AvailabilityPending { - block_hash: _, - envelope, - } => Self::AvailabilityPending(AvailabilityPendingExecutedEnvelope::new( - envelope, - block_root, - payload_verification_outcome, - )), - } - } -} - /// A payload ernvelope that has completed all envelope procesing checks, verification /// by an EL client but does not have all requisite columns to get imported into /// fork choice. @@ -162,10 +121,6 @@ impl AvailabilityPendingExecutedEnvelope { payload_verification_outcome, } } - - pub fn as_envelope(&self) -> &SignedExecutionPayloadEnvelope { - &self.envelope - } } /// A payload envelope that has completed all payload processing checks including verification diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs b/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs index 2337697fab..cbb1fcf711 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs @@ -80,9 +80,6 @@ use types::new_non_zero_usize; /// eviction to prevent race conditions (#7961), so we expect this cache to be full all the time. const OVERFLOW_LRU_CAPACITY_NON_ZERO: NonZeroUsize = new_non_zero_usize(32); -/// Represents available data for a payload - its block root and its data columns. -pub type AvailableData = (Hash256, DataColumnSidecarList); - /// This type is returned after adding a bid / column to the `DataAvailabilityChecker`. /// /// Indicates if the payloads data is fully `Available` or if we need more columns. diff --git a/beacon_node/lighthouse_network/src/rpc/codec.rs b/beacon_node/lighthouse_network/src/rpc/codec.rs index 5d4610b8a6..75e035ae82 100644 --- a/beacon_node/lighthouse_network/src/rpc/codec.rs +++ b/beacon_node/lighthouse_network/src/rpc/codec.rs @@ -587,7 +587,6 @@ fn handle_rpc_request( decoded_buffer, spec.max_request_blocks(current_fork), )?, - fork_name: current_fork, }, ))), SupportedProtocol::PingV1 => Ok(Some(RequestType::Ping(Ping { @@ -1156,7 +1155,6 @@ mod tests { spec.max_request_blocks(fork_name), ) .unwrap(), - fork_name, } } diff --git a/beacon_node/lighthouse_network/src/rpc/methods.rs b/beacon_node/lighthouse_network/src/rpc/methods.rs index 9eb112075a..baabf48683 100644 --- a/beacon_node/lighthouse_network/src/rpc/methods.rs +++ b/beacon_node/lighthouse_network/src/rpc/methods.rs @@ -12,7 +12,6 @@ use std::ops::Deref; use std::sync::Arc; use strum::IntoStaticStr; use superstruct::superstruct; -use types::ForkName; use types::data::BlobIdentifier; use types::light_client::consts::MAX_REQUEST_LIGHT_CLIENT_UPDATES; use types::{ @@ -563,21 +562,16 @@ impl BlobsByRootRequest { pub struct DataColumnsByRootRequest { /// The list of beacon block roots and column indices being requested. pub data_column_ids: RuntimeVariableList>, - pub fork_name: ForkName, } impl DataColumnsByRootRequest { pub fn new( data_column_ids: Vec>, - fork_name: ForkName, max_request_blocks: usize, ) -> Result { let data_column_ids = RuntimeVariableList::new(data_column_ids, max_request_blocks) .map_err(|_| "DataColumnsByRootRequest too many column IDs")?; - Ok(Self { - data_column_ids, - fork_name, - }) + Ok(Self { data_column_ids }) } pub fn max_requested(&self) -> usize { diff --git a/beacon_node/lighthouse_network/tests/rpc_tests.rs b/beacon_node/lighthouse_network/tests/rpc_tests.rs index 2249fbb6f6..65b03189d4 100644 --- a/beacon_node/lighthouse_network/tests/rpc_tests.rs +++ b/beacon_node/lighthouse_network/tests/rpc_tests.rs @@ -982,7 +982,6 @@ fn test_tcp_columns_by_root_chunked_rpc_for_fork(fork_name: ForkName) { }; max_request_blocks ], - fork_name, max_request_blocks, ) .unwrap(); @@ -993,7 +992,6 @@ fn test_tcp_columns_by_root_chunked_rpc_for_fork(fork_name: ForkName) { spec.max_request_blocks(fork_name), ) .unwrap(), - fork_name, }; assert_eq!(req, req_decoded); let rpc_request = RequestType::DataColumnsByRoot(req); 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 2782a65def..8b31b67acb 100644 --- a/beacon_node/network/src/network_beacon_processor/rpc_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/rpc_methods.rs @@ -543,7 +543,6 @@ impl NetworkBeaconProcessor { match self.chain.get_data_columns_checking_all_caches( data_column_ids_by_root.block_root, &indices_to_retrieve, - request.fork_name, ) { Ok(data_columns) => { send_data_column_count += data_columns.len(); diff --git a/beacon_node/network/src/sync/block_lookups/common.rs b/beacon_node/network/src/sync/block_lookups/common.rs index b209e051bc..edd99345b4 100644 --- a/beacon_node/network/src/sync/block_lookups/common.rs +++ b/beacon_node/network/src/sync/block_lookups/common.rs @@ -172,7 +172,7 @@ impl RequestState for CustodyRequestState { _: usize, cx: &mut SyncNetworkContext, ) -> Result { - cx.custody_lookup_request(id, self.slot, self.block_root, lookup_peers) + cx.custody_lookup_request(id, self.block_root, lookup_peers) .map_err(LookupRequestError::SendFailedNetwork) } diff --git a/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs b/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs index 952dc10ecb..23bfd531f0 100644 --- a/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs +++ b/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs @@ -241,7 +241,7 @@ impl SingleBlockLookup { ); } else if cx.chain.should_fetch_custody_columns(block_epoch) { self.component_requests = ComponentRequests::ActiveCustodyRequest( - CustodyRequestState::new(block.slot(), self.block_root), + CustodyRequestState::new(self.block_root), ); } else { self.component_requests = ComponentRequests::NotNeeded("outside da window"); @@ -399,15 +399,13 @@ impl BlobRequestState { pub struct CustodyRequestState { #[educe(Debug(ignore))] pub block_root: Hash256, - pub slot: Slot, pub state: SingleLookupRequestState>, } impl CustodyRequestState { - pub fn new(slot: Slot, block_root: Hash256) -> Self { + pub fn new(block_root: Hash256) -> Self { Self { block_root, - slot, state: SingleLookupRequestState::new(), } } diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index ed28099b2e..ae86176908 100644 --- a/beacon_node/network/src/sync/network_context.rs +++ b/beacon_node/network/src/sync/network_context.rs @@ -1082,7 +1082,6 @@ impl SyncNetworkContext { pub fn custody_lookup_request( &mut self, lookup_id: SingleLookupId, - _slot: Slot, block_root: Hash256, lookup_peers: Arc>>, ) -> Result { @@ -1090,6 +1089,11 @@ impl SyncNetworkContext { .chain .data_availability_checker .cached_data_column_indexes(&block_root) + .or_else(|| { + self.chain + .pending_payload_cache + .cached_data_column_indexes(&block_root) + }) .unwrap_or_default(); let current_epoch = self.chain.epoch().map_err(|e| { diff --git a/beacon_node/network/src/sync/network_context/requests/data_columns_by_root.rs b/beacon_node/network/src/sync/network_context/requests/data_columns_by_root.rs index 0b6769d2e6..5ad0f377c1 100644 --- a/beacon_node/network/src/sync/network_context/requests/data_columns_by_root.rs +++ b/beacon_node/network/src/sync/network_context/requests/data_columns_by_root.rs @@ -26,7 +26,6 @@ impl DataColumnsByRootSingleBlockRequest { block_root: self.block_root, columns, }], - fork_name, spec.max_request_blocks(fork_name), ) } From e742d0b4f1afa1f598b5dd69fbef658c95c4021f Mon Sep 17 00:00:00 2001 From: Daniel Knopik Date: Wed, 29 Apr 2026 23:01:02 +0200 Subject: [PATCH 068/118] fix tests --- beacon_node/beacon_chain/src/pending_payload_cache/mod.rs | 1 + beacon_node/beacon_chain/tests/block_verification.rs | 4 ++-- beacon_node/beacon_chain/tests/column_verification.rs | 7 +++++-- beacon_node/network/src/network_beacon_processor/tests.rs | 2 +- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs b/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs index cbb1fcf711..9b938046e2 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs @@ -760,6 +760,7 @@ mod data_availability_checker_tests { execution_requests: ExecutionRequests::default(), builder_index: 0, beacon_block_root: block_root, + parent_beacon_block_root: Hash256::random(), }, signature: bls::Signature::infinity().expect("should create infinity sig"), }) diff --git a/beacon_node/beacon_chain/tests/block_verification.rs b/beacon_node/beacon_chain/tests/block_verification.rs index b2db85713f..af858874b2 100644 --- a/beacon_node/beacon_chain/tests/block_verification.rs +++ b/beacon_node/beacon_chain/tests/block_verification.rs @@ -1745,7 +1745,7 @@ async fn add_base_block_to_altair_chain() { RangeSyncBlock::new( Arc::new(base_block), AvailableBlockData::NoData, - &harness.chain.pending_block_cache, + &harness.chain.data_availability_checker, harness.spec.clone() ) .unwrap() @@ -1890,7 +1890,7 @@ async fn add_altair_block_to_base_chain() { RangeSyncBlock::new( Arc::new(altair_block), AvailableBlockData::NoData, - &harness.chain.pending_block_cache, + &harness.chain.data_availability_checker, harness.spec.clone() ) .unwrap() diff --git a/beacon_node/beacon_chain/tests/column_verification.rs b/beacon_node/beacon_chain/tests/column_verification.rs index 06a5f44e5f..e779135e3f 100644 --- a/beacon_node/beacon_chain/tests/column_verification.rs +++ b/beacon_node/beacon_chain/tests/column_verification.rs @@ -6,7 +6,8 @@ use beacon_chain::test_utils::{ generate_data_column_sidecars_from_block, test_spec, }; use beacon_chain::{ - AvailabilityProcessingStatus, BlockError, ChainConfig, InvalidSignature, NotifyExecutionLayer, + AvailabilityProcessingStatus, BlockError, BlockOrEnvelopeError, ChainConfig, InvalidSignature, + NotifyExecutionLayer, block_verification_types::{AsBlock, LookupBlock}, }; use bls::{Keypair, Signature}; @@ -111,7 +112,9 @@ async fn rpc_columns_with_invalid_header_signature() { .unwrap_err(); assert!(matches!( err, - BlockError::InvalidSignature(InvalidSignature::ProposerSignature) + BlockOrEnvelopeError::BlockError(BlockError::InvalidSignature( + InvalidSignature::ProposerSignature + )) )); } diff --git a/beacon_node/network/src/network_beacon_processor/tests.rs b/beacon_node/network/src/network_beacon_processor/tests.rs index 75d08eb965..c4e7f8f8d1 100644 --- a/beacon_node/network/src/network_beacon_processor/tests.rs +++ b/beacon_node/network/src/network_beacon_processor/tests.rs @@ -1196,7 +1196,7 @@ async fn accept_processed_gossip_data_columns_without_import() { let block_root = rig.next_block.canonical_root(); rig.chain - .pending_block_cache + .data_availability_checker .put_gossip_verified_data_columns(block_root, rig.next_block.slot(), verified_data_columns) .expect("should put data columns into availability cache"); From ce00ae2dc7bfaa1f92473cec94cefc930f6a4fd0 Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Thu, 30 Apr 2026 00:08:35 +0200 Subject: [PATCH 069/118] Use stable `if matches!` instead of `if let` match guard `if let` guards are nightly-only (rust-lang/rust#51114), causing `error[E0658]` and a CI `check-code` failure. Replace with the stable `if matches!(...)` form suggested by rustc. --- beacon_node/network/src/network_beacon_processor/mod.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/beacon_node/network/src/network_beacon_processor/mod.rs b/beacon_node/network/src/network_beacon_processor/mod.rs index 7a978548a7..c1980215d1 100644 --- a/beacon_node/network/src/network_beacon_processor/mod.rs +++ b/beacon_node/network/src/network_beacon_processor/mod.rs @@ -983,8 +983,10 @@ impl NetworkBeaconProcessor { ); } Err(FetchEngineBlobError::BlobProcessingError(e)) - if let BlockOrEnvelopeError::BlockError(BlockError::DuplicateFullyImported(..)) = - *e => + if matches!( + *e, + BlockOrEnvelopeError::BlockError(BlockError::DuplicateFullyImported(..)) + ) => { debug!( %block_root, From bbffb80612c133b7908bf6b70d6c723871958735 Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Thu, 30 Apr 2026 01:33:27 +0200 Subject: [PATCH 070/118] Rewrite pending_payload_cache tests to use real public API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tests previously wrapped raw columns with `KzgVerifiedDataColumn::__new_for_testing` and `KzgVerifiedCustodyDataColumn::from_asserted_custody`, then called the internal `put_kzg_verified_custody_data_columns`. That bypassed KZG verification entirely and hid the fact that Gloas data column verification is not yet wired up (`verify_kzg_for_data_column` short-circuits because Gloas column sidecars don't carry kzg_commitments — they live in the bid). Drive `put_rpc_custody_columns` directly so the tests exercise real KZG verification. 9 of 12 tests now fail with `InconsistentArrayLength("Gloas data columns require commitments from block")`, which is the actual current state and should be fixed alongside the verifier work. --- .../src/pending_payload_cache/mod.rs | 115 ++++-------------- 1 file changed, 25 insertions(+), 90 deletions(-) diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs b/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs index 9b938046e2..b740212c9d 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs @@ -632,7 +632,6 @@ mod data_availability_checker_tests { use super::*; use crate::block_verification::PayloadVerificationOutcome; - use crate::data_column_verification::{KzgVerifiedCustodyDataColumn, KzgVerifiedDataColumn}; use crate::test_utils::{ NumBlobs, generate_data_column_indices_rand_order, generate_rand_block_and_data_columns, test_spec, @@ -749,10 +748,6 @@ mod data_availability_checker_tests { assert_eq!(cache.block_cache_size(), 0); } - // TODO(gloas): Add tests for `put_rpc_custody_columns` and `put_gossip_verified_data_columns` - // once the Gloas harness can produce KZG-valid columns. These wrappers add KZG verification - // and custody column filtering on top of `put_kzg_verified_custody_data_columns`. - fn make_test_signed_envelope(block_root: Hash256) -> Arc> { Arc::new(SignedExecutionPayloadEnvelope { message: ExecutionPayloadEnvelope { @@ -794,22 +789,14 @@ mod data_availability_checker_tests { &mut rng, &spec, ); - + let slot = block.slot(); let block_root = Hash256::random(); cache.init_pending_block(block_root, Arc::new(block)); - let verified_columns: Vec<_> = data_columns - .into_iter() - .take(1) - .map(|col| { - KzgVerifiedCustodyDataColumn::from_asserted_custody( - KzgVerifiedDataColumn::__new_for_testing(col), - ) - }) - .collect(); + let columns: DataColumnSidecarList = data_columns.into_iter().take(1).collect(); - let result = cache.put_kzg_verified_custody_data_columns(block_root, verified_columns); - assert!(result.is_ok()); + let result = cache.put_rpc_custody_columns(block_root, slot, columns); + assert!(result.is_ok(), "put_rpc_custody_columns failed: {result:?}"); assert_eq!(cache.block_cache_size(), 1); @@ -838,23 +825,19 @@ mod data_availability_checker_tests { &mut rng, &spec, ); - + let slot = block.slot(); let block_root = Hash256::random(); cache.init_pending_block(block_root, Arc::new(block)); let first_column = data_columns.first().cloned().expect("should have column"); let column_index = *first_column.index(); - let verified_column = KzgVerifiedCustodyDataColumn::from_asserted_custody( - KzgVerifiedDataColumn::__new_for_testing(first_column.clone()), - ); - cache - .put_kzg_verified_custody_data_columns(block_root, vec![verified_column.clone()]) + .put_rpc_custody_columns(block_root, slot, vec![first_column.clone()]) .expect("should put column"); cache - .put_kzg_verified_custody_data_columns(block_root, vec![verified_column]) + .put_rpc_custody_columns(block_root, slot, vec![first_column]) .expect("should put column again"); let cached_indices = cache.peek_pending_components(&block_root, |components| { @@ -884,21 +867,12 @@ mod data_availability_checker_tests { &mut rng, &spec, ); - + let slot = block.slot(); let block_root = Hash256::random(); cache.init_pending_block(block_root, Arc::new(block)); - let verified_columns: Vec<_> = data_columns - .into_iter() - .map(|col| { - KzgVerifiedCustodyDataColumn::from_asserted_custody( - KzgVerifiedDataColumn::__new_for_testing(col), - ) - }) - .collect(); - let result = cache - .put_kzg_verified_custody_data_columns(block_root, verified_columns) + .put_rpc_custody_columns(block_root, slot, data_columns) .expect("should put columns"); // Without an executed envelope, should still be missing components @@ -923,21 +897,12 @@ mod data_availability_checker_tests { &mut rng, &spec, ); - + let slot = block.slot(); let block_root = Hash256::random(); cache.init_pending_block(block_root, Arc::new(block)); - let verified_columns: Vec<_> = data_columns - .into_iter() - .map(|col| { - KzgVerifiedCustodyDataColumn::from_asserted_custody( - KzgVerifiedDataColumn::__new_for_testing(col), - ) - }) - .collect(); - let result = cache - .put_kzg_verified_custody_data_columns(block_root, verified_columns) + .put_rpc_custody_columns(block_root, slot, data_columns) .expect("should put columns"); assert!(matches!(result, Availability::MissingComponents(_))); @@ -1006,22 +971,14 @@ mod data_availability_checker_tests { &mut rng, &spec, ); - + let slot = block.slot(); let block_root = Hash256::random(); cache.init_pending_block(block_root, Arc::new(block)); - let verified_columns: Vec<_> = data_columns - .into_iter() - .take(5) - .map(|col| { - KzgVerifiedCustodyDataColumn::from_asserted_custody( - KzgVerifiedDataColumn::__new_for_testing(col), - ) - }) - .collect(); + let columns: DataColumnSidecarList = data_columns.into_iter().take(5).collect(); cache - .put_kzg_verified_custody_data_columns(block_root, verified_columns) + .put_rpc_custody_columns(block_root, slot, columns) .expect("should put columns"); let cached_count = cache.peek_pending_components(&block_root, |components| { @@ -1072,25 +1029,17 @@ mod data_availability_checker_tests { &mut rng, &spec, ); - + let slot = block.slot(); let block_root = Hash256::random(); assert!(cache.get_data_columns(block_root).is_none()); cache.init_pending_block(block_root, Arc::new(block)); - let verified_columns: Vec<_> = data_columns - .into_iter() - .take(3) - .map(|col| { - KzgVerifiedCustodyDataColumn::from_asserted_custody( - KzgVerifiedDataColumn::__new_for_testing(col), - ) - }) - .collect(); + let columns: DataColumnSidecarList = data_columns.into_iter().take(3).collect(); cache - .put_kzg_verified_custody_data_columns(block_root, verified_columns) + .put_rpc_custody_columns(block_root, slot, columns) .expect("should put columns"); let peeked = cache.get_data_columns(block_root); @@ -1116,20 +1065,17 @@ mod data_availability_checker_tests { &mut rng, &spec, ); - + let slot = block.slot(); let block = Arc::new(block); + let first_column = data_columns.first().cloned().expect("should have column"); let mut roots = Vec::new(); for _ in 0..33 { let block_root = Hash256::random(); roots.push(block_root); cache.init_pending_block(block_root, block.clone()); - let col = data_columns.first().cloned().expect("should have column"); - let verified = vec![KzgVerifiedCustodyDataColumn::from_asserted_custody( - KzgVerifiedDataColumn::__new_for_testing(col), - )]; cache - .put_kzg_verified_custody_data_columns(block_root, verified) + .put_rpc_custody_columns(block_root, slot, vec![first_column.clone()]) .expect("should put columns"); } @@ -1156,16 +1102,13 @@ mod data_availability_checker_tests { &mut rng, &spec, ); - + let slot = block.slot(); let block_root = Hash256::random(); cache.init_pending_block(block_root, Arc::new(block)); let col = data_columns.first().cloned().expect("should have column"); - let verified = vec![KzgVerifiedCustodyDataColumn::from_asserted_custody( - KzgVerifiedDataColumn::__new_for_testing(col), - )]; cache - .put_kzg_verified_custody_data_columns(block_root, verified) + .put_rpc_custody_columns(block_root, slot, vec![col]) .expect("should put columns"); assert_eq!(cache.block_cache_size(), 1); @@ -1196,7 +1139,7 @@ mod data_availability_checker_tests { &mut rng, &spec, ); - + let slot = block.slot(); let block_root = Hash256::random(); cache.init_pending_block(block_root, Arc::new(block)); @@ -1206,18 +1149,10 @@ mod data_availability_checker_tests { .expect("should put executed envelope"); // Insert only 1 column (need 128 for fullnode) - let verified_columns: Vec<_> = data_columns - .into_iter() - .take(1) - .map(|col| { - KzgVerifiedCustodyDataColumn::from_asserted_custody( - KzgVerifiedDataColumn::__new_for_testing(col), - ) - }) - .collect(); + let columns: DataColumnSidecarList = data_columns.into_iter().take(1).collect(); let result = cache - .put_kzg_verified_custody_data_columns(block_root, verified_columns) + .put_rpc_custody_columns(block_root, slot, columns) .expect("should put columns"); assert!( From ae17107f7839312065fbcc96a1065ff86cf0cb4b Mon Sep 17 00:00:00 2001 From: Daniel Knopik Date: Thu, 30 Apr 2026 09:16:58 +0200 Subject: [PATCH 071/118] fix test runs --- .../beacon_chain/src/data_availability_checker.rs | 5 +---- .../network/src/sync/block_sidecar_coupling.rs | 13 +++++++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index ef5544930e..928d7a1bad 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -878,10 +878,7 @@ impl AvailableBlock { match &block_data { AvailableBlockData::NoData => { // For Gloas, DA is checked for the PayloadEnvelope, not for the block. - if block.fork_name(&spec).map_err(|_| { - AvailabilityCheckError::Unexpected("Unexpected fork mismatch".to_string()) - })? < ForkName::Gloas - { + if block.fork_name_unchecked() < ForkName::Gloas { if columns_required { return Err(AvailabilityCheckError::MissingCustodyColumns); } else if blobs_required { diff --git a/beacon_node/network/src/sync/block_sidecar_coupling.rs b/beacon_node/network/src/sync/block_sidecar_coupling.rs index 98cf3e0a1f..6d96967fd0 100644 --- a/beacon_node/network/src/sync/block_sidecar_coupling.rs +++ b/beacon_node/network/src/sync/block_sidecar_coupling.rs @@ -549,12 +549,18 @@ mod tests { #[test] fn no_blobs_into_responses() { + let spec = Arc::new(test_spec::()); + let mut rng = XorShiftRng::from_seed([42; 16]); let blocks = (0..4) .map(|_| { - generate_rand_block_and_blobs::(ForkName::Base, NumBlobs::None, &mut rng) - .0 - .into() + generate_rand_block_and_blobs::( + spec.fork_name_at_epoch(Epoch::new(0)), + NumBlobs::None, + &mut rng, + ) + .0 + .into() }) .collect::>>>(); @@ -565,7 +571,6 @@ mod tests { // Send blocks and complete terminate response info.add_blocks(blocks_req_id, blocks).unwrap(); - let spec = Arc::new(test_spec::()); let da_checker = Arc::new(test_da_checker(spec.clone(), NodeCustodyType::Fullnode)); // Assert response is finished and RpcBlocks can be constructed From 95b95616c74c025e187548a069e334dabb1db594 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Thu, 30 Apr 2026 10:33:13 +0200 Subject: [PATCH 072/118] resolve conflicts --- .../beacon_chain/src/payload_envelope_verification/import.rs | 1 - .../beacon_chain/src/payload_envelope_verification/mod.rs | 1 - 2 files changed, 2 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 bb1f1b53c7..1845866e4a 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs @@ -192,7 +192,6 @@ impl BeaconChain { signed_envelope, import_data, payload_verification_outcome, - self.spec.clone(), )) } 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 5558cbc50c..9d61025e96 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs @@ -136,7 +136,6 @@ impl ExecutedEnvelope { envelope: MaybeAvailableEnvelope, import_data: EnvelopeImportData, payload_verification_outcome: PayloadVerificationOutcome, - spec: Arc, ) -> Self { match envelope { MaybeAvailableEnvelope::Available(available_envelope) => { From 666fcbd7c96f4b33d7c0c9ca4545bec2e6dd21db Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Thu, 30 Apr 2026 11:31:08 +0200 Subject: [PATCH 073/118] intro single_envelope_lookup.rs --- .../payload_envelope_verification/import.rs | 18 +++--- .../gossip_methods.rs | 7 ++- .../network/src/sync/block_lookups/mod.rs | 26 ++++++++ .../sync/block_lookups/single_block_lookup.rs | 32 +--------- .../block_lookups/single_envelope_lookup.rs | 62 +++++++++++++++++++ 5 files changed, 106 insertions(+), 39 deletions(-) create mode 100644 beacon_node/network/src/sync/block_lookups/single_envelope_lookup.rs 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 1845866e4a..f6cd7143a8 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs @@ -103,14 +103,18 @@ impl BeaconChain { ExecutedEnvelope::AvailabilityPending { signed_envelope, import_data, - payload_verification_outcome, + payload_verification_outcome: _, } => { - self.import_pending_execution_payload_envelope( - signed_envelope, - import_data, - payload_verification_outcome, - ) - .await + // The envelope has been executed but data columns have not yet arrived. + // Do not import — return MissingComponents so callers know to fetch columns. + // TODO(gloas): once an envelope DA checker exists, cache the envelope here + // (analogous to `data_availability_checker.put_executed_block`) so that + // import is driven automatically when columns arrive. + let slot = signed_envelope.slot(); + let block_root = import_data.block_root; + Ok(AvailabilityProcessingStatus::MissingComponents( + slot, block_root, + )) } } }; 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 1fd5195dd3..b0a6e51704 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -4007,8 +4007,11 @@ impl NetworkBeaconProcessor { block_root: *block_root, }); } - Ok(AvailabilityProcessingStatus::MissingComponents(_, _)) => { - // Nothing to do + Ok(AvailabilityProcessingStatus::MissingComponents(_slot, _block_root)) => { + // TODO(gloas): wire this into the envelope DA checker once it exists, analogous to + // how `process_availability` drives block import once blobs/columns arrive. Until + // then gossip envelopes with missing columns will be stuck until columns arrive via + // gossip or engineGetBlobs. } Err(e) => match e { EnvelopeError::ExecutionPayloadError(epe) if !epe.penalize_peer() => {} diff --git a/beacon_node/network/src/sync/block_lookups/mod.rs b/beacon_node/network/src/sync/block_lookups/mod.rs index 32fcefc501..bb003dc222 100644 --- a/beacon_node/network/src/sync/block_lookups/mod.rs +++ b/beacon_node/network/src/sync/block_lookups/mod.rs @@ -54,6 +54,7 @@ use types::{EthSpec, SignedBeaconBlock}; pub mod common; pub mod parent_chain; mod single_block_lookup; +mod single_envelope_lookup; /// The maximum depth we will search for a parent block. In principle we should have sync'd any /// canonical chain to its head once the peer connects. A chain should not appear where it's depth @@ -645,6 +646,31 @@ impl BlockLookups { self.on_processing_result_inner::>(id, result, cx) } BlockProcessType::SinglePayloadEnvelope { id, block_root } => { + // When envelope processing returns `MissingComponents`, the envelope has been + // executed but data columns are not yet available. Transition the lookup to fetch + // custody columns instead of retrying the envelope or erroring. + if matches!( + &result, + BlockProcessingResult::Ok( + AvailabilityProcessingStatus::MissingComponents { .. } + ) + ) && let Some(lookup) = self.single_block_lookups.get_mut(&id) + && lookup.transition_envelope_to_custody() + { + debug!( + ?block_root, + "Envelope processed, transitioning to custody column lookup" + ); + let lookup_result = lookup.continue_requests(cx); + self.on_lookup_result( + id, + lookup_result, + "envelope_to_custody_transition", + cx, + ); + return; + } + let result = self .on_processing_result_inner::>(id, result, cx); // On successful envelope import, unblock child lookups waiting for this envelope diff --git a/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs b/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs index 3277ad9687..cdcb574219 100644 --- a/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs +++ b/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs @@ -78,8 +78,8 @@ pub struct SingleBlockLookup { /// than the lifetime of a custody request. #[educe(Debug(method(fmt_peer_set_as_len)))] peers: Arc>>, - block_root: Hash256, - awaiting_parent: Option, + pub(super) block_root: Hash256, + pub(super) awaiting_parent: Option, created: Instant, pub(crate) span: Span, } @@ -120,21 +120,6 @@ impl SingleBlockLookup { } } - /// Create an envelope-only lookup. The block is already imported, we just need the envelope. - pub fn new_envelope_only(block_root: Hash256, peers: &[PeerId], id: Id) -> Self { - let mut lookup = Self::new(block_root, peers, id, None); - // Block is already imported, mark as completed - lookup - .block_request_state - .state - .on_completed_request("block already imported") - .expect("block state starts as AwaitingDownload"); - lookup.component_requests = - ComponentRequests::ActiveEnvelopeRequest(EnvelopeRequestState::new(block_root)); - lookup - } - - /// Reset the status of all internal requests pub fn reset_requests(&mut self) { self.block_request_state = BlockRequestState::new(self.block_root); match &self.component_requests { @@ -174,24 +159,11 @@ impl SingleBlockLookup { } } - /// Returns the parent root if awaiting a parent envelope. - pub fn awaiting_parent_envelope(&self) -> Option { - match self.awaiting_parent { - Some(AwaitingParent::Envelope(root)) => Some(root), - _ => None, - } - } - /// Mark this lookup as awaiting a parent block to be imported before processing. pub fn set_awaiting_parent(&mut self, parent_root: Hash256) { self.awaiting_parent = Some(AwaitingParent::Block(parent_root)); } - /// Mark this lookup as awaiting a parent envelope to be imported before processing. - pub fn set_awaiting_parent_envelope(&mut self, parent_root: Hash256) { - self.awaiting_parent = Some(AwaitingParent::Envelope(parent_root)); - } - /// Mark this lookup as no longer awaiting any parent. pub fn resolve_awaiting_parent(&mut self) { self.awaiting_parent = None; diff --git a/beacon_node/network/src/sync/block_lookups/single_envelope_lookup.rs b/beacon_node/network/src/sync/block_lookups/single_envelope_lookup.rs new file mode 100644 index 0000000000..88fa042439 --- /dev/null +++ b/beacon_node/network/src/sync/block_lookups/single_envelope_lookup.rs @@ -0,0 +1,62 @@ +//! Envelope-specific extensions to `SingleBlockLookup`. +//! +//! Envelope-only lookups are created when a block's parent is known and imported but its +//! execution payload envelope has not yet been received. The block download step is skipped +//! (marked complete immediately), and only the envelope — and possibly subsequent custody +//! columns — are fetched. + +use super::single_block_lookup::{ + AwaitingParent, ComponentRequests, CustodyRequestState, EnvelopeRequestState, SingleBlockLookup, +}; +use beacon_chain::BeaconChainTypes; +use lighthouse_network::PeerId; +use lighthouse_network::service::api_types::Id; +use store::Hash256; + +impl SingleBlockLookup { + /// Create an envelope-only lookup. The block is already imported; only the envelope (and + /// potentially custody columns) need to be fetched. + pub fn new_envelope_only(block_root: Hash256, peers: &[PeerId], id: Id) -> Self { + let mut lookup = Self::new(block_root, peers, id, None); + // Block is already imported — advance past the download step immediately. + lookup + .block_request_state + .state + .on_completed_request("block already imported") + .expect("block state starts as AwaitingDownload"); + lookup.component_requests = + ComponentRequests::ActiveEnvelopeRequest(EnvelopeRequestState::new(block_root)); + lookup + } + + /// Transition from `ActiveEnvelopeRequest` to `ActiveCustodyRequest`. + /// + /// Called when envelope processing returns `MissingComponents`: the envelope has been executed + /// but data columns have not yet arrived and must be fetched separately. + /// Returns `true` if the transition was made, `false` if state was not an envelope request. + pub fn transition_envelope_to_custody(&mut self) -> bool { + if matches!( + self.component_requests, + ComponentRequests::ActiveEnvelopeRequest(_) + ) { + self.component_requests = + ComponentRequests::ActiveCustodyRequest(CustodyRequestState::new(self.block_root)); + true + } else { + false + } + } + + /// Returns the parent root if this lookup is awaiting a parent envelope. + pub fn awaiting_parent_envelope(&self) -> Option { + match self.awaiting_parent { + Some(AwaitingParent::Envelope(root)) => Some(root), + _ => None, + } + } + + /// Mark this lookup as awaiting a parent envelope before processing can resume. + pub fn set_awaiting_parent_envelope(&mut self, parent_root: Hash256) { + self.awaiting_parent = Some(AwaitingParent::Envelope(parent_root)); + } +} From bd8cfa35f4a07379182551838df2ac114f3ec52e Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Thu, 30 Apr 2026 12:36:36 +0200 Subject: [PATCH 074/118] Refine Gloas data column availability --- beacon_node/beacon_chain/src/beacon_chain.rs | 86 ++- .../src/data_column_verification.rs | 308 ++++++++-- beacon_node/beacon_chain/src/kzg_utils.rs | 51 ++ .../payload_envelope_verification/import.rs | 6 +- .../src/pending_payload_cache/mod.rs | 575 +++++------------- .../pending_components.rs | 96 +-- .../gossip_methods.rs | 58 +- .../src/network_beacon_processor/tests.rs | 10 +- beacon_node/network/src/sync/manager.rs | 4 +- 9 files changed, 604 insertions(+), 590 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index d276fbc15e..8456bbbc02 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -27,7 +27,7 @@ use crate::data_availability_checker::DataAvailabilityChecker; use crate::data_column_verification::{ GossipDataColumnError, GossipPartialDataColumnError, GossipVerifiedDataColumn, GossipVerifiedPartialDataColumnHeader, KzgVerifiedCustodyPartialDataColumn, - KzgVerifiedPartialDataColumn, PartialColumnVerificationResult, + KzgVerifiedPartialDataColumn, PartialColumnVerificationResult, load_gloas_payload_bid, validate_partial_data_column_sidecar_for_gossip, }; use crate::early_attester_cache::EarlyAttesterCache; @@ -71,7 +71,7 @@ use crate::payload_envelope_verification::EnvelopeError; use crate::pending_payload_cache::PendingPayloadCache; use crate::pending_payload_cache::{ Availability as PayloadAvailability, - DataColumnReconstructionResult as DataColumnReconstructionResultGloas, + DataColumnReconstructionResult as DataColumnReconstructionResultGloas, PendingPayloadBid, }; use crate::pending_payload_envelopes::PendingPayloadEnvelopes; use crate::persisted_beacon_chain::PersistedBeaconChain; @@ -3317,12 +3317,19 @@ impl BeaconChain { .into()); }; - // If this block has already been imported to forkchoice it must have been available, so - // we don't need to process its samples again. - if self - .canonical_head - .fork_choice_read_lock() - .contains_block(&block_root) + let is_gloas = self + .spec + .fork_name_at_slot::(slot) + .gloas_enabled(); + + // Before Gloas, if this block has already been imported to fork choice it must have been + // available, so we don't need to process its samples again. In Gloas the beacon block is + // imported before the payload envelope and data columns, so this check does not apply. + if !is_gloas + && self + .canonical_head + .fork_choice_read_lock() + .contains_block(&block_root) { return Err(BlockError::DuplicateFullyImported(block_root).into()); } @@ -3419,10 +3426,16 @@ impl BeaconChain { .fork_name_at_slot::(slot) .gloas_enabled() { + let Some(bid) = + load_gloas_payload_bid(block_root, self).map_err(EnvelopeError::from)? + else { + return Err(EnvelopeError::BlockRootUnknown { block_root }.into()); + }; let availability = self .pending_payload_cache .put_kzg_verified_custody_data_columns( block_root, + bid, merge_result.full_columns.clone(), ) .map_err(EnvelopeError::from)?; @@ -3631,8 +3644,9 @@ impl BeaconChain { custody_columns.iter().map(|column| column.as_ref()), ); - self.check_rpc_custody_columns_availability_and_import(slot, block_root, custody_columns) - .await + Ok(self + .check_rpc_custody_columns_availability_and_import(slot, block_root, custody_columns) + .await?) } pub async fn reconstruct_data_columns( @@ -3663,11 +3677,16 @@ impl BeaconChain { .gloas_enabled(); if is_gloas { + let Some(bid) = + load_gloas_payload_bid(block_root, self).map_err(EnvelopeError::from)? + else { + return Err(EnvelopeError::BlockRootUnknown { block_root }.into()); + }; let pending_payload_cache = self.pending_payload_cache.clone(); let result = self .task_executor .spawn_blocking_with_rayon_async(RayonPoolType::HighPriority, move || { - pending_payload_cache.reconstruct_data_columns(&block_root) + pending_payload_cache.reconstruct_data_columns(&block_root, bid) }) .await .map_err(|_| EnvelopeError::from(BeaconChainError::RuntimeShutdown))? @@ -3806,6 +3825,15 @@ impl BeaconChain { &chain, notify_execution_layer, )?; + + let block = execution_pending.block.block_cloned(); + if block.fork_name_unchecked().gloas_enabled() { + let bid = PendingPayloadBid::from_block(block.as_ref())?; + chain + .pending_payload_cache + .init_pending_bid(block_root, bid); + } + publish_fn()?; // Record the time it took to complete consensus verification. @@ -3979,9 +4007,16 @@ impl BeaconChain { .fork_name_at_slot::(slot) .gloas_enabled() { + let Some(bid) = + load_gloas_payload_bid(block_root, self).map_err(EnvelopeError::from)? + else { + return Ok(AvailabilityProcessingStatus::MissingComponents( + slot, block_root, + )); + }; let availability = self .pending_payload_cache - .put_gossip_verified_data_columns(block_root, slot, data_columns) + .put_gossip_verified_data_columns(block_root, bid, data_columns) .map_err(EnvelopeError::from)?; Ok(self .process_payload_availability(slot, availability, publish_fn) @@ -4085,9 +4120,16 @@ impl BeaconChain { .fork_name_at_slot::(slot) .gloas_enabled() { + let Some(bid) = + load_gloas_payload_bid(block_root, self).map_err(EnvelopeError::from)? + else { + return Ok(AvailabilityProcessingStatus::MissingComponents( + slot, block_root, + )); + }; let availability = self .pending_payload_cache - .put_kzg_verified_custody_data_columns(block_root, data_columns) + .put_kzg_verified_custody_data_columns(block_root, bid, data_columns) .map_err(EnvelopeError::from)?; Ok(self .process_payload_availability(slot, availability, || Ok(())) @@ -4112,7 +4154,7 @@ impl BeaconChain { slot: Slot, block_root: Hash256, custody_columns: DataColumnSidecarList, - ) -> Result { + ) -> Result { // TODO(gloas) ensure that this check is no longer relevant post gloas self.check_data_column_sidecar_header_signature_and_slashability( block_root, @@ -4127,13 +4169,23 @@ impl BeaconChain { .fork_name_at_slot::(slot) .gloas_enabled() { + let Some(bid) = load_gloas_payload_bid(block_root, self).map_err(BlockError::from)? + else { + return Ok(AvailabilityProcessingStatus::MissingComponents( + slot, block_root, + )); + }; let availability = self .pending_payload_cache - .put_rpc_custody_columns(block_root, slot, custody_columns) - .map_err(EnvelopeError::from)?; + .put_rpc_custody_columns(block_root, bid, custody_columns) + .map_err(BlockError::from)?; Ok(self .process_payload_availability(slot, availability, || Ok(())) - .await?) + .await + .map_err(|error| match error { + EnvelopeError::BlockError(error) => error, + error => BlockError::InternalError(error.to_string()), + })?) } else { let availability = self .data_availability_checker diff --git a/beacon_node/beacon_chain/src/data_column_verification.rs b/beacon_node/beacon_chain/src/data_column_verification.rs index b420965024..e342a05798 100644 --- a/beacon_node/beacon_chain/src/data_column_verification.rs +++ b/beacon_node/beacon_chain/src/data_column_verification.rs @@ -3,11 +3,13 @@ use crate::block_verification::{ }; use crate::data_availability_checker::MissingCellsError; use crate::kzg_utils::{ - reconstruct_data_columns, validate_full_data_columns, validate_partial_data_columns, + reconstruct_data_columns, validate_full_data_columns, + validate_full_data_columns_with_commitments, validate_partial_data_columns, }; use crate::observed_data_sidecars::{ Error as ObservedDataSidecarsError, ObservationKey, ObservationStrategy, Observe, }; +use crate::pending_payload_cache::PendingPayloadBid; use crate::{BeaconChain, BeaconChainError, BeaconChainTypes, metrics}; use educe::Educe; use fork_choice::ProtoBlock; @@ -20,6 +22,7 @@ use std::iter; use std::marker::PhantomData; use std::sync::Arc; use std::time::Duration; +use store::DatabaseBlock; use tracing::{debug, instrument}; use tree_hash::TreeHash; use types::data::{ @@ -27,8 +30,9 @@ use types::data::{ PartialDataColumnSidecarError, }; use types::{ - BeaconStateError, ChainSpec, DataColumnSidecar, DataColumnSidecarFulu, DataColumnSubnetId, - EthSpec, Hash256, PartialDataColumnSidecarRef, SignedBeaconBlockHeader, Slot, + BeaconStateError, ChainSpec, DataColumnSidecar, DataColumnSidecarFulu, DataColumnSidecarGloas, + DataColumnSubnetId, EthSpec, Hash256, KzgCommitment, PartialDataColumnSidecarRef, + SignedBeaconBlockHeader, Slot, }; /// An error occurred while validating a gossip data column. @@ -131,6 +135,24 @@ pub enum GossipDataColumnError { parent_root: Hash256, slot: Slot, }, + /// The block referenced by the data column is unknown. + /// + /// ## Peer scoring + /// + /// We cannot process the column without the referenced block, the peer isn't necessarily faulty. + BlockRootUnknown { + block_root: Hash256, + slot: Slot, + }, + /// The data column slot does not match its referenced block slot. + /// + /// ## Peer scoring + /// + /// The column sidecar is invalid and the peer is faulty. + BlockSlotMismatch { + block_slot: Slot, + data_column_slot: Slot, + }, /// The column conflicts with finalization, no need to propagate. /// /// ## Peer scoring @@ -307,21 +329,32 @@ impl GossipVerifiedDataColumn let header = c.signed_block_header.clone(); // We only process slashing info if the gossip verification failed // since we do not process the data column any further in that case. - validate_data_column_sidecar_for_gossip_fulu::( - column_sidecar, + validate_data_column_sidecar_for_gossip_fulu::(c, subnet_id, chain).map_err( + |e| { + process_block_slash_info::<_, GossipDataColumnError>( + chain, + BlockSlashInfo::from_early_error_data_column(header, e), + ) + }, + )?; + } + DataColumnSidecar::Gloas(data_column_gloas) => { + validate_data_column_sidecar_for_gossip_gloas::( + data_column_gloas, subnet_id, chain, - ) - .map_err(|e| { - process_block_slash_info::<_, GossipDataColumnError>( - chain, - BlockSlashInfo::from_early_error_data_column(header, e), - ) - }) + )?; } - // TODO(gloas) support gloas data column variant - DataColumnSidecar::Gloas(_) => Err(GossipDataColumnError::InvalidVariant), } + + Ok(GossipVerifiedDataColumn { + block_root: column_sidecar.block_root(), + data_column: KzgVerifiedDataColumn { + data: column_sidecar, + seen_timestamp: chain.slot_clock.now_duration().unwrap_or_default(), + }, + _phantom: PhantomData, + }) } /// Create a `GossipVerifiedDataColumn` from `DataColumnSidecar` for block production ONLY. @@ -331,7 +364,28 @@ impl GossipVerifiedDataColumn column_sidecar: Arc>, chain: &BeaconChain, ) -> Result { - verify_data_column_sidecar(&column_sidecar, &chain.spec)?; + match column_sidecar.as_ref() { + DataColumnSidecar::Fulu(data_column_fulu) => { + verify_data_column_sidecar_with_commitments_len( + &column_sidecar, + data_column_fulu.kzg_commitments.len(), + &chain.spec, + )?; + } + DataColumnSidecar::Gloas(_) => { + let kzg_commitments = load_gloas_payload_bid(column_sidecar.block_root(), chain)? + .ok_or(GossipDataColumnError::BlockRootUnknown { + block_root: column_sidecar.block_root(), + slot: column_sidecar.slot(), + })? + .blob_kzg_commitments; + verify_data_column_sidecar_with_commitments_len( + &column_sidecar, + kzg_commitments.len(), + &chain.spec, + )?; + } + } // Check if the data column is already in the DA checker cache. This happens when data columns // are made available through the `engine_getBlobs` method. If it exists in the cache, we know @@ -340,28 +394,19 @@ impl GossipVerifiedDataColumn // In this case, we should accept it for gossip propagation. verify_is_unknown_sidecar(chain, &column_sidecar)?; - match chain - .data_availability_checker - .missing_cells_for_column_sidecar(&column_sidecar) - { - Ok(Some(_)) => Ok(Self { + match missing_cells_for_column_sidecar(chain, &column_sidecar)? { + Some(_) => Ok(Self { block_root: column_sidecar.block_root(), data_column: KzgVerifiedDataColumn::from_execution_verified(column_sidecar), _phantom: Default::default(), }), - Ok(None) => { + None => { // Observe this data column so we don't process it again. if O::observe() { observe_gossip_data_column(&column_sidecar, chain)?; } Err(GossipDataColumnError::PriorKnownUnpublished) } - Err(MissingCellsError::MismatchesCachedColumn) => { - Err(GossipDataColumnError::MismatchesCachedColumn) - } - Err(MissingCellsError::UnexpectedError(_)) => { - todo!("handle unexpected error") - } } } @@ -440,6 +485,22 @@ impl KzgVerifiedDataColumn { .collect()) } + pub fn from_batch_with_scoring_and_commitments( + data_columns: Vec>>, + kzg_commitments: &[KzgCommitment], + kzg: &Kzg, + ) -> Result, (Option, KzgError)> { + let _timer = metrics::start_timer(&metrics::KZG_VERIFICATION_DATA_COLUMN_BATCH_TIMES); + validate_full_data_columns_with_commitments(kzg, data_columns.iter(), kzg_commitments)?; + Ok(data_columns + .into_iter() + .map(|column| Self { + data: column, + seen_timestamp: timestamp_now(), + }) + .collect()) + } + pub fn to_data_column(self) -> Arc> { self.data } @@ -854,6 +915,26 @@ pub fn verify_kzg_for_data_column( }) } +#[instrument(skip_all, level = "debug")] +pub fn verify_kzg_for_data_column_with_commitments( + data_column: Arc>, + cells_to_verify: PartialDataColumnSidecarRef, + kzg_commitments: &[KzgCommitment], + kzg: &Kzg, + seen_timestamp: Duration, +) -> Result, (Option, KzgError)> { + let _timer = metrics::start_timer(&metrics::KZG_VERIFICATION_DATA_COLUMN_SINGLE_TIMES); + validate_partial_data_columns( + kzg, + iter::once((*data_column.index(), cells_to_verify)), + kzg_commitments, + )?; + Ok(KzgVerifiedDataColumn { + data: data_column, + seen_timestamp, + }) +} + /// Complete kzg verification for a `VerifiablePartialDataColumn`. /// /// Returns an error if the kzg verification check fails. @@ -901,16 +982,18 @@ where level = "debug" )] pub fn validate_data_column_sidecar_for_gossip_fulu( - data_column: Arc>, + data_column_fulu: &DataColumnSidecarFulu, subnet: DataColumnSubnetId, chain: &BeaconChain, -) -> Result, GossipDataColumnError> { - let DataColumnSidecar::Fulu(data_column_fulu) = data_column.as_ref() else { - return Err(GossipDataColumnError::InvalidVariant); - }; - +) -> Result<(), GossipDataColumnError> { + let data_column = Arc::new(DataColumnSidecar::Fulu(data_column_fulu.clone())); let column_slot = data_column.slot(); - verify_data_column_sidecar(&data_column, &chain.spec)?; + + verify_data_column_sidecar_with_commitments_len( + &data_column, + data_column_fulu.kzg_commitments.len(), + &chain.spec, + )?; verify_index_matches_subnet(&data_column, subnet, &chain.spec)?; verify_sidecar_not_from_future_slot(chain, column_slot)?; verify_slot_greater_than_latest_finalized_slot(chain, column_slot)?; @@ -947,13 +1030,12 @@ pub fn validate_data_column_sidecar_for_gossip_fulu( + data_column_gloas: &DataColumnSidecarGloas, + subnet: DataColumnSubnetId, + chain: &BeaconChain, +) -> Result<(), GossipDataColumnError> { + let data_column = Arc::new(DataColumnSidecar::Gloas(data_column_gloas.clone())); + let column_slot = data_column.slot(); + + if *data_column.index() >= T::EthSpec::number_of_columns() as u64 { + return Err(GossipDataColumnError::InvalidColumnIndex( + *data_column.index(), + )); + } + + if !chain + .spec + .fork_name_at_slot::(column_slot) + .gloas_enabled() + { + return Err(GossipDataColumnError::InvalidVariant); + } + + verify_index_matches_subnet(&data_column, subnet, &chain.spec)?; + verify_sidecar_not_from_future_slot(chain, column_slot)?; + verify_slot_greater_than_latest_finalized_slot(chain, column_slot)?; + verify_is_unknown_sidecar(chain, &data_column)?; + + let kzg_commitments = load_gloas_payload_bid(data_column.block_root(), chain)? + .ok_or(GossipDataColumnError::BlockRootUnknown { + block_root: data_column.block_root(), + slot: column_slot, + })? + .blob_kzg_commitments; + verify_data_column_sidecar_with_commitments_len( + &data_column, + kzg_commitments.len(), + &chain.spec, + )?; + + let Some(cells_to_kzg_verify) = missing_cells_for_column_sidecar(chain, &data_column)? else { + // Observe this data column so we don't process it again. + if O::observe() { + observe_gossip_data_column(&data_column, chain)?; + } + return Err(GossipDataColumnError::PriorKnownUnpublished); + }; + + let kzg = &chain.kzg; + let seen_timestamp = chain.slot_clock.now_duration().unwrap_or_default(); + verify_kzg_for_data_column_with_commitments( + data_column.clone(), + cells_to_kzg_verify, + kzg_commitments.as_ref(), + kzg, + seen_timestamp, + ) + .map_err(|(_, e)| GossipDataColumnError::InvalidKzgProof(e))?; + + if O::observe() { + observe_gossip_data_column(&data_column, chain)?; + } + + Ok(()) } #[instrument(skip_all, level = "debug")] @@ -1109,9 +1260,9 @@ pub enum PartialColumnVerificationResult { Err(GossipPartialDataColumnError), } -/// Verify if the data column sidecar is valid. -fn verify_data_column_sidecar( +fn verify_data_column_sidecar_with_commitments_len( data_column: &DataColumnSidecar, + commitments_len: usize, spec: &ChainSpec, ) -> Result<(), GossipDataColumnError> { if *data_column.index() >= E::number_of_columns() as u64 { @@ -1120,12 +1271,6 @@ fn verify_data_column_sidecar( )); } - // TODO(gloas): implement Gloas verification that takes kzg_commitments from block as parameter - let commitments_len = match data_column { - DataColumnSidecar::Fulu(dc) => dc.kzg_commitments.len(), - DataColumnSidecar::Gloas(_) => return Err(GossipDataColumnError::InvalidVariant), - }; - if commitments_len == 0 { return Err(GossipDataColumnError::UnexpectedDataColumn); } @@ -1158,6 +1303,63 @@ fn verify_data_column_sidecar( Ok(()) } +pub(crate) fn load_gloas_payload_bid( + block_root: Hash256, + chain: &BeaconChain, +) -> Result>, BeaconChainError> { + let bid = if let Some(bid) = chain.pending_payload_cache.get_bid(&block_root) { + bid + } else if let Some(block) = chain.early_attester_cache.get_block(block_root) { + PendingPayloadBid::from_block(block.as_ref()).map_err(BeaconChainError::BeaconStateError)? + } else { + match chain + .store + .try_get_full_block(&block_root) + .map_err(BeaconChainError::DBError)? + { + Some(DatabaseBlock::Full(block)) => { + PendingPayloadBid::from_block(&block).map_err(BeaconChainError::BeaconStateError)? + } + Some(DatabaseBlock::Blinded(block)) => { + PendingPayloadBid::from_block(&block).map_err(BeaconChainError::BeaconStateError)? + } + None => { + return Ok(None); + } + } + }; + + chain + .pending_payload_cache + .init_pending_bid(block_root, bid.clone()); + + Ok(Some(bid)) +} + +fn missing_cells_for_column_sidecar<'a, T: BeaconChainTypes>( + chain: &'_ BeaconChain, + data_column: &'a DataColumnSidecar, +) -> Result>, GossipDataColumnError> { + let result = if chain + .spec + .fork_name_at_slot::(data_column.slot()) + .gloas_enabled() + { + chain + .pending_payload_cache + .missing_cells_for_column_sidecar(data_column) + } else { + chain + .data_availability_checker + .missing_cells_for_column_sidecar(data_column) + }; + + result.map_err(|err| match err { + MissingCellsError::MismatchesCachedColumn => GossipDataColumnError::MismatchesCachedColumn, + MissingCellsError::UnexpectedError(_) => todo!("handle unexpected error"), + }) +} + /// Verify that `column_sidecar` is not yet known, i.e. this is the first time `column_sidecar` has been received for the tuple: /// `(block_header.slot, block_header.proposer_index, column_sidecar.index)` fn verify_is_unknown_sidecar( @@ -1441,7 +1643,7 @@ mod test { let verify_fn = |column_sidecar: DataColumnSidecar| { let col_index = *column_sidecar.index(); validate_data_column_sidecar_for_gossip_fulu::<_, Observe>( - column_sidecar.into(), + column_sidecar.as_fulu().unwrap(), DataColumnSubnetId::from_column_index(col_index, &harness.spec), &harness.chain, ) diff --git a/beacon_node/beacon_chain/src/kzg_utils.rs b/beacon_node/beacon_chain/src/kzg_utils.rs index b05a896777..5ad1cc115d 100644 --- a/beacon_node/beacon_chain/src/kzg_utils.rs +++ b/beacon_node/beacon_chain/src/kzg_utils.rs @@ -111,6 +111,57 @@ pub fn validate_full_data_columns<'a, E: EthSpec>( kzg.verify_cell_proof_batch(&cells, &proofs, column_indices, &commitments) } +/// Validate a batch of full `DataColumnSidecar`s against commitments supplied out-of-band. +/// +/// Gloas sidecars do not carry commitments. Their commitments come from the block's +/// `ExecutionPayloadBid`. +pub fn validate_full_data_columns_with_commitments<'a, E: EthSpec>( + kzg: &Kzg, + data_column_iter: impl Iterator>>, + kzg_commitments: &[KzgCommitment], +) -> Result<(), (Option, KzgError)> { + let mut cells = Vec::new(); + let mut proofs = Vec::new(); + let mut column_indices = Vec::new(); + let mut commitments = Vec::new(); + + for data_column in data_column_iter { + let col_index = *data_column.index(); + + if data_column.column().is_empty() { + return Err((Some(col_index), KzgError::KzgVerificationFailed)); + } + + for cell in data_column.column() { + cells.push(ssz_cell_to_crypto_cell::(cell).map_err(|e| (Some(col_index), e))?); + column_indices.push(col_index); + } + + for &proof in data_column.kzg_proofs() { + proofs.push(proof.0); + } + + for &commitment in kzg_commitments { + commitments.push(commitment.0); + } + + let expected_len = column_indices.len(); + + // We make this check at each iteration so that the error is attributable to a specific column. + if cells.len() != expected_len + || proofs.len() != expected_len + || commitments.len() != expected_len + { + return Err(( + Some(col_index), + KzgError::InconsistentArrayLength("Invalid data column".to_string()), + )); + } + } + + kzg.verify_cell_proof_batch(&cells, &proofs, column_indices, &commitments) +} + /// Validate a batch of partial `VerifiablePartialDataColumn`s. /// /// Partial columns may have missing cells, indicated by a bitmap. We only verify present cells. 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 beabe0e76c..8b7c7eb4d7 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs @@ -12,6 +12,7 @@ use super::{ AvailableEnvelope, AvailableExecutedEnvelope, EnvelopeError, gossip_verified_envelope::GossipVerifiedEnvelope, }; +use crate::data_column_verification::load_gloas_payload_bid; use crate::pending_payload_cache::Availability as PayloadAvailability; use crate::{ AvailabilityProcessingStatus, BeaconChain, BeaconChainError, BeaconChainTypes, @@ -168,9 +169,12 @@ impl BeaconChain { envelope: AvailabilityPendingExecutedEnvelope, ) -> Result { let slot = envelope.envelope.slot(); + let block_root = envelope.block_root; + let bid = load_gloas_payload_bid(block_root, self)? + .ok_or(EnvelopeError::BlockRootUnknown { block_root })?; let availability = self .pending_payload_cache - .put_executed_payload_envelope(envelope)?; + .put_executed_payload_envelope(bid, envelope)?; self.process_payload_envelope_availability(slot, availability, || Ok(())) .await } diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs b/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs index b740212c9d..21a86f05cc 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs @@ -55,13 +55,12 @@ use task_executor::TaskExecutor; use tracing::{Span, debug, error, instrument, trace}; use types::{ ChainSpec, ColumnIndex, DataColumnSidecar, DataColumnSidecarList, Epoch, EthSpec, Hash256, - PartialDataColumnSidecarRef, SignedBeaconBlock, Slot, + PartialDataColumnSidecarRef, }; mod pending_column; mod pending_components; -use crate::block_verification_types::AsBlock; use crate::data_column_verification::{ GossipVerifiedDataColumn, KzgVerifiedCustodyDataColumn, KzgVerifiedDataColumn, }; @@ -69,6 +68,7 @@ use crate::metrics::{ KZG_DATA_COLUMN_RECONSTRUCTION_ATTEMPTS, KZG_DATA_COLUMN_RECONSTRUCTION_FAILURES, }; use crate::observed_data_sidecars::ObservationStrategy; +pub(crate) use pending_components::PendingPayloadBid; use pending_components::{PendingComponents, ReconstructColumnsDecision}; use types::new_non_zero_usize; @@ -153,19 +153,7 @@ impl PendingPayloadCache { block_root: Hash256, ) -> Option> { self.peek_pending_components(&block_root, |components| { - components.map(|c| { - c.verified_data_columns - .iter() - .filter_map(|(col_idx, col)| { - col.try_to_sidecar( - *col_idx, - c.block.slot(), - block_root, - c.num_blobs_expected(), - ) - }) - .collect() - }) + components.map(|c| c.get_cached_data_columns(block_root)) }) } @@ -177,6 +165,13 @@ impl PendingPayloadCache { }) } + /// Return the cached Gloas payload bid metadata for `block_root`, if present. + pub fn get_bid(&self, block_root: &Hash256) -> Option> { + self.peek_pending_components(block_root, |components| { + components.map(|components| components.bid.clone()) + }) + } + /// Filter out cells that are already cached for the given column sidecar. /// Returns the cells that still need KZG verification, or `None` if all cells are cached. #[instrument(skip_all, level = "trace")] @@ -206,12 +201,13 @@ impl PendingPayloadCache { /// Insert an executed payload envelope into the cache and performs an availability check pub fn put_executed_payload_envelope( &self, + bid: PendingPayloadBid, executed_envelope: AvailabilityPendingExecutedEnvelope, ) -> Result, AvailabilityCheckError> { let epoch = executed_envelope.envelope.epoch(); let beacon_block_root = executed_envelope.envelope.beacon_block_root(); let pending_components = - self.get_pending_components(beacon_block_root, |pending_components| { + self.get_pending_components(beacon_block_root, bid, |pending_components| { pending_components.insert_executed_payload_envelope(executed_envelope); Ok(()) })?; @@ -229,16 +225,10 @@ impl PendingPayloadCache { self.check_availability(beacon_block_root, pending_components, num_expected_columns) } - /// Initialize pending components for a block. Called when the beacon block (containing the - /// bid) arrives. Sets up the slot and expected blob count so that subsequent column insertions - /// know how many cells to expect per column. - pub fn init_pending_block( - &self, - block_root: Hash256, - block: Arc>, - ) { + /// Initialize pending components for a block's Gloas bid. + pub fn init_pending_bid(&self, block_root: Hash256, bid: PendingPayloadBid) { let mut write_lock = self.availability_cache.write(); - write_lock.get_or_insert_mut(block_root, || PendingComponents::empty(block_root, block)); + write_lock.get_or_insert_mut(block_root, || PendingComponents::empty(block_root, bid)); } /// Perform KZG verification on RPC custody columns and insert them into the cache. @@ -247,14 +237,17 @@ impl PendingPayloadCache { pub fn put_rpc_custody_columns( &self, block_root: Hash256, - slot: Slot, + bid: PendingPayloadBid, custody_columns: DataColumnSidecarList, ) -> Result, AvailabilityCheckError> { - let kzg_verified_columns = - KzgVerifiedDataColumn::from_batch_with_scoring(custody_columns, &self.kzg) - .map_err(AvailabilityCheckError::InvalidColumn)?; + let kzg_verified_columns = KzgVerifiedDataColumn::from_batch_with_scoring_and_commitments( + custody_columns, + bid.blob_kzg_commitments.as_ref(), + &self.kzg, + ) + .map_err(AvailabilityCheckError::InvalidColumn)?; - let epoch = slot.epoch(T::EthSpec::slots_per_epoch()); + let epoch = bid.slot.epoch(T::EthSpec::slots_per_epoch()); let sampling_columns = self .custody_context .sampling_columns_for_epoch(epoch, &self.spec); @@ -264,7 +257,7 @@ impl PendingPayloadCache { .map(KzgVerifiedCustodyDataColumn::from_asserted_custody) .collect::>(); - self.put_kzg_verified_custody_data_columns(block_root, verified_custody_columns) + self.put_kzg_verified_custody_data_columns(block_root, bid, verified_custody_columns) } /// Perform KZG verification on gossip verified custody columns and insert them into the cache. @@ -273,10 +266,10 @@ impl PendingPayloadCache { pub fn put_gossip_verified_data_columns( &self, block_root: Hash256, - slot: Slot, + bid: PendingPayloadBid, data_columns: Vec>, ) -> Result, AvailabilityCheckError> { - let epoch = slot.epoch(T::EthSpec::slots_per_epoch()); + let epoch = bid.slot.epoch(T::EthSpec::slots_per_epoch()); let sampling_columns = self .custody_context .sampling_columns_for_epoch(epoch, &self.spec); @@ -286,7 +279,7 @@ impl PendingPayloadCache { .map(|c| KzgVerifiedCustodyDataColumn::from_asserted_custody(c.into_inner())) .collect::>(); - self.put_kzg_verified_custody_data_columns(block_root, custody_columns) + self.put_kzg_verified_custody_data_columns(block_root, bid, custody_columns) } /// Insert KZG verified columns into the cache. @@ -294,11 +287,13 @@ impl PendingPayloadCache { pub fn put_kzg_verified_custody_data_columns( &self, block_root: Hash256, + bid: PendingPayloadBid, kzg_verified_data_columns: Vec>, ) -> Result, AvailabilityCheckError> { - let pending_components = self.get_pending_components(block_root, |pending_components| { - pending_components.merge_data_columns(kzg_verified_data_columns) - })?; + let pending_components = + self.get_pending_components(block_root, bid, |pending_components| { + pending_components.merge_data_columns(kzg_verified_data_columns) + })?; let num_expected_columns = self.get_num_expected_columns(pending_components.epoch()); @@ -317,6 +312,7 @@ impl PendingPayloadCache { pub fn reconstruct_data_columns( &self, block_root: &Hash256, + bid: PendingPayloadBid, ) -> Result, AvailabilityCheckError> { let verified_data_columns = match self.check_and_set_reconstruction_started(block_root) { ReconstructColumnsDecision::Yes(verified_data_columns) => verified_data_columns, @@ -324,6 +320,10 @@ impl PendingPayloadCache { return Ok(DataColumnReconstructionResult::NotStarted(reason)); } }; + let existing_column_indices = verified_data_columns + .iter() + .map(|data_column| *data_column.index()) + .collect::>(); metrics::inc_counter(&KZG_DATA_COLUMN_RECONSTRUCTION_ATTEMPTS); let timer = metrics::start_timer(&metrics::DATA_AVAILABILITY_RECONSTRUCTION_TIME); @@ -344,12 +344,6 @@ impl PendingPayloadCache { AvailabilityCheckError::ReconstructColumnsError(e) })?; - let Some(existing_column_indices) = self.cached_data_column_indexes(block_root) else { - return Err(AvailabilityCheckError::Unexpected( - "block no longer exists in the data availability checker".to_string(), - )); - }; - let Some(slot) = all_data_columns.first().map(|d| d.as_data_column().slot()) else { return Ok(DataColumnReconstructionResult::RecoveredColumnsNotImported( "No new columns to import and publish", @@ -383,6 +377,7 @@ impl PendingPayloadCache { self.put_kzg_verified_custody_data_columns( *block_root, + bid, data_columns_to_import_and_publish.clone(), ) .map(|availability| { @@ -436,16 +431,15 @@ impl PendingPayloadCache { } } - /// Gets existing `PendingComponents` and applies the `update_fn` while holding the write lock. + /// Gets or creates `PendingComponents` and applies the `update_fn` while holding the write lock. /// /// Once the update is complete, the write lock is downgraded and a read guard with a /// reference of the updated `PendingComponents` is returned. /// - /// Returns an error if no pending components exist for the given block root (the block must - /// be initialized via `init_pending_block` first). fn get_pending_components( &self, block_root: Hash256, + bid: PendingPayloadBid, update_fn: F, ) -> Result>, AvailabilityCheckError> where @@ -454,11 +448,8 @@ impl PendingPayloadCache { let mut write_lock = self.availability_cache.write(); { - let pending_components = write_lock.get_mut(&block_root).ok_or_else(|| { - AvailabilityCheckError::Unexpected( - "pending components not initialized for block".to_string(), - ) - })?; + let pending_components = write_lock + .get_or_insert_mut(block_root, || PendingComponents::empty(block_root, bid)); update_fn(pending_components)? } @@ -634,7 +625,6 @@ mod data_availability_checker_tests { use crate::block_verification::PayloadVerificationOutcome; use crate::test_utils::{ NumBlobs, generate_data_column_indices_rand_order, generate_rand_block_and_data_columns, - test_spec, }; use crate::{ custody_context::NodeCustodyType, @@ -652,8 +642,10 @@ mod data_availability_checker_tests { }; type E = MinimalEthSpec; + type T = DiskHarnessType; const LOW_VALIDATOR_COUNT: usize = 32; + const RNG_SEED: u64 = 0xDEADBEEF; fn gloas_spec() -> Arc { let mut spec = E::default_spec(); @@ -703,18 +695,7 @@ mod data_availability_checker_tests { .build() } - async fn setup_harness_and_cache() -> ( - BeaconChainHarness>, - Arc>, - TempDir, - ) - where - T: BeaconChainTypes< - HotStore = BeaconNodeBackend, - ColdStore = BeaconNodeBackend, - EthSpec = E, - >, - { + async fn setup() -> (BeaconChainHarness, Arc>, TempDir) { create_test_tracing_subscriber(); let chain_db_path = tempdir().expect("should get temp dir"); let harness = get_gloas_chain::(&chain_db_path).await; @@ -732,22 +713,6 @@ mod data_availability_checker_tests { (harness, cache, chain_db_path) } - fn is_gloas_enabled() -> bool { - let spec = test_spec::(); - spec.fork_name_at_slot::(Slot::new(0)).gloas_enabled() - } - - #[tokio::test] - async fn test_cache_creation() { - if !is_gloas_enabled() { - return; - } - - type T = DiskHarnessType; - let (_harness, cache, _path) = setup_harness_and_cache::().await; - assert_eq!(cache.block_cache_size(), 0); - } - fn make_test_signed_envelope(block_root: Hash256) -> Arc> { Arc::new(SignedExecutionPayloadEnvelope { message: ExecutionPayloadEnvelope { @@ -771,311 +736,132 @@ mod data_availability_checker_tests { } } + fn init_block( + cache: &PendingPayloadCache, + spec: &ChainSpec, + num_blobs: NumBlobs, + seed: u64, + ) -> (PendingPayloadBid, Hash256, DataColumnSidecarList) { + let mut rng = StdRng::seed_from_u64(seed); + let (block, data_columns) = + generate_rand_block_and_data_columns::(ForkName::Gloas, num_blobs, &mut rng, spec); + let block_root = block.canonical_root(); + let bid = PendingPayloadBid::from_block(&block).expect("should get payload bid"); + cache.init_pending_bid(block_root, bid.clone()); + (bid, block_root, data_columns) + } + #[tokio::test] - async fn test_put_columns_creates_pending_components() { - if !is_gloas_enabled() { - return; + async fn caches_and_deduplicates_columns() { + let (harness, cache, _path) = setup().await; + let (bid, block_root, data_columns) = + init_block(&cache, &harness.spec, NumBlobs::Number(1), RNG_SEED); + let column = data_columns.first().cloned().expect("should have column"); + let column_index = *column.index(); + + for _ in 0..2 { + cache + .put_rpc_custody_columns(block_root, bid.clone(), vec![column.clone()]) + .expect("should put column"); } - type T = DiskHarnessType; - let (harness, cache, _path) = setup_harness_and_cache::().await; - - let mut rng = StdRng::seed_from_u64(0xDEADBEEF); - let spec = harness.spec.clone(); - - let (block, data_columns) = generate_rand_block_and_data_columns::( - ForkName::Gloas, - NumBlobs::Number(1), - &mut rng, - &spec, + assert_eq!( + cache.cached_data_column_indexes(&block_root), + Some(vec![column_index]) + ); + assert_eq!( + cache.get_data_columns(block_root).map(|cols| cols.len()), + Some(1) ); - let slot = block.slot(); - let block_root = Hash256::random(); - cache.init_pending_block(block_root, Arc::new(block)); - - let columns: DataColumnSidecarList = data_columns.into_iter().take(1).collect(); - - let result = cache.put_rpc_custody_columns(block_root, slot, columns); - assert!(result.is_ok(), "put_rpc_custody_columns failed: {result:?}"); - assert_eq!(cache.block_cache_size(), 1); - - let cached_indices = cache.peek_pending_components(&block_root, |components| { - components.map(|c| c.get_cached_data_columns_indices()) - }); - assert!(cached_indices.is_some()); - assert_eq!(cached_indices.unwrap().len(), 1); } #[tokio::test] - async fn test_column_deduplication() { - if !is_gloas_enabled() { - return; - } - - type T = DiskHarnessType; - let (harness, cache, _path) = setup_harness_and_cache::().await; - - let mut rng = StdRng::seed_from_u64(0xDEADBEEF); - let spec = harness.spec.clone(); - - let (block, data_columns) = generate_rand_block_and_data_columns::( - ForkName::Gloas, - NumBlobs::Number(1), - &mut rng, - &spec, - ); - let slot = block.slot(); - let block_root = Hash256::random(); - cache.init_pending_block(block_root, Arc::new(block)); - - let first_column = data_columns.first().cloned().expect("should have column"); - let column_index = *first_column.index(); - - cache - .put_rpc_custody_columns(block_root, slot, vec![first_column.clone()]) - .expect("should put column"); - - cache - .put_rpc_custody_columns(block_root, slot, vec![first_column]) - .expect("should put column again"); - - let cached_indices = cache.peek_pending_components(&block_root, |components| { - components.map(|c| c.get_cached_data_columns_indices()) - }); - assert!(cached_indices.is_some()); - let indices = cached_indices.unwrap(); - assert_eq!(indices.len(), 1); - assert_eq!(indices[0], column_index); - } - - #[tokio::test] - async fn test_columns_without_envelope_not_available() { - if !is_gloas_enabled() { - return; - } - - type T = DiskHarnessType; - let (harness, cache, _path) = setup_harness_and_cache::().await; - - let mut rng = StdRng::seed_from_u64(0xDEADBEEF); - let spec = harness.spec.clone(); - - let (block, data_columns) = generate_rand_block_and_data_columns::( - ForkName::Gloas, - NumBlobs::Number(1), - &mut rng, - &spec, - ); - let slot = block.slot(); - let block_root = Hash256::random(); - cache.init_pending_block(block_root, Arc::new(block)); + async fn requires_columns_and_executed_envelope() { + let (harness, cache, _path) = setup().await; + let (bid, block_root, data_columns) = + init_block(&cache, &harness.spec, NumBlobs::Number(1), RNG_SEED); let result = cache - .put_rpc_custody_columns(block_root, slot, data_columns) + .put_rpc_custody_columns(block_root, bid, data_columns) .expect("should put columns"); + assert!(matches!(result, Availability::MissingComponents(_))); - // Without an executed envelope, should still be missing components + let result = cache + .put_executed_payload_envelope(bid, make_test_executed_envelope(block_root)) + .expect("should put executed envelope"); + let Availability::Available(envelope) = result else { + panic!("expected available envelope"); + }; + assert_eq!(envelope.block_root, block_root); + assert_eq!(envelope.envelope.columns.len(), E::number_of_columns()); + } + + #[tokio::test] + async fn zero_blob_envelope_is_available_without_columns() { + let (harness, cache, _path) = setup().await; + let (bid, block_root, _columns) = + init_block(&cache, &harness.spec, NumBlobs::Number(0), RNG_SEED); + + let result = cache + .put_executed_payload_envelope(bid, make_test_executed_envelope(block_root)) + .expect("should put executed envelope"); + let Availability::Available(envelope) = result else { + panic!("zero-blob block should be available"); + }; + assert!(envelope.envelope.columns.is_empty()); + } + + #[tokio::test] + async fn partial_columns_wait_for_missing_columns() { + let (harness, cache, _path) = setup().await; + let (bid, block_root, data_columns) = + init_block(&cache, &harness.spec, NumBlobs::Number(1), RNG_SEED); + + cache + .put_executed_payload_envelope(bid.clone(), make_test_executed_envelope(block_root)) + .expect("should put executed envelope"); + + let columns = data_columns.into_iter().take(1).collect(); + let result = cache + .put_rpc_custody_columns(block_root, bid, columns) + .expect("should put columns"); assert!(matches!(result, Availability::MissingComponents(_))); } #[tokio::test] - async fn test_full_availability_flow() { - if !is_gloas_enabled() { - return; - } - - type T = DiskHarnessType; - let (harness, cache, _path) = setup_harness_and_cache::().await; - - let mut rng = StdRng::seed_from_u64(0xDEADBEEF); - let spec = harness.spec.clone(); - - let (block, data_columns) = generate_rand_block_and_data_columns::( - ForkName::Gloas, - NumBlobs::Number(1), - &mut rng, - &spec, - ); - let slot = block.slot(); - let block_root = Hash256::random(); - cache.init_pending_block(block_root, Arc::new(block)); - - let result = cache - .put_rpc_custody_columns(block_root, slot, data_columns) - .expect("should put columns"); - - assert!(matches!(result, Availability::MissingComponents(_))); - - let executed_envelope = make_test_executed_envelope(block_root); - let result = cache - .put_executed_payload_envelope(executed_envelope) - .expect("should put executed envelope"); - - assert!( - matches!(result, Availability::Available(_)), - "expected Available, got {:?}", - result - ); - } - - #[tokio::test] - async fn test_zero_blob_immediately_available() { - if !is_gloas_enabled() { - return; - } - - type T = DiskHarnessType; - let (harness, cache, _path) = setup_harness_and_cache::().await; - - let mut rng = StdRng::seed_from_u64(0xDEADBEEF); - let spec = harness.spec.clone(); - - let (block, _) = generate_rand_block_and_data_columns::( - ForkName::Gloas, - NumBlobs::Number(0), - &mut rng, - &spec, - ); - - let block_root = Hash256::random(); - cache.init_pending_block(block_root, Arc::new(block)); - - let executed_envelope = make_test_executed_envelope(block_root); - let result = cache - .put_executed_payload_envelope(executed_envelope) - .expect("should put executed envelope"); - - assert!( - matches!(result, Availability::Available(_)), - "zero-blob block should be immediately available, got {:?}", - result - ); - } - - #[tokio::test] - async fn test_handle_reconstruction_failure_clears_columns() { - if !is_gloas_enabled() { - return; - } - - type T = DiskHarnessType; - let (harness, cache, _path) = setup_harness_and_cache::().await; - - let mut rng = StdRng::seed_from_u64(0xDEADBEEF); - let spec = harness.spec.clone(); - - let (block, data_columns) = generate_rand_block_and_data_columns::( - ForkName::Gloas, - NumBlobs::Number(1), - &mut rng, - &spec, - ); - let slot = block.slot(); - let block_root = Hash256::random(); - cache.init_pending_block(block_root, Arc::new(block)); - - let columns: DataColumnSidecarList = data_columns.into_iter().take(5).collect(); + async fn reconstruction_failure_clears_columns() { + let (harness, cache, _path) = setup().await; + let (bid, block_root, data_columns) = + init_block(&cache, &harness.spec, NumBlobs::Number(1), RNG_SEED); + let columns = data_columns.into_iter().take(5).collect(); cache - .put_rpc_custody_columns(block_root, slot, columns) + .put_rpc_custody_columns(block_root, bid, columns) .expect("should put columns"); - - let cached_count = cache.peek_pending_components(&block_root, |components| { - components.map(|c| c.verified_data_columns.len()) - }); - assert_eq!(cached_count, Some(5)); + assert_eq!( + cache + .cached_data_column_indexes(&block_root) + .map(|indices| indices.len()), + Some(5) + ); cache.handle_reconstruction_failure(&block_root); - - let cached_count_after = cache.peek_pending_components(&block_root, |components| { - components.map(|c| c.verified_data_columns.len()) - }); - assert_eq!(cached_count_after, Some(0)); + assert_eq!(cache.cached_data_column_indexes(&block_root), Some(vec![])); } #[tokio::test] - async fn test_maintenance_removes_old_entries() { - if !is_gloas_enabled() { - return; - } - - type T = DiskHarnessType; - let (_harness, cache, _path) = setup_harness_and_cache::().await; - - let cutoff_epoch = Epoch::new(100); - cache - .do_maintenance(cutoff_epoch) - .expect("maintenance should succeed"); - - assert_eq!(cache.block_cache_size(), 0); - } - - #[tokio::test] - async fn test_peek_data_columns() { - if !is_gloas_enabled() { - return; - } - - type T = DiskHarnessType; - let (harness, cache, _path) = setup_harness_and_cache::().await; - - let mut rng = StdRng::seed_from_u64(0xDEADBEEF); - let spec = harness.spec.clone(); - - let (block, data_columns) = generate_rand_block_and_data_columns::( - ForkName::Gloas, - NumBlobs::Number(1), - &mut rng, - &spec, - ); - let slot = block.slot(); - let block_root = Hash256::random(); - - assert!(cache.get_data_columns(block_root).is_none()); - - cache.init_pending_block(block_root, Arc::new(block)); - - let columns: DataColumnSidecarList = data_columns.into_iter().take(3).collect(); - - cache - .put_rpc_custody_columns(block_root, slot, columns) - .expect("should put columns"); - - let peeked = cache.get_data_columns(block_root); - assert!(peeked.is_some()); - assert_eq!(peeked.unwrap().len(), 3); - } - - #[tokio::test] - async fn test_lru_eviction() { - if !is_gloas_enabled() { - return; - } - - type T = DiskHarnessType; - let (harness, cache, _path) = setup_harness_and_cache::().await; - - let mut rng = StdRng::seed_from_u64(0xDEADBEEF); - let spec = harness.spec.clone(); - - let (block, data_columns) = generate_rand_block_and_data_columns::( - ForkName::Gloas, - NumBlobs::Number(1), - &mut rng, - &spec, - ); - let slot = block.slot(); - let block = Arc::new(block); - let first_column = data_columns.first().cloned().expect("should have column"); - + async fn lru_eviction_keeps_cache_bounded() { + let (harness, cache, _path) = setup().await; let mut roots = Vec::new(); - for _ in 0..33 { - let block_root = Hash256::random(); + + for i in 0..33 { + let (bid, block_root, data_columns) = + init_block(&cache, &harness.spec, NumBlobs::Number(1), RNG_SEED + i); + let column = data_columns.first().cloned().expect("should have column"); roots.push(block_root); - cache.init_pending_block(block_root, block.clone()); cache - .put_rpc_custody_columns(block_root, slot, vec![first_column.clone()]) + .put_rpc_custody_columns(block_root, bid, vec![column]) .expect("should put columns"); } @@ -1085,79 +871,20 @@ mod data_availability_checker_tests { } #[tokio::test] - async fn test_maintenance_prunes_old_entries() { - if !is_gloas_enabled() { - return; - } + async fn maintenance_prunes_old_entries() { + let (harness, cache, _path) = setup().await; + let (bid, block_root, data_columns) = + init_block(&cache, &harness.spec, NumBlobs::Number(1), RNG_SEED); + let column = data_columns.first().cloned().expect("should have column"); - type T = DiskHarnessType; - let (harness, cache, _path) = setup_harness_and_cache::().await; - - let mut rng = StdRng::seed_from_u64(0xDEADBEEF); - let spec = harness.spec.clone(); - - let (block, data_columns) = generate_rand_block_and_data_columns::( - ForkName::Gloas, - NumBlobs::Number(1), - &mut rng, - &spec, - ); - let slot = block.slot(); - let block_root = Hash256::random(); - cache.init_pending_block(block_root, Arc::new(block)); - - let col = data_columns.first().cloned().expect("should have column"); cache - .put_rpc_custody_columns(block_root, slot, vec![col]) + .put_rpc_custody_columns(block_root, bid, vec![column]) .expect("should put columns"); assert_eq!(cache.block_cache_size(), 1); - - // slot=0 → epoch=0 < cutoff=100, should prune cache - .do_maintenance(Epoch::new(100)) + .do_maintenance(Epoch::new(1)) .expect("maintenance should succeed"); - assert_eq!(cache.block_cache_size(), 0); } - - #[tokio::test] - async fn test_partial_columns_missing_components() { - if !is_gloas_enabled() { - return; - } - - type T = DiskHarnessType; - let (harness, cache, _path) = setup_harness_and_cache::().await; - - let mut rng = StdRng::seed_from_u64(0xDEADBEEF); - let spec = harness.spec.clone(); - - let (block, data_columns) = generate_rand_block_and_data_columns::( - ForkName::Gloas, - NumBlobs::Number(1), - &mut rng, - &spec, - ); - let slot = block.slot(); - let block_root = Hash256::random(); - cache.init_pending_block(block_root, Arc::new(block)); - - let executed_envelope = make_test_executed_envelope(block_root); - cache - .put_executed_payload_envelope(executed_envelope) - .expect("should put executed envelope"); - - // Insert only 1 column (need 128 for fullnode) - let columns: DataColumnSidecarList = data_columns.into_iter().take(1).collect(); - - let result = cache - .put_rpc_custody_columns(block_root, slot, columns) - .expect("should put columns"); - - assert!( - matches!(result, Availability::MissingComponents(_)), - "partial columns should not trigger availability" - ); - } } diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs b/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs index 027fd06982..779470e3ce 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs @@ -9,14 +9,35 @@ use std::collections::HashMap; use std::sync::Arc; use tracing::{Span, debug, debug_span}; use types::DataColumnSidecar; -use types::{ColumnIndex, Epoch, EthSpec, Hash256, SignedBeaconBlock}; +use types::{ + AbstractExecPayload, BeaconStateError, ColumnIndex, Epoch, EthSpec, Hash256, KzgCommitments, + SignedBeaconBlock, Slot, +}; + +#[derive(Clone)] +pub struct PendingPayloadBid { + pub slot: Slot, + pub blob_kzg_commitments: KzgCommitments, +} + +impl PendingPayloadBid { + pub fn from_block>( + block: &SignedBeaconBlock, + ) -> Result { + let signed_bid = block.message().body().signed_execution_payload_bid()?; + Ok(Self { + slot: block.slot(), + blob_kzg_commitments: signed_bid.message.blob_kzg_commitments.clone(), + }) + } +} /// This represents the components of a payload pending data availability. /// /// The columns are all gossip and kzg verified. /// The payload is considered "available" when all required columns are received. pub struct PendingComponents { - pub block: Arc>, + pub bid: PendingPayloadBid, /// a cached post executed payload envelope pub envelope: Option>, pub verified_data_columns: HashMap>, @@ -26,7 +47,7 @@ pub struct PendingComponents { impl PendingComponents { pub fn num_blobs_expected(&self) -> usize { - self.block.num_expected_blobs() + self.bid.blob_kzg_commitments.len() } /// Returns the completed custody columns @@ -36,7 +57,7 @@ impl PendingComponents { .filter_map(|(col_idx, col)| { col.try_to_sidecar( *col_idx, - self.block.slot(), + self.bid.slot, block_root, self.num_blobs_expected(), ) @@ -138,7 +159,7 @@ impl PendingComponents { .filter_map(|(col_idx, col)| { col.try_to_sidecar( *col_idx, - self.block.slot(), + self.bid.slot, block_hash, self.num_blobs_expected(), ) @@ -167,11 +188,11 @@ impl PendingComponents { } /// Returns an empty `PendingComponents` object with the given block root. - pub fn empty(block_root: Hash256, block: Arc>) -> Self { + pub fn empty(block_root: Hash256, bid: PendingPayloadBid) -> Self { let span = debug_span!(parent: None, "lh_pending_components", %block_root); let _guard = span.clone().entered(); Self { - block, + bid, envelope: None, verified_data_columns: HashMap::new(), reconstruction_started: false, @@ -181,7 +202,7 @@ impl PendingComponents { /// Returns the epoch of the bid or first data column, if available. pub fn epoch(&self) -> Epoch { - self.block.slot().epoch(E::slots_per_epoch()) + self.bid.slot.epoch(E::slots_per_epoch()) } pub fn status_str(&self, num_expected_columns: usize) -> String { @@ -202,62 +223,3 @@ pub(crate) enum ReconstructColumnsDecision { Yes(Vec>>), No(&'static str), } - -/* -#[cfg(test)] -mod pending_components_tests { - use crate::test_utils::test_spec; - - use super::*; - use types::MinimalEthSpec; - - type E = MinimalEthSpec; - - #[test] - fn test_get_cached_data_columns_indices_empty() { - let spec = Arc::new(test_spec::()); - let block_root = Hash256::random(); - let components = PendingComponents::::empty(block_root, spec); - - let indices = components.get_cached_data_columns_indices(); - assert!(indices.is_empty()); - } - - #[test] - fn test_status_str_no_bid() { - let spec = Arc::new(test_spec::()); - let block_root = Hash256::random(); - let components = PendingComponents::::empty(block_root, spec); - - let status = components.status_str(10); - assert_eq!(status, "data_columns 0/10"); - } - - #[test] - fn test_num_blobs_expected_no_bid() { - let spec = Arc::new(test_spec::()); - let block_root = Hash256::random(); - let components = PendingComponents::::empty(block_root, spec); - - let result = components.num_blobs_expected(); - assert!(result.is_err()); - // Error should be AvailabilityCheckError::Unexpected - assert!(matches!( - result.unwrap_err(), - AvailabilityCheckError::Unexpected(_) - )); - } - - #[test] - fn test_make_available_no_bid_returns_none() { - let spec = Arc::new(test_spec::()); - let block_root = Hash256::random(); - let components = PendingComponents::::empty(block_root, spec); - - // Without a bid, make_available should return Ok(None) - let result = components.make_available(10); - assert!(result.is_ok()); - assert!(result.unwrap().is_none()); - } -} -*/ 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 5e291bd833..6bc3c03b8b 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -698,15 +698,6 @@ impl NetworkBeaconProcessor { } Err(err) => { match err { - GossipDataColumnError::InvalidVariant => { - // TODO(gloas) we should probably penalize the peer here - debug!( - %slot, - %block_root, - %index, - "Invalid gossip data column variant." - ) - } GossipDataColumnError::PriorKnownUnpublished => { debug!( %slot, @@ -732,6 +723,25 @@ impl NetworkBeaconProcessor { column_sidecar, )); } + GossipDataColumnError::BlockRootUnknown { + block_root: unknown_block_root, + .. + } => { + debug!( + action = "requesting block", + %unknown_block_root, + "Unknown block root for column" + ); + self.send_sync_message(SyncMessage::UnknownBlockHashFromAttestation( + peer_id, + unknown_block_root, + )); + self.propagate_validation_result( + message_id, + peer_id, + MessageAcceptance::Ignore, + ); + } GossipDataColumnError::PubkeyCacheTimeout | GossipDataColumnError::BeaconChainError(_) => { crit!( @@ -739,10 +749,12 @@ impl NetworkBeaconProcessor { "Internal error when verifying column sidecar" ) } - GossipDataColumnError::ProposalSignatureInvalid + GossipDataColumnError::InvalidVariant + | GossipDataColumnError::ProposalSignatureInvalid | GossipDataColumnError::UnknownValidator(_) | GossipDataColumnError::ProposerIndexMismatch { .. } | GossipDataColumnError::IsNotLaterThanParent { .. } + | GossipDataColumnError::BlockSlotMismatch { .. } | GossipDataColumnError::InvalidSubnetId { .. } | GossipDataColumnError::InvalidInclusionProof | GossipDataColumnError::InvalidKzgProof { .. } @@ -904,14 +916,6 @@ impl NetworkBeaconProcessor { ) { match err { GossipPartialDataColumnError::GossipDataColumnError(err) => match err { - GossipDataColumnError::InvalidVariant => { - // TODO(gloas) we should probably penalize the peer here - debug!( - %block_root, - %index, - "Invalid gossip partial data column variant." - ) - } GossipDataColumnError::PriorKnownUnpublished => { debug!( %block_root, @@ -933,6 +937,20 @@ impl NetworkBeaconProcessor { slot, }); } + GossipDataColumnError::BlockRootUnknown { + block_root: unknown_block_root, + .. + } => { + debug!( + action = "requesting block", + %unknown_block_root, + "Unknown block root for partial column" + ); + self.send_sync_message(SyncMessage::UnknownBlockHashFromAttestation( + peer_id, + unknown_block_root, + )); + } GossipDataColumnError::PubkeyCacheTimeout | GossipDataColumnError::BeaconChainError(_) => { crit!( @@ -940,10 +958,12 @@ impl NetworkBeaconProcessor { "Internal error when verifying partial column sidecar" ) } - GossipDataColumnError::ProposalSignatureInvalid + GossipDataColumnError::InvalidVariant + | GossipDataColumnError::ProposalSignatureInvalid | GossipDataColumnError::UnknownValidator(_) | GossipDataColumnError::ProposerIndexMismatch { .. } | GossipDataColumnError::IsNotLaterThanParent { .. } + | GossipDataColumnError::BlockSlotMismatch { .. } | GossipDataColumnError::InvalidSubnetId { .. } | GossipDataColumnError::InvalidInclusionProof | GossipDataColumnError::InvalidKzgProof { .. } diff --git a/beacon_node/network/src/network_beacon_processor/tests.rs b/beacon_node/network/src/network_beacon_processor/tests.rs index c4e7f8f8d1..21745e12db 100644 --- a/beacon_node/network/src/network_beacon_processor/tests.rs +++ b/beacon_node/network/src/network_beacon_processor/tests.rs @@ -10,7 +10,7 @@ use crate::{ }; use beacon_chain::block_verification_types::LookupBlock; use beacon_chain::custody_context::NodeCustodyType; -use beacon_chain::data_column_verification::validate_data_column_sidecar_for_gossip_fulu; +use beacon_chain::data_column_verification::GossipVerifiedDataColumn; use beacon_chain::kzg_utils::blobs_to_data_column_sidecars; use beacon_chain::observed_data_sidecars::DoNotObserve; use beacon_chain::test_utils::{ @@ -1185,12 +1185,8 @@ async fn accept_processed_gossip_data_columns_without_import() { .map(|data_column| { let subnet_id = DataColumnSubnetId::from_column_index(*data_column.index(), &rig.chain.spec); - validate_data_column_sidecar_for_gossip_fulu::<_, DoNotObserve>( - data_column, - subnet_id, - &rig.chain, - ) - .expect("should be valid data column") + GossipVerifiedDataColumn::<_, DoNotObserve>::new(data_column, subnet_id, &rig.chain) + .expect("should be valid data column") }) .collect(); diff --git a/beacon_node/network/src/sync/manager.rs b/beacon_node/network/src/sync/manager.rs index fb31e92262..1b45ea7052 100644 --- a/beacon_node/network/src/sync/manager.rs +++ b/beacon_node/network/src/sync/manager.rs @@ -906,9 +906,9 @@ impl SyncManager { }), ); } - // TODO(gloas) support gloas data column variant DataColumnSidecar::Gloas(_) => { - error!("Gloas variant not yet supported") + debug!(%block_root, "Received unknown block data column message"); + self.handle_unknown_block_root(peer_id, block_root); } } } From 0078b6be898d6e8bcc88e84e299ef247b5b5a11f Mon Sep 17 00:00:00 2001 From: Josh King Date: Mon, 27 Apr 2026 23:26:29 +0200 Subject: [PATCH 075/118] fix: gloas from genesis MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix forkchoice update sending zero-hash head to EL at genesis by reading latest_block_hash from state when the genesis bid hashes are all zeros - Simplify genesis block construction — the genesis block body is empty per spec, remove the incorrect bid-copying logic and body root override in initialize_beacon_state_from_eth1 --- consensus/fork_choice/src/fork_choice.rs | 19 ++++++++++++--- consensus/state_processing/src/genesis.rs | 28 +++++------------------ 2 files changed, 22 insertions(+), 25 deletions(-) diff --git a/consensus/fork_choice/src/fork_choice.rs b/consensus/fork_choice/src/fork_choice.rs index 477d1fa3b4..1cee6cba92 100644 --- a/consensus/fork_choice/src/fork_choice.rs +++ b/consensus/fork_choice/src/fork_choice.rs @@ -416,11 +416,24 @@ where let (execution_status, execution_payload_parent_hash, execution_payload_block_hash) = if let Ok(signed_bid) = anchor_block.message().body().signed_execution_payload_bid() { - // Gloas: execution status is irrelevant post-Gloas; payload validation - // is decoupled from beacon blocks. + // At Gloas genesis the block bid is empty (all zeros) per spec, but the + // state holds the EL genesis hash in `latest_block_hash`. Use it so the + // first forkchoice update sends a valid head to the EL. + let parent_hash = if anchor_block.slot() == spec.genesis_slot + && anchor_state.slot() == spec.genesis_slot + && signed_bid.message.parent_block_hash.into_root().is_zero() + && signed_bid.message.block_hash.into_root().is_zero() + { + *anchor_state + .latest_block_hash() + .map_err(Error::BeaconStateError)? + } else { + signed_bid.message.parent_block_hash + }; + ( ExecutionStatus::irrelevant(), - Some(signed_bid.message.parent_block_hash), + Some(parent_hash), Some(signed_bid.message.block_hash), ) } else if let Ok(execution_payload) = anchor_block.message().execution_payload() { diff --git a/consensus/state_processing/src/genesis.rs b/consensus/state_processing/src/genesis.rs index c643ad56e3..46541e0326 100644 --- a/consensus/state_processing/src/genesis.rs +++ b/consensus/state_processing/src/genesis.rs @@ -167,19 +167,12 @@ pub fn initialize_beacon_state_from_eth1( // Remove intermediate Fulu fork from `state.fork`. state.fork_mut().previous_version = spec.gloas_fork_version; - // The genesis block's bid must have block_hash = 0x00 per spec (empty payload). // Retain the EL genesis hash in latest_block_hash and parent_block_hash so the // first post-genesis proposer can build on the correct EL head. let el_genesis_hash = state.latest_execution_payload_bid()?.block_hash; let bid = state.latest_execution_payload_bid_mut()?; bid.parent_block_hash = el_genesis_hash; bid.block_hash = ExecutionBlockHash::default(); - - // Update the `latest_block_header.body_root` so that it matches the body of the - // Gloas genesis block, which embeds `state.latest_execution_payload_bid` in its - // `signed_execution_payload_bid` field (see `genesis_block`). - let genesis_body_root = genesis_block(&state, spec)?.body_root(); - state.latest_block_header_mut().body_root = genesis_body_root; } // Now that we have our validators, initialize the caches (including the committees) @@ -191,25 +184,16 @@ pub fn initialize_beacon_state_from_eth1( Ok(state) } -/// Create an unsigned genesis `BeaconBlock`. +/// Create an unsigned genesis `BeaconBlock` matching the genesis state. /// -/// Per spec, the genesis block body is empty (all default fields) except for Gloas, -/// where `body.signed_execution_payload_bid.message` is initialised from -/// `state.latest_execution_payload_bid` so that the first post-genesis proposer can -/// build on the correct execution layer head. -/// -/// `state.latest_block_header.body_root` is set from this same block's body, so the -/// two must stay in sync. +/// Per spec, the genesis block body is empty (all default fields). +/// `state.latest_block_header.body_root` is set from `BeaconBlock::empty()`, +/// so this function must return the same empty block to keep roots consistent. pub fn genesis_block( - state: &BeaconState, + _genesis_state: &BeaconState, spec: &ChainSpec, ) -> Result, BeaconStateError> { - let mut block = BeaconBlock::empty(spec); - if let BeaconBlock::Gloas(ref mut gloas_block) = block { - let bid = state.latest_execution_payload_bid()?.clone(); - gloas_block.body.signed_execution_payload_bid.message = bid; - } - Ok(block) + Ok(BeaconBlock::empty(spec)) } /// Determine whether a candidate genesis state is suitable for starting the chain. From 5360e7669686936c856eb920539ca580e99a18fa Mon Sep 17 00:00:00 2001 From: hopinheimer Date: Mon, 27 Apr 2026 01:57:22 -0400 Subject: [PATCH 076/118] fix `genesis_block` init in tests --- .../beacon_chain/src/payload_attestation_verification/tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beacon_node/beacon_chain/src/payload_attestation_verification/tests.rs b/beacon_node/beacon_chain/src/payload_attestation_verification/tests.rs index 7faad98e55..5fbcc481de 100644 --- a/beacon_node/beacon_chain/src/payload_attestation_verification/tests.rs +++ b/beacon_node/beacon_chain/src/payload_attestation_verification/tests.rs @@ -7,8 +7,8 @@ use genesis::{generate_deterministic_keypairs, interop_genesis_state}; use parking_lot::RwLock; use proto_array::PayloadStatus; use slot_clock::{SlotClock, TestingSlotClock}; -use state_processing::AllCaches; use state_processing::genesis::genesis_block; +use state_processing::AllCaches; use store::{HotColdDB, StoreConfig}; use types::{ ChainSpec, Checkpoint, Domain, Epoch, EthSpec, Hash256, MinimalEthSpec, PayloadAttestationData, From 0fe75382da0eaf12f774ba8ed47de6a90cafc1b4 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Thu, 30 Apr 2026 13:05:30 +0200 Subject: [PATCH 077/118] fmt --- .../beacon_chain/src/payload_attestation_verification/tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beacon_node/beacon_chain/src/payload_attestation_verification/tests.rs b/beacon_node/beacon_chain/src/payload_attestation_verification/tests.rs index 5fbcc481de..7faad98e55 100644 --- a/beacon_node/beacon_chain/src/payload_attestation_verification/tests.rs +++ b/beacon_node/beacon_chain/src/payload_attestation_verification/tests.rs @@ -7,8 +7,8 @@ use genesis::{generate_deterministic_keypairs, interop_genesis_state}; use parking_lot::RwLock; use proto_array::PayloadStatus; use slot_clock::{SlotClock, TestingSlotClock}; -use state_processing::genesis::genesis_block; use state_processing::AllCaches; +use state_processing::genesis::genesis_block; use store::{HotColdDB, StoreConfig}; use types::{ ChainSpec, Checkpoint, Domain, Epoch, EthSpec, Hash256, MinimalEthSpec, PayloadAttestationData, From 0b7397eb4e1ee249a82b32e054793945c8c3c8e2 Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Thu, 30 Apr 2026 13:39:05 +0200 Subject: [PATCH 078/118] Refactor --- beacon_node/beacon_chain/src/beacon_chain.rs | 100 ++++++------------ .../beacon_chain/src/block_verification.rs | 4 + .../src/data_column_verification.rs | 8 +- beacon_node/beacon_chain/src/errors.rs | 20 ---- .../beacon_chain/src/fetch_blobs/mod.rs | 6 +- beacon_node/beacon_chain/src/lib.rs | 2 +- .../payload_envelope_verification/import.rs | 39 ++++--- .../src/payload_envelope_verification/mod.rs | 28 +---- .../payload_notifier.rs | 3 +- .../beacon_chain/tests/column_verification.rs | 7 +- .../src/beacon/execution_payload_envelope.rs | 3 +- .../gossip_methods.rs | 42 +++----- .../src/network_beacon_processor/mod.rs | 11 +- .../network_beacon_processor/sync_methods.rs | 9 +- .../network/src/sync/block_lookups/mod.rs | 21 +--- beacon_node/network/src/sync/manager.rs | 19 ++-- 16 files changed, 116 insertions(+), 206 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 8456bbbc02..76e7b217ce 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -32,7 +32,7 @@ use crate::data_column_verification::{ }; use crate::early_attester_cache::EarlyAttesterCache; use crate::envelope_times_cache::EnvelopeTimesCache; -use crate::errors::{BeaconChainError as Error, BlockOrEnvelopeError, BlockProductionError}; +use crate::errors::{BeaconChainError as Error, BlockProductionError}; use crate::events::ServerSentEventHandler; use crate::execution_payload::{NotifyExecutionLayer, PreparePayloadHandle, get_execution_payload}; use crate::fetch_blobs::EngineGetBlobsOutput; @@ -67,7 +67,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::EnvelopeError; use crate::pending_payload_cache::PendingPayloadCache; use crate::pending_payload_cache::{ Availability as PayloadAvailability, @@ -3304,7 +3303,7 @@ impl BeaconChain { self: &Arc, data_columns: Vec>, publish_fn: impl FnOnce() -> Result<(), BlockError>, - ) -> Result { + ) -> Result { let Ok((slot, block_root)) = data_columns .iter() .map(|c| (c.slot(), c.block_root())) @@ -3313,8 +3312,7 @@ impl BeaconChain { else { return Err(BlockError::InternalError( "Columns should be from the same block".to_string(), - ) - .into()); + )); }; let is_gloas = self @@ -3331,7 +3329,7 @@ impl BeaconChain { .fork_choice_read_lock() .contains_block(&block_root) { - return Err(BlockError::DuplicateFullyImported(block_root).into()); + return Err(BlockError::DuplicateFullyImported(block_root)); } self.emit_sse_data_column_sidecar_events( @@ -3357,7 +3355,7 @@ impl BeaconChain { verified_partial: KzgVerifiedPartialDataColumn, verified_header: GossipVerifiedPartialDataColumnHeader, slot: Slot, - ) -> Result, BlockOrEnvelopeError> { + ) -> Result, BlockError> { let block_root = verified_partial.block_root(); let partial = verified_partial.as_data_column(); let index_str = partial.index.to_string(); @@ -3382,7 +3380,7 @@ impl BeaconChain { .fork_choice_read_lock() .contains_block(&block_root) { - return Err(BlockError::DuplicateFullyImported(block_root).into()); + return Err(BlockError::DuplicateFullyImported(block_root)); } let Some(assembler) = self.data_availability_checker.partial_assembler() else { @@ -3426,11 +3424,8 @@ impl BeaconChain { .fork_name_at_slot::(slot) .gloas_enabled() { - let Some(bid) = - load_gloas_payload_bid(block_root, self).map_err(EnvelopeError::from)? - else { - return Err(EnvelopeError::BlockRootUnknown { block_root }.into()); - }; + let bid = load_gloas_payload_bid(block_root, self)? + .ok_or(BlockError::EnvelopeBlockRootUnknown { block_root })?; let availability = self .pending_payload_cache .put_kzg_verified_custody_data_columns( @@ -3438,7 +3433,7 @@ impl BeaconChain { bid, merge_result.full_columns.clone(), ) - .map_err(EnvelopeError::from)?; + .map_err(BlockError::from)?; self.process_payload_availability(slot, availability, || Ok(())) .await? } else { @@ -3505,7 +3500,7 @@ impl BeaconChain { slot: Slot, block_root: Hash256, engine_get_blobs_output: EngineGetBlobsOutput, - ) -> Result { + ) -> Result { // If this block has already been imported to forkchoice it must have been available, so // we don't need to process its blobs again. if self @@ -3513,7 +3508,7 @@ impl BeaconChain { .fork_choice_read_lock() .contains_block(&block_root) { - return Err(BlockError::DuplicateFullyImported(block_root).into()); + return Err(BlockError::DuplicateFullyImported(block_root)); } match &engine_get_blobs_output { @@ -3594,7 +3589,7 @@ impl BeaconChain { pub async fn process_rpc_custody_columns( self: &Arc, custody_columns: DataColumnSidecarList, - ) -> Result { + ) -> Result { let Ok((slot, block_root)) = custody_columns .iter() .map(|c| (c.slot(), c.block_root())) @@ -3603,8 +3598,7 @@ impl BeaconChain { else { return Err(BlockError::InternalError( "Columns should be from the same block".to_string(), - ) - .into()); + )); }; // If this block has already been imported to forkchoice it must have been available, so @@ -3616,7 +3610,7 @@ impl BeaconChain { .fork_choice_read_lock() .contains_block(&block_root) { - return Err(BlockError::DuplicateFullyImported(block_root).into()); + return Err(BlockError::DuplicateFullyImported(block_root)); } // Reject RPC columns referencing unknown parents. Otherwise we allow potentially invalid data @@ -3635,7 +3629,7 @@ impl BeaconChain { .fork_choice_read_lock() .contains_block(&parent_root) { - return Err(BlockError::ParentUnknown { parent_root }.into()); + return Err(BlockError::ParentUnknown { parent_root }); } self.emit_sse_data_column_sidecar_events( @@ -3644,9 +3638,8 @@ impl BeaconChain { custody_columns.iter().map(|column| column.as_ref()), ); - Ok(self - .check_rpc_custody_columns_availability_and_import(slot, block_root, custody_columns) - .await?) + self.check_rpc_custody_columns_availability_and_import(slot, block_root, custody_columns) + .await } pub async fn reconstruct_data_columns( @@ -3658,7 +3651,7 @@ impl BeaconChain { AvailabilityProcessingStatus, DataColumnSidecarList, )>, - BlockOrEnvelopeError, + BlockError, > { // As of now we only reconstruct data columns on supernodes, so if the block is already // available on a supernode, there's no need to reconstruct as the node must already have @@ -3677,11 +3670,8 @@ impl BeaconChain { .gloas_enabled(); if is_gloas { - let Some(bid) = - load_gloas_payload_bid(block_root, self).map_err(EnvelopeError::from)? - else { - return Err(EnvelopeError::BlockRootUnknown { block_root }.into()); - }; + let bid = load_gloas_payload_bid(block_root, self)? + .ok_or(BlockError::EnvelopeBlockRootUnknown { block_root })?; let pending_payload_cache = self.pending_payload_cache.clone(); let result = self .task_executor @@ -3689,8 +3679,8 @@ impl BeaconChain { pending_payload_cache.reconstruct_data_columns(&block_root, bid) }) .await - .map_err(|_| EnvelopeError::from(BeaconChainError::RuntimeShutdown))? - .map_err(EnvelopeError::from)?; + .map_err(|_| BlockError::from(BeaconChainError::RuntimeShutdown))? + .map_err(BlockError::from)?; match result { DataColumnReconstructionResultGloas::Success(( @@ -3991,7 +3981,7 @@ impl BeaconChain { block_root: Hash256, data_columns: Vec>, publish_fn: impl FnOnce() -> Result<(), BlockError>, - ) -> Result { + ) -> Result { if let Some(slasher) = self.slasher.as_ref() { for data_column in &data_columns { // TODO(gloas) different gossip checks in gloas @@ -4007,25 +3997,18 @@ impl BeaconChain { .fork_name_at_slot::(slot) .gloas_enabled() { - let Some(bid) = - load_gloas_payload_bid(block_root, self).map_err(EnvelopeError::from)? - else { - return Ok(AvailabilityProcessingStatus::MissingComponents( - slot, block_root, - )); - }; + let bid = load_gloas_payload_bid(block_root, self)? + .ok_or(BlockError::EnvelopeBlockRootUnknown { block_root })?; let availability = self .pending_payload_cache - .put_gossip_verified_data_columns(block_root, bid, data_columns) - .map_err(EnvelopeError::from)?; + .put_gossip_verified_data_columns(block_root, bid, data_columns)?; Ok(self .process_payload_availability(slot, availability, publish_fn) .await?) } else { let availability = self .data_availability_checker - .put_gossip_verified_data_columns(block_root, slot, data_columns) - .map_err(BlockError::from)?; + .put_gossip_verified_data_columns(block_root, slot, data_columns)?; Ok(self .process_availability(slot, availability, publish_fn) .await?) @@ -4088,7 +4071,7 @@ impl BeaconChain { slot: Slot, block_root: Hash256, engine_get_blobs_output: EngineGetBlobsOutput, - ) -> Result { + ) -> Result { match engine_get_blobs_output { EngineGetBlobsOutput::Blobs(blobs) => { self.check_blob_header_signature_and_slashability( @@ -4120,17 +4103,12 @@ impl BeaconChain { .fork_name_at_slot::(slot) .gloas_enabled() { - let Some(bid) = - load_gloas_payload_bid(block_root, self).map_err(EnvelopeError::from)? - else { - return Ok(AvailabilityProcessingStatus::MissingComponents( - slot, block_root, - )); - }; + let bid = load_gloas_payload_bid(block_root, self)? + .ok_or(BlockError::EnvelopeBlockRootUnknown { block_root })?; let availability = self .pending_payload_cache .put_kzg_verified_custody_data_columns(block_root, bid, data_columns) - .map_err(EnvelopeError::from)?; + .map_err(BlockError::from)?; Ok(self .process_payload_availability(slot, availability, || Ok(())) .await?) @@ -4169,23 +4147,15 @@ impl BeaconChain { .fork_name_at_slot::(slot) .gloas_enabled() { - let Some(bid) = load_gloas_payload_bid(block_root, self).map_err(BlockError::from)? - else { - return Ok(AvailabilityProcessingStatus::MissingComponents( - slot, block_root, - )); - }; + let bid = load_gloas_payload_bid(block_root, self)? + .ok_or(BlockError::EnvelopeBlockRootUnknown { block_root })?; let availability = self .pending_payload_cache .put_rpc_custody_columns(block_root, bid, custody_columns) .map_err(BlockError::from)?; Ok(self .process_payload_availability(slot, availability, || Ok(())) - .await - .map_err(|error| match error { - EnvelopeError::BlockError(error) => error, - error => BlockError::InternalError(error.to_string()), - })?) + .await?) } else { let availability = self .data_availability_checker @@ -4256,7 +4226,7 @@ impl BeaconChain { slot: Slot, availability: PayloadAvailability, publish_fn: impl FnOnce() -> Result<(), BlockError>, - ) -> Result { + ) -> Result { match availability { PayloadAvailability::Available(available_envelope) => { publish_fn()?; diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index b70730d047..eeee562e9c 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -286,6 +286,10 @@ pub enum BlockError { /// TODO: We may need to penalize the peer that gave us a potentially invalid rpc blob. /// https://github.com/sigp/lighthouse/issues/4546 AvailabilityCheck(AvailabilityCheckError), + /// The payload envelope's block root is unknown. + EnvelopeBlockRootUnknown { block_root: Hash256 }, + /// Optimistic sync is not supported for Gloas payload envelopes. + OptimisticSyncNotSupported { block_root: Hash256 }, /// A Blob with a slot after PeerDAS is received and is not required to be imported. /// This can happen because we stay subscribed to the blob subnet after 2 epochs, as we could /// still receive valid blobs from a Deneb epoch after PeerDAS is activated. diff --git a/beacon_node/beacon_chain/src/data_column_verification.rs b/beacon_node/beacon_chain/src/data_column_verification.rs index e342a05798..7036ecdc70 100644 --- a/beacon_node/beacon_chain/src/data_column_verification.rs +++ b/beacon_node/beacon_chain/src/data_column_verification.rs @@ -1307,9 +1307,11 @@ pub(crate) fn load_gloas_payload_bid( block_root: Hash256, chain: &BeaconChain, ) -> Result>, BeaconChainError> { - let bid = if let Some(bid) = chain.pending_payload_cache.get_bid(&block_root) { - bid - } else if let Some(block) = chain.early_attester_cache.get_block(block_root) { + if let Some(bid) = chain.pending_payload_cache.get_bid(&block_root) { + return Ok(Some(bid)); + } + + let bid = if let Some(block) = chain.early_attester_cache.get_block(block_root) { PendingPayloadBid::from_block(block.as_ref()).map_err(BeaconChainError::BeaconStateError)? } else { match chain diff --git a/beacon_node/beacon_chain/src/errors.rs b/beacon_node/beacon_chain/src/errors.rs index 361521cea5..9802f091e0 100644 --- a/beacon_node/beacon_chain/src/errors.rs +++ b/beacon_node/beacon_chain/src/errors.rs @@ -1,4 +1,3 @@ -use crate::BlockError; use crate::beacon_block_streamer::Error as BlockStreamerError; use crate::beacon_chain::ForkChoiceError; use crate::beacon_fork_choice_store::Error as ForkChoiceStoreError; @@ -10,7 +9,6 @@ use crate::observed_attesters::Error as ObservedAttestersError; use crate::observed_block_producers::Error as ObservedBlockProducersError; use crate::observed_data_sidecars::Error as ObservedDataSidecarsError; use crate::payload_envelope_streamer::Error as EnvelopeStreamerError; -use crate::payload_envelope_verification::EnvelopeError; use bls::PublicKeyBytes; use execution_layer::PayloadStatus; use fork_choice::ExecutionStatus; @@ -336,21 +334,3 @@ easy_from_to!(SlotProcessingError, BlockProductionError); easy_from_to!(StateAdvanceError, BlockProductionError); easy_from_to!(ForkChoiceError, BlockProductionError); easy_from_to!(EpochCacheError, BlockProductionError); - -#[derive(Debug)] -pub enum BlockOrEnvelopeError { - BlockError(BlockError), - EnvelopeError(EnvelopeError), -} - -easy_from_to!(BlockError, BlockOrEnvelopeError); -easy_from_to!(EnvelopeError, BlockOrEnvelopeError); - -impl AsRef for BlockOrEnvelopeError { - fn as_ref(&self) -> &str { - match self { - BlockOrEnvelopeError::BlockError(e) => e.as_ref(), - BlockOrEnvelopeError::EnvelopeError(e) => e.as_ref(), - } - } -} diff --git a/beacon_node/beacon_chain/src/fetch_blobs/mod.rs b/beacon_node/beacon_chain/src/fetch_blobs/mod.rs index 7d26a603c8..335d76b410 100644 --- a/beacon_node/beacon_chain/src/fetch_blobs/mod.rs +++ b/beacon_node/beacon_chain/src/fetch_blobs/mod.rs @@ -16,13 +16,13 @@ use crate::blob_verification::{GossipBlobError, KzgVerifiedBlob}; use crate::data_column_verification::{ KzgVerifiedCustodyDataColumn, KzgVerifiedCustodyPartialDataColumn, KzgVerifiedPartialDataColumn, }; -use crate::errors::BlockOrEnvelopeError; #[cfg_attr(test, double)] use crate::fetch_blobs::fetch_blobs_beacon_adapter::FetchBlobsBeaconAdapter; use crate::kzg_utils::blobs_to_partial_data_columns; use crate::observed_data_sidecars::ObservationKey; use crate::{ - AvailabilityProcessingStatus, BeaconChain, BeaconChainError, BeaconChainTypes, metrics, + AvailabilityProcessingStatus, BeaconChain, BeaconChainError, BeaconChainTypes, BlockError, + metrics, }; use execution_layer::Error as ExecutionLayerError; use execution_layer::json_structures::{BlobAndProofV1, BlobAndProofV2, BlobAndProofV3}; @@ -50,7 +50,7 @@ pub enum EngineGetBlobsOutput { pub enum FetchEngineBlobError { BeaconStateError(BeaconStateError), BeaconChainError(Box), - BlobProcessingError(Box), + BlobProcessingError(Box), BlobSidecarError(BlobSidecarError), DataColumnSidecarError(DataColumnSidecarError), ExecutionLayerMissing, diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index bd9c4a7c12..804268a613 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -76,7 +76,7 @@ pub use self::beacon_chain::{ }; pub use self::beacon_snapshot::BeaconSnapshot; pub use self::chain_config::ChainConfig; -pub use self::errors::{BeaconChainError, BlockOrEnvelopeError, BlockProductionError}; +pub use self::errors::{BeaconChainError, BlockProductionError}; pub use self::historical_blocks::HistoricalBlockError; pub use attestation_verification::Error as AttestationError; pub use beacon_fork_choice_store::{ 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 8b7c7eb4d7..975a684e95 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs @@ -15,7 +15,7 @@ use super::{ use crate::data_column_verification::load_gloas_payload_bid; use crate::pending_payload_cache::Availability as PayloadAvailability; use crate::{ - AvailabilityProcessingStatus, BeaconChain, BeaconChainError, BeaconChainTypes, + AvailabilityProcessingStatus, BeaconChain, BeaconChainError, BeaconChainTypes, BlockError, NotifyExecutionLayer, block_verification_types::AvailableBlockData, metrics, @@ -85,7 +85,13 @@ impl BeaconChain { // about what the function actually does. let executed_envelope = chain .into_executed_payload_envelope(execution_pending) - .await?; + .await + .map_err(|error| match error { + BlockError::ExecutionPayloadError(error) => { + EnvelopeError::ExecutionPayloadError(error) + } + error => EnvelopeError::ImportError(error), + })?; // Record the time it took to wait for execution layer verification. if let Some(timestamp) = slot_clock.now_duration() { @@ -96,6 +102,7 @@ impl BeaconChain { self.check_envelope_availability_and_import(executed_envelope) .await + .map_err(EnvelopeError::ImportError) }; // Verify and import the payload envelope. @@ -131,6 +138,14 @@ impl BeaconChain { } Err(EnvelopeError::BeaconChainError(e)) } + Err(EnvelopeError::ImportError(BlockError::BeaconChainError(e))) => { + if matches!(e.as_ref(), BeaconChainError::TokioJoin(_)) { + debug!(error = ?e, "Envelope processing cancelled"); + } else { + warn!(error = ?e, "Execution payload envelope rejected"); + } + Err(EnvelopeError::ImportError(BlockError::BeaconChainError(e))) + } Err(other) => { warn!( reason = other.to_string(), @@ -149,8 +164,8 @@ impl BeaconChain { self: &Arc, slot: Slot, availability: PayloadAvailability, - publish_fn: impl FnOnce() -> Result<(), EnvelopeError>, - ) -> Result { + publish_fn: impl FnOnce() -> Result<(), BlockError>, + ) -> Result { match availability { PayloadAvailability::Available(available_envelope) => { publish_fn()?; @@ -167,11 +182,11 @@ impl BeaconChain { async fn check_envelope_availability_and_import( self: &Arc, envelope: AvailabilityPendingExecutedEnvelope, - ) -> Result { + ) -> Result { let slot = envelope.envelope.slot(); let block_root = envelope.block_root; let bid = load_gloas_payload_bid(block_root, self)? - .ok_or(EnvelopeError::BlockRootUnknown { block_root })?; + .ok_or(BlockError::EnvelopeBlockRootUnknown { block_root })?; let availability = self .pending_payload_cache .put_executed_payload_envelope(bid, envelope)?; @@ -187,7 +202,7 @@ impl BeaconChain { async fn into_executed_payload_envelope( self: Arc, pending_envelope: ExecutionPendingEnvelope, - ) -> Result, EnvelopeError> { + ) -> Result, BlockError> { let ExecutionPendingEnvelope { signed_envelope, block_root, @@ -204,7 +219,7 @@ impl BeaconChain { .payload_verification_status .is_optimistic() { - return Err(EnvelopeError::OptimisticSyncNotSupported { block_root }); + return Err(BlockError::OptimisticSyncNotSupported { block_root }); } Ok(AvailabilityPendingExecutedEnvelope::new( @@ -218,7 +233,7 @@ impl BeaconChain { pub async fn import_available_execution_payload_envelope( self: &Arc, envelope: Box>, - ) -> Result { + ) -> Result { let AvailableExecutedEnvelope { envelope, block_root, @@ -255,13 +270,13 @@ impl BeaconChain { signed_envelope: AvailableEnvelope, block_root: Hash256, payload_verification_status: PayloadVerificationStatus, - ) -> Result { + ) -> Result { // 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. let fork_choice_reader = self.canonical_head.fork_choice_upgradable_read_lock(); if !fork_choice_reader.contains_block(&block_root) { - return Err(EnvelopeError::BlockRootUnknown { block_root }); + return Err(BlockError::EnvelopeBlockRootUnknown { block_root }); } // TODO(gloas) add defensive check to see if payload envelope is already in fork choice @@ -276,7 +291,7 @@ impl BeaconChain { // node which can be eligible for head. fork_choice .on_valid_payload_envelope_received(block_root) - .map_err(|e| EnvelopeError::InternalError(format!("{e:?}")))?; + .map_err(|e| BlockError::InternalError(format!("{e:?}")))?; // TODO(gloas) emit SSE event if the payload became the new head payload 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 56f2c2c22c..43dd23f112 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs @@ -18,7 +18,7 @@ //! //! ``` -use state_processing::{BlockProcessingError, envelope_processing::EnvelopeProcessingError}; +use state_processing::envelope_processing::EnvelopeProcessingError; use std::sync::Arc; use store::Error as DBError; use strum::AsRefStr; @@ -38,7 +38,6 @@ pub mod gossip_verified_envelope; pub mod import; mod payload_notifier; -use crate::data_availability_checker::AvailabilityCheckError; pub use execution_pending_envelope::ExecutionPendingEnvelope; #[derive(Debug)] @@ -173,25 +172,16 @@ pub enum EnvelopeError { payload_slot: Slot, latest_finalized_slot: Slot, }, - /// Optimistic sync is not supported for Gloas payload envelopes. - OptimisticSyncNotSupported { block_root: Hash256 }, /// Some Beacon Chain Error BeaconChainError(Arc), /// Some Beacon State error BeaconStateError(BeaconStateError), - /// Some BlockProcessingError (for electra operations) - BlockProcessingError(BlockProcessingError), /// Some EnvelopeProcessingError EnvelopeProcessingError(EnvelopeProcessingError), /// Error verifying the execution payload ExecutionPayloadError(ExecutionPayloadError), - /// An error from block-level checks reused during envelope import - BlockError(BlockError), - /// The envelope satisfied all validity conditions except consistency - /// with the corresponding columns that we received over gossip/rpc. - AvailabilityCheck(AvailabilityCheckError), - /// Internal error - InternalError(String), + /// An error from importing the envelope. + ImportError(BlockError), } impl std::fmt::Display for EnvelopeError { @@ -224,18 +214,6 @@ impl From for EnvelopeError { } } -impl From for EnvelopeError { - fn from(e: BlockError) -> Self { - EnvelopeError::BlockError(e) - } -} - -impl From for EnvelopeError { - fn from(e: AvailabilityCheckError) -> Self { - EnvelopeError::AvailabilityCheck(e) - } -} - impl From for EnvelopeError { fn from(e: EnvelopeProcessingError) -> Self { match e { diff --git a/beacon_node/beacon_chain/src/payload_envelope_verification/payload_notifier.rs b/beacon_node/beacon_chain/src/payload_envelope_verification/payload_notifier.rs index eb5e13b0cc..0bbe32525a 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/payload_notifier.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/payload_notifier.rs @@ -31,7 +31,8 @@ impl PayloadNotifier { match notify_execution_layer { NotifyExecutionLayer::No if chain.config.optimistic_finalized_sync => { - let new_payload_request = Self::build_new_payload_request(&envelope, &block)?; + let new_payload_request = Self::build_new_payload_request(&envelope, &block) + .map_err(EnvelopeError::ImportError)?; // TODO(gloas): check and test RLP block hash calculation post-Gloas if let Err(e) = new_payload_request.perform_optimistic_sync_verifications() { warn!( diff --git a/beacon_node/beacon_chain/tests/column_verification.rs b/beacon_node/beacon_chain/tests/column_verification.rs index e779135e3f..06a5f44e5f 100644 --- a/beacon_node/beacon_chain/tests/column_verification.rs +++ b/beacon_node/beacon_chain/tests/column_verification.rs @@ -6,8 +6,7 @@ use beacon_chain::test_utils::{ generate_data_column_sidecars_from_block, test_spec, }; use beacon_chain::{ - AvailabilityProcessingStatus, BlockError, BlockOrEnvelopeError, ChainConfig, InvalidSignature, - NotifyExecutionLayer, + AvailabilityProcessingStatus, BlockError, ChainConfig, InvalidSignature, NotifyExecutionLayer, block_verification_types::{AsBlock, LookupBlock}, }; use bls::{Keypair, Signature}; @@ -112,9 +111,7 @@ async fn rpc_columns_with_invalid_header_signature() { .unwrap_err(); assert!(matches!( err, - BlockOrEnvelopeError::BlockError(BlockError::InvalidSignature( - InvalidSignature::ProposerSignature - )) + BlockError::InvalidSignature(InvalidSignature::ProposerSignature) )); } diff --git a/beacon_node/http_api/src/beacon/execution_payload_envelope.rs b/beacon_node/http_api/src/beacon/execution_payload_envelope.rs index 65e1a83840..0eb63d8dfa 100644 --- a/beacon_node/http_api/src/beacon/execution_payload_envelope.rs +++ b/beacon_node/http_api/src/beacon/execution_payload_envelope.rs @@ -7,6 +7,7 @@ use crate::version::{ execution_optimistic_finalized_beacon_response, }; use beacon_chain::data_column_verification::{GossipDataColumnError, GossipVerifiedDataColumn}; +use beacon_chain::payload_envelope_verification::EnvelopeError; use beacon_chain::{BeaconChain, BeaconChainTypes, NotifyExecutionLayer}; use bytes::Bytes; use eth2::types as api_types; @@ -148,7 +149,7 @@ pub async fn publish_execution_payload_envelope( PubsubMessage::ExecutionPayload(Box::new(envelope_for_gossip)), ) .map_err(|_| { - beacon_chain::payload_envelope_verification::EnvelopeError::BeaconChainError(Arc::new( + EnvelopeError::BeaconChainError(Arc::new( beacon_chain::BeaconChainError::UnableToPublish, )) }) 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 6bc3c03b8b..aec2b1fcb8 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -14,8 +14,8 @@ use beacon_chain::payload_bid_verification::PayloadBidError; use beacon_chain::proposer_preferences_verification::ProposerPreferencesError; use beacon_chain::store::Error; use beacon_chain::{ - AvailabilityProcessingStatus, BeaconChainError, BeaconChainTypes, BlockError, - BlockOrEnvelopeError, ForkChoiceError, GossipVerifiedBlock, NotifyExecutionLayer, + AvailabilityProcessingStatus, BeaconChainError, BeaconChainTypes, BlockError, ForkChoiceError, + GossipVerifiedBlock, NotifyExecutionLayer, attestation_verification::{self, Error as AttnError, VerifiedAttestation}, data_availability_checker::AvailabilityCheckErrorCategory, light_client_finality_update_verification::Error as LightClientFinalityUpdateError, @@ -1407,7 +1407,7 @@ impl NetworkBeaconProcessor { self.check_reconstruction_trigger(slot, &block_root).await; } }, - Err(BlockOrEnvelopeError::BlockError(BlockError::DuplicateFullyImported(_))) => { + Err(BlockError::DuplicateFullyImported(_)) => { debug!( ?block_root, data_column_index, "Ignoring gossip column already imported" @@ -1538,7 +1538,7 @@ impl NetworkBeaconProcessor { self.check_reconstruction_trigger(*slot, block_root).await; } }, - Err(BlockOrEnvelopeError::BlockError(BlockError::DuplicateFullyImported(_))) => { + Err(BlockError::DuplicateFullyImported(_)) => { debug!( ?block_root, data_column_index, "Ignoring completed gossip column already imported" @@ -1846,7 +1846,10 @@ impl NetworkBeaconProcessor { return None; } // BlobNotRequired is unreachable. Only constructed in `process_gossip_blob` - Err(e @ BlockError::InternalError(_)) | Err(e @ BlockError::BlobNotRequired(_)) => { + Err(e @ BlockError::InternalError(_)) + | Err(e @ BlockError::BlobNotRequired(_)) + | Err(e @ BlockError::EnvelopeBlockRootUnknown { .. }) + | Err(e @ BlockError::OptimisticSyncNotSupported { .. }) => { error!(error = %e, "Internal block gossip validation error"); return None; } @@ -3833,8 +3836,7 @@ impl NetworkBeaconProcessor { | EnvelopeError::UnknownValidator { .. } | EnvelopeError::IncorrectBlockProposer { .. } | EnvelopeError::ExecutionPayloadError(_) - | EnvelopeError::EnvelopeProcessingError(_) - | EnvelopeError::BlockError(_) => { + | EnvelopeError::EnvelopeProcessingError(_) => { self.propagate_validation_result( message_id, peer_id, @@ -3914,12 +3916,9 @@ impl NetworkBeaconProcessor { } EnvelopeError::PriorToFinalization { .. } - | EnvelopeError::OptimisticSyncNotSupported { .. } | EnvelopeError::BeaconChainError(_) | EnvelopeError::BeaconStateError(_) - | EnvelopeError::BlockProcessingError(_) - | EnvelopeError::InternalError(_) - | EnvelopeError::AvailabilityCheck(_) => { + | EnvelopeError::ImportError(_) => { self.propagate_validation_result( message_id, peer_id, @@ -4010,7 +4009,8 @@ impl NetworkBeaconProcessor { | EnvelopeError::BlockHashMismatch { .. } | EnvelopeError::UnknownValidator { .. } | EnvelopeError::IncorrectBlockProposer { .. } - | EnvelopeError::ExecutionPayloadError(_) => { + | EnvelopeError::ExecutionPayloadError(_) + | EnvelopeError::EnvelopeProcessingError(_) => { self.gossip_penalize_peer( peer_id, PeerAction::LowToleranceError, @@ -4018,23 +4018,11 @@ impl NetworkBeaconProcessor { ); } - EnvelopeError::EnvelopeProcessingError(_) - | EnvelopeError::BlockError(_) - | EnvelopeError::BlockRootUnknown { .. } => { - self.gossip_penalize_peer( - peer_id, - PeerAction::LowToleranceError, - "gossip_envelope_processing_error", - ); - } - - EnvelopeError::PriorToFinalization { .. } - | EnvelopeError::OptimisticSyncNotSupported { .. } + EnvelopeError::BlockRootUnknown { .. } + | EnvelopeError::PriorToFinalization { .. } | EnvelopeError::BeaconChainError(_) | EnvelopeError::BeaconStateError(_) - | EnvelopeError::BlockProcessingError(_) - | EnvelopeError::InternalError(_) - | EnvelopeError::AvailabilityCheck(_) => {} + | EnvelopeError::ImportError(_) => {} }, } } diff --git a/beacon_node/network/src/network_beacon_processor/mod.rs b/beacon_node/network/src/network_beacon_processor/mod.rs index c1980215d1..817d4bd5ca 100644 --- a/beacon_node/network/src/network_beacon_processor/mod.rs +++ b/beacon_node/network/src/network_beacon_processor/mod.rs @@ -7,9 +7,7 @@ use beacon_chain::data_column_verification::{GossipDataColumnError, observe_goss use beacon_chain::fetch_blobs::{ EngineGetBlobsOutput, FetchEngineBlobError, fetch_and_process_engine_blobs, }; -use beacon_chain::{ - AvailabilityProcessingStatus, BeaconChain, BeaconChainTypes, BlockError, BlockOrEnvelopeError, -}; +use beacon_chain::{AvailabilityProcessingStatus, BeaconChain, BeaconChainTypes, BlockError}; use beacon_processor::{ BeaconProcessorSend, DuplicateCache, GossipAggregatePackage, GossipAttestationPackage, Work, WorkEvent as BeaconWorkEvent, @@ -983,10 +981,7 @@ impl NetworkBeaconProcessor { ); } Err(FetchEngineBlobError::BlobProcessingError(e)) - if matches!( - *e, - BlockOrEnvelopeError::BlockError(BlockError::DuplicateFullyImported(..)) - ) => + if matches!(*e, BlockError::DuplicateFullyImported(..)) => { debug!( %block_root, @@ -1055,7 +1050,7 @@ impl NetworkBeaconProcessor { "Reconstruction not required for block" ); } - Err(BlockOrEnvelopeError::BlockError(BlockError::DuplicateFullyImported(_))) => { + Err(BlockError::DuplicateFullyImported(_)) => { debug!("Block already imported in parallel with reconstruction"); } Err(e) => { diff --git a/beacon_node/network/src/network_beacon_processor/sync_methods.rs b/beacon_node/network/src/network_beacon_processor/sync_methods.rs index 03be073e7d..61d1b84950 100644 --- a/beacon_node/network/src/network_beacon_processor/sync_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/sync_methods.rs @@ -11,9 +11,8 @@ use beacon_chain::block_verification_types::{AsBlock, RangeSyncBlock}; use beacon_chain::data_availability_checker::AvailabilityCheckError; use beacon_chain::historical_data_columns::HistoricalDataColumnError; use beacon_chain::{ - AvailabilityProcessingStatus, BeaconChainTypes, BlockError, BlockOrEnvelopeError, - ChainSegmentResult, HistoricalBlockError, NotifyExecutionLayer, - validator_monitor::get_slot_delay_ms, + AvailabilityProcessingStatus, BeaconChainTypes, BlockError, ChainSegmentResult, + HistoricalBlockError, NotifyExecutionLayer, validator_monitor::get_slot_delay_ms, }; use beacon_processor::{ AsyncFn, BlockingFn, DuplicateCache, @@ -346,7 +345,7 @@ impl NetworkBeaconProcessor { // Sync handles these results self.send_sync_message(SyncMessage::BlockComponentProcessed { process_type, - result: result.map_err(Into::into).into(), + result: result.into(), }); } @@ -411,7 +410,7 @@ impl NetworkBeaconProcessor { ); } }, - Err(BlockOrEnvelopeError::BlockError(BlockError::DuplicateFullyImported(_))) => { + Err(BlockError::DuplicateFullyImported(_)) => { debug!( block_hash = %block_root, "Custody columns have already been imported" diff --git a/beacon_node/network/src/sync/block_lookups/mod.rs b/beacon_node/network/src/sync/block_lookups/mod.rs index ab9c7bbe38..3929f74aa0 100644 --- a/beacon_node/network/src/sync/block_lookups/mod.rs +++ b/beacon_node/network/src/sync/block_lookups/mod.rs @@ -33,9 +33,7 @@ use beacon_chain::block_verification_types::AsBlock; use beacon_chain::data_availability_checker::{ AvailabilityCheckError, AvailabilityCheckErrorCategory, }; -use beacon_chain::{ - AvailabilityProcessingStatus, BeaconChainTypes, BlockError, BlockOrEnvelopeError, -}; +use beacon_chain::{AvailabilityProcessingStatus, BeaconChainTypes, BlockError}; pub use common::RequestState; use fnv::FnvHashMap; use lighthouse_network::service::api_types::SingleLookupReqId; @@ -591,12 +589,8 @@ impl BlockLookups { let action = match result { BlockProcessingResult::Ok(AvailabilityProcessingStatus::Imported(_)) - | BlockProcessingResult::Err(BlockOrEnvelopeError::BlockError( - BlockError::DuplicateFullyImported(..), - )) - | BlockProcessingResult::Err(BlockOrEnvelopeError::BlockError( - BlockError::GenesisBlock, - )) => { + | BlockProcessingResult::Err(BlockError::DuplicateFullyImported(..)) + | BlockProcessingResult::Err(BlockError::GenesisBlock) => { // Successfully imported request_state.on_processing_success()?; Action::Continue @@ -620,9 +614,7 @@ impl BlockLookups { Action::Retry } } - BlockProcessingResult::Err(BlockOrEnvelopeError::BlockError( - BlockError::DuplicateImportStatusUnknown(..), - )) => { + BlockProcessingResult::Err(BlockError::DuplicateImportStatusUnknown(..)) => { // This is unreachable because RPC blocks do not undergo gossip verification, and // this error can *only* come from gossip verification. error!(?block_root, "Single block lookup hit unreachable condition"); @@ -638,11 +630,6 @@ impl BlockLookups { Action::Drop("Block processing ignored".to_owned()) } BlockProcessingResult::Err(e) => { - let BlockOrEnvelopeError::BlockError(e) = e else { - // TODO(gloas): handle properly - return Err(LookupRequestError::Failed(format!("{e:?}"))); - }; - match e { BlockError::BeaconChainError(e) => { // Internal error diff --git a/beacon_node/network/src/sync/manager.rs b/beacon_node/network/src/sync/manager.rs index 1b45ea7052..8eba8300cd 100644 --- a/beacon_node/network/src/sync/manager.rs +++ b/beacon_node/network/src/sync/manager.rs @@ -50,8 +50,7 @@ use crate::sync::custody_backfill_sync::CustodyBackFillSync; use crate::sync::network_context::{PeerGroup, RpcResponseResult}; use beacon_chain::block_verification_types::AsBlock; use beacon_chain::{ - AvailabilityProcessingStatus, BeaconChain, BeaconChainTypes, BlockError, BlockOrEnvelopeError, - EngineState, + AvailabilityProcessingStatus, BeaconChain, BeaconChainTypes, BlockError, EngineState, }; use futures::StreamExt; use lighthouse_network::SyncInfo; @@ -207,7 +206,7 @@ impl BlockProcessType { #[derive(Debug)] pub enum BlockProcessingResult { Ok(AvailabilityProcessingStatus), - Err(BlockOrEnvelopeError), + Err(BlockError), Ignored, } @@ -1450,8 +1449,8 @@ impl SyncManager { } } -impl From> for BlockProcessingResult { - fn from(result: Result) -> Self { +impl From> for BlockProcessingResult { + fn from(result: Result) -> Self { match result { Ok(status) => BlockProcessingResult::Ok(status), Err(e) => BlockProcessingResult::Err(e), @@ -1459,14 +1458,8 @@ impl From> for BlockP } } -impl From for BlockProcessingResult { - fn from(e: BlockOrEnvelopeError) -> Self { +impl From for BlockProcessingResult { + fn from(e: BlockError) -> Self { BlockProcessingResult::Err(e) } } - -impl From for BlockProcessingResult { - fn from(e: BlockError) -> Self { - BlockProcessingResult::Err(BlockOrEnvelopeError::BlockError(e)) - } -} From 23d5be1a0e57403dcb27b5ff2689a3e2db7dc350 Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Thu, 30 Apr 2026 13:40:39 +0200 Subject: [PATCH 079/118] Fix pending payload cache test lint --- beacon_node/beacon_chain/src/pending_payload_cache/mod.rs | 4 ++-- .../network/src/network_beacon_processor/sync_methods.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs b/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs index 21a86f05cc..c65d5ec556 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs @@ -638,7 +638,7 @@ mod data_availability_checker_tests { use tempfile::{TempDir, tempdir}; use types::{ ExecutionPayloadEnvelope, ExecutionPayloadGloas, ExecutionRequests, ForkName, - MinimalEthSpec, SignedExecutionPayloadEnvelope, Slot, + MinimalEthSpec, SignedExecutionPayloadEnvelope, }; type E = MinimalEthSpec; @@ -783,7 +783,7 @@ mod data_availability_checker_tests { init_block(&cache, &harness.spec, NumBlobs::Number(1), RNG_SEED); let result = cache - .put_rpc_custody_columns(block_root, bid, data_columns) + .put_rpc_custody_columns(block_root, bid.clone(), data_columns) .expect("should put columns"); assert!(matches!(result, Availability::MissingComponents(_))); diff --git a/beacon_node/network/src/network_beacon_processor/sync_methods.rs b/beacon_node/network/src/network_beacon_processor/sync_methods.rs index 61d1b84950..988a68c9dd 100644 --- a/beacon_node/network/src/network_beacon_processor/sync_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/sync_methods.rs @@ -234,7 +234,7 @@ impl NetworkBeaconProcessor { // Sync handles these results self.send_sync_message(SyncMessage::BlockComponentProcessed { process_type, - result: result.map_err(Into::into).into(), + result: result.into(), }); // Drop the handle to remove the entry from the cache From 4b1aab9fb7aa8828409fe91198f932fcc8e56f78 Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Thu, 30 Apr 2026 13:44:10 +0200 Subject: [PATCH 080/118] Allow large stack frame in proposer_boost_re_org_test Rust 1.94.0 raised `clippy::large_stack_frames` against this function (548365 bytes vs. 512000 default threshold), failing pre-push lint. Match the per-function `#[allow]` pattern used elsewhere in the codebase (store_tests.rs, basic_sim.rs, etc.). --- beacon_node/http_api/tests/interactive_tests.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/beacon_node/http_api/tests/interactive_tests.rs b/beacon_node/http_api/tests/interactive_tests.rs index 15f61537a0..d82f291a1c 100644 --- a/beacon_node/http_api/tests/interactive_tests.rs +++ b/beacon_node/http_api/tests/interactive_tests.rs @@ -385,6 +385,7 @@ pub async fn proposer_boost_re_org_weight_misprediction() { /// - `num_empty_votes`: percentage of comm of attestations for the parent block /// - `num_head_votes`: number of attestations for the head block /// - `should_re_org`: whether the proposer should build on the parent rather than the head +#[allow(clippy::large_stack_frames)] pub async fn proposer_boost_re_org_test( ReOrgTest { head_slot, From 98d858d94202fcc864be550a3a13263d771dc63b Mon Sep 17 00:00:00 2001 From: Josh King Date: Thu, 30 Apr 2026 14:21:02 +0200 Subject: [PATCH 081/118] fix: restore genesis_block bid population for ef-tests The alpha-7 spec tests expect the Gloas genesis block body to contain the execution payload bid from state. Restores the genesis_block() function and body_root fixup that were removed in PR #9244. The fork choice from_anchor fix (reading latest_block_hash when bid hashes are zero) remains for Kurtosis/external genesis compatibility. --- consensus/state_processing/src/genesis.rs | 28 ++++++++++++++++++----- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/consensus/state_processing/src/genesis.rs b/consensus/state_processing/src/genesis.rs index 46541e0326..c643ad56e3 100644 --- a/consensus/state_processing/src/genesis.rs +++ b/consensus/state_processing/src/genesis.rs @@ -167,12 +167,19 @@ pub fn initialize_beacon_state_from_eth1( // Remove intermediate Fulu fork from `state.fork`. state.fork_mut().previous_version = spec.gloas_fork_version; + // The genesis block's bid must have block_hash = 0x00 per spec (empty payload). // Retain the EL genesis hash in latest_block_hash and parent_block_hash so the // first post-genesis proposer can build on the correct EL head. let el_genesis_hash = state.latest_execution_payload_bid()?.block_hash; let bid = state.latest_execution_payload_bid_mut()?; bid.parent_block_hash = el_genesis_hash; bid.block_hash = ExecutionBlockHash::default(); + + // Update the `latest_block_header.body_root` so that it matches the body of the + // Gloas genesis block, which embeds `state.latest_execution_payload_bid` in its + // `signed_execution_payload_bid` field (see `genesis_block`). + let genesis_body_root = genesis_block(&state, spec)?.body_root(); + state.latest_block_header_mut().body_root = genesis_body_root; } // Now that we have our validators, initialize the caches (including the committees) @@ -184,16 +191,25 @@ pub fn initialize_beacon_state_from_eth1( Ok(state) } -/// Create an unsigned genesis `BeaconBlock` matching the genesis state. +/// Create an unsigned genesis `BeaconBlock`. /// -/// Per spec, the genesis block body is empty (all default fields). -/// `state.latest_block_header.body_root` is set from `BeaconBlock::empty()`, -/// so this function must return the same empty block to keep roots consistent. +/// Per spec, the genesis block body is empty (all default fields) except for Gloas, +/// where `body.signed_execution_payload_bid.message` is initialised from +/// `state.latest_execution_payload_bid` so that the first post-genesis proposer can +/// build on the correct execution layer head. +/// +/// `state.latest_block_header.body_root` is set from this same block's body, so the +/// two must stay in sync. pub fn genesis_block( - _genesis_state: &BeaconState, + state: &BeaconState, spec: &ChainSpec, ) -> Result, BeaconStateError> { - Ok(BeaconBlock::empty(spec)) + let mut block = BeaconBlock::empty(spec); + if let BeaconBlock::Gloas(ref mut gloas_block) = block { + let bid = state.latest_execution_payload_bid()?.clone(); + gloas_block.body.signed_execution_payload_bid.message = bid; + } + Ok(block) } /// Determine whether a candidate genesis state is suitable for starting the chain. From 8e199552d672c41964dbf2cc3d335afc4861f39c Mon Sep 17 00:00:00 2001 From: Daniel Knopik Date: Thu, 30 Apr 2026 14:52:45 +0200 Subject: [PATCH 082/118] Fix pending_payload_cache tests for sampling column filtering Tests were switched from put_kzg_verified_custody_data_columns (no filtering) to put_rpc_custody_columns (filters to sampling columns) but assertions still assumed all provided columns would be stored. Account for the sampling filter in each test's setup and assertions. --- .../src/pending_payload_cache/mod.rs | 35 ++++++++++++++++--- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs b/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs index c65d5ec556..01a38c3821 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs @@ -756,7 +756,15 @@ mod data_availability_checker_tests { let (harness, cache, _path) = setup().await; let (bid, block_root, data_columns) = init_block(&cache, &harness.spec, NumBlobs::Number(1), RNG_SEED); - let column = data_columns.first().cloned().expect("should have column"); + let epoch = bid.slot.epoch(E::slots_per_epoch()); + let sampling_cols = cache + .custody_context() + .sampling_columns_for_epoch(epoch, &harness.spec); + let column = data_columns + .iter() + .find(|c| sampling_cols.contains(c.index())) + .cloned() + .expect("should have a sampling column"); let column_index = *column.index(); for _ in 0..2 { @@ -782,6 +790,12 @@ mod data_availability_checker_tests { let (bid, block_root, data_columns) = init_block(&cache, &harness.spec, NumBlobs::Number(1), RNG_SEED); + let epoch = bid.slot.epoch(E::slots_per_epoch()); + let num_sampling_columns = cache + .custody_context() + .sampling_columns_for_epoch(epoch, &harness.spec) + .len(); + let result = cache .put_rpc_custody_columns(block_root, bid.clone(), data_columns) .expect("should put columns"); @@ -794,7 +808,7 @@ mod data_availability_checker_tests { panic!("expected available envelope"); }; assert_eq!(envelope.block_root, block_root); - assert_eq!(envelope.envelope.columns.len(), E::number_of_columns()); + assert_eq!(envelope.envelope.columns.len(), num_sampling_columns); } #[tokio::test] @@ -834,7 +848,17 @@ mod data_availability_checker_tests { let (harness, cache, _path) = setup().await; let (bid, block_root, data_columns) = init_block(&cache, &harness.spec, NumBlobs::Number(1), RNG_SEED); - let columns = data_columns.into_iter().take(5).collect(); + + let epoch = bid.slot.epoch(E::slots_per_epoch()); + let sampling_cols = cache + .custody_context() + .sampling_columns_for_epoch(epoch, &harness.spec); + let columns: Vec<_> = data_columns + .into_iter() + .filter(|c| sampling_cols.contains(c.index())) + .take(5) + .collect(); + let num_columns = columns.len(); cache .put_rpc_custody_columns(block_root, bid, columns) @@ -843,7 +867,7 @@ mod data_availability_checker_tests { cache .cached_data_column_indexes(&block_root) .map(|indices| indices.len()), - Some(5) + Some(num_columns) ); cache.handle_reconstruction_failure(&block_root); @@ -875,6 +899,7 @@ mod data_availability_checker_tests { let (harness, cache, _path) = setup().await; let (bid, block_root, data_columns) = init_block(&cache, &harness.spec, NumBlobs::Number(1), RNG_SEED); + let block_epoch = bid.slot.epoch(E::slots_per_epoch()); let column = data_columns.first().cloned().expect("should have column"); cache @@ -883,7 +908,7 @@ mod data_availability_checker_tests { assert_eq!(cache.block_cache_size(), 1); cache - .do_maintenance(Epoch::new(1)) + .do_maintenance(block_epoch + 1) .expect("maintenance should succeed"); assert_eq!(cache.block_cache_size(), 0); } From 22a207ffa8a6d8c1d88da7545607f989e3976d05 Mon Sep 17 00:00:00 2001 From: Josh King Date: Thu, 30 Apr 2026 15:26:20 +0200 Subject: [PATCH 083/118] fix: fallback to empty genesis block for external genesis states --- beacon_node/beacon_chain/src/builder.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index d70561db9b..5fb572d15c 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -46,8 +46,8 @@ use tracing::{debug, error, info, warn}; use tree_hash::TreeHash; use types::data::CustodyIndex; use types::{ - BeaconState, BlobSidecarList, ChainSpec, ColumnIndex, DataColumnSidecarList, Epoch, EthSpec, - Hash256, SignedBeaconBlock, Slot, + BeaconBlock, BeaconState, BlobSidecarList, ChainSpec, ColumnIndex, DataColumnSidecarList, + Epoch, EthSpec, Hash256, SignedBeaconBlock, Slot, }; /// An empty struct used to "witness" all the `BeaconChainTypes` traits. It has no user-facing @@ -1177,9 +1177,19 @@ fn make_genesis_block( genesis_state: &mut BeaconState, spec: &ChainSpec, ) -> Result, String> { + // For Gloas, genesis_block() populates the bid in the block body. However, if + // the genesis state was produced by an external tool (e.g. ethereum-genesis-generator), + // its latest_block_header.body_root may correspond to an empty block. In that case, + // use an empty block so the stored block root matches what fork choice derives from + // the state's latest_block_header. let mut block = genesis_block(genesis_state, spec) .map_err(|e| format!("Error building genesis block: {:?}", e))?; + let state_body_root = genesis_state.latest_block_header().body_root; + if state_body_root != block.body_root() { + block = BeaconBlock::empty(spec); + } + *block.state_root_mut() = genesis_state .update_tree_hash_cache() .map_err(|e| format!("Error hashing genesis state: {:?}", e))?; From a840c40cbe2b6bc0a691e3e18b094d45d570420f Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Thu, 30 Apr 2026 16:40:05 +0200 Subject: [PATCH 084/118] Gloas range sync --- beacon_node/network/src/sync/block_sidecar_coupling.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/beacon_node/network/src/sync/block_sidecar_coupling.rs b/beacon_node/network/src/sync/block_sidecar_coupling.rs index 5de38ad9a5..4eab7f9e67 100644 --- a/beacon_node/network/src/sync/block_sidecar_coupling.rs +++ b/beacon_node/network/src/sync/block_sidecar_coupling.rs @@ -1279,6 +1279,7 @@ mod tests { execution_requests: ExecutionRequests::default(), builder_index: 0, beacon_block_root: Hash256::from(rng.random::<[u8; 32]>()), + parent_beacon_block_root: Hash256::repeat_byte(0), }; Arc::new(SignedExecutionPayloadEnvelope { message: envelope, From aa6fdb8d6a191ae49daf020e4a5ab0ace4e69985 Mon Sep 17 00:00:00 2001 From: Josh King Date: Thu, 30 Apr 2026 17:27:37 +0200 Subject: [PATCH 085/118] fix: tighten genesis block fallback to match empty body root only --- beacon_node/beacon_chain/src/builder.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index 5fb572d15c..95b5f32a94 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -1186,7 +1186,9 @@ fn make_genesis_block( .map_err(|e| format!("Error building genesis block: {:?}", e))?; let state_body_root = genesis_state.latest_block_header().body_root; - if state_body_root != block.body_root() { + if state_body_root != block.body_root() + && state_body_root == BeaconBlock::::empty(spec).body_root() + { block = BeaconBlock::empty(spec); } From 361bc374ddcf6086f251d63b6075bedd96587921 Mon Sep 17 00:00:00 2001 From: Daniel Knopik Date: Fri, 1 May 2026 01:03:33 +0200 Subject: [PATCH 086/118] fix gloas tests --- beacon_node/beacon_chain/tests/events.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/beacon_node/beacon_chain/tests/events.rs b/beacon_node/beacon_chain/tests/events.rs index e943514c4e..2fded30b2a 100644 --- a/beacon_node/beacon_chain/tests/events.rs +++ b/beacon_node/beacon_chain/tests/events.rs @@ -84,6 +84,11 @@ async fn data_column_sidecar_event_on_process_gossip_data_column() { let epoch = slot.epoch(E::slots_per_epoch()); random_sidecar.slot = slot; random_sidecar.index = harness.chain.sampling_columns_for_epoch(epoch)[0]; + + // For gloas, the bid must be known, e.g. in the pending payload cache + let bid = harness.chain.pending_payload_cache.init_pending_bid(random_sidecar.beacon_block_root, PendingPa) + + DataColumnSidecar::Gloas(random_sidecar) } else { let mut random_sidecar = DataColumnSidecarFulu::random_for_test(&mut rng); From 16a3dfbc8997f109d5f88357113530dbabcf78c9 Mon Sep 17 00:00:00 2001 From: Daniel Knopik Date: Fri, 1 May 2026 01:52:47 +0200 Subject: [PATCH 087/118] fix events test --- .../beacon_chain/src/pending_payload_cache/mod.rs | 2 +- beacon_node/beacon_chain/tests/events.rs | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs b/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs index 01a38c3821..b490b11522 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs @@ -68,7 +68,7 @@ use crate::metrics::{ KZG_DATA_COLUMN_RECONSTRUCTION_ATTEMPTS, KZG_DATA_COLUMN_RECONSTRUCTION_FAILURES, }; use crate::observed_data_sidecars::ObservationStrategy; -pub(crate) use pending_components::PendingPayloadBid; +pub use pending_components::PendingPayloadBid; use pending_components::{PendingComponents, ReconstructColumnsDecision}; use types::new_non_zero_usize; diff --git a/beacon_node/beacon_chain/tests/events.rs b/beacon_node/beacon_chain/tests/events.rs index 2fded30b2a..26d004d069 100644 --- a/beacon_node/beacon_chain/tests/events.rs +++ b/beacon_node/beacon_chain/tests/events.rs @@ -1,5 +1,6 @@ use beacon_chain::blob_verification::GossipVerifiedBlob; use beacon_chain::data_column_verification::GossipVerifiedDataColumn; +use beacon_chain::pending_payload_cache::PendingPayloadBid; use beacon_chain::test_utils::{ BeaconChainHarness, fork_name_from_env, generate_data_column_sidecars_from_block, test_spec, }; @@ -11,7 +12,8 @@ use types::data::FixedBlobSidecarList; use types::test_utils::TestRandom; use types::{ BlobSidecar, DataColumnSidecar, DataColumnSidecarFulu, DataColumnSidecarGloas, Domain, EthSpec, - MinimalEthSpec, PayloadAttestationData, PayloadAttestationMessage, SignedRoot, Slot, + KzgCommitments, MinimalEthSpec, PayloadAttestationData, PayloadAttestationMessage, SignedRoot, + Slot, }; type E = MinimalEthSpec; @@ -86,8 +88,13 @@ async fn data_column_sidecar_event_on_process_gossip_data_column() { random_sidecar.index = harness.chain.sampling_columns_for_epoch(epoch)[0]; // For gloas, the bid must be known, e.g. in the pending payload cache - let bid = harness.chain.pending_payload_cache.init_pending_bid(random_sidecar.beacon_block_root, PendingPa) - + harness.chain.pending_payload_cache.init_pending_bid( + random_sidecar.beacon_block_root, + PendingPayloadBid { + slot: Slot::new(10), + blob_kzg_commitments: KzgCommitments::::empty(), + }, + ); DataColumnSidecar::Gloas(random_sidecar) } else { From 3e331ff207a651c2f38e6adb30e4e29ffeea02ef Mon Sep 17 00:00:00 2001 From: Daniel Knopik Date: Fri, 1 May 2026 01:02:28 +0200 Subject: [PATCH 088/118] fix another bug --- beacon_node/http_api/src/beacon/execution_payload_envelope.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/beacon_node/http_api/src/beacon/execution_payload_envelope.rs b/beacon_node/http_api/src/beacon/execution_payload_envelope.rs index 0eb63d8dfa..aea0ccdd89 100644 --- a/beacon_node/http_api/src/beacon/execution_payload_envelope.rs +++ b/beacon_node/http_api/src/beacon/execution_payload_envelope.rs @@ -273,7 +273,8 @@ fn build_gloas_data_columns( let index = *col.index(); match GossipVerifiedDataColumn::new_for_block_publishing(col, chain) { Ok(verified) => Some(verified), - Err(GossipDataColumnError::PriorKnownUnpublished) => None, + Err(GossipDataColumnError::PriorKnownUnpublished) + | Err(GossipDataColumnError::PriorKnown { .. }) => None, Err(e) => { warn!( %slot, From 48b24e9029236e6730932c935ff507b7674b7906 Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Fri, 1 May 2026 02:05:48 +0200 Subject: [PATCH 089/118] Use SignedExecutionPayloadBid directly in pending payload cache - Replace the `PendingPayloadBid` projection (slot + blob_kzg_commitments) with `Arc>`. Cloning becomes a cheap Arc bump and the bid carries enough context for future bid<->envelope cross-checks. Add a `signed_payload_bid_from_block` helper. - `PendingColumn` switches from a pre-sized `Vec>` to a sparse `HashMap`; the `new_with_capacity(num_blobs)` constructor is gone since callers no longer need to know the blob count up front. - `PendingComponents::merge_data_columns` takes a slice instead of an owning iterator (it only borrows + clones cells). - Store `block_root` in `PendingComponents` so `make_available` and `get_cached_data_columns` no longer require it as an argument (the arg was misnamed `block_hash` in `make_available`). - Rename `PendingComponents::empty` -> `new`; it is the only constructor. --- beacon_node/beacon_chain/src/beacon_chain.rs | 9 +-- .../src/data_column_verification.rs | 31 ++++---- .../src/pending_payload_cache/mod.rs | 70 ++++++++++-------- .../pending_payload_cache/pending_column.rs | 41 ++++------- .../pending_components.rs | 71 ++++++++----------- beacon_node/beacon_chain/tests/events.rs | 18 +++-- 6 files changed, 111 insertions(+), 129 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 76e7b217ce..7a8a747d8c 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -70,7 +70,8 @@ use crate::payload_envelope_streamer::{EnvelopeRequestSource, launch_payload_env use crate::pending_payload_cache::PendingPayloadCache; use crate::pending_payload_cache::{ Availability as PayloadAvailability, - DataColumnReconstructionResult as DataColumnReconstructionResultGloas, PendingPayloadBid, + DataColumnReconstructionResult as DataColumnReconstructionResultGloas, + signed_payload_bid_from_block, }; use crate::pending_payload_envelopes::PendingPayloadEnvelopes; use crate::persisted_beacon_chain::PersistedBeaconChain; @@ -3431,7 +3432,7 @@ impl BeaconChain { .put_kzg_verified_custody_data_columns( block_root, bid, - merge_result.full_columns.clone(), + &merge_result.full_columns, ) .map_err(BlockError::from)?; self.process_payload_availability(slot, availability, || Ok(())) @@ -3818,7 +3819,7 @@ impl BeaconChain { let block = execution_pending.block.block_cloned(); if block.fork_name_unchecked().gloas_enabled() { - let bid = PendingPayloadBid::from_block(block.as_ref())?; + let bid = signed_payload_bid_from_block(block.as_ref())?; chain .pending_payload_cache .init_pending_bid(block_root, bid); @@ -4107,7 +4108,7 @@ impl BeaconChain { .ok_or(BlockError::EnvelopeBlockRootUnknown { block_root })?; let availability = self .pending_payload_cache - .put_kzg_verified_custody_data_columns(block_root, bid, data_columns) + .put_kzg_verified_custody_data_columns(block_root, bid, &data_columns) .map_err(BlockError::from)?; Ok(self .process_payload_availability(slot, availability, || Ok(())) diff --git a/beacon_node/beacon_chain/src/data_column_verification.rs b/beacon_node/beacon_chain/src/data_column_verification.rs index 7036ecdc70..9a48477ce8 100644 --- a/beacon_node/beacon_chain/src/data_column_verification.rs +++ b/beacon_node/beacon_chain/src/data_column_verification.rs @@ -9,7 +9,7 @@ use crate::kzg_utils::{ use crate::observed_data_sidecars::{ Error as ObservedDataSidecarsError, ObservationKey, ObservationStrategy, Observe, }; -use crate::pending_payload_cache::PendingPayloadBid; +use crate::pending_payload_cache::signed_payload_bid_from_block; use crate::{BeaconChain, BeaconChainError, BeaconChainTypes, metrics}; use educe::Educe; use fork_choice::ProtoBlock; @@ -32,7 +32,7 @@ use types::data::{ use types::{ BeaconStateError, ChainSpec, DataColumnSidecar, DataColumnSidecarFulu, DataColumnSidecarGloas, DataColumnSubnetId, EthSpec, Hash256, KzgCommitment, PartialDataColumnSidecarRef, - SignedBeaconBlockHeader, Slot, + SignedBeaconBlockHeader, SignedExecutionPayloadBid, Slot, }; /// An error occurred while validating a gossip data column. @@ -373,15 +373,15 @@ impl GossipVerifiedDataColumn )?; } DataColumnSidecar::Gloas(_) => { - let kzg_commitments = load_gloas_payload_bid(column_sidecar.block_root(), chain)? - .ok_or(GossipDataColumnError::BlockRootUnknown { + let bid = load_gloas_payload_bid(column_sidecar.block_root(), chain)?.ok_or( + GossipDataColumnError::BlockRootUnknown { block_root: column_sidecar.block_root(), slot: column_sidecar.slot(), - })? - .blob_kzg_commitments; + }, + )?; verify_data_column_sidecar_with_commitments_len( &column_sidecar, - kzg_commitments.len(), + bid.message.blob_kzg_commitments.len(), &chain.spec, )?; } @@ -1091,12 +1091,13 @@ pub fn validate_data_column_sidecar_for_gossip_gloas< verify_slot_greater_than_latest_finalized_slot(chain, column_slot)?; verify_is_unknown_sidecar(chain, &data_column)?; - let kzg_commitments = load_gloas_payload_bid(data_column.block_root(), chain)? - .ok_or(GossipDataColumnError::BlockRootUnknown { + let bid = load_gloas_payload_bid(data_column.block_root(), chain)?.ok_or( + GossipDataColumnError::BlockRootUnknown { block_root: data_column.block_root(), slot: column_slot, - })? - .blob_kzg_commitments; + }, + )?; + let kzg_commitments = &bid.message.blob_kzg_commitments; verify_data_column_sidecar_with_commitments_len( &data_column, kzg_commitments.len(), @@ -1306,13 +1307,13 @@ fn verify_data_column_sidecar_with_commitments_len( pub(crate) fn load_gloas_payload_bid( block_root: Hash256, chain: &BeaconChain, -) -> Result>, BeaconChainError> { +) -> Result>>, BeaconChainError> { if let Some(bid) = chain.pending_payload_cache.get_bid(&block_root) { return Ok(Some(bid)); } let bid = if let Some(block) = chain.early_attester_cache.get_block(block_root) { - PendingPayloadBid::from_block(block.as_ref()).map_err(BeaconChainError::BeaconStateError)? + signed_payload_bid_from_block(block.as_ref()).map_err(BeaconChainError::BeaconStateError)? } else { match chain .store @@ -1320,10 +1321,10 @@ pub(crate) fn load_gloas_payload_bid( .map_err(BeaconChainError::DBError)? { Some(DatabaseBlock::Full(block)) => { - PendingPayloadBid::from_block(&block).map_err(BeaconChainError::BeaconStateError)? + signed_payload_bid_from_block(&block).map_err(BeaconChainError::BeaconStateError)? } Some(DatabaseBlock::Blinded(block)) => { - PendingPayloadBid::from_block(&block).map_err(BeaconChainError::BeaconStateError)? + signed_payload_bid_from_block(&block).map_err(BeaconChainError::BeaconStateError)? } None => { return Ok(None); diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs b/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs index b490b11522..0d5900c374 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs @@ -68,8 +68,9 @@ use crate::metrics::{ KZG_DATA_COLUMN_RECONSTRUCTION_ATTEMPTS, KZG_DATA_COLUMN_RECONSTRUCTION_FAILURES, }; use crate::observed_data_sidecars::ObservationStrategy; -pub use pending_components::PendingPayloadBid; +pub use pending_components::signed_payload_bid_from_block; use pending_components::{PendingComponents, ReconstructColumnsDecision}; +use types::SignedExecutionPayloadBid; use types::new_non_zero_usize; /// The LRU Cache stores `PendingComponents`, which store the block root, the execution payload bid, and its associated column data. @@ -153,7 +154,7 @@ impl PendingPayloadCache { block_root: Hash256, ) -> Option> { self.peek_pending_components(&block_root, |components| { - components.map(|c| c.get_cached_data_columns(block_root)) + components.map(|c| c.get_cached_data_columns()) }) } @@ -165,8 +166,11 @@ impl PendingPayloadCache { }) } - /// Return the cached Gloas payload bid metadata for `block_root`, if present. - pub fn get_bid(&self, block_root: &Hash256) -> Option> { + /// Return the cached Gloas payload bid for `block_root`, if present. + pub fn get_bid( + &self, + block_root: &Hash256, + ) -> Option>> { self.peek_pending_components(block_root, |components| { components.map(|components| components.bid.clone()) }) @@ -201,7 +205,7 @@ impl PendingPayloadCache { /// Insert an executed payload envelope into the cache and performs an availability check pub fn put_executed_payload_envelope( &self, - bid: PendingPayloadBid, + bid: Arc>, executed_envelope: AvailabilityPendingExecutedEnvelope, ) -> Result, AvailabilityCheckError> { let epoch = executed_envelope.envelope.epoch(); @@ -226,9 +230,13 @@ impl PendingPayloadCache { } /// Initialize pending components for a block's Gloas bid. - pub fn init_pending_bid(&self, block_root: Hash256, bid: PendingPayloadBid) { + pub fn init_pending_bid( + &self, + block_root: Hash256, + bid: Arc>, + ) { let mut write_lock = self.availability_cache.write(); - write_lock.get_or_insert_mut(block_root, || PendingComponents::empty(block_root, bid)); + write_lock.get_or_insert_mut(block_root, || PendingComponents::new(block_root, bid)); } /// Perform KZG verification on RPC custody columns and insert them into the cache. @@ -237,17 +245,17 @@ impl PendingPayloadCache { pub fn put_rpc_custody_columns( &self, block_root: Hash256, - bid: PendingPayloadBid, + bid: Arc>, custody_columns: DataColumnSidecarList, ) -> Result, AvailabilityCheckError> { let kzg_verified_columns = KzgVerifiedDataColumn::from_batch_with_scoring_and_commitments( custody_columns, - bid.blob_kzg_commitments.as_ref(), + bid.message.blob_kzg_commitments.as_ref(), &self.kzg, ) .map_err(AvailabilityCheckError::InvalidColumn)?; - let epoch = bid.slot.epoch(T::EthSpec::slots_per_epoch()); + let epoch = bid.message.slot.epoch(T::EthSpec::slots_per_epoch()); let sampling_columns = self .custody_context .sampling_columns_for_epoch(epoch, &self.spec); @@ -257,7 +265,7 @@ impl PendingPayloadCache { .map(KzgVerifiedCustodyDataColumn::from_asserted_custody) .collect::>(); - self.put_kzg_verified_custody_data_columns(block_root, bid, verified_custody_columns) + self.put_kzg_verified_custody_data_columns(block_root, bid, &verified_custody_columns) } /// Perform KZG verification on gossip verified custody columns and insert them into the cache. @@ -266,10 +274,10 @@ impl PendingPayloadCache { pub fn put_gossip_verified_data_columns( &self, block_root: Hash256, - bid: PendingPayloadBid, + bid: Arc>, data_columns: Vec>, ) -> Result, AvailabilityCheckError> { - let epoch = bid.slot.epoch(T::EthSpec::slots_per_epoch()); + let epoch = bid.message.slot.epoch(T::EthSpec::slots_per_epoch()); let sampling_columns = self .custody_context .sampling_columns_for_epoch(epoch, &self.spec); @@ -279,7 +287,7 @@ impl PendingPayloadCache { .map(|c| KzgVerifiedCustodyDataColumn::from_asserted_custody(c.into_inner())) .collect::>(); - self.put_kzg_verified_custody_data_columns(block_root, bid, custody_columns) + self.put_kzg_verified_custody_data_columns(block_root, bid, &custody_columns) } /// Insert KZG verified columns into the cache. @@ -287,8 +295,8 @@ impl PendingPayloadCache { pub fn put_kzg_verified_custody_data_columns( &self, block_root: Hash256, - bid: PendingPayloadBid, - kzg_verified_data_columns: Vec>, + bid: Arc>, + kzg_verified_data_columns: &[KzgVerifiedCustodyDataColumn], ) -> Result, AvailabilityCheckError> { let pending_components = self.get_pending_components(block_root, bid, |pending_components| { @@ -312,7 +320,7 @@ impl PendingPayloadCache { pub fn reconstruct_data_columns( &self, block_root: &Hash256, - bid: PendingPayloadBid, + bid: Arc>, ) -> Result, AvailabilityCheckError> { let verified_data_columns = match self.check_and_set_reconstruction_started(block_root) { ReconstructColumnsDecision::Yes(verified_data_columns) => verified_data_columns, @@ -378,7 +386,7 @@ impl PendingPayloadCache { self.put_kzg_verified_custody_data_columns( *block_root, bid, - data_columns_to_import_and_publish.clone(), + &data_columns_to_import_and_publish, ) .map(|availability| { DataColumnReconstructionResult::Success(( @@ -413,9 +421,7 @@ impl PendingPayloadCache { pending_components: MappedRwLockReadGuard<'_, PendingComponents>, num_expected_columns: usize, ) -> Result, AvailabilityCheckError> { - if let Some(available_envelope) = - pending_components.make_available(block_root, num_expected_columns)? - { + if let Some(available_envelope) = pending_components.make_available(num_expected_columns)? { // Explicitly drop read lock before acquiring write lock drop(pending_components); if let Some(components) = self.availability_cache.write().get_mut(&block_root) { @@ -439,7 +445,7 @@ impl PendingPayloadCache { fn get_pending_components( &self, block_root: Hash256, - bid: PendingPayloadBid, + bid: Arc>, update_fn: F, ) -> Result>, AvailabilityCheckError> where @@ -449,7 +455,7 @@ impl PendingPayloadCache { { let pending_components = write_lock - .get_or_insert_mut(block_root, || PendingComponents::empty(block_root, bid)); + .get_or_insert_mut(block_root, || PendingComponents::new(block_root, bid)); update_fn(pending_components)? } @@ -499,7 +505,7 @@ impl PendingPayloadCache { } pending_components.reconstruction_started = true; - ReconstructColumnsDecision::Yes(pending_components.get_cached_data_columns(*block_root)) + ReconstructColumnsDecision::Yes(pending_components.get_cached_data_columns()) } /// This could mean some invalid data columns made it through to the `DataAvailabilityChecker`. @@ -741,12 +747,16 @@ mod data_availability_checker_tests { spec: &ChainSpec, num_blobs: NumBlobs, seed: u64, - ) -> (PendingPayloadBid, Hash256, DataColumnSidecarList) { + ) -> ( + Arc>, + Hash256, + DataColumnSidecarList, + ) { let mut rng = StdRng::seed_from_u64(seed); let (block, data_columns) = generate_rand_block_and_data_columns::(ForkName::Gloas, num_blobs, &mut rng, spec); let block_root = block.canonical_root(); - let bid = PendingPayloadBid::from_block(&block).expect("should get payload bid"); + let bid = signed_payload_bid_from_block(&block).expect("should get payload bid"); cache.init_pending_bid(block_root, bid.clone()); (bid, block_root, data_columns) } @@ -756,7 +766,7 @@ mod data_availability_checker_tests { let (harness, cache, _path) = setup().await; let (bid, block_root, data_columns) = init_block(&cache, &harness.spec, NumBlobs::Number(1), RNG_SEED); - let epoch = bid.slot.epoch(E::slots_per_epoch()); + let epoch = bid.message.slot.epoch(E::slots_per_epoch()); let sampling_cols = cache .custody_context() .sampling_columns_for_epoch(epoch, &harness.spec); @@ -790,7 +800,7 @@ mod data_availability_checker_tests { let (bid, block_root, data_columns) = init_block(&cache, &harness.spec, NumBlobs::Number(1), RNG_SEED); - let epoch = bid.slot.epoch(E::slots_per_epoch()); + let epoch = bid.message.slot.epoch(E::slots_per_epoch()); let num_sampling_columns = cache .custody_context() .sampling_columns_for_epoch(epoch, &harness.spec) @@ -849,7 +859,7 @@ mod data_availability_checker_tests { let (bid, block_root, data_columns) = init_block(&cache, &harness.spec, NumBlobs::Number(1), RNG_SEED); - let epoch = bid.slot.epoch(E::slots_per_epoch()); + let epoch = bid.message.slot.epoch(E::slots_per_epoch()); let sampling_cols = cache .custody_context() .sampling_columns_for_epoch(epoch, &harness.spec); @@ -899,7 +909,7 @@ mod data_availability_checker_tests { let (harness, cache, _path) = setup().await; let (bid, block_root, data_columns) = init_block(&cache, &harness.spec, NumBlobs::Number(1), RNG_SEED); - let block_epoch = bid.slot.epoch(E::slots_per_epoch()); + let block_epoch = bid.message.slot.epoch(E::slots_per_epoch()); let column = data_columns.first().cloned().expect("should have column"); cache diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/pending_column.rs b/beacon_node/beacon_chain/src/pending_payload_cache/pending_column.rs index 021983f8a5..5ace1e25b2 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/pending_column.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/pending_column.rs @@ -1,37 +1,28 @@ use kzg::KzgProof; use ssz_types::VariableList; +use std::collections::HashMap; use std::sync::Arc; use types::{Cell, ColumnIndex, DataColumnSidecar, DataColumnSidecarGloas, EthSpec, Hash256, Slot}; -#[derive(Clone)] +#[derive(Clone, Default)] pub struct PendingColumn { - cells: Vec, KzgProof)>>, + cells: HashMap, KzgProof)>, } impl PendingColumn { - pub fn new_with_capacity(blobs: usize) -> Self { - Self { - cells: vec![None; blobs], - } - } - pub fn insert(&mut self, index: usize, cell: &Cell, proof: &KzgProof) { - if let Some(existing_cell) = self.cells.get_mut(index) - && existing_cell.is_none() - { - *existing_cell = Some((cell.clone(), *proof)); - } + self.cells + .entry(index) + .or_insert_with(|| (cell.clone(), *proof)); } pub fn cell_matches(&self, index: usize, cell: &Cell, proof: &KzgProof) -> Option { - self.cells - .get(index)? - .as_ref() - .map(|(c, p)| c == cell && p == proof) + let (c, p) = self.cells.get(&index)?; + Some(c == cell && p == proof) } pub fn is_complete(&self, blob_count: usize) -> bool { - self.cells.len() == blob_count && self.cells.iter().all(|cell| cell.is_some()) + (0..blob_count).all(|i| self.cells.contains_key(&i)) } pub fn try_to_sidecar( @@ -41,17 +32,11 @@ impl PendingColumn { beacon_block_root: Hash256, blob_count: usize, ) -> Option>> { - if self.cells.len() != blob_count { - return None; - } + let mut column = Vec::with_capacity(blob_count); + let mut kzg_proofs = Vec::with_capacity(blob_count); - let mut column = Vec::with_capacity(self.cells.len()); - let mut kzg_proofs = Vec::with_capacity(self.cells.len()); - - for cell in self.cells.iter() { - let Some((cell, proof)) = cell else { - return None; - }; + for i in 0..blob_count { + let (cell, proof) = self.cells.get(&i)?; // TODO(gloas): we likely want to go and arc all cells column.push(cell.clone()); kzg_proofs.push(*proof); diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs b/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs index 779470e3ce..de351cd527 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs @@ -10,26 +10,23 @@ use std::sync::Arc; use tracing::{Span, debug, debug_span}; use types::DataColumnSidecar; use types::{ - AbstractExecPayload, BeaconStateError, ColumnIndex, Epoch, EthSpec, Hash256, KzgCommitments, - SignedBeaconBlock, Slot, + AbstractExecPayload, BeaconStateError, ColumnIndex, Epoch, EthSpec, Hash256, SignedBeaconBlock, + SignedExecutionPayloadBid, }; -#[derive(Clone)] -pub struct PendingPayloadBid { - pub slot: Slot, - pub blob_kzg_commitments: KzgCommitments, -} - -impl PendingPayloadBid { - pub fn from_block>( - block: &SignedBeaconBlock, - ) -> Result { - let signed_bid = block.message().body().signed_execution_payload_bid()?; - Ok(Self { - slot: block.slot(), - blob_kzg_commitments: signed_bid.message.blob_kzg_commitments.clone(), - }) - } +/// Extract the signed execution payload bid from a Gloas block as a shareable `Arc`. +/// +/// Returns `Err` if the block is not a Gloas block. +pub fn signed_payload_bid_from_block>( + block: &SignedBeaconBlock, +) -> Result>, BeaconStateError> { + Ok(Arc::new( + block + .message() + .body() + .signed_execution_payload_bid()? + .clone(), + )) } /// This represents the components of a payload pending data availability. @@ -37,7 +34,8 @@ impl PendingPayloadBid { /// The columns are all gossip and kzg verified. /// The payload is considered "available" when all required columns are received. pub struct PendingComponents { - pub bid: PendingPayloadBid, + pub block_root: Hash256, + pub bid: Arc>, /// a cached post executed payload envelope pub envelope: Option>, pub verified_data_columns: HashMap>, @@ -47,18 +45,18 @@ pub struct PendingComponents { impl PendingComponents { pub fn num_blobs_expected(&self) -> usize { - self.bid.blob_kzg_commitments.len() + self.bid.message.blob_kzg_commitments.len() } /// Returns the completed custody columns - pub fn get_cached_data_columns(&self, block_root: Hash256) -> Vec>> { + pub fn get_cached_data_columns(&self) -> Vec>> { self.verified_data_columns .iter() .filter_map(|(col_idx, col)| { col.try_to_sidecar( *col_idx, - self.bid.slot, - block_root, + self.bid.message.slot, + self.block_root, self.num_blobs_expected(), ) }) @@ -77,17 +75,16 @@ impl PendingComponents { } /// Merges a given set of data columns into the cache. - pub(crate) fn merge_data_columns>>( + pub(crate) fn merge_data_columns( &mut self, - kzg_verified_data_columns: I, + kzg_verified_data_columns: &[KzgVerifiedCustodyDataColumn], ) -> Result<(), AvailabilityCheckError> { - let num_blobs_expected = self.num_blobs_expected(); for data_column in kzg_verified_data_columns { let data_column = data_column.as_data_column(); let col = self .verified_data_columns .entry(*data_column.index()) - .or_insert_with(|| PendingColumn::new_with_capacity(num_blobs_expected)); + .or_default(); for (cell_idx, (cell, proof)) in data_column .column() .iter() @@ -121,7 +118,6 @@ impl PendingComponents { /// Returns `Some` if the envelope and all required data columns have been received. pub fn make_available( &self, - block_hash: Hash256, num_expected_columns: usize, ) -> Result>, AvailabilityCheckError> { // Check if the payload has been received and executed @@ -154,17 +150,7 @@ impl PendingComponents { debug!("All data columns received, data is available"); }); - self.verified_data_columns - .iter() - .filter_map(|(col_idx, col)| { - col.try_to_sidecar( - *col_idx, - self.bid.slot, - block_hash, - self.num_blobs_expected(), - ) - }) - .collect() + self.get_cached_data_columns() } Ordering::Less => { // Not enough data columns received yet @@ -187,11 +173,12 @@ impl PendingComponents { })) } - /// Returns an empty `PendingComponents` object with the given block root. - pub fn empty(block_root: Hash256, bid: PendingPayloadBid) -> Self { + /// Constructs a fresh `PendingComponents` with no envelope and no columns yet. + pub fn new(block_root: Hash256, bid: Arc>) -> Self { let span = debug_span!(parent: None, "lh_pending_components", %block_root); let _guard = span.clone().entered(); Self { + block_root, bid, envelope: None, verified_data_columns: HashMap::new(), @@ -202,7 +189,7 @@ impl PendingComponents { /// Returns the epoch of the bid or first data column, if available. pub fn epoch(&self) -> Epoch { - self.bid.slot.epoch(E::slots_per_epoch()) + self.bid.message.slot.epoch(E::slots_per_epoch()) } pub fn status_str(&self, num_expected_columns: usize) -> String { diff --git a/beacon_node/beacon_chain/tests/events.rs b/beacon_node/beacon_chain/tests/events.rs index 26d004d069..b45083bfad 100644 --- a/beacon_node/beacon_chain/tests/events.rs +++ b/beacon_node/beacon_chain/tests/events.rs @@ -1,6 +1,5 @@ use beacon_chain::blob_verification::GossipVerifiedBlob; use beacon_chain::data_column_verification::GossipVerifiedDataColumn; -use beacon_chain::pending_payload_cache::PendingPayloadBid; use beacon_chain::test_utils::{ BeaconChainHarness, fork_name_from_env, generate_data_column_sidecars_from_block, test_spec, }; @@ -12,8 +11,8 @@ use types::data::FixedBlobSidecarList; use types::test_utils::TestRandom; use types::{ BlobSidecar, DataColumnSidecar, DataColumnSidecarFulu, DataColumnSidecarGloas, Domain, EthSpec, - KzgCommitments, MinimalEthSpec, PayloadAttestationData, PayloadAttestationMessage, SignedRoot, - Slot, + MinimalEthSpec, PayloadAttestationData, PayloadAttestationMessage, SignedExecutionPayloadBid, + SignedRoot, Slot, }; type E = MinimalEthSpec; @@ -88,13 +87,12 @@ async fn data_column_sidecar_event_on_process_gossip_data_column() { random_sidecar.index = harness.chain.sampling_columns_for_epoch(epoch)[0]; // For gloas, the bid must be known, e.g. in the pending payload cache - harness.chain.pending_payload_cache.init_pending_bid( - random_sidecar.beacon_block_root, - PendingPayloadBid { - slot: Slot::new(10), - blob_kzg_commitments: KzgCommitments::::empty(), - }, - ); + let mut bid = SignedExecutionPayloadBid::::empty(); + bid.message.slot = Slot::new(10); + harness + .chain + .pending_payload_cache + .init_pending_bid(random_sidecar.beacon_block_root, Arc::new(bid)); DataColumnSidecar::Gloas(random_sidecar) } else { From 4b76f4da280d3a093d28a930128fd221949b2af2 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Fri, 1 May 2026 02:12:46 +0200 Subject: [PATCH 090/118] minor fixes --- .../src/pending_payload_cache/mod.rs | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs b/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs index b490b11522..899a2b438c 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs @@ -3,7 +3,7 @@ //! is received we can begin using it to verify data columns. //! //! When a payload envelope is received over gossip/p2p we first insert it as a pre-executed envelope. A separate -//! thread eventually executes the payload envelope against the EL. Assuming the payload is executed succesfully +//! thread eventually executes the payload envelope against the EL. Assuming the payload is executed successfully //! the envelope is updated in the cache from `PreExecuted` -> `Executed`. Once all required custody columns //! have been kzg verified and the envelope has been executed we can import the envelope into fork choice and store it to disk. //! @@ -54,8 +54,7 @@ use std::sync::Arc; use task_executor::TaskExecutor; use tracing::{Span, debug, error, instrument, trace}; use types::{ - ChainSpec, ColumnIndex, DataColumnSidecar, DataColumnSidecarList, Epoch, EthSpec, Hash256, - PartialDataColumnSidecarRef, + ChainSpec, ColumnIndex, DataColumnSidecar, DataColumnSidecarList, Epoch, EthSpec, ExecutionPayloadBid, Hash256, PartialDataColumnSidecarRef }; mod pending_column; @@ -94,7 +93,6 @@ impl Debug for Availability { Self::MissingComponents(block_root) => { write!(f, "MissingComponents({})", block_root) } - // TODO(gloas) fix success case Self::Available(envelope) => { write!(f, "Available({:?})", envelope.block_root) } @@ -212,7 +210,9 @@ impl PendingPayloadCache { Ok(()) })?; - let num_expected_columns = self.get_num_expected_columns(epoch); + let num_expected_columns = self + .custody_context + .num_of_data_columns_to_sample(epoch, &self.spec); pending_components.span.in_scope(|| { debug!( @@ -226,7 +226,7 @@ impl PendingPayloadCache { } /// Initialize pending components for a block's Gloas bid. - pub fn init_pending_bid(&self, block_root: Hash256, bid: PendingPayloadBid) { + pub fn insert_bid(&self, block_root: Hash256, bid: ExecutionPayloadBid) { let mut write_lock = self.availability_cache.write(); write_lock.get_or_insert_mut(block_root, || PendingComponents::empty(block_root, bid)); } @@ -295,7 +295,9 @@ impl PendingPayloadCache { pending_components.merge_data_columns(kzg_verified_data_columns) })?; - let num_expected_columns = self.get_num_expected_columns(pending_components.epoch()); + let num_expected_columns = self + .custody_context + .num_of_data_columns_to_sample(epoch, &self.spec); pending_components.span.in_scope(|| { debug!( @@ -512,11 +514,6 @@ impl PendingPayloadCache { } } - fn get_num_expected_columns(&self, epoch: Epoch) -> usize { - self.custody_context - .num_of_data_columns_to_sample(epoch, &self.spec) - } - /// Maintain the cache by removing entries older than the cutoff epoch. pub fn do_maintenance(&self, cutoff_epoch: Epoch) -> Result<(), AvailabilityCheckError> { let mut write_lock = self.availability_cache.write(); From e9ffd1913e69f9d1f9304fdab89031cb78bd0602 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Fri, 1 May 2026 02:17:20 +0200 Subject: [PATCH 091/118] smol fixes --- beacon_node/beacon_chain/src/beacon_chain.rs | 4 +--- .../beacon_chain/src/data_column_verification.rs | 2 +- .../beacon_chain/src/pending_payload_cache/mod.rs | 11 ++++------- beacon_node/beacon_chain/tests/events.rs | 2 +- 4 files changed, 7 insertions(+), 12 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 7a8a747d8c..618a062b4f 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -3820,9 +3820,7 @@ impl BeaconChain { let block = execution_pending.block.block_cloned(); if block.fork_name_unchecked().gloas_enabled() { let bid = signed_payload_bid_from_block(block.as_ref())?; - chain - .pending_payload_cache - .init_pending_bid(block_root, bid); + chain.pending_payload_cache.insert_bid(block_root, bid); } publish_fn()?; diff --git a/beacon_node/beacon_chain/src/data_column_verification.rs b/beacon_node/beacon_chain/src/data_column_verification.rs index 9a48477ce8..d205604a71 100644 --- a/beacon_node/beacon_chain/src/data_column_verification.rs +++ b/beacon_node/beacon_chain/src/data_column_verification.rs @@ -1334,7 +1334,7 @@ pub(crate) fn load_gloas_payload_bid( chain .pending_payload_cache - .init_pending_bid(block_root, bid.clone()); + .insert_bid(block_root, bid.clone()); Ok(Some(bid)) } diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs b/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs index 3341eb7591..71b5872d59 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs @@ -54,7 +54,8 @@ use std::sync::Arc; use task_executor::TaskExecutor; use tracing::{Span, debug, error, instrument, trace}; use types::{ - ChainSpec, ColumnIndex, DataColumnSidecar, DataColumnSidecarList, Epoch, EthSpec, Hash256, PartialDataColumnSidecarRef + ChainSpec, ColumnIndex, DataColumnSidecar, DataColumnSidecarList, Epoch, EthSpec, Hash256, + PartialDataColumnSidecarRef, }; mod pending_column; @@ -230,11 +231,7 @@ impl PendingPayloadCache { } /// Inserts a bid into the pending payload cache. - pub fn insert_bid( - &self, - block_root: Hash256, - bid: Arc>, - ) { + pub fn insert_bid(&self, block_root: Hash256, bid: Arc>) { let mut write_lock = self.availability_cache.write(); write_lock.get_or_insert_mut(block_root, || PendingComponents::new(block_root, bid)); } @@ -756,7 +753,7 @@ mod data_availability_checker_tests { generate_rand_block_and_data_columns::(ForkName::Gloas, num_blobs, &mut rng, spec); let block_root = block.canonical_root(); let bid = signed_payload_bid_from_block(&block).expect("should get payload bid"); - cache.init_pending_bid(block_root, bid.clone()); + cache.insert_bid(block_root, bid.clone()); (bid, block_root, data_columns) } diff --git a/beacon_node/beacon_chain/tests/events.rs b/beacon_node/beacon_chain/tests/events.rs index b45083bfad..a2ca6ce2dd 100644 --- a/beacon_node/beacon_chain/tests/events.rs +++ b/beacon_node/beacon_chain/tests/events.rs @@ -92,7 +92,7 @@ async fn data_column_sidecar_event_on_process_gossip_data_column() { harness .chain .pending_payload_cache - .init_pending_bid(random_sidecar.beacon_block_root, Arc::new(bid)); + .insert_bid(random_sidecar.beacon_block_root, Arc::new(bid)); DataColumnSidecar::Gloas(random_sidecar) } else { From fd1a8e1564035dd686c38c199124d72a7e8f40fb Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Fri, 1 May 2026 02:54:57 +0200 Subject: [PATCH 092/118] use slot so we dont hit the cache twice --- beacon_node/beacon_chain/src/beacon_chain.rs | 33 ++++++++++++------- .../fetch_blobs/fetch_blobs_beacon_adapter.rs | 15 ++++----- .../beacon_chain/src/fetch_blobs/mod.rs | 2 +- .../beacon_chain/src/fetch_blobs/tests.rs | 2 +- .../network/src/sync/network_context.rs | 16 +++++---- 5 files changed, 38 insertions(+), 30 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 618a062b4f..ff67fc0b3b 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -1208,6 +1208,24 @@ impl BeaconChain { } } + pub fn cached_data_column_indexes( + &self, + block_root: &Hash256, + slot: Slot, + ) -> Option> { + if self + .spec + .fork_name_at_slot::(slot) + .gloas_enabled() + { + self.pending_payload_cache + .cached_data_column_indexes(block_root) + } else { + self.data_availability_checker + .cached_data_column_indexes(block_root) + } + } + /// Returns the block at the given root, if any. /// /// ## Errors @@ -3561,18 +3579,9 @@ impl BeaconChain { if let Some(event_handler) = self.event_handler.as_ref() && event_handler.has_data_column_sidecar_subscribers() { - let imported_data_columns = if self - .spec - .fork_name_at_slot::(slot) - .gloas_enabled() - { - self.pending_payload_cache - .cached_data_column_indexes(block_root) - } else { - self.data_availability_checker - .cached_data_column_indexes(block_root) - } - .unwrap_or_default(); + let imported_data_columns = self + .cached_data_column_indexes(block_root, slot) + .unwrap_or_default(); let new_data_columns = data_columns_iter.filter(|b| !imported_data_columns.contains(b.index())); diff --git a/beacon_node/beacon_chain/src/fetch_blobs/fetch_blobs_beacon_adapter.rs b/beacon_node/beacon_chain/src/fetch_blobs/fetch_blobs_beacon_adapter.rs index c319514b0e..31852a4a61 100644 --- a/beacon_node/beacon_chain/src/fetch_blobs/fetch_blobs_beacon_adapter.rs +++ b/beacon_node/beacon_chain/src/fetch_blobs/fetch_blobs_beacon_adapter.rs @@ -119,15 +119,12 @@ impl FetchBlobsBeaconAdapter { .cached_blob_indexes(block_root) } - pub(crate) fn cached_data_column_indexes(&self, block_root: &Hash256) -> Option> { - self.chain - .data_availability_checker - .cached_data_column_indexes(block_root) - .or_else(|| { - self.chain - .pending_payload_cache - .cached_data_column_indexes(block_root) - }) + pub(crate) fn cached_data_column_indexes( + &self, + block_root: &Hash256, + slot: Slot, + ) -> Option> { + self.chain.cached_data_column_indexes(block_root, slot) } pub(crate) async fn process_engine_blobs( diff --git a/beacon_node/beacon_chain/src/fetch_blobs/mod.rs b/beacon_node/beacon_chain/src/fetch_blobs/mod.rs index 335d76b410..f6e0b9345e 100644 --- a/beacon_node/beacon_chain/src/fetch_blobs/mod.rs +++ b/beacon_node/beacon_chain/src/fetch_blobs/mod.rs @@ -445,7 +445,7 @@ async fn compute_custody_columns_to_import( // Only consider columns that are not already known to data availability. if let Some(known_columns) = - chain_adapter_cloned.cached_data_column_indexes(&block_root) + chain_adapter_cloned.cached_data_column_indexes(&block_root, header.slot()) { custody_columns.retain(|col| !known_columns.contains(&col.index())); if custody_columns.is_empty() { diff --git a/beacon_node/beacon_chain/src/fetch_blobs/tests.rs b/beacon_node/beacon_chain/src/fetch_blobs/tests.rs index ef282a3eaa..37d40f3a27 100644 --- a/beacon_node/beacon_chain/src/fetch_blobs/tests.rs +++ b/beacon_node/beacon_chain/src/fetch_blobs/tests.rs @@ -199,7 +199,7 @@ mod get_blobs_v2 { .returning(|_| None); mock_adapter .expect_cached_data_column_indexes() - .returning(|_| None); + .returning(|_, _| None); mock_process_engine_blobs_result( &mut mock_adapter, Ok(AvailabilityProcessingStatus::Imported(block_root)), diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index ae86176908..7cf8a67845 100644 --- a/beacon_node/network/src/sync/network_context.rs +++ b/beacon_node/network/src/sync/network_context.rs @@ -1085,15 +1085,17 @@ impl SyncNetworkContext { block_root: Hash256, lookup_peers: Arc>>, ) -> Result { + let slot = self + .chain + .canonical_head + .fork_choice_read_lock() + .get_block(&block_root) + .map(|block| block.slot) + .unwrap_or_else(|| self.chain.slot().unwrap_or_default()); + let custody_indexes_imported = self .chain - .data_availability_checker - .cached_data_column_indexes(&block_root) - .or_else(|| { - self.chain - .pending_payload_cache - .cached_data_column_indexes(&block_root) - }) + .cached_data_column_indexes(&block_root, slot) .unwrap_or_default(); let current_epoch = self.chain.epoch().map_err(|e| { From 64c53c6553ca91e8220ddb69286322fbb31a773e Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Fri, 1 May 2026 03:13:36 +0200 Subject: [PATCH 093/118] remove duplicate fn impl --- beacon_node/beacon_chain/src/beacon_chain.rs | 12 ++++----- .../payload_envelope_verification/import.rs | 25 +------------------ 2 files changed, 7 insertions(+), 30 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index ff67fc0b3b..e17b4789f8 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -3453,7 +3453,7 @@ impl BeaconChain { &merge_result.full_columns, ) .map_err(BlockError::from)?; - self.process_payload_availability(slot, availability, || Ok(())) + self.process_payload_envelope_availability(slot, availability, || Ok(())) .await? } else { let availability = self @@ -3702,7 +3702,7 @@ impl BeaconChain { }; Ok(self - .process_payload_availability(slot, availability, || Ok(())) + .process_payload_envelope_availability(slot, availability, || Ok(())) .await .map(|status| Some((status, data_columns_to_publish)))?) } @@ -4011,7 +4011,7 @@ impl BeaconChain { .pending_payload_cache .put_gossip_verified_data_columns(block_root, bid, data_columns)?; Ok(self - .process_payload_availability(slot, availability, publish_fn) + .process_payload_envelope_availability(slot, availability, publish_fn) .await?) } else { let availability = self @@ -4118,7 +4118,7 @@ impl BeaconChain { .put_kzg_verified_custody_data_columns(block_root, bid, &data_columns) .map_err(BlockError::from)?; Ok(self - .process_payload_availability(slot, availability, || Ok(())) + .process_payload_envelope_availability(slot, availability, || Ok(())) .await?) } else { let availability = self @@ -4162,7 +4162,7 @@ impl BeaconChain { .put_rpc_custody_columns(block_root, bid, custody_columns) .map_err(BlockError::from)?; Ok(self - .process_payload_availability(slot, availability, || Ok(())) + .process_payload_envelope_availability(slot, availability, || Ok(())) .await?) } else { let availability = self @@ -4229,7 +4229,7 @@ impl BeaconChain { } } - async fn process_payload_availability( + pub(crate) async fn process_payload_envelope_availability( self: &Arc, slot: Slot, availability: PayloadAvailability, 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 975a684e95..10d97add1d 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs @@ -6,14 +6,13 @@ use fork_choice::PayloadVerificationStatus; use slot_clock::SlotClock; use store::StoreOp; use tracing::{debug, error, info, info_span, instrument, warn}; -use types::{BlockImportSource, Hash256, SignedExecutionPayloadEnvelope, Slot}; +use types::{BlockImportSource, Hash256, SignedExecutionPayloadEnvelope}; use super::{ AvailableEnvelope, AvailableExecutedEnvelope, EnvelopeError, gossip_verified_envelope::GossipVerifiedEnvelope, }; use crate::data_column_verification::load_gloas_payload_bid; -use crate::pending_payload_cache::Availability as PayloadAvailability; use crate::{ AvailabilityProcessingStatus, BeaconChain, BeaconChainError, BeaconChainTypes, BlockError, NotifyExecutionLayer, @@ -156,28 +155,6 @@ impl BeaconChain { } } - /// Imports a fully available payload envelope. Otherwise, returns `AvailabilityProcessingStatus::MissingComponents` - /// - /// An error is returned if the enveope was unable to be imported. It may be partially imported - /// (i.e., this function is not atomic). - async fn process_payload_envelope_availability( - self: &Arc, - slot: Slot, - availability: PayloadAvailability, - publish_fn: impl FnOnce() -> Result<(), BlockError>, - ) -> Result { - match availability { - PayloadAvailability::Available(available_envelope) => { - publish_fn()?; - self.import_available_execution_payload_envelope(available_envelope) - .await - } - PayloadAvailability::MissingComponents(block_root) => Ok( - AvailabilityProcessingStatus::MissingComponents(slot, block_root), - ), - } - } - #[instrument(skip_all)] async fn check_envelope_availability_and_import( self: &Arc, From 33c250b54fd0e1f07a01898248b680781eef0869 Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Fri, 1 May 2026 02:53:53 +0200 Subject: [PATCH 094/118] pending payload cache: restore vec-backed column shape --- .../pending_payload_cache/pending_column.rs | 45 +++++++++++++------ .../pending_components.rs | 3 +- 2 files changed, 34 insertions(+), 14 deletions(-) diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/pending_column.rs b/beacon_node/beacon_chain/src/pending_payload_cache/pending_column.rs index 5ace1e25b2..a69bd22d34 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/pending_column.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/pending_column.rs @@ -1,28 +1,41 @@ use kzg::KzgProof; use ssz_types::VariableList; -use std::collections::HashMap; use std::sync::Arc; use types::{Cell, ColumnIndex, DataColumnSidecar, DataColumnSidecarGloas, EthSpec, Hash256, Slot}; -#[derive(Clone, Default)] +#[derive(Clone)] pub struct PendingColumn { - cells: HashMap, KzgProof)>, + cells: Vec, KzgProof)>>, +} + +impl Default for PendingColumn { + fn default() -> Self { + Self { cells: Vec::new() } + } } impl PendingColumn { + pub fn new_with_capacity(_blobs: usize) -> Self { + Self { cells: Vec::new() } + } + pub fn insert(&mut self, index: usize, cell: &Cell, proof: &KzgProof) { - self.cells - .entry(index) - .or_insert_with(|| (cell.clone(), *proof)); + if let Some(existing_cell) = self.cells.get_mut(index) + && existing_cell.is_none() + { + *existing_cell = Some((cell.clone(), *proof)); + } } pub fn cell_matches(&self, index: usize, cell: &Cell, proof: &KzgProof) -> Option { - let (c, p) = self.cells.get(&index)?; - Some(c == cell && p == proof) + self.cells + .get(index)? + .as_ref() + .map(|(c, p)| c == cell && p == proof) } pub fn is_complete(&self, blob_count: usize) -> bool { - (0..blob_count).all(|i| self.cells.contains_key(&i)) + self.cells.len() == blob_count && self.cells.iter().all(|cell| cell.is_some()) } pub fn try_to_sidecar( @@ -32,11 +45,17 @@ impl PendingColumn { beacon_block_root: Hash256, blob_count: usize, ) -> Option>> { - let mut column = Vec::with_capacity(blob_count); - let mut kzg_proofs = Vec::with_capacity(blob_count); + if self.cells.len() != blob_count { + return None; + } - for i in 0..blob_count { - let (cell, proof) = self.cells.get(&i)?; + let mut column = Vec::with_capacity(blob_count); + let mut kzg_proofs = Vec::with_capacity(self.cells.len()); + + for cell in self.cells.iter() { + let Some((cell, proof)) = cell else { + return None; + }; // TODO(gloas): we likely want to go and arc all cells column.push(cell.clone()); kzg_proofs.push(*proof); diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs b/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs index de351cd527..d3b4e3007b 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs @@ -81,10 +81,11 @@ impl PendingComponents { ) -> Result<(), AvailabilityCheckError> { for data_column in kzg_verified_data_columns { let data_column = data_column.as_data_column(); + let num_blobs_expected = self.num_blobs_expected(); let col = self .verified_data_columns .entry(*data_column.index()) - .or_default(); + .or_insert_with(|| PendingColumn::new_with_capacity(num_blobs_expected)); for (cell_idx, (cell, proof)) in data_column .column() .iter() From a4e8b689d8d8cb205dcccae401b482bc09beabed Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Fri, 1 May 2026 03:03:24 +0200 Subject: [PATCH 095/118] pending payload cache: drop unused vec ctor arg --- .../beacon_chain/src/pending_payload_cache/pending_column.rs | 4 ---- .../src/pending_payload_cache/pending_components.rs | 3 +-- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/pending_column.rs b/beacon_node/beacon_chain/src/pending_payload_cache/pending_column.rs index a69bd22d34..1c7a0389c3 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/pending_column.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/pending_column.rs @@ -15,10 +15,6 @@ impl Default for PendingColumn { } impl PendingColumn { - pub fn new_with_capacity(_blobs: usize) -> Self { - Self { cells: Vec::new() } - } - pub fn insert(&mut self, index: usize, cell: &Cell, proof: &KzgProof) { if let Some(existing_cell) = self.cells.get_mut(index) && existing_cell.is_none() diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs b/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs index d3b4e3007b..de351cd527 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs @@ -81,11 +81,10 @@ impl PendingComponents { ) -> Result<(), AvailabilityCheckError> { for data_column in kzg_verified_data_columns { let data_column = data_column.as_data_column(); - let num_blobs_expected = self.num_blobs_expected(); let col = self .verified_data_columns .entry(*data_column.index()) - .or_insert_with(|| PendingColumn::new_with_capacity(num_blobs_expected)); + .or_default(); for (cell_idx, (cell, proof)) in data_column .column() .iter() From f7c7ed84577c82897ede21b17e37192412cfa167 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Fri, 1 May 2026 03:31:51 +0200 Subject: [PATCH 096/118] clean up gloas-payload-cache --- beacon_node/beacon_chain/src/beacon_chain.rs | 9 ++++----- .../beacon_chain/src/pending_payload_cache/mod.rs | 4 ++-- .../src/pending_payload_cache/pending_components.rs | 7 +------ .../types/src/execution/signed_execution_payload_bid.rs | 8 ++++++++ 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index e17b4789f8..a7f289b3f1 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -3352,7 +3352,6 @@ impl BeaconChain { } self.emit_sse_data_column_sidecar_events( - slot, &block_root, data_columns.iter().map(|column| column.as_data_column()), ); @@ -3430,7 +3429,6 @@ impl BeaconChain { ); self.emit_sse_data_column_sidecar_events( - slot, &block_root, merge_result .full_columns @@ -3536,7 +3534,6 @@ impl BeaconChain { } EngineGetBlobsOutput::CustodyColumns(columns) => { self.emit_sse_data_column_sidecar_events( - slot, &block_root, columns.iter().map(|column| column.as_data_column()), ); @@ -3570,7 +3567,6 @@ impl BeaconChain { fn emit_sse_data_column_sidecar_events<'a, I>( self: &Arc, - slot: Slot, block_root: &Hash256, data_columns_iter: I, ) where @@ -3579,6 +3575,10 @@ impl BeaconChain { if let Some(event_handler) = self.event_handler.as_ref() && event_handler.has_data_column_sidecar_subscribers() { + let mut data_columns_iter = data_columns_iter.peekable(); + let Some(slot) = data_columns_iter.peek().map(|col| col.slot()) else { + return; + }; let imported_data_columns = self .cached_data_column_indexes(block_root, slot) .unwrap_or_default(); @@ -3643,7 +3643,6 @@ impl BeaconChain { } self.emit_sse_data_column_sidecar_events( - slot, &block_root, custody_columns.iter().map(|column| column.as_ref()), ); diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs b/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs index 71b5872d59..964b526288 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs @@ -487,7 +487,7 @@ impl PendingPayloadCache { return ReconstructColumnsDecision::No("block already imported"); }; - let epoch = pending_components.epoch(); + let epoch = pending_components.bid.epoch(); let total_column_count = T::EthSpec::number_of_columns(); let sampling_column_count = self @@ -524,7 +524,7 @@ impl PendingPayloadCache { let mut write_lock = self.availability_cache.write(); let mut keys_to_remove = vec![]; for (key, value) in write_lock.iter() { - if value.epoch() < cutoff_epoch { + if value.bid.epoch() < cutoff_epoch { keys_to_remove.push(*key); } } diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs b/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs index de351cd527..b69c897bda 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs @@ -10,7 +10,7 @@ use std::sync::Arc; use tracing::{Span, debug, debug_span}; use types::DataColumnSidecar; use types::{ - AbstractExecPayload, BeaconStateError, ColumnIndex, Epoch, EthSpec, Hash256, SignedBeaconBlock, + AbstractExecPayload, BeaconStateError, ColumnIndex, EthSpec, Hash256, SignedBeaconBlock, SignedExecutionPayloadBid, }; @@ -187,11 +187,6 @@ impl PendingComponents { } } - /// Returns the epoch of the bid or first data column, if available. - pub fn epoch(&self) -> Epoch { - self.bid.message.slot.epoch(E::slots_per_epoch()) - } - pub fn status_str(&self, num_expected_columns: usize) -> String { format!( "envelope {}, data_columns {}/{}", diff --git a/consensus/types/src/execution/signed_execution_payload_bid.rs b/consensus/types/src/execution/signed_execution_payload_bid.rs index 48da445332..37ccb9cf79 100644 --- a/consensus/types/src/execution/signed_execution_payload_bid.rs +++ b/consensus/types/src/execution/signed_execution_payload_bid.rs @@ -25,6 +25,14 @@ pub struct SignedExecutionPayloadBid { } impl SignedExecutionPayloadBid { + pub fn epoch(&self) -> crate::Epoch { + self.message.slot.epoch(E::slots_per_epoch()) + } + + pub fn slot(&self) -> crate::Slot { + self.message.slot + } + pub fn empty() -> Self { Self { message: ExecutionPayloadBid::default(), From f14e7b13b57fda3af2041242a4ae6ffd032127f9 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Fri, 1 May 2026 03:50:50 +0200 Subject: [PATCH 097/118] remove unneeded field --- .../src/payload_envelope_verification/mod.rs | 9 ++++----- .../src/pending_payload_cache/pending_components.rs | 7 +------ 2 files changed, 5 insertions(+), 11 deletions(-) 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 43dd23f112..b4f53b9df8 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs @@ -42,22 +42,21 @@ pub use execution_pending_envelope::ExecutionPendingEnvelope; #[derive(Debug)] pub struct AvailableEnvelope { - pub execution_block_hash: ExecutionBlockHash, - pub envelope: Arc>, + envelope: Arc>, pub columns: DataColumnSidecarList, + // TODO(gloas) this field is unread, do we need it? + #[expect(dead_code)] /// Timestamp at which this envelope first became available (UNIX timestamp, time since 1970). - pub columns_available_timestamp: Option, + columns_available_timestamp: Option, } impl AvailableEnvelope { pub fn new( - execution_block_hash: ExecutionBlockHash, envelope: Arc>, columns: DataColumnSidecarList, columns_available_timestamp: Option, ) -> Self { Self { - execution_block_hash, envelope, columns, columns_available_timestamp, diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs b/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs index b69c897bda..20e6d1b52f 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs @@ -159,12 +159,7 @@ impl PendingComponents { } }; - let available_envelope = AvailableEnvelope { - execution_block_hash: envelope.block_hash(), - envelope: envelope.clone(), - columns, - columns_available_timestamp: None, - }; + let available_envelope = AvailableEnvelope::new(envelope.clone(), columns, None); Ok(Some(AvailableExecutedEnvelope { envelope: available_envelope, From aa531bac2284423f2a7ae11aa93ac06a82336fa4 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Fri, 1 May 2026 03:55:09 +0200 Subject: [PATCH 098/118] yeet some comments --- .../src/pending_payload_cache/mod.rs | 27 ------------------- 1 file changed, 27 deletions(-) diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs b/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs index 964b526288..b056eb1af9 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs @@ -9,33 +9,6 @@ //! //! Note that the block must have arrived before the envelope for the envelope to pass upstream verification checks and reach this cache. //! However data columns can potentially arrive before the block. -//! -//! -//! SignedBeaconBlock -//! | -//! | -> SignedExecutionPayloadBid -//! -//! -//! DataColumnSidecarList -//! | -//! | -> Perform data column verification against `SignedExecutionPayloadBid` -//! │ │ -//! │ ▼ -//! | -> KzgVerifiedCustodyDataColumn -//! -//! -//! SignedExecutionPayloadEnvelope -//! │ -//! | -> CachedPayloadEnvelope::PreExecution -//! │ │ -//! │ ▼ -//! | -> AvailabilityPendingExecutedEnvelope -//! │ │ -//! │ ▼ -//! │ -> CachedPayloadEnvelope::Executed -//! │ │ -//! │ ▼ -//! | -> AvailableExecutedEnvelope (all columns present, payload executed against the EL, ready to import) use crate::data_availability_checker::{AvailabilityCheckError, MissingCellsError}; use crate::payload_envelope_verification::{ From 73ba76312eadf169e7c77acdf7a30cae2b379f2e Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Fri, 1 May 2026 03:46:10 +0200 Subject: [PATCH 099/118] Gloas: fix test failures (KZG verifier wiring, harness columns, WSS sync) Brings the FORK_NAME=gloas beacon_chain test suite from 31 failures to green: - v1 KZG batch verifier couldn't verify Gloas columns. Added verify_columns_against_block helper that picks commitments per fork (Fulu: inline on column; Gloas: signed_execution_payload_bid). - BeaconChainHarness::process_envelope didn't persist columns. Now mirrors what production does in import_available_execution_payload_envelope. - get_or_reconstruct_blobs returned an error for Gloas. Now short-circuits to Ok(None); WSS test copies columns from source to dest directly. - update_data_column_signed_header (block_verification tests) only handled Fulu shape. Added a Gloas branch that re-keys to canonical_root. - BlockError::EnvelopeBlockRootUnknown changed to tuple variant. - Removed duplicate process_payload_envelope_availability. --- beacon_node/beacon_chain/src/beacon_chain.rs | 20 +++++-- .../beacon_chain/src/block_verification.rs | 2 +- .../src/data_availability_checker.rs | 50 ++++++++++++---- beacon_node/beacon_chain/src/kzg_utils.rs | 19 ++++-- .../payload_envelope_verification/import.rs | 4 +- .../pending_payload_cache/pending_column.rs | 8 +++ .../pending_components.rs | 6 +- beacon_node/beacon_chain/src/test_utils.rs | 46 ++++++++++++--- .../beacon_chain/tests/block_verification.rs | 40 +++++++++---- beacon_node/beacon_chain/tests/store_tests.rs | 58 ++++++++++++++++++- .../gossip_methods.rs | 2 +- 11 files changed, 205 insertions(+), 50 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index a7f289b3f1..0a48a3b168 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -1331,6 +1331,14 @@ impl BeaconChain { return Ok(None); }; + // Gloas removes the standalone `BlobSidecar` shape — KZG commitments live in the bid and + // there's no signed-block-header / inclusion-proof to populate a `BlobSidecar` from. The + // canonical data is the column sidecar set on disk; callers needing data for a Gloas + // block should consume columns directly via `get_data_columns`. + if block.fork_name_unchecked().gloas_enabled() { + return Ok(None); + } + if self.spec.is_peer_das_enabled_for_epoch(block.epoch()) { let fork_name = self.spec.fork_name_at_epoch(block.epoch()); if let Some(columns) = self.store.get_data_columns(block_root, fork_name)? { @@ -3442,7 +3450,7 @@ impl BeaconChain { .gloas_enabled() { let bid = load_gloas_payload_bid(block_root, self)? - .ok_or(BlockError::EnvelopeBlockRootUnknown { block_root })?; + .ok_or(BlockError::EnvelopeBlockRootUnknown(block_root))?; let availability = self .pending_payload_cache .put_kzg_verified_custody_data_columns( @@ -3680,7 +3688,7 @@ impl BeaconChain { if is_gloas { let bid = load_gloas_payload_bid(block_root, self)? - .ok_or(BlockError::EnvelopeBlockRootUnknown { block_root })?; + .ok_or(BlockError::EnvelopeBlockRootUnknown(block_root))?; let pending_payload_cache = self.pending_payload_cache.clone(); let result = self .task_executor @@ -4005,7 +4013,7 @@ impl BeaconChain { .gloas_enabled() { let bid = load_gloas_payload_bid(block_root, self)? - .ok_or(BlockError::EnvelopeBlockRootUnknown { block_root })?; + .ok_or(BlockError::EnvelopeBlockRootUnknown(block_root))?; let availability = self .pending_payload_cache .put_gossip_verified_data_columns(block_root, bid, data_columns)?; @@ -4111,7 +4119,7 @@ impl BeaconChain { .gloas_enabled() { let bid = load_gloas_payload_bid(block_root, self)? - .ok_or(BlockError::EnvelopeBlockRootUnknown { block_root })?; + .ok_or(BlockError::EnvelopeBlockRootUnknown(block_root))?; let availability = self .pending_payload_cache .put_kzg_verified_custody_data_columns(block_root, bid, &data_columns) @@ -4155,7 +4163,7 @@ impl BeaconChain { .gloas_enabled() { let bid = load_gloas_payload_bid(block_root, self)? - .ok_or(BlockError::EnvelopeBlockRootUnknown { block_root })?; + .ok_or(BlockError::EnvelopeBlockRootUnknown(block_root))?; let availability = self .pending_payload_cache .put_rpc_custody_columns(block_root, bid, custody_columns) @@ -7750,7 +7758,7 @@ impl BeaconChain { ) } - pub(crate) fn get_blobs_or_columns_store_op( + pub fn get_blobs_or_columns_store_op( &self, block_root: Hash256, block_slot: Slot, diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index eeee562e9c..24f971f736 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -287,7 +287,7 @@ pub enum BlockError { /// https://github.com/sigp/lighthouse/issues/4546 AvailabilityCheck(AvailabilityCheckError), /// The payload envelope's block root is unknown. - EnvelopeBlockRootUnknown { block_root: Hash256 }, + EnvelopeBlockRootUnknown(Hash256), /// Optimistic sync is not supported for Gloas payload envelopes. OptimisticSyncNotSupported { block_root: Hash256 }, /// A Blob with a slot after PeerDAS is received and is not required to be imported. diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index 928d7a1bad..bdf16ec52c 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -33,6 +33,7 @@ use crate::data_column_verification::{ GossipVerifiedDataColumn, KzgVerifiedCustodyDataColumn, KzgVerifiedDataColumn, verify_kzg_for_data_column_list, }; +use crate::kzg_utils::validate_full_data_columns_with_commitments; use crate::metrics::{ KZG_DATA_COLUMN_RECONSTRUCTION_ATTEMPTS, KZG_DATA_COLUMN_RECONSTRUCTION_FAILURES, }; @@ -490,8 +491,7 @@ impl DataAvailabilityChecker { AvailableBlockData::Blobs(blobs) => verify_kzg_for_blob_list(blobs.iter(), &self.kzg) .map_err(AvailabilityCheckError::InvalidBlobs), AvailableBlockData::DataColumns(columns) => { - verify_kzg_for_data_column_list(columns.iter(), &self.kzg) - .map_err(AvailabilityCheckError::InvalidColumn) + verify_columns_against_block(&self.kzg, available_block.block(), columns) } } } @@ -504,13 +504,17 @@ impl DataAvailabilityChecker { available_blocks: &[AvailableBlock], ) -> Result<(), AvailabilityCheckError> { let mut all_blobs = Vec::new(); - let mut all_data_columns = Vec::new(); for available_block in available_blocks { - match available_block.data().to_owned() { + match available_block.data() { AvailableBlockData::NoData => {} - AvailableBlockData::Blobs(blobs) => all_blobs.extend(blobs), - AvailableBlockData::DataColumns(columns) => all_data_columns.extend(columns), + AvailableBlockData::Blobs(blobs) => all_blobs.extend(blobs.iter().cloned()), + AvailableBlockData::DataColumns(columns) => { + // Each block has its own commitments. For Gloas they live in the bid; for + // Fulu they live inline on the column. Verify per block and let the helper + // pick the right path. + verify_columns_against_block(&self.kzg, available_block.block(), columns)?; + } } } @@ -519,11 +523,6 @@ impl DataAvailabilityChecker { .map_err(AvailabilityCheckError::InvalidBlobs)?; } - if !all_data_columns.is_empty() { - verify_kzg_for_data_column_list(all_data_columns.iter(), &self.kzg) - .map_err(AvailabilityCheckError::InvalidColumn)?; - } - Ok(()) } @@ -679,6 +678,35 @@ impl DataAvailabilityChecker { } } +/// Verify a batch of data columns belonging to a single block, picking the right commitment +/// source for the block's fork (Fulu: inline on column; Gloas: from the embedded payload bid). +fn verify_columns_against_block( + kzg: &Kzg, + block: &SignedBeaconBlock, + columns: &[Arc>], +) -> Result<(), AvailabilityCheckError> { + if columns.is_empty() { + return Ok(()); + } + if block.fork_name_unchecked().gloas_enabled() { + let commitments = block + .message() + .body() + .signed_execution_payload_bid() + .map(|bid| bid.message.blob_kzg_commitments.clone()) + .map_err(|_| { + AvailabilityCheckError::Unexpected( + "Gloas block missing signed_execution_payload_bid".to_string(), + ) + })?; + validate_full_data_columns_with_commitments(kzg, columns.iter(), commitments.as_ref()) + .map_err(AvailabilityCheckError::InvalidColumn) + } else { + verify_kzg_for_data_column_list(columns.iter(), kzg) + .map_err(AvailabilityCheckError::InvalidColumn) + } +} + /// Helper struct to group data availability checker metrics. pub struct DataAvailabilityCheckerMetrics { pub block_cache_size: usize, diff --git a/beacon_node/beacon_chain/src/kzg_utils.rs b/beacon_node/beacon_chain/src/kzg_utils.rs index 5ad1cc115d..e1558f050b 100644 --- a/beacon_node/beacon_chain/src/kzg_utils.rs +++ b/beacon_node/beacon_chain/src/kzg_utils.rs @@ -676,12 +676,19 @@ pub fn reconstruct_blobs( let blob_indices: Vec = match blob_indices_opt { Some(indices) => indices.into_iter().map(|i| i as usize).collect(), None => { - // TODO(gloas): support blob reconstruction for Gloas - // https://github.com/sigp/lighthouse/issues/7413 - let num_of_blobs = first_data_column - .kzg_commitments() - .map_err(|_| "Gloas blob reconstruction not yet supported".to_string())? - .len(); + // Fulu columns carry commitments inline; Gloas columns don't, so fall back to + // the block's payload bid commitments. + let num_of_blobs = match first_data_column.kzg_commitments() { + Ok(commitments) => commitments.len(), + Err(_) => signed_block + .message() + .body() + .signed_execution_payload_bid() + .map(|bid| bid.message.blob_kzg_commitments.len()) + .map_err(|_| { + "Gloas blob reconstruction: block missing payload bid".to_string() + })?, + }; (0..num_of_blobs).collect() } }; 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 10d97add1d..e7c900dcd7 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs @@ -163,7 +163,7 @@ impl BeaconChain { let slot = envelope.envelope.slot(); let block_root = envelope.block_root; let bid = load_gloas_payload_bid(block_root, self)? - .ok_or(BlockError::EnvelopeBlockRootUnknown { block_root })?; + .ok_or(BlockError::EnvelopeBlockRootUnknown(block_root))?; let availability = self .pending_payload_cache .put_executed_payload_envelope(bid, envelope)?; @@ -253,7 +253,7 @@ impl BeaconChain { // been imported. We don't want to repeat work importing a block that is already imported. let fork_choice_reader = self.canonical_head.fork_choice_upgradable_read_lock(); if !fork_choice_reader.contains_block(&block_root) { - return Err(BlockError::EnvelopeBlockRootUnknown { block_root }); + return Err(BlockError::EnvelopeBlockRootUnknown(block_root)); } // TODO(gloas) add defensive check to see if payload envelope is already in fork choice diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/pending_column.rs b/beacon_node/beacon_chain/src/pending_payload_cache/pending_column.rs index 1c7a0389c3..db13bec574 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/pending_column.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/pending_column.rs @@ -15,6 +15,14 @@ impl Default for PendingColumn { } impl PendingColumn { + /// Allocate a `PendingColumn` whose `cells` vec has space for `blob_count` entries, all + /// initialised to `None`. Required so that `insert(idx, ...)` can write into `cells[idx]`. + pub fn new_with_capacity(blob_count: usize) -> Self { + Self { + cells: vec![None; blob_count], + } + } + pub fn insert(&mut self, index: usize, cell: &Cell, proof: &KzgProof) { if let Some(existing_cell) = self.cells.get_mut(index) && existing_cell.is_none() diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs b/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs index 20e6d1b52f..0516afd4e9 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs @@ -79,12 +79,16 @@ impl PendingComponents { &mut self, kzg_verified_data_columns: &[KzgVerifiedCustodyDataColumn], ) -> Result<(), AvailabilityCheckError> { + let num_blobs_expected = self.num_blobs_expected(); for data_column in kzg_verified_data_columns { let data_column = data_column.as_data_column(); + // The Vec-backed `PendingColumn` keys cells by index, so we have to allocate up to + // `num_blobs_expected` entries before inserting; otherwise `cells.get_mut(idx)` returns + // None and the insert is a no-op. let col = self .verified_data_columns .entry(*data_column.index()) - .or_default(); + .or_insert_with(|| PendingColumn::new_with_capacity(num_blobs_expected)); for (cell_idx, (cell, proof)) in data_column .column() .iter() diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index 4a9b7f7fa0..68cec11b76 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -2832,11 +2832,42 @@ where .await .expect("newPayload should succeed"); - // Store the envelope. + // Store the envelope and the data columns derived from the block. + // + // Production stores columns inside `import_available_execution_payload_envelope` after + // the cache is satisfied. The harness sidesteps that flow but must still persist columns + // or the `DataColumnMissing` invariant fires for any block with `num_expected_blobs > 0`. + let block = self + .chain + .store + .get_blinded_block(&block_root) + .expect("should read block from store") + .expect("block should exist in store"); + let mut ops = vec![]; + let block_with_full_payload = self + .chain + .store + .make_full_block(&block_root, block.clone()) + .expect("should reconstruct full block"); + let columns = + generate_data_column_sidecars_from_block(&block_with_full_payload, &self.spec); + if !columns.is_empty() + && let Some(store_op) = self.chain.get_blobs_or_columns_store_op( + block_root, + block.slot(), + AvailableBlockData::DataColumns(columns), + ) + { + ops.push(store_op); + } + ops.push(store::StoreOp::PutPayloadEnvelope( + block_root, + std::sync::Arc::new(signed_envelope), + )); self.chain .store - .put_payload_envelope(&block_root, &signed_envelope) - .expect("should store envelope"); + .do_atomically_with_block_and_blobs_cache(ops) + .expect("should persist envelope and columns"); // Update fork choice so it knows the payload was received. self.chain @@ -2857,11 +2888,10 @@ where block: Arc>, ) -> RangeSyncBlock { let block_root = block_root.unwrap_or_else(|| get_block_root(&block)); - let has_blobs = block - .message() - .body() - .blob_kzg_commitments() - .is_ok_and(|c| !c.is_empty()); + // For Gloas, kzg commitments live in the bid (`signed_execution_payload_bid`), so the + // body's `blob_kzg_commitments()` accessor returns Err. `num_expected_blobs` already + // handles both shapes. + let has_blobs = block.num_expected_blobs() > 0; if !has_blobs { return RangeSyncBlock::new( block, diff --git a/beacon_node/beacon_chain/tests/block_verification.rs b/beacon_node/beacon_chain/tests/block_verification.rs index af858874b2..38636540a1 100644 --- a/beacon_node/beacon_chain/tests/block_verification.rs +++ b/beacon_node/beacon_chain/tests/block_verification.rs @@ -323,18 +323,34 @@ fn update_data_column_signed_header( ) { for old_custody_column_sidecar in data_columns.as_mut_slice() { let old_column_sidecar = old_custody_column_sidecar.as_data_column(); - let new_column_sidecar = Arc::new(DataColumnSidecar::Fulu(DataColumnSidecarFulu { - index: *old_column_sidecar.index(), - column: old_column_sidecar.column().clone(), - kzg_commitments: old_column_sidecar.kzg_commitments().unwrap().clone(), - kzg_proofs: old_column_sidecar.kzg_proofs().clone(), - signed_block_header: signed_block.signed_block_header(), - kzg_commitments_inclusion_proof: signed_block - .message() - .body() - .kzg_commitments_merkle_proof() - .unwrap(), - })); + let new_column_sidecar = match old_column_sidecar.as_ref() { + DataColumnSidecar::Fulu(_) => { + Arc::new(DataColumnSidecar::Fulu(DataColumnSidecarFulu { + index: *old_column_sidecar.index(), + column: old_column_sidecar.column().clone(), + kzg_commitments: old_column_sidecar.kzg_commitments().unwrap().clone(), + kzg_proofs: old_column_sidecar.kzg_proofs().clone(), + signed_block_header: signed_block.signed_block_header(), + kzg_commitments_inclusion_proof: signed_block + .message() + .body() + .kzg_commitments_merkle_proof() + .unwrap(), + })) + } + // Gloas columns reference the block by `beacon_block_root` instead of holding the + // block header inline, so updating the parent root just means re-keying the column to + // the new canonical root. + DataColumnSidecar::Gloas(g) => { + Arc::new(DataColumnSidecar::Gloas(types::DataColumnSidecarGloas { + index: g.index, + column: g.column.clone(), + kzg_proofs: g.kzg_proofs.clone(), + slot: g.slot, + beacon_block_root: signed_block.canonical_root(), + })) + } + }; *old_custody_column_sidecar = CustodyDataColumn::from_asserted_custody(new_column_sidecar); } } diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index 86adf50995..171c83b2a9 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -3133,6 +3133,29 @@ async fn weak_subjectivity_sync_test( let beacon_chain = Arc::new(beacon_chain); let wss_block_root = wss_block.canonical_root(); + + // For Gloas, blobs aren't a standalone shape — the WSS data is the column sidecar set, which + // `get_or_reconstruct_blobs` returns `None` for. Copy the WSS block's columns straight from + // the source store so that the destination has them after checkpoint sync, matching what + // network-driven WSS would produce in production. + if wss_block.fork_name_unchecked().gloas_enabled() + && let Ok(Some(source_columns)) = harness + .chain + .store + .get_data_columns(&wss_block_root, ForkName::Gloas) + && !source_columns.is_empty() + && let Some(store_op) = beacon_chain.get_blobs_or_columns_store_op( + wss_block_root, + wss_block.slot(), + beacon_chain::block_verification_types::AvailableBlockData::DataColumns(source_columns), + ) + { + beacon_chain + .store + .do_atomically_with_block_and_blobs_cache(vec![store_op]) + .unwrap(); + } + let store_wss_block = harness .chain .get_block(&wss_block_root) @@ -3200,12 +3223,43 @@ async fn weak_subjectivity_sync_test( .await .unwrap(); - // Store the envelope and apply it to fork choice. + // Store the envelope, its columns, and apply to fork choice. if let Some(envelope) = &snapshot.execution_envelope { + // Persist data columns for Gloas blocks. This mirrors what production does in + // `import_available_execution_payload_envelope` and what the harness now does in + // `process_envelope` — the WSS forward-sync loop bypasses both, so do it directly. + let mut ops = vec![]; + let columns_block = beacon_chain + .store + .get_blinded_block(&block_root) + .unwrap() + .and_then(|b| beacon_chain.store.make_full_block(&block_root, b).ok()); + if let Some(full_block) = columns_block { + let columns = beacon_chain::test_utils::generate_data_column_sidecars_from_block( + &full_block, + &beacon_chain.spec, + ); + if !columns.is_empty() + && let Some(store_op) = beacon_chain.get_blobs_or_columns_store_op( + block_root, + full_block.slot(), + beacon_chain::block_verification_types::AvailableBlockData::DataColumns( + columns, + ), + ) + { + ops.push(store_op); + } + } + ops.push(store::StoreOp::PutPayloadEnvelope( + block_root, + std::sync::Arc::new(envelope.as_ref().clone()), + )); beacon_chain .store - .put_payload_envelope(&block_root, envelope) + .do_atomically_with_block_and_blobs_cache(ops) .unwrap(); + // Update fork choice so head selection accounts for Full payload status. beacon_chain .canonical_head 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 aec2b1fcb8..4e636060e0 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -1848,7 +1848,7 @@ impl NetworkBeaconProcessor { // BlobNotRequired is unreachable. Only constructed in `process_gossip_blob` Err(e @ BlockError::InternalError(_)) | Err(e @ BlockError::BlobNotRequired(_)) - | Err(e @ BlockError::EnvelopeBlockRootUnknown { .. }) + | Err(e @ BlockError::EnvelopeBlockRootUnknown(_)) | Err(e @ BlockError::OptimisticSyncNotSupported { .. }) => { error!(error = %e, "Internal block gossip validation error"); return None; From dac8a6ec8d4c22446daf68294b852e0c3ca873f0 Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Fri, 1 May 2026 03:46:10 +0200 Subject: [PATCH 100/118] Gloas: fix test failures (KZG verifier wiring, harness columns, WSS sync) Brings the FORK_NAME=gloas beacon_chain test suite from 31 failures to green: - v1 KZG batch verifier couldn't verify Gloas columns. Added verify_columns_against_block helper that picks commitments per fork (Fulu: inline on column; Gloas: signed_execution_payload_bid). - BeaconChainHarness::process_envelope didn't persist columns. Now mirrors what production does in import_available_execution_payload_envelope. - get_or_reconstruct_blobs returned an error for Gloas. Now short-circuits to Ok(None); WSS test copies columns from source to dest directly. - update_data_column_signed_header (block_verification tests) only handled Fulu shape. Added a Gloas branch that re-keys to canonical_root. - BlockError::EnvelopeBlockRootUnknown changed to tuple variant. - Removed duplicate process_payload_envelope_availability. --- beacon_node/beacon_chain/src/beacon_chain.rs | 20 +++++-- .../beacon_chain/src/block_verification.rs | 2 +- .../src/data_availability_checker.rs | 50 ++++++++++++---- beacon_node/beacon_chain/src/kzg_utils.rs | 19 ++++-- .../payload_envelope_verification/import.rs | 4 +- .../pending_payload_cache/pending_column.rs | 14 +++-- .../pending_components.rs | 6 +- beacon_node/beacon_chain/src/test_utils.rs | 46 ++++++++++++--- .../beacon_chain/tests/block_verification.rs | 40 +++++++++---- beacon_node/beacon_chain/tests/store_tests.rs | 58 ++++++++++++++++++- .../gossip_methods.rs | 2 +- 11 files changed, 205 insertions(+), 56 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index a7f289b3f1..0a48a3b168 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -1331,6 +1331,14 @@ impl BeaconChain { return Ok(None); }; + // Gloas removes the standalone `BlobSidecar` shape — KZG commitments live in the bid and + // there's no signed-block-header / inclusion-proof to populate a `BlobSidecar` from. The + // canonical data is the column sidecar set on disk; callers needing data for a Gloas + // block should consume columns directly via `get_data_columns`. + if block.fork_name_unchecked().gloas_enabled() { + return Ok(None); + } + if self.spec.is_peer_das_enabled_for_epoch(block.epoch()) { let fork_name = self.spec.fork_name_at_epoch(block.epoch()); if let Some(columns) = self.store.get_data_columns(block_root, fork_name)? { @@ -3442,7 +3450,7 @@ impl BeaconChain { .gloas_enabled() { let bid = load_gloas_payload_bid(block_root, self)? - .ok_or(BlockError::EnvelopeBlockRootUnknown { block_root })?; + .ok_or(BlockError::EnvelopeBlockRootUnknown(block_root))?; let availability = self .pending_payload_cache .put_kzg_verified_custody_data_columns( @@ -3680,7 +3688,7 @@ impl BeaconChain { if is_gloas { let bid = load_gloas_payload_bid(block_root, self)? - .ok_or(BlockError::EnvelopeBlockRootUnknown { block_root })?; + .ok_or(BlockError::EnvelopeBlockRootUnknown(block_root))?; let pending_payload_cache = self.pending_payload_cache.clone(); let result = self .task_executor @@ -4005,7 +4013,7 @@ impl BeaconChain { .gloas_enabled() { let bid = load_gloas_payload_bid(block_root, self)? - .ok_or(BlockError::EnvelopeBlockRootUnknown { block_root })?; + .ok_or(BlockError::EnvelopeBlockRootUnknown(block_root))?; let availability = self .pending_payload_cache .put_gossip_verified_data_columns(block_root, bid, data_columns)?; @@ -4111,7 +4119,7 @@ impl BeaconChain { .gloas_enabled() { let bid = load_gloas_payload_bid(block_root, self)? - .ok_or(BlockError::EnvelopeBlockRootUnknown { block_root })?; + .ok_or(BlockError::EnvelopeBlockRootUnknown(block_root))?; let availability = self .pending_payload_cache .put_kzg_verified_custody_data_columns(block_root, bid, &data_columns) @@ -4155,7 +4163,7 @@ impl BeaconChain { .gloas_enabled() { let bid = load_gloas_payload_bid(block_root, self)? - .ok_or(BlockError::EnvelopeBlockRootUnknown { block_root })?; + .ok_or(BlockError::EnvelopeBlockRootUnknown(block_root))?; let availability = self .pending_payload_cache .put_rpc_custody_columns(block_root, bid, custody_columns) @@ -7750,7 +7758,7 @@ impl BeaconChain { ) } - pub(crate) fn get_blobs_or_columns_store_op( + pub fn get_blobs_or_columns_store_op( &self, block_root: Hash256, block_slot: Slot, diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index eeee562e9c..24f971f736 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -287,7 +287,7 @@ pub enum BlockError { /// https://github.com/sigp/lighthouse/issues/4546 AvailabilityCheck(AvailabilityCheckError), /// The payload envelope's block root is unknown. - EnvelopeBlockRootUnknown { block_root: Hash256 }, + EnvelopeBlockRootUnknown(Hash256), /// Optimistic sync is not supported for Gloas payload envelopes. OptimisticSyncNotSupported { block_root: Hash256 }, /// A Blob with a slot after PeerDAS is received and is not required to be imported. diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index 928d7a1bad..bdf16ec52c 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -33,6 +33,7 @@ use crate::data_column_verification::{ GossipVerifiedDataColumn, KzgVerifiedCustodyDataColumn, KzgVerifiedDataColumn, verify_kzg_for_data_column_list, }; +use crate::kzg_utils::validate_full_data_columns_with_commitments; use crate::metrics::{ KZG_DATA_COLUMN_RECONSTRUCTION_ATTEMPTS, KZG_DATA_COLUMN_RECONSTRUCTION_FAILURES, }; @@ -490,8 +491,7 @@ impl DataAvailabilityChecker { AvailableBlockData::Blobs(blobs) => verify_kzg_for_blob_list(blobs.iter(), &self.kzg) .map_err(AvailabilityCheckError::InvalidBlobs), AvailableBlockData::DataColumns(columns) => { - verify_kzg_for_data_column_list(columns.iter(), &self.kzg) - .map_err(AvailabilityCheckError::InvalidColumn) + verify_columns_against_block(&self.kzg, available_block.block(), columns) } } } @@ -504,13 +504,17 @@ impl DataAvailabilityChecker { available_blocks: &[AvailableBlock], ) -> Result<(), AvailabilityCheckError> { let mut all_blobs = Vec::new(); - let mut all_data_columns = Vec::new(); for available_block in available_blocks { - match available_block.data().to_owned() { + match available_block.data() { AvailableBlockData::NoData => {} - AvailableBlockData::Blobs(blobs) => all_blobs.extend(blobs), - AvailableBlockData::DataColumns(columns) => all_data_columns.extend(columns), + AvailableBlockData::Blobs(blobs) => all_blobs.extend(blobs.iter().cloned()), + AvailableBlockData::DataColumns(columns) => { + // Each block has its own commitments. For Gloas they live in the bid; for + // Fulu they live inline on the column. Verify per block and let the helper + // pick the right path. + verify_columns_against_block(&self.kzg, available_block.block(), columns)?; + } } } @@ -519,11 +523,6 @@ impl DataAvailabilityChecker { .map_err(AvailabilityCheckError::InvalidBlobs)?; } - if !all_data_columns.is_empty() { - verify_kzg_for_data_column_list(all_data_columns.iter(), &self.kzg) - .map_err(AvailabilityCheckError::InvalidColumn)?; - } - Ok(()) } @@ -679,6 +678,35 @@ impl DataAvailabilityChecker { } } +/// Verify a batch of data columns belonging to a single block, picking the right commitment +/// source for the block's fork (Fulu: inline on column; Gloas: from the embedded payload bid). +fn verify_columns_against_block( + kzg: &Kzg, + block: &SignedBeaconBlock, + columns: &[Arc>], +) -> Result<(), AvailabilityCheckError> { + if columns.is_empty() { + return Ok(()); + } + if block.fork_name_unchecked().gloas_enabled() { + let commitments = block + .message() + .body() + .signed_execution_payload_bid() + .map(|bid| bid.message.blob_kzg_commitments.clone()) + .map_err(|_| { + AvailabilityCheckError::Unexpected( + "Gloas block missing signed_execution_payload_bid".to_string(), + ) + })?; + validate_full_data_columns_with_commitments(kzg, columns.iter(), commitments.as_ref()) + .map_err(AvailabilityCheckError::InvalidColumn) + } else { + verify_kzg_for_data_column_list(columns.iter(), kzg) + .map_err(AvailabilityCheckError::InvalidColumn) + } +} + /// Helper struct to group data availability checker metrics. pub struct DataAvailabilityCheckerMetrics { pub block_cache_size: usize, diff --git a/beacon_node/beacon_chain/src/kzg_utils.rs b/beacon_node/beacon_chain/src/kzg_utils.rs index 5ad1cc115d..e1558f050b 100644 --- a/beacon_node/beacon_chain/src/kzg_utils.rs +++ b/beacon_node/beacon_chain/src/kzg_utils.rs @@ -676,12 +676,19 @@ pub fn reconstruct_blobs( let blob_indices: Vec = match blob_indices_opt { Some(indices) => indices.into_iter().map(|i| i as usize).collect(), None => { - // TODO(gloas): support blob reconstruction for Gloas - // https://github.com/sigp/lighthouse/issues/7413 - let num_of_blobs = first_data_column - .kzg_commitments() - .map_err(|_| "Gloas blob reconstruction not yet supported".to_string())? - .len(); + // Fulu columns carry commitments inline; Gloas columns don't, so fall back to + // the block's payload bid commitments. + let num_of_blobs = match first_data_column.kzg_commitments() { + Ok(commitments) => commitments.len(), + Err(_) => signed_block + .message() + .body() + .signed_execution_payload_bid() + .map(|bid| bid.message.blob_kzg_commitments.len()) + .map_err(|_| { + "Gloas blob reconstruction: block missing payload bid".to_string() + })?, + }; (0..num_of_blobs).collect() } }; 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 10d97add1d..e7c900dcd7 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs @@ -163,7 +163,7 @@ impl BeaconChain { let slot = envelope.envelope.slot(); let block_root = envelope.block_root; let bid = load_gloas_payload_bid(block_root, self)? - .ok_or(BlockError::EnvelopeBlockRootUnknown { block_root })?; + .ok_or(BlockError::EnvelopeBlockRootUnknown(block_root))?; let availability = self .pending_payload_cache .put_executed_payload_envelope(bid, envelope)?; @@ -253,7 +253,7 @@ impl BeaconChain { // been imported. We don't want to repeat work importing a block that is already imported. let fork_choice_reader = self.canonical_head.fork_choice_upgradable_read_lock(); if !fork_choice_reader.contains_block(&block_root) { - return Err(BlockError::EnvelopeBlockRootUnknown { block_root }); + return Err(BlockError::EnvelopeBlockRootUnknown(block_root)); } // TODO(gloas) add defensive check to see if payload envelope is already in fork choice diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/pending_column.rs b/beacon_node/beacon_chain/src/pending_payload_cache/pending_column.rs index 1c7a0389c3..0844bbf0f9 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/pending_column.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/pending_column.rs @@ -8,13 +8,15 @@ pub struct PendingColumn { cells: Vec, KzgProof)>>, } -impl Default for PendingColumn { - fn default() -> Self { - Self { cells: Vec::new() } - } -} - impl PendingColumn { + /// Allocate a `PendingColumn` whose `cells` vec has space for `blob_count` entries, all + /// initialised to `None`. Required so that `insert(idx, ...)` can write into `cells[idx]`. + pub fn new_with_capacity(blob_count: usize) -> Self { + Self { + cells: vec![None; blob_count], + } + } + pub fn insert(&mut self, index: usize, cell: &Cell, proof: &KzgProof) { if let Some(existing_cell) = self.cells.get_mut(index) && existing_cell.is_none() diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs b/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs index 20e6d1b52f..0516afd4e9 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs @@ -79,12 +79,16 @@ impl PendingComponents { &mut self, kzg_verified_data_columns: &[KzgVerifiedCustodyDataColumn], ) -> Result<(), AvailabilityCheckError> { + let num_blobs_expected = self.num_blobs_expected(); for data_column in kzg_verified_data_columns { let data_column = data_column.as_data_column(); + // The Vec-backed `PendingColumn` keys cells by index, so we have to allocate up to + // `num_blobs_expected` entries before inserting; otherwise `cells.get_mut(idx)` returns + // None and the insert is a no-op. let col = self .verified_data_columns .entry(*data_column.index()) - .or_default(); + .or_insert_with(|| PendingColumn::new_with_capacity(num_blobs_expected)); for (cell_idx, (cell, proof)) in data_column .column() .iter() diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index 4a9b7f7fa0..68cec11b76 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -2832,11 +2832,42 @@ where .await .expect("newPayload should succeed"); - // Store the envelope. + // Store the envelope and the data columns derived from the block. + // + // Production stores columns inside `import_available_execution_payload_envelope` after + // the cache is satisfied. The harness sidesteps that flow but must still persist columns + // or the `DataColumnMissing` invariant fires for any block with `num_expected_blobs > 0`. + let block = self + .chain + .store + .get_blinded_block(&block_root) + .expect("should read block from store") + .expect("block should exist in store"); + let mut ops = vec![]; + let block_with_full_payload = self + .chain + .store + .make_full_block(&block_root, block.clone()) + .expect("should reconstruct full block"); + let columns = + generate_data_column_sidecars_from_block(&block_with_full_payload, &self.spec); + if !columns.is_empty() + && let Some(store_op) = self.chain.get_blobs_or_columns_store_op( + block_root, + block.slot(), + AvailableBlockData::DataColumns(columns), + ) + { + ops.push(store_op); + } + ops.push(store::StoreOp::PutPayloadEnvelope( + block_root, + std::sync::Arc::new(signed_envelope), + )); self.chain .store - .put_payload_envelope(&block_root, &signed_envelope) - .expect("should store envelope"); + .do_atomically_with_block_and_blobs_cache(ops) + .expect("should persist envelope and columns"); // Update fork choice so it knows the payload was received. self.chain @@ -2857,11 +2888,10 @@ where block: Arc>, ) -> RangeSyncBlock { let block_root = block_root.unwrap_or_else(|| get_block_root(&block)); - let has_blobs = block - .message() - .body() - .blob_kzg_commitments() - .is_ok_and(|c| !c.is_empty()); + // For Gloas, kzg commitments live in the bid (`signed_execution_payload_bid`), so the + // body's `blob_kzg_commitments()` accessor returns Err. `num_expected_blobs` already + // handles both shapes. + let has_blobs = block.num_expected_blobs() > 0; if !has_blobs { return RangeSyncBlock::new( block, diff --git a/beacon_node/beacon_chain/tests/block_verification.rs b/beacon_node/beacon_chain/tests/block_verification.rs index af858874b2..38636540a1 100644 --- a/beacon_node/beacon_chain/tests/block_verification.rs +++ b/beacon_node/beacon_chain/tests/block_verification.rs @@ -323,18 +323,34 @@ fn update_data_column_signed_header( ) { for old_custody_column_sidecar in data_columns.as_mut_slice() { let old_column_sidecar = old_custody_column_sidecar.as_data_column(); - let new_column_sidecar = Arc::new(DataColumnSidecar::Fulu(DataColumnSidecarFulu { - index: *old_column_sidecar.index(), - column: old_column_sidecar.column().clone(), - kzg_commitments: old_column_sidecar.kzg_commitments().unwrap().clone(), - kzg_proofs: old_column_sidecar.kzg_proofs().clone(), - signed_block_header: signed_block.signed_block_header(), - kzg_commitments_inclusion_proof: signed_block - .message() - .body() - .kzg_commitments_merkle_proof() - .unwrap(), - })); + let new_column_sidecar = match old_column_sidecar.as_ref() { + DataColumnSidecar::Fulu(_) => { + Arc::new(DataColumnSidecar::Fulu(DataColumnSidecarFulu { + index: *old_column_sidecar.index(), + column: old_column_sidecar.column().clone(), + kzg_commitments: old_column_sidecar.kzg_commitments().unwrap().clone(), + kzg_proofs: old_column_sidecar.kzg_proofs().clone(), + signed_block_header: signed_block.signed_block_header(), + kzg_commitments_inclusion_proof: signed_block + .message() + .body() + .kzg_commitments_merkle_proof() + .unwrap(), + })) + } + // Gloas columns reference the block by `beacon_block_root` instead of holding the + // block header inline, so updating the parent root just means re-keying the column to + // the new canonical root. + DataColumnSidecar::Gloas(g) => { + Arc::new(DataColumnSidecar::Gloas(types::DataColumnSidecarGloas { + index: g.index, + column: g.column.clone(), + kzg_proofs: g.kzg_proofs.clone(), + slot: g.slot, + beacon_block_root: signed_block.canonical_root(), + })) + } + }; *old_custody_column_sidecar = CustodyDataColumn::from_asserted_custody(new_column_sidecar); } } diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index 86adf50995..171c83b2a9 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -3133,6 +3133,29 @@ async fn weak_subjectivity_sync_test( let beacon_chain = Arc::new(beacon_chain); let wss_block_root = wss_block.canonical_root(); + + // For Gloas, blobs aren't a standalone shape — the WSS data is the column sidecar set, which + // `get_or_reconstruct_blobs` returns `None` for. Copy the WSS block's columns straight from + // the source store so that the destination has them after checkpoint sync, matching what + // network-driven WSS would produce in production. + if wss_block.fork_name_unchecked().gloas_enabled() + && let Ok(Some(source_columns)) = harness + .chain + .store + .get_data_columns(&wss_block_root, ForkName::Gloas) + && !source_columns.is_empty() + && let Some(store_op) = beacon_chain.get_blobs_or_columns_store_op( + wss_block_root, + wss_block.slot(), + beacon_chain::block_verification_types::AvailableBlockData::DataColumns(source_columns), + ) + { + beacon_chain + .store + .do_atomically_with_block_and_blobs_cache(vec![store_op]) + .unwrap(); + } + let store_wss_block = harness .chain .get_block(&wss_block_root) @@ -3200,12 +3223,43 @@ async fn weak_subjectivity_sync_test( .await .unwrap(); - // Store the envelope and apply it to fork choice. + // Store the envelope, its columns, and apply to fork choice. if let Some(envelope) = &snapshot.execution_envelope { + // Persist data columns for Gloas blocks. This mirrors what production does in + // `import_available_execution_payload_envelope` and what the harness now does in + // `process_envelope` — the WSS forward-sync loop bypasses both, so do it directly. + let mut ops = vec![]; + let columns_block = beacon_chain + .store + .get_blinded_block(&block_root) + .unwrap() + .and_then(|b| beacon_chain.store.make_full_block(&block_root, b).ok()); + if let Some(full_block) = columns_block { + let columns = beacon_chain::test_utils::generate_data_column_sidecars_from_block( + &full_block, + &beacon_chain.spec, + ); + if !columns.is_empty() + && let Some(store_op) = beacon_chain.get_blobs_or_columns_store_op( + block_root, + full_block.slot(), + beacon_chain::block_verification_types::AvailableBlockData::DataColumns( + columns, + ), + ) + { + ops.push(store_op); + } + } + ops.push(store::StoreOp::PutPayloadEnvelope( + block_root, + std::sync::Arc::new(envelope.as_ref().clone()), + )); beacon_chain .store - .put_payload_envelope(&block_root, envelope) + .do_atomically_with_block_and_blobs_cache(ops) .unwrap(); + // Update fork choice so head selection accounts for Full payload status. beacon_chain .canonical_head 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 aec2b1fcb8..4e636060e0 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -1848,7 +1848,7 @@ impl NetworkBeaconProcessor { // BlobNotRequired is unreachable. Only constructed in `process_gossip_blob` Err(e @ BlockError::InternalError(_)) | Err(e @ BlockError::BlobNotRequired(_)) - | Err(e @ BlockError::EnvelopeBlockRootUnknown { .. }) + | Err(e @ BlockError::EnvelopeBlockRootUnknown(_)) | Err(e @ BlockError::OptimisticSyncNotSupported { .. }) => { error!(error = %e, "Internal block gossip validation error"); return None; From 0ce058835a92e68ae2546dce89f2f8fb13cb0de5 Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Fri, 1 May 2026 10:16:06 +0200 Subject: [PATCH 101/118] Address review comments - data_availability_checker.rs: use !gloas_enabled() instead of < ForkName::Gloas (jimmygchen, dapplion). - beacon_chain.rs: get_data_columns checks data_availability_checker first, then pending_payload_cache (dapplion). - pending_components.rs: merge_data_columns drops the unused Result return (jimmygchen). num_completed_columns uses filter() instead of filter_map (jimmygchen). - pending_column.rs: TODO marker on the hard-coded Gloas variant in try_to_sidecar (jimmygchen). - pending_payload_cache/mod.rs: gloas_spec test helper collapsed to ForkName::Gloas.make_genesis_spec(E::default_spec()) (jimmygchen). - gossip_methods.rs / sync/manager.rs: replace UnknownBlockHashFromAttestation fallback with TODO(gloas) for proper Gloas lookup sync (dapplion). --- beacon_node/beacon_chain/src/beacon_chain.rs | 4 ++-- .../beacon_chain/src/data_availability_checker.rs | 4 ++-- .../beacon_chain/src/pending_payload_cache/mod.rs | 15 +++------------ .../src/pending_payload_cache/pending_column.rs | 2 ++ .../pending_payload_cache/pending_components.rs | 6 ++---- .../network_beacon_processor/gossip_methods.rs | 10 +++++----- beacon_node/network/src/sync/manager.rs | 4 ++++ 7 files changed, 20 insertions(+), 25 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 0a48a3b168..30beefb8ce 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -1187,9 +1187,9 @@ impl BeaconChain { indices: &[ColumnIndex], ) -> Result, Error> { let all_cached_columns_opt = self - .pending_payload_cache + .data_availability_checker .get_data_columns(block_root) - .or_else(|| self.data_availability_checker.get_data_columns(block_root)) + .or_else(|| self.pending_payload_cache.get_data_columns(block_root)) .or_else(|| self.early_attester_cache.get_data_columns(block_root)); if let Some(mut all_cached_columns) = all_cached_columns_opt { diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index bdf16ec52c..54f21b97e0 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -21,7 +21,7 @@ use tracing::{debug, error, instrument}; use types::data::{BlobIdentifier, FixedBlobSidecarList, PartialDataColumn}; use types::{ BlobSidecar, BlobSidecarList, BlockImportSource, ChainSpec, DataColumnSidecar, - DataColumnSidecarList, Epoch, EthSpec, ForkName, Hash256, PartialDataColumnSidecarError, + DataColumnSidecarList, Epoch, EthSpec, Hash256, PartialDataColumnSidecarError, PartialDataColumnSidecarRef, SignedBeaconBlock, Slot, new_non_zero_usize, }; @@ -906,7 +906,7 @@ impl AvailableBlock { match &block_data { AvailableBlockData::NoData => { // For Gloas, DA is checked for the PayloadEnvelope, not for the block. - if block.fork_name_unchecked() < ForkName::Gloas { + if !block.fork_name_unchecked().gloas_enabled() { if columns_required { return Err(AvailabilityCheckError::MissingCustodyColumns); } else if blobs_required { diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs b/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs index b056eb1af9..58a276b179 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs @@ -185,7 +185,6 @@ impl PendingPayloadCache { let pending_components = self.get_pending_components(beacon_block_root, bid, |pending_components| { pending_components.insert_executed_payload_envelope(executed_envelope); - Ok(()) })?; let num_expected_columns = self @@ -423,14 +422,14 @@ impl PendingPayloadCache { update_fn: F, ) -> Result>, AvailabilityCheckError> where - F: FnOnce(&mut PendingComponents) -> Result<(), AvailabilityCheckError>, + F: FnOnce(&mut PendingComponents), { let mut write_lock = self.availability_cache.write(); { let pending_components = write_lock .get_or_insert_mut(block_root, || PendingComponents::new(block_root, bid)); - update_fn(pending_components)? + update_fn(pending_components) } RwLockReadGuard::try_map(RwLockWriteGuard::downgrade(write_lock), |cache| { @@ -623,15 +622,7 @@ mod data_availability_checker_tests { const RNG_SEED: u64 = 0xDEADBEEF; fn gloas_spec() -> Arc { - let mut spec = E::default_spec(); - spec.altair_fork_epoch = Some(Epoch::new(0)); - spec.bellatrix_fork_epoch = Some(Epoch::new(0)); - spec.capella_fork_epoch = Some(Epoch::new(0)); - spec.deneb_fork_epoch = Some(Epoch::new(0)); - spec.electra_fork_epoch = Some(Epoch::new(0)); - spec.fulu_fork_epoch = Some(Epoch::new(0)); - spec.gloas_fork_epoch = Some(Epoch::new(0)); - Arc::new(spec) + Arc::new(ForkName::Gloas.make_genesis_spec(E::default_spec())) } fn get_store_with_spec( diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/pending_column.rs b/beacon_node/beacon_chain/src/pending_payload_cache/pending_column.rs index 0844bbf0f9..6fd4428f56 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/pending_column.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/pending_column.rs @@ -59,6 +59,8 @@ impl PendingColumn { kzg_proofs.push(*proof); } + // TODO(gloas): this hard-codes the Gloas sidecar variant. Pass the fork in once + // post-Gloas variants are introduced (or move construction to a fork-aware helper). Some(Arc::new(DataColumnSidecar::Gloas(DataColumnSidecarGloas { index, // TODO(gloas): this should not error, but we need to catch it diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs b/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs index 0516afd4e9..b9679a12a3 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs @@ -78,7 +78,7 @@ impl PendingComponents { pub(crate) fn merge_data_columns( &mut self, kzg_verified_data_columns: &[KzgVerifiedCustodyDataColumn], - ) -> Result<(), AvailabilityCheckError> { + ) { let num_blobs_expected = self.num_blobs_expected(); for data_column in kzg_verified_data_columns { let data_column = data_column.as_data_column(); @@ -98,8 +98,6 @@ impl PendingComponents { col.insert(cell_idx, cell, proof); } } - - Ok(()) } // TODO(gloas): merge partial columns @@ -115,7 +113,7 @@ impl PendingComponents { pub fn num_completed_columns(&self) -> usize { self.verified_data_columns .values() - .filter_map(|col| col.is_complete(self.num_blobs_expected()).then_some(())) + .filter(|col| col.is_complete(self.num_blobs_expected())) .count() } 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 4e636060e0..b2b5960597 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -728,14 +728,14 @@ impl NetworkBeaconProcessor { .. } => { debug!( - action = "requesting block", + action = "ignoring", %unknown_block_root, "Unknown block root for column" ); - self.send_sync_message(SyncMessage::UnknownBlockHashFromAttestation( - peer_id, - unknown_block_root, - )); + // TODO(gloas): wire this into proper lookup sync. Sending + // `UnknownBlockHashFromAttestation` here is a Fulu-shaped fallback that + // mixes column processing with the attestation lookup path and is not + // the right primitive for Gloas column lookups. self.propagate_validation_result( message_id, peer_id, diff --git a/beacon_node/network/src/sync/manager.rs b/beacon_node/network/src/sync/manager.rs index 8eba8300cd..68d193ff50 100644 --- a/beacon_node/network/src/sync/manager.rs +++ b/beacon_node/network/src/sync/manager.rs @@ -906,6 +906,10 @@ impl SyncManager { ); } DataColumnSidecar::Gloas(_) => { + // TODO(gloas): proper lookup sync for Gloas. Routing into + // `handle_unknown_block_root` here mixes column processing with the + // single-block-lookup path; the Gloas column-arrives-before-block + // case wants its own queue/wakeup. debug!(%block_root, "Received unknown block data column message"); self.handle_unknown_block_root(peer_id, block_root); } From d75963bb865090d1f20c0221aa4fa77d44071bb7 Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Fri, 1 May 2026 10:28:59 +0200 Subject: [PATCH 102/118] Address remaining review comments - pending_column.rs: split try_to_sidecar into is_complete-checked to_sidecar with typed PendingColumnError so 'incomplete column' is no longer conflated with VariableList size-bound failures (jimmygchen, dapplion). - pending_components.rs: get_cached_data_columns filters by is_complete first, then logs an error if a complete column fails to assemble (dknopik's sanity check on filter_map silent drops). - data_column_verification.rs: add the missing column.slot == bid.slot consistency check in validate_data_column_sidecar_for_gossip_gloas, using the previously-defined-but-unused BlockSlotMismatch error variant (jimmygchen). --- .../src/data_column_verification.rs | 6 +++ .../pending_payload_cache/pending_column.rs | 39 ++++++++++++------- .../pending_components.rs | 33 +++++++++++----- 3 files changed, 53 insertions(+), 25 deletions(-) diff --git a/beacon_node/beacon_chain/src/data_column_verification.rs b/beacon_node/beacon_chain/src/data_column_verification.rs index d205604a71..1c1ba74582 100644 --- a/beacon_node/beacon_chain/src/data_column_verification.rs +++ b/beacon_node/beacon_chain/src/data_column_verification.rs @@ -1097,6 +1097,12 @@ pub fn validate_data_column_sidecar_for_gossip_gloas< slot: column_slot, }, )?; + if bid.message.slot != column_slot { + return Err(GossipDataColumnError::BlockSlotMismatch { + block_slot: bid.message.slot, + data_column_slot: column_slot, + }); + } let kzg_commitments = &bid.message.blob_kzg_commitments; verify_data_column_sidecar_with_commitments_len( &data_column, diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/pending_column.rs b/beacon_node/beacon_chain/src/pending_payload_cache/pending_column.rs index 6fd4428f56..22bb6d3620 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/pending_column.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/pending_column.rs @@ -36,24 +36,22 @@ impl PendingColumn { self.cells.len() == blob_count && self.cells.iter().all(|cell| cell.is_some()) } - pub fn try_to_sidecar( + /// Build a `DataColumnSidecar` from the cached cells. + /// + /// Caller MUST have checked `is_complete(blob_count)` first; this returns `Err` only on the + /// (currently theoretically impossible) `VariableList` size-bound failures, which we surface + /// as a typed error so the caller can log/metric it instead of silently producing nothing. + pub fn to_sidecar( &self, index: ColumnIndex, slot: Slot, beacon_block_root: Hash256, - blob_count: usize, - ) -> Option>> { - if self.cells.len() != blob_count { - return None; - } - - let mut column = Vec::with_capacity(blob_count); + ) -> Result>, PendingColumnError> { + let mut column = Vec::with_capacity(self.cells.len()); let mut kzg_proofs = Vec::with_capacity(self.cells.len()); for cell in self.cells.iter() { - let Some((cell, proof)) = cell else { - return None; - }; + let (cell, proof) = cell.as_ref().ok_or(PendingColumnError::IncompleteColumn)?; // TODO(gloas): we likely want to go and arc all cells column.push(cell.clone()); kzg_proofs.push(*proof); @@ -61,13 +59,24 @@ impl PendingColumn { // TODO(gloas): this hard-codes the Gloas sidecar variant. Pass the fork in once // post-Gloas variants are introduced (or move construction to a fork-aware helper). - Some(Arc::new(DataColumnSidecar::Gloas(DataColumnSidecarGloas { + Ok(Arc::new(DataColumnSidecar::Gloas(DataColumnSidecarGloas { index, - // TODO(gloas): this should not error, but we need to catch it - column: VariableList::try_from(column).ok()?, - kzg_proofs: VariableList::try_from(kzg_proofs).ok()?, + column: VariableList::try_from(column) + .map_err(|_| PendingColumnError::ColumnSizeExceedsBound)?, + kzg_proofs: VariableList::try_from(kzg_proofs) + .map_err(|_| PendingColumnError::ProofsSizeExceedsBound)?, slot, beacon_block_root, }))) } } + +/// Errors returned by [`PendingColumn::to_sidecar`]. `IncompleteColumn` should never fire if the +/// caller checks [`PendingColumn::is_complete`] first; the size-bound variants reflect spec-bound +/// invariants and should never fire in practice. +#[derive(Debug, Clone)] +pub enum PendingColumnError { + IncompleteColumn, + ColumnSizeExceedsBound, + ProofsSizeExceedsBound, +} diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs b/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs index b9679a12a3..4ff429fd87 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs @@ -7,7 +7,7 @@ use crate::pending_payload_cache::pending_column::PendingColumn; use std::cmp::Ordering; use std::collections::HashMap; use std::sync::Arc; -use tracing::{Span, debug, debug_span}; +use tracing::{Span, debug, debug_span, error}; use types::DataColumnSidecar; use types::{ AbstractExecPayload, BeaconStateError, ColumnIndex, EthSpec, Hash256, SignedBeaconBlock, @@ -48,18 +48,31 @@ impl PendingComponents { self.bid.message.blob_kzg_commitments.len() } - /// Returns the completed custody columns + /// Returns the completed custody columns. + /// + /// Skips columns that are not yet complete and logs an error if a complete column fails to + /// build (a spec-bound invariant would have to be violated; should never happen in practice). pub fn get_cached_data_columns(&self) -> Vec>> { + let blob_count = self.num_blobs_expected(); + let slot = self.bid.message.slot; + let block_root = self.block_root; self.verified_data_columns .iter() - .filter_map(|(col_idx, col)| { - col.try_to_sidecar( - *col_idx, - self.bid.message.slot, - self.block_root, - self.num_blobs_expected(), - ) - }) + .filter(|(_, col)| col.is_complete(blob_count)) + .filter_map( + |(col_idx, col)| match col.to_sidecar(*col_idx, slot, block_root) { + Ok(sidecar) => Some(sidecar), + Err(e) => { + error!( + ?e, + column_index = %col_idx, + ?block_root, + "Failed to build sidecar for complete column" + ); + None + } + }, + ) .collect() } From 8510bb462db9f9d66d65fbc1e01c403838e9f4ea Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Fri, 1 May 2026 10:51:05 +0200 Subject: [PATCH 103/118] smol cleanup --- beacon_node/beacon_chain/src/beacon_chain.rs | 9 ++++-- .../src/data_column_verification.rs | 32 ++++++++++++++----- .../src/pending_payload_cache/mod.rs | 10 ++++-- .../pending_components.rs | 20 +----------- 4 files changed, 40 insertions(+), 31 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 0a48a3b168..91aefda40f 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -71,7 +71,6 @@ use crate::pending_payload_cache::PendingPayloadCache; use crate::pending_payload_cache::{ Availability as PayloadAvailability, DataColumnReconstructionResult as DataColumnReconstructionResultGloas, - signed_payload_bid_from_block, }; use crate::pending_payload_envelopes::PendingPayloadEnvelopes; use crate::persisted_beacon_chain::PersistedBeaconChain; @@ -3835,7 +3834,13 @@ impl BeaconChain { let block = execution_pending.block.block_cloned(); if block.fork_name_unchecked().gloas_enabled() { - let bid = signed_payload_bid_from_block(block.as_ref())?; + let bid = Arc::new( + block + .message() + .body() + .signed_execution_payload_bid()? + .clone(), + ); chain.pending_payload_cache.insert_bid(block_root, bid); } diff --git a/beacon_node/beacon_chain/src/data_column_verification.rs b/beacon_node/beacon_chain/src/data_column_verification.rs index d205604a71..2cc078871b 100644 --- a/beacon_node/beacon_chain/src/data_column_verification.rs +++ b/beacon_node/beacon_chain/src/data_column_verification.rs @@ -9,7 +9,6 @@ use crate::kzg_utils::{ use crate::observed_data_sidecars::{ Error as ObservedDataSidecarsError, ObservationKey, ObservationStrategy, Observe, }; -use crate::pending_payload_cache::signed_payload_bid_from_block; use crate::{BeaconChain, BeaconChainError, BeaconChainTypes, metrics}; use educe::Educe; use fork_choice::ProtoBlock; @@ -1313,19 +1312,36 @@ pub(crate) fn load_gloas_payload_bid( } let bid = if let Some(block) = chain.early_attester_cache.get_block(block_root) { - signed_payload_bid_from_block(block.as_ref()).map_err(BeaconChainError::BeaconStateError)? + Arc::new( + block + .message() + .body() + .signed_execution_payload_bid() + .map_err(BeaconChainError::BeaconStateError)? + .clone(), + ) } else { match chain .store .try_get_full_block(&block_root) .map_err(BeaconChainError::DBError)? { - Some(DatabaseBlock::Full(block)) => { - signed_payload_bid_from_block(&block).map_err(BeaconChainError::BeaconStateError)? - } - Some(DatabaseBlock::Blinded(block)) => { - signed_payload_bid_from_block(&block).map_err(BeaconChainError::BeaconStateError)? - } + Some(DatabaseBlock::Full(block)) => Arc::new( + block + .message() + .body() + .signed_execution_payload_bid() + .map_err(BeaconChainError::BeaconStateError)? + .clone(), + ), + Some(DatabaseBlock::Blinded(block)) => Arc::new( + block + .message() + .body() + .signed_execution_payload_bid() + .map_err(BeaconChainError::BeaconStateError)? + .clone(), + ), None => { return Ok(None); } diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs b/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs index b056eb1af9..3aaf688602 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs @@ -41,7 +41,6 @@ use crate::metrics::{ KZG_DATA_COLUMN_RECONSTRUCTION_ATTEMPTS, KZG_DATA_COLUMN_RECONSTRUCTION_FAILURES, }; use crate::observed_data_sidecars::ObservationStrategy; -pub use pending_components::signed_payload_bid_from_block; use pending_components::{PendingComponents, ReconstructColumnsDecision}; use types::SignedExecutionPayloadBid; use types::new_non_zero_usize; @@ -725,7 +724,14 @@ mod data_availability_checker_tests { let (block, data_columns) = generate_rand_block_and_data_columns::(ForkName::Gloas, num_blobs, &mut rng, spec); let block_root = block.canonical_root(); - let bid = signed_payload_bid_from_block(&block).expect("should get payload bid"); + let bid = Arc::new( + block + .message() + .body() + .signed_execution_payload_bid() + .expect("should get payload bid") + .clone(), + ); cache.insert_bid(block_root, bid.clone()); (bid, block_root, data_columns) } diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs b/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs index 0516afd4e9..63c5f4dcda 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/pending_components.rs @@ -9,25 +9,7 @@ use std::collections::HashMap; use std::sync::Arc; use tracing::{Span, debug, debug_span}; use types::DataColumnSidecar; -use types::{ - AbstractExecPayload, BeaconStateError, ColumnIndex, EthSpec, Hash256, SignedBeaconBlock, - SignedExecutionPayloadBid, -}; - -/// Extract the signed execution payload bid from a Gloas block as a shareable `Arc`. -/// -/// Returns `Err` if the block is not a Gloas block. -pub fn signed_payload_bid_from_block>( - block: &SignedBeaconBlock, -) -> Result>, BeaconStateError> { - Ok(Arc::new( - block - .message() - .body() - .signed_execution_payload_bid()? - .clone(), - )) -} +use types::{ColumnIndex, EthSpec, Hash256, SignedExecutionPayloadBid}; /// This represents the components of a payload pending data availability. /// From 5ce7c59f5eddbe44f969627992714b9eb0c2be5e Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Fri, 1 May 2026 11:10:15 +0200 Subject: [PATCH 104/118] Rename validate_full_data_columns_with_commitments --- beacon_node/beacon_chain/src/data_availability_checker.rs | 4 ++-- beacon_node/beacon_chain/src/data_column_verification.rs | 6 +++--- beacon_node/beacon_chain/src/kzg_utils.rs | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index 54f21b97e0..637e8acdc8 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -33,7 +33,7 @@ use crate::data_column_verification::{ GossipVerifiedDataColumn, KzgVerifiedCustodyDataColumn, KzgVerifiedDataColumn, verify_kzg_for_data_column_list, }; -use crate::kzg_utils::validate_full_data_columns_with_commitments; +use crate::kzg_utils::validate_data_columns_with_commitments; use crate::metrics::{ KZG_DATA_COLUMN_RECONSTRUCTION_ATTEMPTS, KZG_DATA_COLUMN_RECONSTRUCTION_FAILURES, }; @@ -699,7 +699,7 @@ fn verify_columns_against_block( "Gloas block missing signed_execution_payload_bid".to_string(), ) })?; - validate_full_data_columns_with_commitments(kzg, columns.iter(), commitments.as_ref()) + validate_data_columns_with_commitments(kzg, columns.iter(), commitments.as_ref()) .map_err(AvailabilityCheckError::InvalidColumn) } else { verify_kzg_for_data_column_list(columns.iter(), kzg) diff --git a/beacon_node/beacon_chain/src/data_column_verification.rs b/beacon_node/beacon_chain/src/data_column_verification.rs index 08c4bbd5fd..3710937b1d 100644 --- a/beacon_node/beacon_chain/src/data_column_verification.rs +++ b/beacon_node/beacon_chain/src/data_column_verification.rs @@ -3,8 +3,8 @@ use crate::block_verification::{ }; use crate::data_availability_checker::MissingCellsError; use crate::kzg_utils::{ - reconstruct_data_columns, validate_full_data_columns, - validate_full_data_columns_with_commitments, validate_partial_data_columns, + reconstruct_data_columns, validate_data_columns_with_commitments, validate_full_data_columns, + validate_partial_data_columns, }; use crate::observed_data_sidecars::{ Error as ObservedDataSidecarsError, ObservationKey, ObservationStrategy, Observe, @@ -490,7 +490,7 @@ impl KzgVerifiedDataColumn { kzg: &Kzg, ) -> Result, (Option, KzgError)> { let _timer = metrics::start_timer(&metrics::KZG_VERIFICATION_DATA_COLUMN_BATCH_TIMES); - validate_full_data_columns_with_commitments(kzg, data_columns.iter(), kzg_commitments)?; + validate_data_columns_with_commitments(kzg, data_columns.iter(), kzg_commitments)?; Ok(data_columns .into_iter() .map(|column| Self { diff --git a/beacon_node/beacon_chain/src/kzg_utils.rs b/beacon_node/beacon_chain/src/kzg_utils.rs index e1558f050b..14eb7ee07a 100644 --- a/beacon_node/beacon_chain/src/kzg_utils.rs +++ b/beacon_node/beacon_chain/src/kzg_utils.rs @@ -115,7 +115,7 @@ pub fn validate_full_data_columns<'a, E: EthSpec>( /// /// Gloas sidecars do not carry commitments. Their commitments come from the block's /// `ExecutionPayloadBid`. -pub fn validate_full_data_columns_with_commitments<'a, E: EthSpec>( +pub fn validate_data_columns_with_commitments<'a, E: EthSpec>( kzg: &Kzg, data_column_iter: impl Iterator>>, kzg_commitments: &[KzgCommitment], From cbe7bec40dc560203c75c4d02000fee51494288c Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Fri, 1 May 2026 11:10:49 +0200 Subject: [PATCH 105/118] Update beacon_node/beacon_chain/src/pending_payload_cache/mod.rs --- beacon_node/beacon_chain/src/pending_payload_cache/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs b/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs index fbe68c97b2..3eaad08193 100644 --- a/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs +++ b/beacon_node/beacon_chain/src/pending_payload_cache/mod.rs @@ -1,5 +1,5 @@ //! This module builds out the data availability cache for Gloas. When a beacon block is received -//! over gossip/p2p we insert its payload into this cache, keyed by block root. As soon as the bid +//! over gossip/p2p we insert its bid into this cache, keyed by block root. As soon as the bid //! is received we can begin using it to verify data columns. //! //! When a payload envelope is received over gossip/p2p we first insert it as a pre-executed envelope. A separate From 47bcd0b347e908c844b15409214d04fb60baeb3e Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Fri, 1 May 2026 11:12:18 +0200 Subject: [PATCH 106/118] Fix TODO --- beacon_node/network/src/network_beacon_processor/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beacon_node/network/src/network_beacon_processor/mod.rs b/beacon_node/network/src/network_beacon_processor/mod.rs index 817d4bd5ca..b11e78bd9b 100644 --- a/beacon_node/network/src/network_beacon_processor/mod.rs +++ b/beacon_node/network/src/network_beacon_processor/mod.rs @@ -998,7 +998,7 @@ impl NetworkBeaconProcessor { } // Publish partial columns without eager send - // TODO(gloas): implement + // TODO(gloas): implement publish partial columns without eager send if let Some(assembler) = self.chain.data_availability_checker.partial_assembler() { let columns = assembler.get_partials_and_mark_as_local_fetched(block_root, &header); if !columns.is_empty() { From d6e3b006ead00eff6b016ecbfa562706375a50b4 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Fri, 1 May 2026 22:18:15 +0200 Subject: [PATCH 107/118] fix --- beacon_node/beacon_chain/src/beacon_chain.rs | 16 +++++- .../beacon_chain/src/block_verification.rs | 10 ++++ .../payload_envelope_verification/import.rs | 52 +++++++++++++++++++ 3 files changed, 76 insertions(+), 2 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index da904726d1..9fcaabc7c4 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -3185,11 +3185,12 @@ impl BeaconChain { }; // Import the blocks into the chain. - for (signature_verified_block, _envelope) in signature_verified_blocks { + for (signature_verified_block, envelope) in signature_verified_blocks { let block_slot = signature_verified_block.slot(); + let block_root = signature_verified_block.block_root(); match self .process_block( - signature_verified_block.block_root(), + block_root, signature_verified_block, notify_execution_layer, BlockImportSource::RangeSync, @@ -3200,6 +3201,17 @@ impl BeaconChain { Ok(status) => { match status { AvailabilityProcessingStatus::Imported(block_root) => { + // Import the envelope if one was provided (Gloas+). + if let Some(envelope) = envelope + && let Err(e) = self.import_envelope_from_range_sync( + *envelope, block_root, + ) + { + return ChainSegmentResult::Failed { + imported_blocks, + error: e, + }; + } // The block was imported successfully. imported_blocks.push((block_root, block_slot)); } diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 597b557f69..7d411c8154 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -2019,10 +2019,20 @@ fn load_parent>( // If the parent's execution payload envelope hasn't arrived yet, // return an unknown parent error so the block gets sent to the // reprocess queue. + // + // Skip this check if the parent is at or before the finalized slot (e.g. after + // checkpoint sync the finalized block won't have a stored envelope). + let finalized_slot = chain + .canonical_head + .cached_head() + .finalized_checkpoint() + .epoch + .start_slot(T::EthSpec::slots_per_epoch()); if chain .spec .fork_name_at_slot::(parent_block.slot()) .gloas_enabled() + && parent_block.slot() > finalized_slot { let _envelope = chain .store 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 e7c900dcd7..36da758db4 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs @@ -392,4 +392,56 @@ impl BeaconChain { )); } } + + /// Import an execution payload envelope received via range sync. + /// + /// This is a simplified import path that trusts the envelope since it was fetched alongside + /// a valid block during range sync. It stores the envelope to the database and marks it as + /// received in fork choice. + pub fn import_envelope_from_range_sync( + &self, + envelope: AvailableEnvelope, + block_root: Hash256, + ) -> Result<(), BlockError> { + let fork_choice_reader = self.canonical_head.fork_choice_upgradable_read_lock(); + if !fork_choice_reader.contains_block(&block_root) { + return Err(BlockError::EnvelopeBlockRootUnknown(block_root)); + } + + let mut fork_choice = parking_lot::RwLockUpgradableReadGuard::upgrade(fork_choice_reader); + + fork_choice + .on_valid_payload_envelope_received(block_root) + .map_err(|e| BlockError::InternalError(format!("{e:?}")))?; + + let (signed_envelope, columns) = envelope.deconstruct(); + + let mut ops = vec![]; + + if let Some(blobs_or_columns_store_op) = self.get_blobs_or_columns_store_op( + block_root, + signed_envelope.slot(), + AvailableBlockData::DataColumns(columns), + ) { + ops.push(blobs_or_columns_store_op); + } + + ops.push(StoreOp::PutPayloadEnvelope( + block_root, + signed_envelope, + )); + + drop(fork_choice); + + if let Err(e) = self.store.do_atomically_with_block_and_blobs_cache(ops) { + error!( + msg = "Failed to store range sync envelope", + error = ?e, + "Database write failed!" + ); + return Err(e.into()); + } + + Ok(()) + } } From 6af1a927b87977b4e369db0b702e572adb5139e7 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Mon, 4 May 2026 14:57:49 +0300 Subject: [PATCH 108/118] Actually do something with the responses... --- beacon_node/network/src/router.rs | 74 +++++++++++++++++++++++-- beacon_node/network/src/sync/manager.rs | 47 ++++++++++++++++ 2 files changed, 117 insertions(+), 4 deletions(-) diff --git a/beacon_node/network/src/router.rs b/beacon_node/network/src/router.rs index 443fa51cc6..5326778794 100644 --- a/beacon_node/network/src/router.rs +++ b/beacon_node/network/src/router.rs @@ -26,6 +26,7 @@ use tokio_stream::wrappers::UnboundedReceiverStream; use tracing::{debug, error, trace, warn}; use types::{ BlobSidecar, DataColumnSidecar, EthSpec, ForkContext, PartialDataColumn, SignedBeaconBlock, + SignedExecutionPayloadEnvelope, }; /// Handles messages from the network and routes them to the appropriate service to be handled. @@ -341,10 +342,19 @@ impl Router { Response::DataColumnsByRange(data_column) => { self.on_data_columns_by_range_response(peer_id, app_request_id, data_column); } - // TODO(EIP-7732): implement outgoing payload envelopes by range and root - // responses once sync manager requests them. - Response::PayloadEnvelopesByRoot(_) | Response::PayloadEnvelopesByRange(_) => { - debug!("Requesting envelopes by root and by range not supported yet"); + Response::PayloadEnvelopesByRoot(payload_envelope) => { + self.on_payload_envelopes_by_root_response( + peer_id, + app_request_id, + payload_envelope, + ); + } + Response::PayloadEnvelopesByRange(payload_envelope) => { + self.on_payload_envelopes_by_range_response( + peer_id, + app_request_id, + payload_envelope, + ); } // Light client responses should not be received Response::LightClientBootstrap(_) @@ -809,6 +819,62 @@ impl Router { } } + pub fn on_payload_envelopes_by_root_response( + &mut self, + peer_id: PeerId, + app_request_id: AppRequestId, + payload_envelope: Option>>, + ) { + let sync_request_id = match app_request_id { + AppRequestId::Sync(sync_id) => match sync_id { + id @ SyncRequestId::SinglePayloadEnvelope { .. } => id, + other => { + crit!(request = ?other, "PayloadEnvelopesByRoot response on incorrect request"); + return; + } + }, + AppRequestId::Router => { + crit!(%peer_id, "All PayloadEnvelopesByRoot requests belong to sync"); + return; + } + AppRequestId::Internal => unreachable!("Handled internally"), + }; + + trace!( + %peer_id, + "Received PayloadEnvelopesByRoot Response" + ); + self.send_to_sync(SyncMessage::RpcPayloadEnvelope { + sync_request_id, + peer_id, + payload_envelope, + seen_timestamp: self.chain.slot_clock.now_duration().unwrap_or_default(), + }); + } + + pub fn on_payload_envelopes_by_range_response( + &mut self, + peer_id: PeerId, + app_request_id: AppRequestId, + payload_envelope: Option>>, + ) { + trace!( + %peer_id, + "Received PayloadEnvelopesByRange Response" + ); + + if let AppRequestId::Sync(sync_request_id) = app_request_id { + self.send_to_sync(SyncMessage::RpcPayloadEnvelope { + peer_id, + sync_request_id, + payload_envelope, + seen_timestamp: self.chain.slot_clock.now_duration().unwrap_or_default(), + }); + } else { + crit!("All payload envelopes by range responses should belong to sync"); + } + } + fn handle_beacon_processor_send_result( &mut self, result: Result<(), crate::network_beacon_processor::Error>, diff --git a/beacon_node/network/src/sync/manager.rs b/beacon_node/network/src/sync/manager.rs index 5db0f9e921..c3b3daf4d6 100644 --- a/beacon_node/network/src/sync/manager.rs +++ b/beacon_node/network/src/sync/manager.rs @@ -134,6 +134,14 @@ pub enum SyncMessage { seen_timestamp: Duration, }, + /// A payload envelope has been received from the RPC. + RpcPayloadEnvelope { + sync_request_id: SyncRequestId, + peer_id: PeerId, + payload_envelope: Option>>, + seen_timestamp: Duration, + }, + /// A block with an unknown parent has been received. UnknownParentBlock(PeerId, Arc>, Hash256), @@ -853,6 +861,17 @@ impl SyncManager { } => { self.rpc_data_column_received(sync_request_id, peer_id, data_column, seen_timestamp) } + SyncMessage::RpcPayloadEnvelope { + sync_request_id, + peer_id, + payload_envelope, + seen_timestamp, + } => self.rpc_payload_envelope_received( + sync_request_id, + peer_id, + payload_envelope, + seen_timestamp, + ), SyncMessage::UnknownParentBlock(peer_id, block, block_root) => { let block_slot = block.slot(); let parent_root = block.parent_root(); @@ -1238,6 +1257,34 @@ impl SyncManager { } } + fn rpc_payload_envelope_received( + &mut self, + sync_request_id: SyncRequestId, + peer_id: PeerId, + payload_envelope: Option>>, + seen_timestamp: Duration, + ) { + match sync_request_id { + SyncRequestId::SinglePayloadEnvelope { id } => { + self.on_single_envelope_response( + id, + peer_id, + RpcEvent::from_chunk(payload_envelope, seen_timestamp), + ); + } + SyncRequestId::PayloadEnvelopesByRange(req_id) => { + self.on_payload_envelopes_by_range_response( + req_id, + peer_id, + RpcEvent::from_chunk(payload_envelope, seen_timestamp), + ); + } + _ => { + crit!(%peer_id, "bad request id for payload_envelope"); + } + } + } + fn on_single_blob_response( &mut self, id: SingleLookupReqId, From 75d4333776c25f7bdf7d975504fe24c37e1500fc Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Mon, 4 May 2026 15:46:48 +0300 Subject: [PATCH 109/118] Hack for checkpoint sync --- beacon_node/beacon_chain/src/beacon_chain.rs | 24 ++++++++++++++++++- .../beacon_chain/src/block_verification.rs | 9 +++---- .../network_beacon_processor/sync_methods.rs | 10 ++++++++ 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index d5920928aa..190988589e 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -3041,7 +3041,29 @@ impl BeaconChain { // 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 { .. }) => { + // For Gloas blocks, persist the envelope even though we're skipping + // the block. This is needed after checkpoint sync: the checkpoint + // block's envelope must be in the store so that `load_parent` can + // verify it when importing the first post-checkpoint block. + if let RangeSyncBlock::Gloas { + envelope: Some(ref available_envelope), + .. + } = block + { + let (signed_envelope, _columns) = available_envelope.clone().deconstruct(); + if let Err(e) = self + .store + .put_payload_envelope(&block_root, &signed_envelope) + { + return Err(Box::new(ChainSegmentResult::Failed { + imported_blocks, + error: BlockError::BeaconChainError(Box::new(e.into())), + })); + } + } + continue; + } // 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 }) => { diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 7a81a4465e..9b6e71ccbf 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -2017,10 +2017,11 @@ fn load_parent>( // If the parent's execution payload envelope hasn't arrived yet, // return an unknown parent error so the block gets sent to the // reprocess queue. - if chain - .spec - .fork_name_at_slot::(parent_block.slot()) - .gloas_enabled() + if parent_block.slot() != 0 + && chain + .spec + .fork_name_at_slot::(parent_block.slot()) + .gloas_enabled() { let _envelope = chain .store diff --git a/beacon_node/network/src/network_beacon_processor/sync_methods.rs b/beacon_node/network/src/network_beacon_processor/sync_methods.rs index 8f89b66948..968120b4bf 100644 --- a/beacon_node/network/src/network_beacon_processor/sync_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/sync_methods.rs @@ -864,6 +864,16 @@ impl NetworkBeaconProcessor { peer_action: Some(PeerAction::LowToleranceError), }) } + BlockError::ParentEnvelopeUnknown { parent_root } => { + Err(ChainSegmentFailed { + message: format!( + "Block's parent envelope has not been received: {}", + parent_root + ), + // Don't penalize the peer, the envelope may arrive later. + peer_action: None, + }) + } BlockError::DuplicateFullyImported(_) | BlockError::DuplicateImportStatusUnknown(..) => { // This can happen for many reasons. Head sync's can download multiples and parent From fff72716ba48ed1cc7d526e5ca3552287bed694b Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Mon, 4 May 2026 16:33:03 +0300 Subject: [PATCH 110/118] hacky fix --- beacon_node/beacon_chain/src/beacon_chain.rs | 34 ++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 190988589e..3f2e8687e8 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -3172,11 +3172,41 @@ impl BeaconChain { }; // Import the blocks into the chain. - for (signature_verified_block, _envelope) in signature_verified_blocks { + for (signature_verified_block, envelope) in signature_verified_blocks { + let block_root = signature_verified_block.block_root(); let block_slot = signature_verified_block.slot(); + + // For Gloas blocks, persist the envelope and notify fork choice + // before importing the block. The next block's `load_parent` will + // check for this envelope in the store. + if let Some(available_envelope) = envelope { + let (signed_envelope, _columns) = available_envelope.deconstruct(); + if let Err(e) = self + .store + .put_payload_envelope(&block_root, &signed_envelope) + { + return ChainSegmentResult::Failed { + imported_blocks, + error: BlockError::BeaconChainError(Box::new(e.into())), + }; + } + if let Err(e) = self + .canonical_head + .fork_choice_write_lock() + .on_valid_payload_envelope_received(block_root) + { + return ChainSegmentResult::Failed { + imported_blocks, + error: BlockError::BeaconChainError(Box::new( + BeaconChainError::ForkChoiceError(e), + )), + }; + } + } + match self .process_block( - signature_verified_block.block_root(), + block_root, signature_verified_block, notify_execution_layer, BlockImportSource::RangeSync, From e0048b52060a11943ffde2c728b806b606021b90 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Mon, 4 May 2026 16:50:35 +0300 Subject: [PATCH 111/118] Another hack --- .../network/src/sync/range_sync/range.rs | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/beacon_node/network/src/sync/range_sync/range.rs b/beacon_node/network/src/sync/range_sync/range.rs index 6509ac3cb3..80fee1c5ca 100644 --- a/beacon_node/network/src/sync/range_sync/range.rs +++ b/beacon_node/network/src/sync/range_sync/range.rs @@ -93,6 +93,22 @@ where } } + /// For Gloas, start range sync one epoch earlier so the first batch fetches the + /// parent block's payload envelope. Without this, the first block in the batch + /// fails `load_parent` because the preceding block's envelope isn't in the store. + fn gloas_adjusted_start_epoch(&self, epoch: Epoch) -> Epoch { + if self + .beacon_chain + .spec + .gloas_fork_epoch + .is_some_and(|gloas_epoch| epoch > gloas_epoch) + { + epoch.saturating_sub(1_u64) + } else { + epoch + } + } + #[cfg(test)] pub(crate) fn __failed_chains(&mut self) -> Vec { self.failed_chains.keys().copied().collect() @@ -156,8 +172,13 @@ where // Note: We keep current head chains. These can continue syncing whilst we complete // this new finalized chain. + // Start one epoch earlier for Gloas so the first batch includes + // the parent block's envelope. Without this, the first block in the + // batch fails because `load_parent` can't find the parent's envelope. + let start_epoch = self.gloas_adjusted_start_epoch(local_info.finalized_epoch); + self.chains.add_peer_or_create_chain( - local_info.finalized_epoch, + start_epoch, remote_info.finalized_root, target_head_slot, peer_id, @@ -188,6 +209,7 @@ where let start_epoch = std::cmp::min(local_info.head_slot, remote_finalized_slot) .epoch(T::EthSpec::slots_per_epoch()); + let start_epoch = self.gloas_adjusted_start_epoch(start_epoch); self.chains.add_peer_or_create_chain( start_epoch, remote_info.head_root, From d8e359a7b652ccb68f3316ec2d9dac1fd9016e80 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Thu, 7 May 2026 12:37:03 +0300 Subject: [PATCH 112/118] Prevent chain stall --- beacon_node/beacon_chain/src/block_verification.rs | 10 ++++++---- beacon_node/network/src/sync/block_lookups/mod.rs | 7 +------ 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 84df30e75f..24aae7b999 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -930,16 +930,17 @@ impl GossipVerifiedBlock { let (parent_block, block) = verify_parent_block_is_known::(&fork_choice_read_lock, block)?; + let Ok(bid) = block.message().body().signed_execution_payload_bid() else { + return Err(BlockError::InternalError("Invalid variant".to_string())); + }; + // [New in Gloas]: Verify bid.parent_block_root matches block.parent_root. - if let Ok(bid) = block.message().body().signed_execution_payload_bid() - && bid.message.parent_block_root != block.message().parent_root() - { + if bid.message.parent_block_root != block.message().parent_root() { return Err(BlockError::BidParentRootMismatch { bid_parent_root: bid.message.parent_block_root, block_parent_root: block.message().parent_root(), }); } - // Check that we've received the parent envelope. If not, issue a single envelope // lookup for the parent and queue this block in the reprocess queue. // @@ -955,6 +956,7 @@ impl GossipVerifiedBlock { if parent_is_gloas && !parent_is_anchor && !fork_choice_read_lock.is_payload_received(&block.message().parent_root()) + && Some(bid.message.parent_block_hash) != parent_block.execution_payload_block_hash { return Err(BlockError::ParentEnvelopeUnknown { parent_root: block.message().parent_root(), diff --git a/beacon_node/network/src/sync/block_lookups/mod.rs b/beacon_node/network/src/sync/block_lookups/mod.rs index bb003dc222..a9d08c30a4 100644 --- a/beacon_node/network/src/sync/block_lookups/mod.rs +++ b/beacon_node/network/src/sync/block_lookups/mod.rs @@ -662,12 +662,7 @@ impl BlockLookups { "Envelope processed, transitioning to custody column lookup" ); let lookup_result = lookup.continue_requests(cx); - self.on_lookup_result( - id, - lookup_result, - "envelope_to_custody_transition", - cx, - ); + self.on_lookup_result(id, lookup_result, "envelope_to_custody_transition", cx); return; } From 5a93cbdf778f298e67d38635e73e459a202f6e0c Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Thu, 7 May 2026 12:50:19 +0000 Subject: [PATCH 113/118] debug + fix: store envelope for DuplicateFullyImported blocks After checkpoint sync, blocks between the finalized checkpoint and head are already in fork choice but their envelopes aren't in the store. When range sync downloads these blocks, they get skipped as DuplicateFullyImported without persisting the envelope. The next new block then fails load_parent because it can't find its parent's envelope. Fix: persist the envelope in the DuplicateFullyImported path, same as the existing WouldRevertFinalizedSlot path. --- beacon_node/beacon_chain/src/beacon_chain.rs | 34 ++++++++++++++++++- .../beacon_chain/src/block_verification.rs | 13 ++++--- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 4a148596a6..e76c258e0c 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -3060,7 +3060,34 @@ impl BeaconChain { // // Note that `check_block_relevancy` is incapable of returning // `DuplicateImportStatusUnknown` so we don't need to handle that case here. - Err(BlockError::DuplicateFullyImported(_)) => continue, + Err(BlockError::DuplicateFullyImported(_)) => { + debug!( + block_root = %block_root, + slot = %block.slot(), + "Skipping DuplicateFullyImported block in chain segment", + ); + // For Gloas blocks, persist the envelope even though we're + // skipping the block. After checkpoint sync, blocks between + // the finalized checkpoint and the head are already in fork + // choice but their envelopes aren't in the store. + if let RangeSyncBlock::Gloas { + envelope: Some(ref available_envelope), + .. + } = block + { + let (signed_envelope, _columns) = available_envelope.clone().deconstruct(); + if let Err(e) = self + .store + .put_payload_envelope(&block_root, &signed_envelope) + { + return Err(Box::new(ChainSegmentResult::Failed { + imported_blocks, + error: BlockError::BeaconChainError(Box::new(e.into())), + })); + } + } + continue; + } // If the block is the genesis block, simply ignore this block. Err(BlockError::GenesisBlock) => continue, // If the block is is for a finalized slot, simply ignore this block. @@ -3077,6 +3104,11 @@ 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 { .. }) => { + debug!( + block_root = %block_root, + slot = %block.slot(), + "Skipping WouldRevertFinalizedSlot block in chain segment", + ); // For Gloas blocks, persist the envelope even though we're skipping // the block. This is needed after checkpoint sync: the checkpoint // block's envelope must be in the store so that `load_parent` can diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 6417493605..a5fc59a828 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -2054,10 +2054,15 @@ fn load_parent>( .gloas_enabled() && parent_block.slot() > finalized_slot { - let _envelope = chain - .store - .get_payload_envelope(&root)? - .ok_or(BlockError::ParentEnvelopeUnknown { parent_root: root })?; + if chain.store.get_payload_envelope(&root)?.is_none() { + debug!( + parent_root = %root, + parent_slot = %parent_block.slot(), + %finalized_slot, + "load_parent: parent envelope not in store", + ); + return Err(BlockError::ParentEnvelopeUnknown { parent_root: root }); + } } // Load the parent block's state from the database, returning an error if it is not found. From def1a6cacc8644e2bae263319f8c649bdf276943 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Thu, 7 May 2026 12:50:19 +0000 Subject: [PATCH 114/118] fix: store envelope for DuplicateFullyImported + fix load_parent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two fixes for GLOaS range sync: 1. DuplicateFullyImported: persist envelope for blocks that are already in fork choice (e.g. post-checkpoint-sync blocks between finalized and head). 2. load_parent: if parent envelope isn't in store, check if parent is already in fork choice. If it is, the parent was already imported and validated — proceed without requiring the envelope in store. This handles the case where PayloadEnvelopesByRange doesn't return envelopes for all blocks (fewer envelopes than blocks). --- beacon_node/beacon_chain/src/beacon_chain.rs | 34 ++++++++++++++++++- .../beacon_chain/src/block_verification.rs | 23 ++++++++++--- 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 4a148596a6..e76c258e0c 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -3060,7 +3060,34 @@ impl BeaconChain { // // Note that `check_block_relevancy` is incapable of returning // `DuplicateImportStatusUnknown` so we don't need to handle that case here. - Err(BlockError::DuplicateFullyImported(_)) => continue, + Err(BlockError::DuplicateFullyImported(_)) => { + debug!( + block_root = %block_root, + slot = %block.slot(), + "Skipping DuplicateFullyImported block in chain segment", + ); + // For Gloas blocks, persist the envelope even though we're + // skipping the block. After checkpoint sync, blocks between + // the finalized checkpoint and the head are already in fork + // choice but their envelopes aren't in the store. + if let RangeSyncBlock::Gloas { + envelope: Some(ref available_envelope), + .. + } = block + { + let (signed_envelope, _columns) = available_envelope.clone().deconstruct(); + if let Err(e) = self + .store + .put_payload_envelope(&block_root, &signed_envelope) + { + return Err(Box::new(ChainSegmentResult::Failed { + imported_blocks, + error: BlockError::BeaconChainError(Box::new(e.into())), + })); + } + } + continue; + } // If the block is the genesis block, simply ignore this block. Err(BlockError::GenesisBlock) => continue, // If the block is is for a finalized slot, simply ignore this block. @@ -3077,6 +3104,11 @@ 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 { .. }) => { + debug!( + block_root = %block_root, + slot = %block.slot(), + "Skipping WouldRevertFinalizedSlot block in chain segment", + ); // For Gloas blocks, persist the envelope even though we're skipping // the block. This is needed after checkpoint sync: the checkpoint // block's envelope must be in the store so that `load_parent` can diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 6417493605..c8737e8c8c 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -2054,10 +2054,25 @@ fn load_parent>( .gloas_enabled() && parent_block.slot() > finalized_slot { - let _envelope = chain - .store - .get_payload_envelope(&root)? - .ok_or(BlockError::ParentEnvelopeUnknown { parent_root: root })?; + let in_store = chain.store.get_payload_envelope(&root)?.is_some(); + if !in_store { + // If the parent is already in fork choice it was previously imported. + // Its envelope may not be in the store if PayloadEnvelopesByRange + // didn't return it, but the block itself is valid and trusted. + let in_fork_choice = chain + .canonical_head + .fork_choice_read_lock() + .contains_block(&root); + if !in_fork_choice { + debug!( + parent_root = %root, + parent_slot = %parent_block.slot(), + %finalized_slot, + "load_parent: parent envelope not in store and not in fork choice", + ); + return Err(BlockError::ParentEnvelopeUnknown { parent_root: root }); + } + } } // Load the parent block's state from the database, returning an error if it is not found. From f818612795587b5bdb22a64dd1238a42353fa781 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Thu, 7 May 2026 13:47:55 +0000 Subject: [PATCH 115/118] fix: remove premature on_valid_payload_envelope_received + disable backfill for GLOaS 1. Remove on_valid_payload_envelope_received call before process_block in chain segment import. The block isn't in fork choice yet, so it always fails with NodeUnknown. import_envelope_from_range_sync handles this correctly after process_block. 2. Disable backfill sync when GLOaS is scheduled. Backfill calls into_available_block which panics on GLOaS RangeSyncBlock variant. Backfill for GLOaS is not yet implemented. --- beacon_node/beacon_chain/src/beacon_chain.rs | 16 ++++------------ .../network/src/sync/backfill_sync/mod.rs | 5 +++++ 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index e76c258e0c..717f9e41f2 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -3257,18 +3257,10 @@ impl BeaconChain { error: BlockError::BeaconChainError(Box::new(e.into())), }; } - if let Err(e) = self - .canonical_head - .fork_choice_write_lock() - .on_valid_payload_envelope_received(block_root) - { - return ChainSegmentResult::Failed { - imported_blocks, - error: BlockError::BeaconChainError(Box::new( - BeaconChainError::ForkChoiceError(e), - )), - }; - } + // Note: we do NOT call on_valid_payload_envelope_received here + // because the block hasn't been added to fork choice yet (that + // happens in process_block below). The fork choice update is + // handled by import_envelope_from_range_sync after process_block. } match self diff --git a/beacon_node/network/src/sync/backfill_sync/mod.rs b/beacon_node/network/src/sync/backfill_sync/mod.rs index 0f80138d24..c690d5e584 100644 --- a/beacon_node/network/src/sync/backfill_sync/mod.rs +++ b/beacon_node/network/src/sync/backfill_sync/mod.rs @@ -215,6 +215,11 @@ impl BackFillSync { &mut self, network: &mut SyncNetworkContext, ) -> Result { + // Skip backfill sync for GLOaS — not yet implemented for this fork. + if self.beacon_chain.spec.gloas_fork_epoch.is_some_and(|e| e != Epoch::max_value()) { + return Ok(SyncStart::NotSyncing); + } + match self.state() { BackFillState::Syncing => {} // already syncing ignore. BackFillState::Paused => { From c52cee9d9517f2265a34a74060aefd8001f020d1 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Thu, 7 May 2026 14:53:36 +0000 Subject: [PATCH 116/118] Allow optimistic payload verification for GLOaS envelopes Remove the OptimisticSyncNotSupported check in into_executed_payload_envelope. When the EL returns SYNCING (optimistic) for a payload envelope's newPayload call, the envelope import should proceed rather than being rejected. This commonly occurs after range sync imports blocks that the EL hasn't yet validated - subsequent envelopes arriving via gossip would be rejected because the EL is still catching up. The downstream import path already handles optimistic status correctly: - fork choice marks the payload as received regardless of EL status - the payload_verification_status is propagated to SSE events/metrics - if the payload is later found invalid, the normal invalidation mechanism handles it (same as for blocks) --- .../src/payload_envelope_verification/import.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 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 a8e8a59ede..88b021e8a6 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs @@ -192,13 +192,11 @@ 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(BlockError::OptimisticSyncNotSupported { block_root }); - } + // NOTE: We allow optimistic (SYNCING) payload verification status here. + // This can happen when the EL is still catching up (e.g., after range sync imports + // blocks that the EL hasn't validated yet). The envelope import will proceed and + // fork choice will mark the payload as received. If the payload is later found to + // be invalid, the normal invalidation mechanism will handle it. Ok(AvailabilityPendingExecutedEnvelope::new( signed_envelope, From b53a969c300b310239e5a6ea21c63af587c24073 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Fri, 8 May 2026 08:53:25 +0000 Subject: [PATCH 117/118] fix: use execution_payload_block_hash for Pending payload status in fcU MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When gloas envelopes are imported optimistically (EL returns SYNCING), the payload status is Pending. Previously, Pending used execution_payload_parent_hash for the head_hash in forkchoiceUpdated, which tells geth to stay at the parent — never advancing. Fix: use execution_payload_block_hash (the bid's committed hash) for Pending status, same as Full. This tells geth to sync to the new head, which is the purpose of optimistic sync. Geth will validate it and transition from SYNCING to VALID on subsequent newPayload calls. Also re-enables backfill sync for gloas with a dedicated import_historical_gloas_block_batch that properly handles RangeSyncBlock::Gloas variants (storing envelopes alongside blocks). --- .../beacon_chain/src/historical_blocks.rs | 209 +++++++++++++++++- .../network_beacon_processor/sync_methods.rs | 167 ++++++++------ .../network/src/sync/backfill_sync/mod.rs | 5 - consensus/fork_choice/src/fork_choice.rs | 6 +- 4 files changed, 311 insertions(+), 76 deletions(-) diff --git a/beacon_node/beacon_chain/src/historical_blocks.rs b/beacon_node/beacon_chain/src/historical_blocks.rs index bfda52558e..938fbc4104 100644 --- a/beacon_node/beacon_chain/src/historical_blocks.rs +++ b/beacon_node/beacon_chain/src/historical_blocks.rs @@ -1,3 +1,4 @@ +use crate::block_verification_types::{AsBlock, RangeSyncBlock}; use crate::data_availability_checker::{AvailableBlock, AvailableBlockData}; use crate::{BeaconChain, BeaconChainTypes, WhenSlotSkipped, metrics}; use fixed_bytes::FixedBytesExtended; @@ -8,12 +9,13 @@ use state_processing::{ }; use std::borrow::Cow; use std::iter; +use std::sync::Arc; use std::time::Duration; use store::metadata::DataColumnInfo; use store::{AnchorInfo, BlobInfo, DBColumn, Error as StoreError, KeyValueStore, KeyValueStoreOp}; use strum::IntoStaticStr; use tracing::{debug, debug_span, instrument}; -use types::{Hash256, Slot}; +use types::{Hash256, SignedExecutionPayloadEnvelope, Slot}; /// Use a longer timeout on the pubkey cache. /// @@ -315,4 +317,209 @@ impl BeaconChain { Ok(num_relevant) } + + /// Store a batch of historical GLOaS blocks in the database. + /// + /// Similar to `import_historical_block_batch` but handles `RangeSyncBlock::Gloas` variants, + /// storing both the beacon block and the execution payload envelope. + /// + /// The `blocks` should be given in slot-ascending order. Block root verification, + /// signature verification, and anchor updates follow the same logic as the pre-GLOaS path. + #[instrument(skip_all)] + pub fn import_historical_gloas_block_batch( + &self, + mut blocks: Vec>, + ) -> Result { + let anchor_info = self.store.get_anchor_info(); + + // Take all blocks with slots less than or equal to the oldest block slot. + let num_relevant = blocks.partition_point(|block| { + block.slot() <= anchor_info.oldest_block_slot + }); + + let total_blocks = blocks.len(); + blocks.truncate(num_relevant); + let blocks_to_import = blocks; + + if blocks_to_import.len() != total_blocks { + debug!( + oldest_block_slot = %anchor_info.oldest_block_slot, + total_blocks, + ignored = total_blocks.saturating_sub(blocks_to_import.len()), + "Ignoring some historic GLOaS blocks" + ); + } + + if blocks_to_import.is_empty() { + return Ok(0); + } + + let mut expected_block_root = anchor_info.oldest_block_parent; + let mut last_block_root = expected_block_root; + let mut prev_block_slot = anchor_info.oldest_block_slot; + + let mut cold_batch = Vec::with_capacity(blocks_to_import.len()); + let mut hot_batch = Vec::with_capacity(blocks_to_import.len()); + let mut signed_blocks = Vec::with_capacity(blocks_to_import.len()); + let mut envelopes_to_store: Vec<(Hash256, Arc>)> = + Vec::new(); + + for range_block in blocks_to_import.into_iter().rev() { + let block_root = range_block.block_root(); + let block = range_block.block_cloned(); + + // Extract envelope if this is a GLOaS block with one. + if let RangeSyncBlock::Gloas { + envelope: Some(available_envelope), + .. + } = range_block + { + let (signed_envelope, _columns) = available_envelope.deconstruct(); + envelopes_to_store.push((block_root, signed_envelope)); + } + + if block.slot() == anchor_info.oldest_block_slot { + // When reimporting, verify that this is actually the same block (same block root). + let oldest_block_root = self + .block_root_at_slot(block.slot(), WhenSlotSkipped::None) + .ok() + .flatten() + .ok_or(HistoricalBlockError::MissingOldestBlockRoot { slot: block.slot() })?; + if block_root != oldest_block_root { + return Err(HistoricalBlockError::MismatchedBlockRoot { + block_root, + expected_block_root: oldest_block_root, + }); + } + + debug!( + ?block_root, + slot = %block.slot(), + "Re-importing historic GLOaS block" + ); + last_block_root = block_root; + } else if block_root != expected_block_root { + return Err(HistoricalBlockError::MismatchedBlockRoot { + block_root, + expected_block_root, + }); + } + + // Store block in the hot database. + // GLOaS blocks always have their payload in the envelope, so we store blinded. + let blinded_block = block.clone_as_blinded(); + self.store.blinded_block_as_kv_store_ops( + &block_root, + &blinded_block, + &mut hot_batch, + ); + + // Store block roots, including at all skip slots in the freezer DB. + for slot in (block.slot().as_u64()..prev_block_slot.as_u64()).rev() { + cold_batch.push(KeyValueStoreOp::PutKeyValue( + DBColumn::BeaconBlockRoots, + slot.to_be_bytes().to_vec(), + block_root.as_slice().to_vec(), + )); + } + + prev_block_slot = block.slot(); + expected_block_root = block.message().parent_root(); + signed_blocks.push(block); + + // If we've reached genesis, add the genesis block root to the batch. + if expected_block_root == self.genesis_block_root { + let genesis_slot = self.spec.genesis_slot; + for slot in genesis_slot.as_u64()..prev_block_slot.as_u64() { + cold_batch.push(KeyValueStoreOp::PutKeyValue( + DBColumn::BeaconBlockRoots, + slot.to_be_bytes().to_vec(), + self.genesis_block_root.as_slice().to_vec(), + )); + } + prev_block_slot = genesis_slot; + expected_block_root = Hash256::zero(); + break; + } + } + // Blocks were pushed in reverse order so reverse again. + signed_blocks.reverse(); + + // Verify signatures in one batch. + let sig_timer = metrics::start_timer(&metrics::BACKFILL_SIGNATURE_TOTAL_TIMES); + let setup_timer = metrics::start_timer(&metrics::BACKFILL_SIGNATURE_SETUP_TIMES); + let pubkey_cache = self + .validator_pubkey_cache + .try_read_for(PUBKEY_CACHE_LOCK_TIMEOUT) + .ok_or(HistoricalBlockError::ValidatorPubkeyCacheTimeout)?; + let block_roots = signed_blocks + .get(1..) + .ok_or(HistoricalBlockError::IndexOutOfBounds)? + .iter() + .map(|block| block.parent_root()) + .chain(iter::once(last_block_root)); + let signature_set = signed_blocks + .iter() + .zip_eq(block_roots) + .filter(|&(_block, block_root)| block_root != self.genesis_block_root) + .map(|(block, block_root)| { + block_proposal_signature_set_from_parts( + block, + Some(block_root), + block.message().proposer_index(), + &self.spec.fork_at_epoch(block.message().epoch()), + self.genesis_validators_root, + |validator_index| pubkey_cache.get(validator_index).cloned().map(Cow::Owned), + &self.spec, + ) + }) + .collect::, _>>() + .map_err(HistoricalBlockError::SignatureSet) + .map(ParallelSignatureSets::from)?; + drop(pubkey_cache); + drop(setup_timer); + + let verify_timer = metrics::start_timer(&metrics::BACKFILL_SIGNATURE_VERIFY_TIMES); + if !signature_set.verify() { + return Err(HistoricalBlockError::InvalidSignature); + } + drop(verify_timer); + drop(sig_timer); + + // Write envelopes to the hot DB. + for (block_root, signed_envelope) in &envelopes_to_store { + self.store + .put_payload_envelope(block_root, signed_envelope)?; + } + + // Write the block batches to disk. + { + let _span = debug_span!("backfill_write_hot_db").entered(); + self.store.hot_db.do_atomically(hot_batch)?; + } + { + let _span = debug_span!("backfill_write_cold_db").entered(); + self.store.cold_db.do_atomically(cold_batch)?; + } + + // Update the anchor. + let new_anchor = AnchorInfo { + oldest_block_slot: prev_block_slot, + oldest_block_parent: expected_block_root, + ..anchor_info + }; + let backfill_complete = new_anchor.block_backfill_complete(self.genesis_backfill_slot); + let anchor_batch = vec![ + self.store + .compare_and_set_anchor_info(anchor_info, new_anchor)?, + ]; + self.store.hot_db.do_atomically(anchor_batch)?; + + // If backfill has completed, trigger reconstruction. + if backfill_complete && self.genesis_backfill_slot == Slot::new(0) && self.config.archive { + self.store_migrator.process_reconstruction(); + } + + Ok(num_relevant) + } } diff --git a/beacon_node/network/src/network_beacon_processor/sync_methods.rs b/beacon_node/network/src/network_beacon_processor/sync_methods.rs index 6a5586c126..0443a85cab 100644 --- a/beacon_node/network/src/network_beacon_processor/sync_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/sync_methods.rs @@ -799,13 +799,38 @@ impl NetworkBeaconProcessor { downloaded_blocks: Vec>, ) -> (usize, Result<(), ChainSegmentFailed>) { let total_blocks = downloaded_blocks.len(); + + // Check if this batch contains GLOaS blocks. + let is_gloas_batch = downloaded_blocks + .first() + .map(|b| matches!(b, RangeSyncBlock::Gloas { .. })) + .unwrap_or(false); + + if is_gloas_batch { + // GLOaS blocks: store blocks and envelopes directly. + // KZG verification for columns was already done during coupling. + match self.chain.import_historical_gloas_block_batch(downloaded_blocks) { + Ok(imported_blocks) => { + metrics::inc_counter( + &metrics::BEACON_PROCESSOR_BACKFILL_CHAIN_SEGMENT_SUCCESS_TOTAL, + ); + return (imported_blocks, Ok(())); + } + Err(e) => { + metrics::inc_counter( + &metrics::BEACON_PROCESSOR_BACKFILL_CHAIN_SEGMENT_FAILED_TOTAL, + ); + return self.handle_historical_block_error(e); + } + } + } + + // Pre-GLOaS path: convert to AvailableBlocks and verify KZG. let available_blocks = downloaded_blocks .into_iter() .map(|block| block.into_available_block()) .collect::>(); - // TODO(gloas) when implementing backfill sync for gloas - // we need a batch verify kzg function in the new da checker match self .chain .data_availability_checker @@ -859,75 +884,83 @@ impl NetworkBeaconProcessor { metrics::inc_counter( &metrics::BEACON_PROCESSOR_BACKFILL_CHAIN_SEGMENT_FAILED_TOTAL, ); - let peer_action = match &e { - HistoricalBlockError::MismatchedBlockRoot { - block_root, - expected_block_root, - } => { - debug!( - error = "mismatched_block_root", - ?block_root, - expected_root = ?expected_block_root, - "Backfill batch processing error" - ); - // The peer is faulty if they send blocks with bad roots. - Some(PeerAction::LowToleranceError) - } - HistoricalBlockError::InvalidSignature - | HistoricalBlockError::SignatureSet(_) => { - warn!( - error = ?e, - "Backfill batch processing error" - ); - // The peer is faulty if they bad signatures. - Some(PeerAction::LowToleranceError) - } - HistoricalBlockError::MissingOldestBlockRoot { slot } => { - warn!( - %slot, - error = "missing_oldest_block_root", - "Backfill batch processing error" - ); - // This is an internal error, do not penalize the peer. - None - } - - HistoricalBlockError::ValidatorPubkeyCacheTimeout => { - warn!( - error = "pubkey_cache_timeout", - "Backfill batch processing error" - ); - // This is an internal error, do not penalize the peer. - None - } - HistoricalBlockError::IndexOutOfBounds => { - error!( - error = ?e, - "Backfill batch OOB error" - ); - // This should never occur, don't penalize the peer. - None - } - HistoricalBlockError::StoreError(e) => { - warn!(error = ?e, "Backfill batch processing error"); - // This is an internal error, don't penalize the peer. - None - } // - // Do not use a fallback match, handle all errors explicitly - }; - let err_str: &'static str = e.into(); - ( - 0, - Err(ChainSegmentFailed { - message: format!("{:?}", err_str), - // This is an internal error, don't penalize the peer. - peer_action, - }), - ) + self.handle_historical_block_error(e) } } } + /// Maps a `HistoricalBlockError` to the appropriate peer action and error tuple. + fn handle_historical_block_error( + &self, + e: HistoricalBlockError, + ) -> (usize, Result<(), ChainSegmentFailed>) { + let peer_action = match &e { + HistoricalBlockError::MismatchedBlockRoot { + block_root, + expected_block_root, + } => { + debug!( + error = "mismatched_block_root", + ?block_root, + expected_root = ?expected_block_root, + "Backfill batch processing error" + ); + // The peer is faulty if they send blocks with bad roots. + Some(PeerAction::LowToleranceError) + } + HistoricalBlockError::InvalidSignature + | HistoricalBlockError::SignatureSet(_) => { + warn!( + error = ?e, + "Backfill batch processing error" + ); + // The peer is faulty if they bad signatures. + Some(PeerAction::LowToleranceError) + } + HistoricalBlockError::MissingOldestBlockRoot { slot } => { + warn!( + %slot, + error = "missing_oldest_block_root", + "Backfill batch processing error" + ); + // This is an internal error, do not penalize the peer. + None + } + + HistoricalBlockError::ValidatorPubkeyCacheTimeout => { + warn!( + error = "pubkey_cache_timeout", + "Backfill batch processing error" + ); + // This is an internal error, do not penalize the peer. + None + } + HistoricalBlockError::IndexOutOfBounds => { + error!( + error = ?e, + "Backfill batch OOB error" + ); + // This should never occur, don't penalize the peer. + None + } + HistoricalBlockError::StoreError(e) => { + warn!(error = ?e, "Backfill batch processing error"); + // This is an internal error, don't penalize the peer. + None + } // + // Do not use a fallback match, handle all errors explicitly + }; + let err_str: &'static str = e.into(); + ( + 0, + Err(ChainSegmentFailed { + message: format!("{:?}", err_str), + // This is an internal error, don't penalize the peer. + peer_action, + }), + ) + } + /// Helper function to handle a `BlockError` from `process_chain_segment` fn handle_failed_chain_segment(&self, error: BlockError) -> Result<(), ChainSegmentFailed> { match error { diff --git a/beacon_node/network/src/sync/backfill_sync/mod.rs b/beacon_node/network/src/sync/backfill_sync/mod.rs index c690d5e584..0f80138d24 100644 --- a/beacon_node/network/src/sync/backfill_sync/mod.rs +++ b/beacon_node/network/src/sync/backfill_sync/mod.rs @@ -215,11 +215,6 @@ impl BackFillSync { &mut self, network: &mut SyncNetworkContext, ) -> Result { - // Skip backfill sync for GLOaS — not yet implemented for this fork. - if self.beacon_chain.spec.gloas_fork_epoch.is_some_and(|e| e != Epoch::max_value()) { - return Ok(SyncStart::NotSyncing); - } - match self.state() { BackFillState::Syncing => {} // already syncing ignore. BackFillState::Paused => { diff --git a/consensus/fork_choice/src/fork_choice.rs b/consensus/fork_choice/src/fork_choice.rs index b78e6652b3..ba69fc265e 100644 --- a/consensus/fork_choice/src/fork_choice.rs +++ b/consensus/fork_choice/src/fork_choice.rs @@ -584,10 +584,10 @@ where b.execution_status .block_hash() .or(match head_payload_status { - PayloadStatus::Full => b.execution_payload_block_hash, - PayloadStatus::Pending | PayloadStatus::Empty => { - b.execution_payload_parent_hash + PayloadStatus::Full | PayloadStatus::Pending => { + b.execution_payload_block_hash } + PayloadStatus::Empty => b.execution_payload_parent_hash, }) }); let justified_root = self.justified_checkpoint().root; From 7aec7a768ecdbc32fb4dc720d38107d6d770b71e Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Fri, 8 May 2026 11:37:22 +0000 Subject: [PATCH 118/118] fix: only require parent envelope when bid references parent's payload The previous check triggered ParentEnvelopeUnknown for any block whose parent didn't have its envelope received, even when the block's bid didn't actually reference that parent's payload (e.g. building on grandparent's execution state). This caused a permanent stall when an envelope was rejected (e.g. WithdrawalsRootMismatch from a buggy proposer): the parent's payload_received stayed false, and all subsequent child blocks would trigger infinite lookup retries for the broken envelope. Fix: only require the parent's envelope if the block's bid parent_block_hash matches the parent's execution_payload_block_hash, meaning this block directly depends on the parent's payload. --- beacon_node/beacon_chain/src/block_verification.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index c8737e8c8c..da9e7de855 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -983,10 +983,15 @@ impl GossipVerifiedBlock { .gloas_enabled(); let parent_is_anchor = parent_block.parent_root.is_none(); + // Check if this block's bid references a payload envelope we haven't received. + // Only trigger a lookup if the bid's parent_block_hash matches the parent block's + // committed execution_payload_block_hash (meaning this block builds directly on + // the parent's payload). If they don't match, the block is building on an older + // execution state (e.g. grandparent's) and doesn't need the parent's envelope. if parent_is_gloas && !parent_is_anchor + && Some(bid.message.parent_block_hash) == parent_block.execution_payload_block_hash && !fork_choice_read_lock.is_payload_received(&block.message().parent_root()) - && Some(bid.message.parent_block_hash) != parent_block.execution_payload_block_hash { return Err(BlockError::ParentEnvelopeUnknown { parent_root: block.message().parent_root(),