From d7ab88cf858ec90696b1dbba199af34823d20e61 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Tue, 8 Jul 2025 12:20:57 +1000 Subject: [PATCH 01/13] Remove KZG verification from local block production and blobs fetched from local EL. --- beacon_node/beacon_chain/src/beacon_chain.rs | 32 ++--------------- .../src/data_column_verification.rs | 6 ++++ .../fetch_blobs/fetch_blobs_beacon_adapter.rs | 14 ++------ .../beacon_chain/src/fetch_blobs/mod.rs | 34 ++++++++----------- .../beacon_chain/src/fetch_blobs/tests.rs | 14 +++----- beacon_node/beacon_chain/src/metrics.rs | 9 ----- 6 files changed, 29 insertions(+), 80 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 9900535b2c7..d30109728f5 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -70,7 +70,7 @@ use crate::validator_monitor::{ }; use crate::validator_pubkey_cache::ValidatorPubkeyCache; use crate::{ - kzg_utils, metrics, AvailabilityPendingExecutedBlock, BeaconChainError, BeaconForkChoiceStore, + metrics, AvailabilityPendingExecutedBlock, BeaconChainError, BeaconForkChoiceStore, BeaconSnapshot, CachedHead, }; use eth2::types::{ @@ -5748,8 +5748,6 @@ impl BeaconChain { let (mut block, _) = block.deconstruct(); *block.state_root_mut() = state_root; - let blobs_verification_timer = - metrics::start_timer(&metrics::BLOCK_PRODUCTION_BLOBS_VERIFICATION_TIMES); let blob_items = match maybe_blobs_and_proofs { Some((blobs, proofs)) => { let expected_kzg_commitments = @@ -5768,37 +5766,11 @@ impl BeaconChain { ))); } - let kzg_proofs = Vec::from(proofs); - - let kzg = self.kzg.as_ref(); - if self - .spec - .is_peer_das_enabled_for_epoch(slot.epoch(T::EthSpec::slots_per_epoch())) - { - kzg_utils::validate_blobs_and_cell_proofs::( - kzg, - blobs.iter().collect(), - &kzg_proofs, - expected_kzg_commitments, - ) - .map_err(BlockProductionError::KzgError)?; - } else { - kzg_utils::validate_blobs::( - kzg, - expected_kzg_commitments, - blobs.iter().collect(), - &kzg_proofs, - ) - .map_err(BlockProductionError::KzgError)?; - } - - Some((kzg_proofs.into(), blobs)) + Some((proofs, blobs)) } None => None, }; - drop(blobs_verification_timer); - metrics::inc_counter(&metrics::BLOCK_PRODUCTION_SUCCESSES); trace!( diff --git a/beacon_node/beacon_chain/src/data_column_verification.rs b/beacon_node/beacon_chain/src/data_column_verification.rs index 3009522bf60..e079b5ab78c 100644 --- a/beacon_node/beacon_chain/src/data_column_verification.rs +++ b/beacon_node/beacon_chain/src/data_column_verification.rs @@ -266,6 +266,12 @@ impl KzgVerifiedDataColumn { verify_kzg_for_data_column(data_column, kzg) } + /// Mark a data column as KZG verified. Caller must ONLY use this on columns constructed + /// from EL blobs. + pub fn from_execution_verified(data_column: Arc>) -> Self { + Self { data: data_column } + } + /// Create a `KzgVerifiedDataColumn` from `DataColumnSidecar` for testing ONLY. pub(crate) fn __new_for_testing(data_column: Arc>) -> Self { Self { data: data_column } 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 4a7a5aeea21..fe8af5b70ea 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 @@ -1,17 +1,16 @@ use crate::blob_verification::{GossipBlobError, GossipVerifiedBlob}; -use crate::data_column_verification::KzgVerifiedDataColumn; use crate::fetch_blobs::{EngineGetBlobsOutput, FetchEngineBlobError}; use crate::observed_block_producers::ProposalKey; use crate::observed_data_sidecars::DoNotObserve; use crate::{AvailabilityProcessingStatus, BeaconChain, BeaconChainTypes}; use execution_layer::json_structures::{BlobAndProofV1, BlobAndProofV2}; -use kzg::{Error as KzgError, Kzg}; +use kzg::Kzg; #[cfg(test)] use mockall::automock; use std::collections::HashSet; use std::sync::Arc; use task_executor::TaskExecutor; -use types::{BlobSidecar, ChainSpec, ColumnIndex, DataColumnSidecar, Hash256, Slot}; +use types::{BlobSidecar, ChainSpec, ColumnIndex, Hash256, Slot}; /// An adapter to the `BeaconChain` functionalities to remove `BeaconChain` from direct dependency to enable testing fetch blobs logic. pub(crate) struct FetchBlobsBeaconAdapter { @@ -77,14 +76,7 @@ impl FetchBlobsBeaconAdapter { GossipVerifiedBlob::::new(blob.clone(), blob.index, &self.chain) } - pub(crate) fn verify_data_columns_kzg( - &self, - data_columns: Vec>>, - ) -> Result>, KzgError> { - KzgVerifiedDataColumn::from_batch(data_columns, &self.chain.kzg) - } - - pub(crate) fn known_for_proposal( + pub(crate) fn data_column_known_for_proposal( &self, proposal_key: ProposalKey, ) -> Option> { diff --git a/beacon_node/beacon_chain/src/fetch_blobs/mod.rs b/beacon_node/beacon_chain/src/fetch_blobs/mod.rs index e02405ddbad..21a27ace119 100644 --- a/beacon_node/beacon_chain/src/fetch_blobs/mod.rs +++ b/beacon_node/beacon_chain/src/fetch_blobs/mod.rs @@ -14,7 +14,7 @@ mod tests; use crate::blob_verification::{GossipBlobError, GossipVerifiedBlob}; use crate::block_verification_types::AsBlock; -use crate::data_column_verification::KzgVerifiedCustodyDataColumn; +use crate::data_column_verification::{KzgVerifiedCustodyDataColumn, KzgVerifiedDataColumn}; #[cfg_attr(test, double)] use crate::fetch_blobs::fetch_blobs_beacon_adapter::FetchBlobsBeaconAdapter; use crate::kzg_utils::blobs_to_data_column_sidecars; @@ -358,17 +358,24 @@ async fn compute_custody_columns_to_import( // `DataAvailabilityChecker` requires a strict match on custody columns count to // consider a block available. let mut custody_columns = data_columns_result - .map(|mut data_columns| { - data_columns.retain(|col| custody_columns_indices.contains(&col.index)); + .map(|data_columns| { data_columns + .into_iter() + .filter(|col| custody_columns_indices.contains(&col.index)) + .map(|col| { + KzgVerifiedCustodyDataColumn::from_asserted_custody( + KzgVerifiedDataColumn::from_execution_verified(col), + ) + }) + .collect::>() }) .map_err(FetchEngineBlobError::DataColumnSidecarError)?; // Only consider columns that are not already observed on gossip. - if let Some(observed_columns) = chain_adapter_cloned.known_for_proposal( + if let Some(observed_columns) = chain_adapter_cloned.data_column_known_for_proposal( ProposalKey::new(block.message().proposer_index(), block.slot()), ) { - custody_columns.retain(|col| !observed_columns.contains(&col.index)); + custody_columns.retain(|col| !observed_columns.contains(&col.index())); if custody_columns.is_empty() { return Ok(vec![]); } @@ -378,26 +385,13 @@ async fn compute_custody_columns_to_import( if let Some(known_columns) = chain_adapter_cloned.cached_data_column_indexes(&block_root) { - custody_columns.retain(|col| !known_columns.contains(&col.index)); + custody_columns.retain(|col| !known_columns.contains(&col.index())); if custody_columns.is_empty() { return Ok(vec![]); } } - // KZG verify data columns before publishing. This prevents blobs with invalid - // KZG proofs from the EL making it into the data availability checker. We do not - // immediately add these blobs to the observed blobs/columns cache because we want - // to allow blobs/columns to arrive on gossip and be accepted (and propagated) while - // we are waiting to publish. Just before publishing we will observe the blobs/columns - // and only proceed with publishing if they are not yet seen. - let verified = chain_adapter_cloned - .verify_data_columns_kzg(custody_columns) - .map_err(FetchEngineBlobError::KzgError)?; - - Ok(verified - .into_iter() - .map(KzgVerifiedCustodyDataColumn::from_asserted_custody) - .collect()) + Ok(custody_columns) }, "compute_custody_columns_to_import", ) diff --git a/beacon_node/beacon_chain/src/fetch_blobs/tests.rs b/beacon_node/beacon_chain/src/fetch_blobs/tests.rs index 3178020c758..9cb597c6df9 100644 --- a/beacon_node/beacon_chain/src/fetch_blobs/tests.rs +++ b/beacon_node/beacon_chain/src/fetch_blobs/tests.rs @@ -1,4 +1,3 @@ -use crate::data_column_verification::KzgVerifiedDataColumn; use crate::fetch_blobs::fetch_blobs_beacon_adapter::MockFetchBlobsBeaconAdapter; use crate::fetch_blobs::{ fetch_and_process_engine_blobs_inner, EngineGetBlobsOutput, FetchEngineBlobError, @@ -156,7 +155,7 @@ mod get_blobs_v2 { mock_fork_choice_contains_block(&mut mock_adapter, vec![]); // All data columns already seen on gossip mock_adapter - .expect_known_for_proposal() + .expect_data_column_known_for_proposal() .returning(|_| Some(hashset![0, 1, 2])); // No blobs should be processed mock_adapter.expect_process_engine_blobs().times(0); @@ -192,17 +191,12 @@ mod get_blobs_v2 { // All blobs returned, fork choice doesn't contain block mock_get_blobs_v2_response(&mut mock_adapter, Some(blobs_and_proofs)); mock_fork_choice_contains_block(&mut mock_adapter, vec![]); - mock_adapter.expect_known_for_proposal().returning(|_| None); mock_adapter - .expect_cached_data_column_indexes() + .expect_data_column_known_for_proposal() .returning(|_| None); mock_adapter - .expect_verify_data_columns_kzg() - .returning(|c| { - Ok(c.into_iter() - .map(KzgVerifiedDataColumn::__new_for_testing) - .collect()) - }); + .expect_cached_data_column_indexes() + .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 5ca764821f2..7696d386fe7 100644 --- a/beacon_node/beacon_chain/src/metrics.rs +++ b/beacon_node/beacon_chain/src/metrics.rs @@ -1824,15 +1824,6 @@ pub static KZG_VERIFICATION_DATA_COLUMN_BATCH_TIMES: LazyLock> ) }); -pub static BLOCK_PRODUCTION_BLOBS_VERIFICATION_TIMES: LazyLock> = LazyLock::new( - || { - try_create_histogram( - "beacon_block_production_blobs_verification_seconds", - "Time taken to verify blobs against commitments and creating BlobSidecar objects in block production" - ) - }, -); - /* * Data Availability cache metrics */ From 47c892fcd1528bfd2871d6a5babd38ffddae2eb0 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Tue, 22 Jul 2025 17:30:30 +1000 Subject: [PATCH 02/13] Skip KZG checks on EL fetch blob sidecars. --- beacon_node/beacon_chain/src/beacon_chain.rs | 2 +- .../beacon_chain/src/blob_verification.rs | 26 ++--- .../src/data_availability_checker.rs | 13 ++- .../fetch_blobs/fetch_blobs_beacon_adapter.rs | 24 ++-- .../beacon_chain/src/fetch_blobs/mod.rs | 103 +++++++++--------- .../beacon_chain/src/fetch_blobs/tests.rs | 36 ++++-- .../src/network_beacon_processor/mod.rs | 14 ++- 7 files changed, 121 insertions(+), 97 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index d30109728f5..fcce8c00efb 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -3664,7 +3664,7 @@ impl BeaconChain { EngineGetBlobsOutput::Blobs(blobs) => { self.check_blobs_for_slashability(block_root, blobs.iter().map(|b| b.as_blob()))?; self.data_availability_checker - .put_gossip_verified_blobs(block_root, blobs)? + .put_kzg_verified_blobs(block_root, blobs)? } EngineGetBlobsOutput::CustodyColumns(data_columns) => { self.check_columns_for_slashability( diff --git a/beacon_node/beacon_chain/src/blob_verification.rs b/beacon_node/beacon_chain/src/blob_verification.rs index a78224fb708..958416552db 100644 --- a/beacon_node/beacon_chain/src/blob_verification.rs +++ b/beacon_node/beacon_chain/src/blob_verification.rs @@ -9,7 +9,7 @@ use crate::block_verification::{ BlockSlashInfo, }; use crate::kzg_utils::{validate_blob, validate_blobs}; -use crate::observed_data_sidecars::{DoNotObserve, ObservationStrategy, Observe}; +use crate::observed_data_sidecars::{ObservationStrategy, Observe}; use crate::{metrics, BeaconChainError}; use kzg::{Error as KzgError, Kzg, KzgCommitment}; use ssz_derive::{Decode, Encode}; @@ -304,6 +304,14 @@ impl KzgVerifiedBlob { seen_timestamp: Duration::from_secs(0), } } + /// Mark a blob as KZG verified. Caller must ONLY use this on blob sidecars constructed + /// from EL blobs. + pub fn from_execution_verified(blob: Arc>, seen_timestamp: Duration) -> Self { + Self { + blob, + seen_timestamp, + } + } } /// Complete kzg verification for a `BlobSidecar`. @@ -594,21 +602,7 @@ pub fn validate_blob_sidecar_for_gossip GossipVerifiedBlob { - pub fn observe( - self, - chain: &BeaconChain, - ) -> Result, GossipBlobError> { - observe_gossip_blob(&self.blob.blob, chain)?; - Ok(GossipVerifiedBlob { - block_root: self.block_root, - blob: self.blob, - _phantom: PhantomData, - }) - } -} - -fn observe_gossip_blob( +pub fn observe_gossip_blob( blob_sidecar: &BlobSidecar, chain: &BeaconChain, ) -> Result<(), GossipBlobError> { diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index 54047180480..4f05e8a3b6f 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -1,4 +1,6 @@ -use crate::blob_verification::{verify_kzg_for_blob_list, GossipVerifiedBlob, KzgVerifiedBlobList}; +use crate::blob_verification::{ + verify_kzg_for_blob_list, GossipVerifiedBlob, KzgVerifiedBlob, KzgVerifiedBlobList, +}; use crate::block_verification_types::{ AvailabilityPendingExecutedBlock, AvailableExecutedBlock, RpcBlock, }; @@ -264,6 +266,15 @@ impl DataAvailabilityChecker { .put_kzg_verified_blobs(block_root, blobs.into_iter().map(|b| b.into_inner())) } + pub fn put_kzg_verified_blobs>>( + &self, + block_root: Hash256, + blobs: I, + ) -> Result, AvailabilityCheckError> { + self.availability_cache + .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. 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 fe8af5b70ea..9526921da73 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 @@ -1,7 +1,5 @@ -use crate::blob_verification::{GossipBlobError, GossipVerifiedBlob}; use crate::fetch_blobs::{EngineGetBlobsOutput, FetchEngineBlobError}; use crate::observed_block_producers::ProposalKey; -use crate::observed_data_sidecars::DoNotObserve; use crate::{AvailabilityProcessingStatus, BeaconChain, BeaconChainTypes}; use execution_layer::json_structures::{BlobAndProofV1, BlobAndProofV2}; use kzg::Kzg; @@ -10,7 +8,7 @@ use mockall::automock; use std::collections::HashSet; use std::sync::Arc; use task_executor::TaskExecutor; -use types::{BlobSidecar, ChainSpec, ColumnIndex, Hash256, Slot}; +use types::{ChainSpec, ColumnIndex, Hash256, Slot}; /// An adapter to the `BeaconChain` functionalities to remove `BeaconChain` from direct dependency to enable testing fetch blobs logic. pub(crate) struct FetchBlobsBeaconAdapter { @@ -69,11 +67,17 @@ impl FetchBlobsBeaconAdapter { .map_err(FetchEngineBlobError::RequestFailed) } - pub(crate) fn verify_blob_for_gossip( + pub(crate) fn blobs_known_for_proposal( &self, - blob: &Arc>, - ) -> Result, GossipBlobError> { - GossipVerifiedBlob::::new(blob.clone(), blob.index, &self.chain) + proposer: u64, + slot: Slot, + ) -> Option> { + let proposer_key = ProposalKey::new(proposer, slot); + self.chain + .observed_blob_sidecars + .read() + .known_for_proposal(&proposer_key) + .cloned() } pub(crate) fn data_column_known_for_proposal( @@ -87,6 +91,12 @@ impl FetchBlobsBeaconAdapter { .cloned() } + pub(crate) fn cached_blob_indexes(&self, block_root: &Hash256) -> Option> { + self.chain + .data_availability_checker + .cached_blob_indexes(block_root) + } + pub(crate) fn cached_data_column_indexes(&self, block_root: &Hash256) -> Option> { self.chain .data_availability_checker diff --git a/beacon_node/beacon_chain/src/fetch_blobs/mod.rs b/beacon_node/beacon_chain/src/fetch_blobs/mod.rs index 21a27ace119..efc7854fa5f 100644 --- a/beacon_node/beacon_chain/src/fetch_blobs/mod.rs +++ b/beacon_node/beacon_chain/src/fetch_blobs/mod.rs @@ -12,14 +12,14 @@ mod fetch_blobs_beacon_adapter; #[cfg(test)] mod tests; -use crate::blob_verification::{GossipBlobError, GossipVerifiedBlob}; +use crate::blob_verification::{GossipBlobError, KzgVerifiedBlob}; use crate::block_verification_types::AsBlock; use crate::data_column_verification::{KzgVerifiedCustodyDataColumn, KzgVerifiedDataColumn}; #[cfg_attr(test, double)] use crate::fetch_blobs::fetch_blobs_beacon_adapter::FetchBlobsBeaconAdapter; use crate::kzg_utils::blobs_to_data_column_sidecars; use crate::observed_block_producers::ProposalKey; -use crate::observed_data_sidecars::DoNotObserve; +use crate::validator_monitor::timestamp_now; use crate::{ metrics, AvailabilityProcessingStatus, BeaconChain, BeaconChainError, BeaconChainTypes, BlockError, @@ -34,11 +34,11 @@ use state_processing::per_block_processing::deneb::kzg_commitment_to_versioned_h use std::collections::HashSet; use std::sync::Arc; use tracing::{debug, warn}; -use types::blob_sidecar::{BlobSidecarError, FixedBlobSidecarList}; +use types::blob_sidecar::BlobSidecarError; use types::data_column_sidecar::DataColumnSidecarError; use types::{ - BeaconStateError, Blob, BlobSidecar, ChainSpec, ColumnIndex, EthSpec, FullPayload, Hash256, - KzgProofs, SignedBeaconBlock, SignedBeaconBlockHeader, VersionedHash, + BeaconStateError, Blob, BlobSidecar, ColumnIndex, EthSpec, FullPayload, Hash256, KzgProofs, + SignedBeaconBlock, SignedBeaconBlockHeader, VersionedHash, }; /// Result from engine get blobs to be passed onto `DataAvailabilityChecker` and published to the @@ -46,7 +46,7 @@ use types::{ /// be published immediately. #[derive(Debug)] pub enum EngineGetBlobsOutput { - Blobs(Vec>), + Blobs(Vec>), /// A filtered list of custody data columns to be imported into the `DataAvailabilityChecker`. CustodyColumns(Vec>), } @@ -186,46 +186,47 @@ async fn fetch_and_process_blobs_v1( .signed_block_header_and_kzg_commitments_proof() .map_err(FetchEngineBlobError::BeaconStateError)?; - let fixed_blob_sidecar_list = build_blob_sidecars( + let mut blob_sidecar_list = build_blob_sidecars( &block, response, signed_block_header, &kzg_commitments_proof, - chain_adapter.spec(), )?; - // Gossip verify blobs before publishing. This prevents blobs with invalid KZG proofs from - // the EL making it into the data availability checker. We do not immediately add these - // blobs to the observed blobs/columns cache because we want to allow blobs/columns to arrive on gossip - // and be accepted (and propagated) while we are waiting to publish. Just before publishing - // we will observe the blobs/columns and only proceed with publishing if they are not yet seen. - let blobs_to_import_and_publish = fixed_blob_sidecar_list - .into_iter() - .filter_map(|opt_blob| { - let blob = opt_blob.as_ref()?; - match chain_adapter.verify_blob_for_gossip(blob) { - Ok(verified) => Some(Ok(verified)), - // Ignore already seen blobs. - Err(GossipBlobError::RepeatBlob { .. }) => None, - Err(e) => Some(Err(e)), - } - }) - .collect::, _>>() - .map_err(FetchEngineBlobError::GossipBlob)?; + if let Some(observed_blobs) = + chain_adapter.blobs_known_for_proposal(block.message().proposer_index(), block.slot()) + { + blob_sidecar_list.retain(|blob| !observed_blobs.contains(&blob.blob_index())); + if blob_sidecar_list.is_empty() { + debug!( + info = "blobs have already been seen on gossip", + "Ignoring EL blobs response" + ); + return Ok(None); + } + } - if blobs_to_import_and_publish.is_empty() { - return Ok(None); + if let Some(known_blobs) = chain_adapter.cached_blob_indexes(&block_root) { + blob_sidecar_list.retain(|blob| !known_blobs.contains(&blob.blob_index())); + if blob_sidecar_list.is_empty() { + debug!( + info = "blobs have already been imported into data availability checker", + "Ignoring EL blobs response" + ); + return Ok(None); + } } - publish_fn(EngineGetBlobsOutput::Blobs( - blobs_to_import_and_publish.clone(), - )); + // Up until this point we have not observed the blobs in the gossip cache, which allows them to + // arrive independently while this function is running. In `publish_fn` we will observe them + // and then publish any blobs that had not already been observed. + publish_fn(EngineGetBlobsOutput::Blobs(blob_sidecar_list.clone())); let availability_processing_status = chain_adapter .process_engine_blobs( block.slot(), block_root, - EngineGetBlobsOutput::Blobs(blobs_to_import_and_publish), + EngineGetBlobsOutput::Blobs(blob_sidecar_list), ) .await?; @@ -311,6 +312,9 @@ async fn fetch_and_process_blobs_v2( return Ok(None); } + // Up until this point we have not observed the data columns in the gossip cache, which allows + // them to arrive independently while this function is running. In publish_fn we will observe + // them and then publish any columns that had not already been observed. publish_fn(EngineGetBlobsOutput::CustodyColumns( custody_columns_to_import.clone(), )); @@ -405,37 +409,28 @@ fn build_blob_sidecars( response: Vec>>, signed_block_header: SignedBeaconBlockHeader, kzg_commitments_inclusion_proof: &FixedVector, - spec: &ChainSpec, -) -> Result, FetchEngineBlobError> { - let epoch = block.epoch(); - let mut fixed_blob_sidecar_list = - FixedBlobSidecarList::default(spec.max_blobs_per_block(epoch) as usize); +) -> Result>, FetchEngineBlobError> { + let mut sidecars = vec![]; for (index, blob_and_proof) in response .into_iter() .enumerate() - .filter_map(|(i, opt_blob)| Some((i, opt_blob?))) + .filter_map(|(index, opt_blob)| Some((index, opt_blob?))) { - match BlobSidecar::new_with_existing_proof( + let blob_sidecar = BlobSidecar::new_with_existing_proof( index, blob_and_proof.blob, block, signed_block_header.clone(), kzg_commitments_inclusion_proof, blob_and_proof.proof, - ) { - Ok(blob) => { - if let Some(blob_mut) = fixed_blob_sidecar_list.get_mut(index) { - *blob_mut = Some(Arc::new(blob)); - } else { - return Err(FetchEngineBlobError::InternalError(format!( - "Blobs from EL contains blob with invalid index {index}" - ))); - } - } - Err(e) => { - return Err(FetchEngineBlobError::BlobSidecarError(e)); - } - } + ) + .map_err(FetchEngineBlobError::BlobSidecarError)?; + + sidecars.push(KzgVerifiedBlob::from_execution_verified( + Arc::new(blob_sidecar), + timestamp_now(), + )); } - Ok(fixed_blob_sidecar_list) + + Ok(sidecars) } diff --git a/beacon_node/beacon_chain/src/fetch_blobs/tests.rs b/beacon_node/beacon_chain/src/fetch_blobs/tests.rs index 9cb597c6df9..7b2e22160f1 100644 --- a/beacon_node/beacon_chain/src/fetch_blobs/tests.rs +++ b/beacon_node/beacon_chain/src/fetch_blobs/tests.rs @@ -252,6 +252,7 @@ mod get_blobs_v1 { use super::*; use crate::blob_verification::{GossipBlobError, GossipVerifiedBlob}; use crate::block_verification_types::AsBlock; + use std::collections::HashSet; const ELECTRA_FORK: ForkName = ForkName::Electra; @@ -325,10 +326,13 @@ mod get_blobs_v1 { mock_get_blobs_v1_response(&mut mock_adapter, blob_and_proof_opts); // AND block is not imported into fork choice mock_fork_choice_contains_block(&mut mock_adapter, vec![]); - // AND all blobs returned are valid + // AND all blobs have not yet been seen mock_adapter - .expect_verify_blob_for_gossip() - .returning(|b| Ok(GossipVerifiedBlob::__assumed_valid(b.clone()))); + .expect_cached_blob_indexes() + .returning(|_| None); + mock_adapter + .expect_blobs_known_for_proposal() + .returning(|_, _| None); // Returned blobs should be processed mock_process_engine_blobs_result( &mut mock_adapter, @@ -408,17 +412,22 @@ mod get_blobs_v1 { // **GIVEN**: // All blobs returned let blob_and_proof_opts = blobs_and_proofs.into_iter().map(Some).collect::>(); + let all_blob_indices = blob_and_proof_opts + .iter() + .enumerate() + .map(|(i, _)| i as u64) + .collect::>(); + mock_get_blobs_v1_response(&mut mock_adapter, blob_and_proof_opts); // block not yet imported into fork choice mock_fork_choice_contains_block(&mut mock_adapter, vec![]); // All blobs already seen on gossip - mock_adapter.expect_verify_blob_for_gossip().returning(|b| { - Err(GossipBlobError::RepeatBlob { - proposer: b.block_proposer_index(), - slot: b.slot(), - index: b.index, - }) - }); + mock_adapter + .expect_cached_blob_indexes() + .returning(|_| None); + mock_adapter + .expect_blobs_known_for_proposal() + .returning(|_, _| Some(all_blob_indices.clone())); // **WHEN**: Trigger `fetch_blobs` on the block let custody_columns = hashset![0, 1, 2]; @@ -454,8 +463,11 @@ mod get_blobs_v1 { mock_get_blobs_v1_response(&mut mock_adapter, blob_and_proof_opts); mock_fork_choice_contains_block(&mut mock_adapter, vec![]); mock_adapter - .expect_verify_blob_for_gossip() - .returning(|b| Ok(GossipVerifiedBlob::__assumed_valid(b.clone()))); + .expect_cached_blob_indexes() + .returning(|_| None); + mock_adapter + .expect_blobs_known_for_proposal() + .returning(|_, _| None); mock_process_engine_blobs_result( &mut mock_adapter, Ok(AvailabilityProcessingStatus::Imported(block_root)), diff --git a/beacon_node/network/src/network_beacon_processor/mod.rs b/beacon_node/network/src/network_beacon_processor/mod.rs index f7c3a1bf8db..9e7d4693401 100644 --- a/beacon_node/network/src/network_beacon_processor/mod.rs +++ b/beacon_node/network/src/network_beacon_processor/mod.rs @@ -1,13 +1,12 @@ use crate::sync::manager::BlockProcessType; use crate::sync::SamplingId; use crate::{service::NetworkMessage, sync::manager::SyncMessage}; -use beacon_chain::blob_verification::{GossipBlobError, GossipVerifiedBlob}; +use beacon_chain::blob_verification::{observe_gossip_blob, GossipBlobError}; use beacon_chain::block_verification_types::RpcBlock; use beacon_chain::data_column_verification::{observe_gossip_data_column, GossipDataColumnError}; use beacon_chain::fetch_blobs::{ fetch_and_process_engine_blobs, EngineGetBlobsOutput, FetchEngineBlobError, }; -use beacon_chain::observed_data_sidecars::DoNotObserve; use beacon_chain::{ AvailabilityProcessingStatus, BeaconChain, BeaconChainTypes, BlockError, NotifyExecutionLayer, }; @@ -798,7 +797,10 @@ impl NetworkBeaconProcessor { if publish_blobs { match blobs_or_data_column { EngineGetBlobsOutput::Blobs(blobs) => { - self_cloned.publish_blobs_gradually(blobs, block_root); + self_cloned.publish_blobs_gradually( + blobs.into_iter().map(|b| b.clone_blob()).collect(), + block_root, + ); } EngineGetBlobsOutput::CustodyColumns(columns) => { self_cloned.publish_data_columns_gradually( @@ -941,7 +943,7 @@ impl NetworkBeaconProcessor { /// publisher exists for a blob, it will eventually get published here. fn publish_blobs_gradually( self: &Arc, - mut blobs: Vec>, + mut blobs: Vec>>, block_root: Hash256, ) { let self_clone = self.clone(); @@ -972,8 +974,8 @@ impl NetworkBeaconProcessor { while blobs_iter.peek().is_some() { let batch = blobs_iter.by_ref().take(batch_size); let publishable = batch - .filter_map(|unobserved| match unobserved.observe(&chain) { - Ok(observed) => Some(observed.clone_blob()), + .filter_map(|blob| match observe_gossip_blob(&blob, &chain) { + Ok(()) => Some(blob), Err(GossipBlobError::RepeatBlob { .. }) => None, Err(e) => { warn!( From 267a3d37771c99cf023150e0a96d692a1db3e85b Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Tue, 22 Jul 2025 18:00:47 +1000 Subject: [PATCH 03/13] Fix lint --- beacon_node/beacon_chain/src/fetch_blobs/tests.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/beacon_node/beacon_chain/src/fetch_blobs/tests.rs b/beacon_node/beacon_chain/src/fetch_blobs/tests.rs index 7b2e22160f1..f1ffabdd8f8 100644 --- a/beacon_node/beacon_chain/src/fetch_blobs/tests.rs +++ b/beacon_node/beacon_chain/src/fetch_blobs/tests.rs @@ -250,7 +250,6 @@ mod get_blobs_v2 { mod get_blobs_v1 { use super::*; - use crate::blob_verification::{GossipBlobError, GossipVerifiedBlob}; use crate::block_verification_types::AsBlock; use std::collections::HashSet; @@ -427,7 +426,7 @@ mod get_blobs_v1 { .returning(|_| None); mock_adapter .expect_blobs_known_for_proposal() - .returning(|_, _| Some(all_blob_indices.clone())); + .returning(move |_, _| Some(all_blob_indices.clone())); // **WHEN**: Trigger `fetch_blobs` on the block let custody_columns = hashset![0, 1, 2]; From 661ee56e49383d5ef02475541a334a0d099dd6ec Mon Sep 17 00:00:00 2001 From: Pawan Dhananjay Date: Tue, 22 Jul 2025 18:46:01 -0700 Subject: [PATCH 04/13] Initial commit --- beacon_node/builder_client/src/lib.rs | 130 ++++++++++++++++++++++++- beacon_node/execution_layer/src/lib.rs | 13 ++- 2 files changed, 138 insertions(+), 5 deletions(-) diff --git a/beacon_node/builder_client/src/lib.rs b/beacon_node/builder_client/src/lib.rs index d193eaf1d80..1cbff4aff16 100644 --- a/beacon_node/builder_client/src/lib.rs +++ b/beacon_node/builder_client/src/lib.rs @@ -292,10 +292,25 @@ impl BuilderHttpClient { Ok(()) } - /// `POST /eth/v1/builder/blinded_blocks` with SSZ serialized request body pub async fn post_builder_blinded_blocks_ssz( &self, blinded_block: &SignedBlindedBeaconBlock, + ) -> Result>, Error> { + if blinded_block.fork_name_unchecked().fulu_enabled() { + self.post_builder_blinded_blocks_v2_ssz(blinded_block) + .await + .map(|_| None) + } else { + self.post_builder_blinded_blocks_v1_ssz(blinded_block) + .await + .map(Some) + } + } + + /// `POST /eth/v1/builder/blinded_blocks` with SSZ serialized request body + pub async fn post_builder_blinded_blocks_v1_ssz( + &self, + blinded_block: &SignedBlindedBeaconBlock, ) -> Result, Error> { let mut path = self.server.full.clone(); @@ -340,10 +355,73 @@ impl BuilderHttpClient { .map_err(Error::InvalidSsz) } - /// `POST /eth/v1/builder/blinded_blocks` + /// `POST /eth/v2/builder/blinded_blocks` with SSZ serialized request body + pub async fn post_builder_blinded_blocks_v2_ssz( + &self, + blinded_block: &SignedBlindedBeaconBlock, + ) -> Result<(), Error> { + let mut path = self.server.full.clone(); + + let body = blinded_block.as_ssz_bytes(); + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("eth") + .push("v2") + .push("builder") + .push("blinded_blocks"); + + let mut headers = HeaderMap::new(); + headers.insert( + CONSENSUS_VERSION_HEADER, + HeaderValue::from_str(&blinded_block.fork_name_unchecked().to_string()) + .map_err(|e| Error::InvalidHeaders(format!("{}", e)))?, + ); + headers.insert( + CONTENT_TYPE_HEADER, + HeaderValue::from_str(SSZ_CONTENT_TYPE_HEADER) + .map_err(|e| Error::InvalidHeaders(format!("{}", e)))?, + ); + headers.insert( + ACCEPT, + HeaderValue::from_str(PREFERENCE_ACCEPT_VALUE) + .map_err(|e| Error::InvalidHeaders(format!("{}", e)))?, + ); + + let result = self + .post_ssz_with_raw_response( + path, + body, + headers, + Some(self.timeouts.post_blinded_blocks), + ) + .await?; + + if result.status() == StatusCode::ACCEPTED { + Ok(()) + } else { + // ACCEPTED is the only valid status code response + Err(Error::StatusCode(result.status())) + } + } + pub async fn post_builder_blinded_blocks( &self, blinded_block: &SignedBlindedBeaconBlock, + ) -> Result>>, Error> { + if blinded_block.fork_name_unchecked().fulu_enabled() { + self.post_builder_blinded_blocks_v2(blinded_block) + .await + .map(|_| None) + } else { + self.post_builder_blinded_blocks_v1(blinded_block).await.map(Some) + } + } + + /// `POST /eth/v1/builder/blinded_blocks` + pub async fn post_builder_blinded_blocks_v1( + &self, + blinded_block: &SignedBlindedBeaconBlock, ) -> Result>, Error> { let mut path = self.server.full.clone(); @@ -383,6 +461,54 @@ impl BuilderHttpClient { .await?) } + /// `POST /eth/v2/builder/blinded_blocks` + pub async fn post_builder_blinded_blocks_v2( + &self, + blinded_block: &SignedBlindedBeaconBlock, + ) -> Result<(), Error> { + let mut path = self.server.full.clone(); + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("eth") + .push("v2") + .push("builder") + .push("blinded_blocks"); + + let mut headers = HeaderMap::new(); + headers.insert( + CONSENSUS_VERSION_HEADER, + HeaderValue::from_str(&blinded_block.fork_name_unchecked().to_string()) + .map_err(|e| Error::InvalidHeaders(format!("{}", e)))?, + ); + headers.insert( + CONTENT_TYPE_HEADER, + HeaderValue::from_str(JSON_CONTENT_TYPE_HEADER) + .map_err(|e| Error::InvalidHeaders(format!("{}", e)))?, + ); + headers.insert( + ACCEPT, + HeaderValue::from_str(JSON_ACCEPT_VALUE) + .map_err(|e| Error::InvalidHeaders(format!("{}", e)))?, + ); + + let result = self + .post_with_raw_response( + path, + &blinded_block, + headers, + Some(self.timeouts.post_blinded_blocks), + ) + .await?; + + if result.status() == StatusCode::ACCEPTED { + Ok(()) + } else { + // ACCEPTED is the only valid status code response + Err(Error::StatusCode(result.status())) + } + } + /// `GET /eth/v1/builder/header` pub async fn get_builder_header( &self, diff --git a/beacon_node/execution_layer/src/lib.rs b/beacon_node/execution_layer/src/lib.rs index cf751138d63..fc1155588b4 100644 --- a/beacon_node/execution_layer/src/lib.rs +++ b/beacon_node/execution_layer/src/lib.rs @@ -1893,7 +1893,7 @@ impl ExecutionLayer { &self, block_root: Hash256, block: &SignedBlindedBeaconBlock, - ) -> Result, Error> { + ) -> Result>, Error> { debug!(?block_root, "Sending block to builder"); if let Some(builder) = self.builder() { @@ -1915,13 +1915,13 @@ impl ExecutionLayer { .post_builder_blinded_blocks(block) .await .map_err(Error::Builder) - .map(|d| d.data) + .map(|d| d.map(|resp| resp.data)) } }) .await; match &payload_result { - Ok(unblinded_response) => { + Ok(Some(unblinded_response)) => { metrics::inc_counter_vec( &metrics::EXECUTION_LAYER_BUILDER_REVEAL_PAYLOAD_OUTCOME, &[metrics::SUCCESS], @@ -1936,6 +1936,13 @@ impl ExecutionLayer { "Builder successfully revealed payload" ) } + Ok(None) => { + info!( + relay_response_ms = duration.as_millis(), + ?block_root, + "Builder returned a successful response" + ); + } Err(e) => { metrics::inc_counter_vec( &metrics::EXECUTION_LAYER_BUILDER_REVEAL_PAYLOAD_OUTCOME, From ced43c3808d98d36ac2fb5251ba6fef5fe208735 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Wed, 23 Jul 2025 11:46:29 +1000 Subject: [PATCH 05/13] Serve ConfigAndPreset for latest_stable fork --- beacon_node/http_api/tests/tests.rs | 18 ++++++++++++------ consensus/types/src/config_and_preset.rs | 8 +++++--- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index ecd20f3f79c..f6dc435e607 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -2616,12 +2616,18 @@ impl ApiTester { } pub async fn test_get_config_spec(self) -> Self { - let result = self - .client - .get_config_spec::() - .await - .map(|res| ConfigAndPreset::Fulu(res.data)) - .unwrap(); + let result = if self.chain.spec.fulu_fork_epoch.is_some() { + self.client + .get_config_spec::() + .await + .map(|res| ConfigAndPreset::Fulu(res.data)) + } else { + self.client + .get_config_spec::() + .await + .map(|res| ConfigAndPreset::Electra(res.data)) + } + .unwrap(); let expected = ConfigAndPreset::from_chain_spec::(&self.chain.spec, None); assert_eq!(result, expected); diff --git a/consensus/types/src/config_and_preset.rs b/consensus/types/src/config_and_preset.rs index 235bf202382..728fa2d8dd6 100644 --- a/consensus/types/src/config_and_preset.rs +++ b/consensus/types/src/config_and_preset.rs @@ -43,7 +43,6 @@ pub struct ConfigAndPreset { } impl ConfigAndPreset { - // DEPRECATED: the `fork_name` argument is never used, we should remove it. pub fn from_chain_spec(spec: &ChainSpec, fork_name: Option) -> Self { let config = Config::from_chain_spec::(spec); let base_preset = BasePreset::from_chain_spec::(spec); @@ -53,8 +52,11 @@ impl ConfigAndPreset { let deneb_preset = DenebPreset::from_chain_spec::(spec); let extra_fields = get_extra_fields(spec); + // The latest stable fork is used if the `fork_name` is not specified. + let latest_stable_fork = ForkName::latest_stable(); + if spec.fulu_fork_epoch.is_some() - || fork_name.is_none() + || latest_stable_fork == ForkName::Fulu || fork_name == Some(ForkName::Fulu) { let electra_preset = ElectraPreset::from_chain_spec::(spec); @@ -72,7 +74,7 @@ impl ConfigAndPreset { extra_fields, }) } else if spec.electra_fork_epoch.is_some() - || fork_name.is_none() + || latest_stable_fork == ForkName::Electra || fork_name == Some(ForkName::Electra) { let electra_preset = ElectraPreset::from_chain_spec::(spec); From 097f7c6c1c72aad4ce7286eb53b24c1d65bca4b3 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Wed, 23 Jul 2025 12:54:08 +1000 Subject: [PATCH 06/13] Fix extra fields roundtrip test --- consensus/types/src/config_and_preset.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/consensus/types/src/config_and_preset.rs b/consensus/types/src/config_and_preset.rs index 728fa2d8dd6..84339b11b61 100644 --- a/consensus/types/src/config_and_preset.rs +++ b/consensus/types/src/config_and_preset.rs @@ -173,8 +173,8 @@ mod test { .write(false) .open(tmp_file.as_ref()) .expect("error while opening the file"); - let from: ConfigAndPresetFulu = + let from: ConfigAndPresetElectra = serde_yaml::from_reader(reader).expect("error while deserializing"); - assert_eq!(ConfigAndPreset::Fulu(from), yamlconfig); + assert_eq!(ConfigAndPreset::Electra(from), yamlconfig); } } From 696a7a76cd5c379462f963d200c44a7a8812ab19 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Wed, 23 Jul 2025 13:12:59 +1000 Subject: [PATCH 07/13] Fix VC test --- validator_client/http_api/src/tests.rs | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/validator_client/http_api/src/tests.rs b/validator_client/http_api/src/tests.rs index 7d421cd7d58..d1e682568ba 100644 --- a/validator_client/http_api/src/tests.rs +++ b/validator_client/http_api/src/tests.rs @@ -206,13 +206,20 @@ impl ApiTester { } pub async fn test_get_lighthouse_spec(self) -> Self { - let result = self - .client - .get_lighthouse_spec::() - .await - .map(|res| ConfigAndPreset::Fulu(res.data)) - .unwrap(); - let expected = ConfigAndPreset::from_chain_spec::(&E::default_spec(), None); + let spec = E::default_spec(); + let result = if spec.fulu_fork_epoch.is_some() { + self.client + .get_lighthouse_spec::() + .await + .map(|res| ConfigAndPreset::Fulu(res.data)) + } else { + self.client + .get_lighthouse_spec::() + .await + .map(|res| ConfigAndPreset::Electra(res.data)) + } + .unwrap(); + let expected = ConfigAndPreset::from_chain_spec::(&spec, None); assert_eq!(result, expected); From 053d4504eafa4533935339560b8eccd1623dfd3f Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Wed, 23 Jul 2025 15:11:03 +1000 Subject: [PATCH 08/13] Complete Fulu builder API changes. --- beacon_node/builder_client/src/lib.rs | 28 ------ beacon_node/execution_layer/src/lib.rs | 100 ++++++++++++++++++--- beacon_node/http_api/src/publish_blocks.rs | 54 +++++++---- 3 files changed, 123 insertions(+), 59 deletions(-) diff --git a/beacon_node/builder_client/src/lib.rs b/beacon_node/builder_client/src/lib.rs index 1cbff4aff16..c1066042854 100644 --- a/beacon_node/builder_client/src/lib.rs +++ b/beacon_node/builder_client/src/lib.rs @@ -292,21 +292,6 @@ impl BuilderHttpClient { Ok(()) } - pub async fn post_builder_blinded_blocks_ssz( - &self, - blinded_block: &SignedBlindedBeaconBlock, - ) -> Result>, Error> { - if blinded_block.fork_name_unchecked().fulu_enabled() { - self.post_builder_blinded_blocks_v2_ssz(blinded_block) - .await - .map(|_| None) - } else { - self.post_builder_blinded_blocks_v1_ssz(blinded_block) - .await - .map(Some) - } - } - /// `POST /eth/v1/builder/blinded_blocks` with SSZ serialized request body pub async fn post_builder_blinded_blocks_v1_ssz( &self, @@ -405,19 +390,6 @@ impl BuilderHttpClient { } } - pub async fn post_builder_blinded_blocks( - &self, - blinded_block: &SignedBlindedBeaconBlock, - ) -> Result>>, Error> { - if blinded_block.fork_name_unchecked().fulu_enabled() { - self.post_builder_blinded_blocks_v2(blinded_block) - .await - .map(|_| None) - } else { - self.post_builder_blinded_blocks_v1(blinded_block).await.map(Some) - } - } - /// `POST /eth/v1/builder/blinded_blocks` pub async fn post_builder_blinded_blocks_v1( &self, diff --git a/beacon_node/execution_layer/src/lib.rs b/beacon_node/execution_layer/src/lib.rs index fc1155588b4..bfde2be17b0 100644 --- a/beacon_node/execution_layer/src/lib.rs +++ b/beacon_node/execution_layer/src/lib.rs @@ -411,6 +411,11 @@ pub enum FailedCondition { EpochsSinceFinalization, } +pub enum SubmitBlindedBlockResponse { + V1(Box>), + V2, +} + type PayloadContentsRefTuple<'a, E> = (ExecutionPayloadRef<'a, E>, Option<&'a BlobsBundle>); struct Inner { @@ -1893,9 +1898,25 @@ impl ExecutionLayer { &self, block_root: Hash256, block: &SignedBlindedBeaconBlock, - ) -> Result>, Error> { + spec: &ChainSpec, + ) -> Result, Error> { debug!(?block_root, "Sending block to builder"); + if spec.is_fulu_scheduled() { + self.post_builder_blinded_blocks_v2(block_root, block) + .await + .map(|()| SubmitBlindedBlockResponse::V2) + } else { + self.post_builder_blinded_blocks_v1(block_root, block) + .await + .map(|full_payload| SubmitBlindedBlockResponse::V1(Box::new(full_payload))) + } + } + async fn post_builder_blinded_blocks_v1( + &self, + block_root: Hash256, + block: &SignedBlindedBeaconBlock, + ) -> Result, Error> { if let Some(builder) = self.builder() { let (payload_result, duration) = timed_future(metrics::POST_BLINDED_PAYLOAD_BUILDER, async { @@ -1903,25 +1924,25 @@ impl ExecutionLayer { debug!( ?block_root, ssz = ssz_enabled, - "Calling submit_blinded_block on builder" + "Calling submit_blinded_block v1 on builder" ); if ssz_enabled { builder - .post_builder_blinded_blocks_ssz(block) + .post_builder_blinded_blocks_v1_ssz(block) .await .map_err(Error::Builder) } else { builder - .post_builder_blinded_blocks(block) + .post_builder_blinded_blocks_v1(block) .await .map_err(Error::Builder) - .map(|d| d.map(|resp| resp.data)) + .map(|d| d.data) } }) .await; match &payload_result { - Ok(Some(unblinded_response)) => { + Ok(unblinded_response) => { metrics::inc_counter_vec( &metrics::EXECUTION_LAYER_BUILDER_REVEAL_PAYLOAD_OUTCOME, &[metrics::SUCCESS], @@ -1936,13 +1957,6 @@ impl ExecutionLayer { "Builder successfully revealed payload" ) } - Ok(None) => { - info!( - relay_response_ms = duration.as_millis(), - ?block_root, - "Builder returned a successful response" - ); - } Err(e) => { metrics::inc_counter_vec( &metrics::EXECUTION_LAYER_BUILDER_REVEAL_PAYLOAD_OUTCOME, @@ -1968,6 +1982,66 @@ impl ExecutionLayer { Err(Error::NoPayloadBuilder) } } + + async fn post_builder_blinded_blocks_v2( + &self, + block_root: Hash256, + block: &SignedBlindedBeaconBlock, + ) -> Result<(), Error> { + if let Some(builder) = self.builder() { + let (result, duration) = timed_future(metrics::POST_BLINDED_PAYLOAD_BUILDER, async { + let ssz_enabled = builder.is_ssz_available(); + debug!( + ?block_root, + ssz = ssz_enabled, + "Calling submit_blinded_block v2 on builder" + ); + if ssz_enabled { + builder + .post_builder_blinded_blocks_v2_ssz(block) + .await + .map_err(Error::Builder) + } else { + builder + .post_builder_blinded_blocks_v2(block) + .await + .map_err(Error::Builder) + } + }) + .await; + + match result { + Ok(()) => { + metrics::inc_counter_vec( + &metrics::EXECUTION_LAYER_BUILDER_REVEAL_PAYLOAD_OUTCOME, + &[metrics::SUCCESS], + ); + info!( + relay_response_ms = duration.as_millis(), + ?block_root, + "Successfully submitted blinded block to the builder" + ) + } + Err(e) => { + metrics::inc_counter_vec( + &metrics::EXECUTION_LAYER_BUILDER_REVEAL_PAYLOAD_OUTCOME, + &[metrics::FAILURE], + ); + error!( + info = "this may result in a missed block proposal", + error = ?e, + relay_response_ms = duration.as_millis(), + ?block_root, + "Failed to submit blinded block to the builder" + ) + } + } + + Ok(()) + } else { + Err(Error::NoPayloadBuilder) + } + } } #[derive(AsRefStr)] diff --git a/beacon_node/http_api/src/publish_blocks.rs b/beacon_node/http_api/src/publish_blocks.rs index 5d581859ae9..c1b86416b15 100644 --- a/beacon_node/http_api/src/publish_blocks.rs +++ b/beacon_node/http_api/src/publish_blocks.rs @@ -13,7 +13,7 @@ use eth2::types::{ BlobsBundle, BroadcastValidation, ErrorMessage, ExecutionPayloadAndBlobs, FullPayloadContents, PublishBlockRequest, SignedBlockContents, }; -use execution_layer::ProvenancedPayload; +use execution_layer::{ProvenancedPayload, SubmitBlindedBlockResponse}; use futures::TryFutureExt; use lighthouse_network::{NetworkGlobals, PubsubMessage}; use network::NetworkMessage; @@ -636,27 +636,37 @@ pub async fn publish_blinded_block( network_globals: Arc>, ) -> Result { let block_root = blinded_block.canonical_root(); - let full_block = reconstruct_block(chain.clone(), block_root, blinded_block).await?; - publish_block::( - Some(block_root), - full_block, - chain, - network_tx, - validation_level, - duplicate_status_code, - network_globals, - ) - .await + let full_block_opt = reconstruct_block(chain.clone(), block_root, blinded_block).await?; + + if let Some(full_block) = full_block_opt { + publish_block::( + Some(block_root), + full_block, + chain, + network_tx, + validation_level, + duplicate_status_code, + network_globals, + ) + .await + } else { + // From the fulu fork, builders are responsible for publishing and + // will no longer return the full payload and blobs. + Ok(warp::reply().into_response()) + } } /// Deconstruct the given blinded block, and construct a full block. This attempts to use the /// execution layer's payload cache, and if that misses, attempts a blind block proposal to retrieve /// the full payload. +/// +/// From the Fulu fork, external builders no longer return the full payload and blobs, and this +/// function will always return `Ok(None)` on successful submission of blinded block. pub async fn reconstruct_block( chain: Arc>, block_root: Hash256, block: Arc>, -) -> Result>>, Rejection> { +) -> Result>>>, Rejection> { let full_payload_opt = if let Ok(payload_header) = block.message().body().execution_payload() { let el = chain.execution_layer.as_ref().ok_or_else(|| { warp_utils::reject::custom_server_error("Missing execution layer".to_string()) @@ -696,17 +706,24 @@ pub async fn reconstruct_block( "builder", ); - let full_payload = el - .propose_blinded_beacon_block(block_root, &block) + match el + .propose_blinded_beacon_block(block_root, &block, &chain.spec) .await .map_err(|e| { warp_utils::reject::custom_server_error(format!( "Blind block proposal failed: {:?}", e )) - })?; - info!(block_hash = ?full_payload.block_hash(), "Successfully published a block to the builder network"); - ProvenancedPayload::Builder(full_payload) + })? { + SubmitBlindedBlockResponse::V1(full_payload) => { + info!(block_root = ?block_root, "Successfully published a block to the builder network"); + ProvenancedPayload::Builder(*full_payload) + } + SubmitBlindedBlockResponse::V2 => { + info!(block_root = ?block_root, "Successfully published a block to the builder network"); + return Ok(None); + } + } }; Some(full_payload_contents) @@ -734,6 +751,7 @@ pub async fn reconstruct_block( .map(|(block, blobs)| ProvenancedBlock::builder(block, blobs)) } } + .map(Some) .map_err(|e| { warp_utils::reject::custom_server_error(format!("Unable to add payload to block: {e:?}")) }) From 3b8d8e762d6b81f6bcfa1c26b3af29a6d5914951 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Wed, 23 Jul 2025 15:23:25 +1000 Subject: [PATCH 09/13] Fix tests. --- beacon_node/http_api/tests/broadcast_validation_tests.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/beacon_node/http_api/tests/broadcast_validation_tests.rs b/beacon_node/http_api/tests/broadcast_validation_tests.rs index 27831b3a232..95c21d8fe2f 100644 --- a/beacon_node/http_api/tests/broadcast_validation_tests.rs +++ b/beacon_node/http_api/tests/broadcast_validation_tests.rs @@ -1275,14 +1275,17 @@ pub async fn blinded_equivocation_consensus_late_equivocation() { Arc::new(block_a), ) .await - .unwrap(); + .expect("failed to reconstruct block") + .expect("block expected"); + let unblinded_block_b = reconstruct_block( tester.harness.chain.clone(), block_b.canonical_root(), block_b.clone(), ) .await - .unwrap(); + .expect("failed to reconstruct block") + .expect("block expected"); let inner_block_a = match unblinded_block_a { ProvenancedBlock::Local(a, _, _) => a, From d107ff74a1df1a72f5199106a0c2fd74148321b4 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Wed, 23 Jul 2025 14:41:56 -0700 Subject: [PATCH 10/13] Add block with columns step to fork choice test --- testing/ef_tests/Makefile | 2 +- testing/ef_tests/src/cases/fork_choice.rs | 247 ++++++++++++++++------ testing/ef_tests/src/handler.rs | 1 - 3 files changed, 184 insertions(+), 66 deletions(-) diff --git a/testing/ef_tests/Makefile b/testing/ef_tests/Makefile index 48afcae4b2b..333a443889a 100644 --- a/testing/ef_tests/Makefile +++ b/testing/ef_tests/Makefile @@ -1,6 +1,6 @@ # To download/extract nightly tests, run: # CONSENSUS_SPECS_TEST_VERSION=nightly make -CONSENSUS_SPECS_TEST_VERSION ?= v1.6.0-alpha.1 +CONSENSUS_SPECS_TEST_VERSION ?= v1.6.0-alpha.3 REPO_NAME := consensus-spec-tests OUTPUT_DIR := ./$(REPO_NAME) diff --git a/testing/ef_tests/src/cases/fork_choice.rs b/testing/ef_tests/src/cases/fork_choice.rs index af3b0bce2de..2a1ba69b0c9 100644 --- a/testing/ef_tests/src/cases/fork_choice.rs +++ b/testing/ef_tests/src/cases/fork_choice.rs @@ -8,6 +8,7 @@ use beacon_chain::chain_config::{ DisallowedReOrgOffsets, DEFAULT_RE_ORG_HEAD_THRESHOLD, DEFAULT_RE_ORG_MAX_EPOCHS_SINCE_FINALIZATION, DEFAULT_RE_ORG_PARENT_THRESHOLD, }; +use beacon_chain::data_column_verification::GossipVerifiedDataColumn; use beacon_chain::slot_clock::SlotClock; use beacon_chain::{ attestation_verification::{ @@ -26,8 +27,9 @@ use std::sync::Arc; use std::time::Duration; use types::{ Attestation, AttestationRef, AttesterSlashing, AttesterSlashingRef, BeaconBlock, BeaconState, - BlobSidecar, BlobsList, BlockImportSource, Checkpoint, ExecutionBlockHash, Hash256, - IndexedAttestation, KzgProof, ProposerPreparationData, SignedBeaconBlock, Slot, Uint256, + BlobSidecar, BlobsList, BlockImportSource, Checkpoint, DataColumnSidecarList, + ExecutionBlockHash, Hash256, IndexedAttestation, KzgProof, ProposerPreparationData, + SignedBeaconBlock, Slot, Uint256, }; // When set to true, cache any states fetched from the db. @@ -91,14 +93,14 @@ impl From for PayloadStatusV1 { #[derive(Debug, Clone, Deserialize)] #[serde(untagged, deny_unknown_fields)] -pub enum Step { +pub enum Step { Tick { tick: u64, }, ValidBlock { block: TBlock, }, - MaybeValidBlock { + MaybeValidBlockAndBlobs { block: TBlock, blobs: Option, proofs: Option>, @@ -120,6 +122,11 @@ pub enum Step { Checks { checks: Box, }, + MaybeValidBlockAndColumns { + block: TBlock, + columns: Option, + valid: bool, + }, } #[derive(Debug, Clone, Deserialize)] @@ -136,7 +143,14 @@ pub struct ForkChoiceTest { pub anchor_block: BeaconBlock, #[allow(clippy::type_complexity)] pub steps: Vec< - Step, BlobsList, Attestation, AttesterSlashing, PowBlock>, + Step< + SignedBeaconBlock, + BlobsList, + DataColumnSidecarList, + Attestation, + AttesterSlashing, + PowBlock, + >, >, } @@ -150,7 +164,7 @@ impl LoadCase for ForkChoiceTest { .expect("path must be valid OsStr") .to_string(); let spec = &testing_spec::(fork_name); - let steps: Vec> = + let steps: Vec, String, String, String>> = yaml_decode_file(&path.join("steps.yaml"))?; // Resolve the object names in `steps.yaml` into actual decoded block/attestation objects. let steps = steps @@ -163,7 +177,7 @@ impl LoadCase for ForkChoiceTest { }) .map(|block| Step::ValidBlock { block }) } - Step::MaybeValidBlock { + Step::MaybeValidBlockAndBlobs { block, blobs, proofs, @@ -176,7 +190,7 @@ impl LoadCase for ForkChoiceTest { let blobs = blobs .map(|blobs| ssz_decode_file(&path.join(format!("{blobs}.ssz_snappy")))) .transpose()?; - Ok(Step::MaybeValidBlock { + Ok(Step::MaybeValidBlockAndBlobs { block, blobs, proofs, @@ -223,6 +237,31 @@ impl LoadCase for ForkChoiceTest { payload_status, }), Step::Checks { checks } => Ok(Step::Checks { checks }), + Step::MaybeValidBlockAndColumns { + block, + columns, + valid, + } => { + let block = + ssz_decode_file_with(&path.join(format!("{block}.ssz_snappy")), |bytes| { + SignedBeaconBlock::from_ssz_bytes(bytes, spec) + })?; + let columns = columns + .map(|columns_vec| { + columns_vec + .into_iter() + .map(|column| { + ssz_decode_file(&path.join(format!("{column}.ssz_snappy"))) + }) + .collect::, _>>() + }) + .transpose()?; + Ok(Step::MaybeValidBlockAndColumns { + block, + columns, + valid, + }) + } }) .collect::>()?; let anchor_state = ssz_decode_state(&path.join("anchor_state.ssz_snappy"), spec)?; @@ -263,14 +302,19 @@ impl Case for ForkChoiceTest { match step { Step::Tick { tick } => tester.set_tick(*tick), Step::ValidBlock { block } => { - tester.process_block(block.clone(), None, None, true)? + tester.process_block_and_blobs(block.clone(), None, None, true)? } - Step::MaybeValidBlock { + Step::MaybeValidBlockAndBlobs { block, blobs, proofs, valid, - } => tester.process_block(block.clone(), blobs.clone(), proofs.clone(), *valid)?, + } => tester.process_block_and_blobs( + block.clone(), + blobs.clone(), + proofs.clone(), + *valid, + )?, Step::Attestation { attestation } => tester.process_attestation(attestation)?, Step::AttesterSlashing { attester_slashing } => { tester.process_attester_slashing(attester_slashing.to_ref()) @@ -344,6 +388,14 @@ impl Case for ForkChoiceTest { tester.check_expected_proposer_head(*expected_proposer_head)?; } } + + Step::MaybeValidBlockAndColumns { + block, + columns, + valid, + } => { + tester.process_block_and_columns(block.clone(), columns.clone(), *valid)?; + } } } @@ -384,6 +436,7 @@ impl Tester { .genesis_state_ephemeral_store(case.anchor_state.clone()) .mock_execution_layer() .recalculate_fork_times_with_genesis(0) + .import_all_data_columns(true) .mock_execution_layer_all_payloads_valid() .build(); @@ -454,7 +507,66 @@ impl Tester { .unwrap(); } - pub fn process_block( + pub fn process_block_and_columns( + &self, + block: SignedBeaconBlock, + columns: Option>, + valid: bool, + ) -> Result<(), Error> { + let block_root = block.canonical_root(); + let mut data_column_success = true; + + if let Some(columns) = columns.clone() { + let gossip_verified_data_columns = columns + .into_iter() + .map(|column| { + GossipVerifiedDataColumn::new(column.clone(), column.index, &self.harness.chain) + .unwrap_or_else(|_| { + data_column_success = false; + GossipVerifiedDataColumn::__new_for_testing(column) + }) + }) + .collect(); + + let result = self.block_on_dangerous( + self.harness + .chain + .process_gossip_data_columns(gossip_verified_data_columns, || Ok(())), + )?; + if valid { + assert!(result.is_ok()); + } + }; + + let block = Arc::new(block); + let result: Result, _> = self + .block_on_dangerous(self.harness.chain.process_block( + block_root, + RpcBlock::new_without_blobs(Some(block_root), block.clone()), + NotifyExecutionLayer::Yes, + BlockImportSource::Lookup, + || Ok(()), + ))? + .map(|avail: AvailabilityProcessingStatus| avail.try_into()); + let success = data_column_success && result.as_ref().is_ok_and(|inner| inner.is_ok()); + if success != valid { + return Err(Error::DidntFail(format!( + "block with root {} was valid={} whilst test expects valid={}. result: {:?}", + block_root, + result.is_ok(), + valid, + result + ))); + } + + if !valid && columns.is_none() { + self.apply_invalid_block(&block)?; + } + + Ok(()) + } + + pub fn process_block_and_blobs( &self, block: SignedBeaconBlock, blobs: Option>, @@ -537,66 +649,73 @@ impl Tester { ))); } - // Apply invalid blocks directly against the fork choice `on_block` function. This ensures - // that the block is being rejected by `on_block`, not just some upstream block processing - // function. When blobs exist, we don't do this. if !valid && blobs.is_none() { - // A missing parent block whilst `valid == false` means the test should pass. - if let Some(parent_block) = self + self.apply_invalid_block(&block)?; + } + + Ok(()) + } + + // Apply invalid blocks directly against the fork choice `on_block` function. This ensures + // that the block is being rejected by `on_block`, not just some upstream block processing + // function. When data columns or blobs exist, we don't do this. + fn apply_invalid_block(&self, block: &Arc>) -> Result<(), Error> { + let block_root = block.canonical_root(); + // A missing parent block whilst `valid == false` means the test should pass. + if let Some(parent_block) = self + .harness + .chain + .get_blinded_block(&block.parent_root()) + .unwrap() + { + let parent_state_root = parent_block.state_root(); + + let mut state = self .harness .chain - .get_blinded_block(&block.parent_root()) + .get_state( + &parent_state_root, + Some(parent_block.slot()), + CACHE_STATE_IN_TESTS, + ) .unwrap() - { - let parent_state_root = parent_block.state_root(); + .unwrap(); - let mut state = self - .harness - .chain - .get_state( - &parent_state_root, - Some(parent_block.slot()), - CACHE_STATE_IN_TESTS, - ) - .unwrap() - .unwrap(); - - complete_state_advance( - &mut state, - Some(parent_state_root), - block.slot(), - &self.harness.chain.spec, - ) + complete_state_advance( + &mut state, + Some(parent_state_root), + block.slot(), + &self.harness.chain.spec, + ) + .unwrap(); + + let block_delay = self + .harness + .chain + .slot_clock + .seconds_from_current_slot_start() .unwrap(); - let block_delay = self - .harness - .chain - .slot_clock - .seconds_from_current_slot_start() - .unwrap(); + let result = self + .harness + .chain + .canonical_head + .fork_choice_write_lock() + .on_block( + self.harness.chain.slot().unwrap(), + block.message(), + block_root, + block_delay, + &state, + PayloadVerificationStatus::Irrelevant, + &self.harness.chain.spec, + ); - let result = self - .harness - .chain - .canonical_head - .fork_choice_write_lock() - .on_block( - self.harness.chain.slot().unwrap(), - block.message(), - block_root, - block_delay, - &state, - PayloadVerificationStatus::Irrelevant, - &self.harness.chain.spec, - ); - - if result.is_ok() { - return Err(Error::DidntFail(format!( - "block with root {} should fail on_block", - block_root, - ))); - } + if result.is_ok() { + return Err(Error::DidntFail(format!( + "block with root {} should fail on_block", + block_root, + ))); } } diff --git a/testing/ef_tests/src/handler.rs b/testing/ef_tests/src/handler.rs index fd2bea6e8e4..ed6a20381cd 100644 --- a/testing/ef_tests/src/handler.rs +++ b/testing/ef_tests/src/handler.rs @@ -93,7 +93,6 @@ pub trait Handler { .filter_map(as_directory) .map(|test_case_dir| { let path = test_case_dir.path(); - let case = Self::Case::load_from_dir(&path, fork_name).expect("test should load"); (path, case) }) From 8acbed39eeccd4b613ea06dd6b5038f68b887179 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Fri, 25 Jul 2025 13:10:17 +1000 Subject: [PATCH 11/13] Remove clone --- 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 9e7d4693401..613a0692fb4 100644 --- a/beacon_node/network/src/network_beacon_processor/mod.rs +++ b/beacon_node/network/src/network_beacon_processor/mod.rs @@ -798,7 +798,7 @@ impl NetworkBeaconProcessor { match blobs_or_data_column { EngineGetBlobsOutput::Blobs(blobs) => { self_cloned.publish_blobs_gradually( - blobs.into_iter().map(|b| b.clone_blob()).collect(), + blobs.into_iter().map(|b| b.to_blob()).collect(), block_root, ); } From 8daa4526f09745352b721352bbeceb814947de0a Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Fri, 25 Jul 2025 14:07:57 +1000 Subject: [PATCH 12/13] Clean up --- beacon_node/http_api/src/lib.rs | 2 +- beacon_node/http_api/tests/tests.rs | 2 +- consensus/types/src/config_and_preset.rs | 30 ++++--------------- .../src/mock_beacon_node.rs | 2 +- validator_client/http_api/src/lib.rs | 2 +- validator_client/http_api/src/test_utils.rs | 2 +- validator_client/http_api/src/tests.rs | 9 +++--- 7 files changed, 15 insertions(+), 34 deletions(-) diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index 83422090caa..c66ddacdaf6 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -2763,7 +2763,7 @@ pub fn serve( move |task_spawner: TaskSpawner, chain: Arc>| { task_spawner.blocking_json_task(Priority::P0, move || { let config_and_preset = - ConfigAndPreset::from_chain_spec::(&chain.spec, None); + ConfigAndPreset::from_chain_spec::(&chain.spec); Ok(api_types::GenericResponse::from(config_and_preset)) }) }, diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index f6dc435e607..71ffdb75e76 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -2628,7 +2628,7 @@ impl ApiTester { .map(|res| ConfigAndPreset::Electra(res.data)) } .unwrap(); - let expected = ConfigAndPreset::from_chain_spec::(&self.chain.spec, None); + let expected = ConfigAndPreset::from_chain_spec::(&self.chain.spec); assert_eq!(result, expected); diff --git a/consensus/types/src/config_and_preset.rs b/consensus/types/src/config_and_preset.rs index 84339b11b61..94cdb28cd54 100644 --- a/consensus/types/src/config_and_preset.rs +++ b/consensus/types/src/config_and_preset.rs @@ -1,6 +1,6 @@ use crate::{ consts::altair, consts::deneb, AltairPreset, BasePreset, BellatrixPreset, CapellaPreset, - ChainSpec, Config, DenebPreset, ElectraPreset, EthSpec, ForkName, FuluPreset, + ChainSpec, Config, DenebPreset, ElectraPreset, EthSpec, FuluPreset, }; use maplit::hashmap; use serde::{Deserialize, Serialize}; @@ -43,7 +43,7 @@ pub struct ConfigAndPreset { } impl ConfigAndPreset { - pub fn from_chain_spec(spec: &ChainSpec, fork_name: Option) -> Self { + pub fn from_chain_spec(spec: &ChainSpec) -> Self { let config = Config::from_chain_spec::(spec); let base_preset = BasePreset::from_chain_spec::(spec); let altair_preset = AltairPreset::from_chain_spec::(spec); @@ -52,13 +52,7 @@ impl ConfigAndPreset { let deneb_preset = DenebPreset::from_chain_spec::(spec); let extra_fields = get_extra_fields(spec); - // The latest stable fork is used if the `fork_name` is not specified. - let latest_stable_fork = ForkName::latest_stable(); - - if spec.fulu_fork_epoch.is_some() - || latest_stable_fork == ForkName::Fulu - || fork_name == Some(ForkName::Fulu) - { + if spec.is_fulu_scheduled() { let electra_preset = ElectraPreset::from_chain_spec::(spec); let fulu_preset = FuluPreset::from_chain_spec::(spec); @@ -73,10 +67,7 @@ impl ConfigAndPreset { fulu_preset, extra_fields, }) - } else if spec.electra_fork_epoch.is_some() - || latest_stable_fork == ForkName::Electra - || fork_name == Some(ForkName::Electra) - { + } else { let electra_preset = ElectraPreset::from_chain_spec::(spec); ConfigAndPreset::Electra(ConfigAndPresetElectra { @@ -89,16 +80,6 @@ impl ConfigAndPreset { electra_preset, extra_fields, }) - } else { - ConfigAndPreset::Deneb(ConfigAndPresetDeneb { - config, - base_preset, - altair_preset, - bellatrix_preset, - capella_preset, - deneb_preset, - extra_fields, - }) } } } @@ -155,8 +136,7 @@ mod test { .open(tmp_file.as_ref()) .expect("error opening file"); let mainnet_spec = ChainSpec::mainnet(); - let mut yamlconfig = - ConfigAndPreset::from_chain_spec::(&mainnet_spec, None); + let mut yamlconfig = ConfigAndPreset::from_chain_spec::(&mainnet_spec); let (k1, v1) = ("SAMPLE_HARDFORK_KEY1", "123456789"); let (k2, v2) = ("SAMPLE_HARDFORK_KEY2", "987654321"); let (k3, v3) = ("SAMPLE_HARDFORK_KEY3", 32); diff --git a/testing/validator_test_rig/src/mock_beacon_node.rs b/testing/validator_test_rig/src/mock_beacon_node.rs index 7a902709138..ff1e772d544 100644 --- a/testing/validator_test_rig/src/mock_beacon_node.rs +++ b/testing/validator_test_rig/src/mock_beacon_node.rs @@ -41,7 +41,7 @@ impl MockBeaconNode { pub fn mock_config_spec(&mut self, spec: &ChainSpec) { let path_pattern = Regex::new(r"^/eth/v1/config/spec$").unwrap(); - let config_and_preset = ConfigAndPreset::from_chain_spec::(spec, None); + let config_and_preset = ConfigAndPreset::from_chain_spec::(spec); let data = GenericResponse::from(config_and_preset); self.server .mock("GET", Matcher::Regex(path_pattern.to_string())) diff --git a/validator_client/http_api/src/lib.rs b/validator_client/http_api/src/lib.rs index d5de24229c4..02a677212cb 100644 --- a/validator_client/http_api/src/lib.rs +++ b/validator_client/http_api/src/lib.rs @@ -315,7 +315,7 @@ pub fn serve( .and(spec_filter.clone()) .then(|spec: Arc<_>| { blocking_json_task(move || { - let config = ConfigAndPreset::from_chain_spec::(&spec, None); + let config = ConfigAndPreset::from_chain_spec::(&spec); Ok(api_types::GenericResponse::from(config)) }) }); diff --git a/validator_client/http_api/src/test_utils.rs b/validator_client/http_api/src/test_utils.rs index feb71c3a467..53bcf7baebb 100644 --- a/validator_client/http_api/src/test_utils.rs +++ b/validator_client/http_api/src/test_utils.rs @@ -260,7 +260,7 @@ impl ApiTester { .await .map(|res| ConfigAndPreset::Fulu(res.data)) .unwrap(); - let expected = ConfigAndPreset::from_chain_spec::(&E::default_spec(), None); + let expected = ConfigAndPreset::from_chain_spec::(&E::default_spec()); assert_eq!(result, expected); diff --git a/validator_client/http_api/src/tests.rs b/validator_client/http_api/src/tests.rs index d1e682568ba..b021186e77a 100644 --- a/validator_client/http_api/src/tests.rs +++ b/validator_client/http_api/src/tests.rs @@ -45,6 +45,7 @@ struct ApiTester { validator_store: Arc>, url: SensitiveUrl, slot_clock: TestingSlotClock, + spec: Arc, _validator_dir: TempDir, _secrets_dir: TempDir, _test_runtime: TestRuntime, @@ -117,7 +118,7 @@ impl ApiTester { validator_store: Some(validator_store.clone()), graffiti_file: None, graffiti_flag: Some(Graffiti::default()), - spec: E::default_spec().into(), + spec: spec.clone(), config: HttpConfig { enabled: true, listen_addr: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), @@ -152,6 +153,7 @@ impl ApiTester { validator_store, url, slot_clock, + spec, _validator_dir: validator_dir, _secrets_dir: secrets_dir, _test_runtime: test_runtime, @@ -206,8 +208,7 @@ impl ApiTester { } pub async fn test_get_lighthouse_spec(self) -> Self { - let spec = E::default_spec(); - let result = if spec.fulu_fork_epoch.is_some() { + let result = if self.spec.is_fulu_scheduled() { self.client .get_lighthouse_spec::() .await @@ -219,7 +220,7 @@ impl ApiTester { .map(|res| ConfigAndPreset::Electra(res.data)) } .unwrap(); - let expected = ConfigAndPreset::from_chain_spec::(&spec, None); + let expected = ConfigAndPreset::from_chain_spec::(&self.spec); assert_eq!(result, expected); From ad99f18defe1556e4516352876b15201aec27608 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Fri, 25 Jul 2025 14:40:41 +1000 Subject: [PATCH 13/13] Fix merge snafus --- beacon_node/http_api/tests/tests.rs | 2 +- consensus/types/src/config_and_preset.rs | 12 +++++------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 71ffdb75e76..5ac8cd91864 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -2616,7 +2616,7 @@ impl ApiTester { } pub async fn test_get_config_spec(self) -> Self { - let result = if self.chain.spec.fulu_fork_epoch.is_some() { + let result = if self.chain.spec.is_fulu_scheduled() { self.client .get_config_spec::() .await diff --git a/consensus/types/src/config_and_preset.rs b/consensus/types/src/config_and_preset.rs index e64b5e3e1b5..cf5cff8ea67 100644 --- a/consensus/types/src/config_and_preset.rs +++ b/consensus/types/src/config_and_preset.rs @@ -52,11 +52,6 @@ impl ConfigAndPreset { let deneb_preset = DenebPreset::from_chain_spec::(spec); let extra_fields = get_extra_fields(spec); - // Remove blob schedule for backwards-compatibility. - if spec.is_fulu_scheduled() { - config.blob_schedule.set_skip_serializing(); - } - if spec.is_fulu_scheduled() { let electra_preset = ElectraPreset::from_chain_spec::(spec); let fulu_preset = FuluPreset::from_chain_spec::(spec); @@ -73,6 +68,9 @@ impl ConfigAndPreset { extra_fields, }) } else { + // Remove blob schedule for backwards-compatibility. + config.blob_schedule.set_skip_serializing(); + let electra_preset = ElectraPreset::from_chain_spec::(spec); ConfigAndPreset::Electra(ConfigAndPresetElectra { @@ -160,8 +158,8 @@ mod test { .write(false) .open(tmp_file.as_ref()) .expect("error while opening the file"); - let from: ConfigAndPresetElectra = + let from: ConfigAndPresetFulu = serde_yaml::from_reader(reader).expect("error while deserializing"); - assert_eq!(ConfigAndPreset::Electra(from), yamlconfig); + assert_eq!(ConfigAndPreset::Fulu(from), yamlconfig); } }