From 4e7c0d199d486fda3920b8181552177b4894d677 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Mon, 5 Feb 2024 19:53:19 +1100 Subject: [PATCH 01/76] 1D PeerDAS prototype: Data format and Distribution (#5050) * Build and publish column sidecars. Add stubs for gossip. * Add blob column subnets * Add `BlobColumnSubnetId` and initial compute subnet logic. * Subscribe to blob column subnets. * Introduce `BLOB_COLUMN_SUBNET_COUNT` based on DAS configuration parameter changes. * Fix column sidecar type to use `VariableList` for data. * Fix lint errors. * Update types and naming to latest consensus-spec #3574. * Fix test and some cleanups. --- beacon_node/beacon_chain/src/beacon_chain.rs | 31 ++- .../beacon_chain/src/blob_verification.rs | 41 +++- beacon_node/beacon_chain/src/errors.rs | 1 + beacon_node/beacon_chain/src/metrics.rs | 12 + beacon_node/beacon_processor/src/lib.rs | 27 ++- beacon_node/beacon_processor/src/metrics.rs | 5 + beacon_node/http_api/src/publish_blocks.rs | 24 +- .../lighthouse_network/src/discovery/mod.rs | 3 + .../src/discovery/subnet_predicate.rs | 2 + .../src/peer_manager/mod.rs | 4 + .../src/peer_manager/peerdb/peer_info.rs | 2 + .../src/service/gossip_cache.rs | 1 + .../lighthouse_network/src/service/mod.rs | 2 + .../lighthouse_network/src/service/utils.rs | 8 +- .../lighthouse_network/src/types/pubsub.rs | 48 +++- .../lighthouse_network/src/types/subnet.rs | 4 +- .../lighthouse_network/src/types/topics.rs | 17 +- beacon_node/network/src/metrics.rs | 23 ++ .../gossip_methods.rs | 89 +++++++- .../src/network_beacon_processor/mod.rs | 30 +++ beacon_node/network/src/router.rs | 14 ++ beacon_node/network/src/service.rs | 44 +++- consensus/types/presets/gnosis/deneb.yaml | 9 + consensus/types/presets/mainnet/deneb.yaml | 9 + consensus/types/presets/minimal/deneb.yaml | 9 + consensus/types/src/chain_spec.rs | 28 ++- consensus/types/src/data_column_sidecar.rs | 216 ++++++++++++++++++ consensus/types/src/data_column_subnet_id.rs | 170 ++++++++++++++ consensus/types/src/eth_spec.rs | 50 ++++ consensus/types/src/lib.rs | 4 + consensus/types/src/preset.rs | 11 + 31 files changed, 912 insertions(+), 26 deletions(-) create mode 100644 consensus/types/src/data_column_sidecar.rs create mode 100644 consensus/types/src/data_column_subnet_id.rs diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index fcd7be791da..51bc34bffac 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -7,7 +7,9 @@ use crate::attester_cache::{AttesterCache, AttesterCacheKey}; use crate::beacon_block_streamer::{BeaconBlockStreamer, CheckEarlyAttesterCache}; use crate::beacon_proposer_cache::compute_proposer_duties_from_head; use crate::beacon_proposer_cache::BeaconProposerCache; -use crate::blob_verification::{GossipBlobError, GossipVerifiedBlob}; +use crate::blob_verification::{ + GossipBlobError, GossipVerifiedBlob, GossipVerifiedDataColumnSidecar, +}; use crate::block_times_cache::BlockTimesCache; use crate::block_verification::POS_PANDA_BANNER; use crate::block_verification::{ @@ -2070,6 +2072,19 @@ impl BeaconChain { }) } + pub fn verify_data_column_sidecar_for_gossip( + self: &Arc, + data_column_sidecar: Arc>, + subnet_id: u64, + ) -> Result, GossipBlobError> { + metrics::inc_counter(&metrics::BLOBS_COLUMN_SIDECAR_PROCESSING_REQUESTS); + let _timer = metrics::start_timer(&metrics::DATA_COLUMN_SIDECAR_GOSSIP_VERIFICATION_TIMES); + GossipVerifiedDataColumnSidecar::new(data_column_sidecar, subnet_id, self).map(|v| { + metrics::inc_counter(&metrics::DATA_COLUMNS_SIDECAR_PROCESSING_SUCCESSES); + v + }) + } + pub fn verify_blob_sidecar_for_gossip( self: &Arc, blob_sidecar: Arc>, @@ -2885,6 +2900,20 @@ impl BeaconChain { self.remove_notified(&block_root, r) } + pub fn process_gossip_data_column( + self: &Arc, + gossip_verified_data_column: GossipVerifiedDataColumnSidecar, + ) { + let data_column = gossip_verified_data_column.as_data_column(); + // TODO(das) send to DA checker + info!( + self.log, + "Processed gossip data column"; + "index" => data_column.index, + "slot" => data_column.slot().as_u64() + ); + } + /// Cache the blobs in the processing cache, process it, then evict it from the cache if it was /// imported or errors. pub async fn process_rpc_blobs( diff --git a/beacon_node/beacon_chain/src/blob_verification.rs b/beacon_node/beacon_chain/src/blob_verification.rs index f2d150d72bf..ffc64f9d10e 100644 --- a/beacon_node/beacon_chain/src/blob_verification.rs +++ b/beacon_node/beacon_chain/src/blob_verification.rs @@ -17,7 +17,8 @@ use ssz_types::VariableList; use tree_hash::TreeHash; use types::blob_sidecar::BlobIdentifier; use types::{ - BeaconStateError, BlobSidecar, CloneConfig, EthSpec, Hash256, SignedBeaconBlockHeader, Slot, + BeaconStateError, BlobSidecar, CloneConfig, DataColumnSidecar, EthSpec, Hash256, + SignedBeaconBlockHeader, Slot, }; /// An error occurred while validating a gossip blob. @@ -184,6 +185,33 @@ pub type GossipVerifiedBlobList = VariableList< <::EthSpec as EthSpec>::MaxBlobsPerBlock, >; +#[derive(Debug)] +pub struct GossipVerifiedDataColumnSidecar { + data_column_sidecar: Arc>, +} + +impl GossipVerifiedDataColumnSidecar { + pub fn new( + column_sidecar: Arc>, + subnet_id: u64, + chain: &BeaconChain, + ) -> Result> { + let header = column_sidecar.signed_block_header.clone(); + // We only process slashing info if the gossip verification failed + // since we do not process the blob any further in that case. + validate_data_column_sidecar_for_gossip(column_sidecar, subnet_id, chain).map_err(|e| { + process_block_slash_info::<_, GossipBlobError>( + chain, + BlockSlashInfo::from_early_error_blob(header, e), + ) + }) + } + + pub fn as_data_column(&self) -> &Arc> { + &self.data_column_sidecar + } +} + /// A wrapper around a `BlobSidecar` that indicates it has been approved for re-gossiping on /// the p2p network. #[derive(Debug)] @@ -647,6 +675,17 @@ pub fn validate_blob_sidecar_for_gossip( }) } +pub fn validate_data_column_sidecar_for_gossip( + data_column_sidecar: Arc>, + _subnet: u64, + _chain: &BeaconChain, +) -> Result, GossipBlobError> { + // TODO(das): validate kzg commitments, cell proofs etc + Ok(GossipVerifiedDataColumnSidecar { + data_column_sidecar: data_column_sidecar.clone(), + }) +} + /// Returns the canonical root of the given `blob`. /// /// Use this function to ensure that we report the blob hashing time Prometheus metric. diff --git a/beacon_node/beacon_chain/src/errors.rs b/beacon_node/beacon_chain/src/errors.rs index 9c1ba06f853..010488a558b 100644 --- a/beacon_node/beacon_chain/src/errors.rs +++ b/beacon_node/beacon_chain/src/errors.rs @@ -220,6 +220,7 @@ pub enum BeaconChainError { InconsistentFork(InconsistentFork), ProposerHeadForkChoiceError(fork_choice::Error), UnableToPublish, + UnableToBuildColumnSidecar(String), AvailabilityCheckError(AvailabilityCheckError), LightClientError(LightClientError), UnsupportedFork, diff --git a/beacon_node/beacon_chain/src/metrics.rs b/beacon_node/beacon_chain/src/metrics.rs index ad095b37b51..4a2187be610 100644 --- a/beacon_node/beacon_chain/src/metrics.rs +++ b/beacon_node/beacon_chain/src/metrics.rs @@ -1014,14 +1014,26 @@ lazy_static! { "beacon_blobs_sidecar_processing_requests_total", "Count of all blob sidecars submitted for processing" ); + pub static ref BLOBS_COLUMN_SIDECAR_PROCESSING_REQUESTS: Result = try_create_int_counter( + "beacon_blobs_column_sidecar_processing_requests_total", + "Count of all data column sidecars submitted for processing" + ); pub static ref BLOBS_SIDECAR_PROCESSING_SUCCESSES: Result = try_create_int_counter( "beacon_blobs_sidecar_processing_successes_total", "Number of blob sidecars verified for gossip" ); + pub static ref DATA_COLUMNS_SIDECAR_PROCESSING_SUCCESSES: Result = try_create_int_counter( + "beacon_blobs_column_sidecar_processing_successes_total", + "Number of data column sidecars verified for gossip" + ); pub static ref BLOBS_SIDECAR_GOSSIP_VERIFICATION_TIMES: Result = try_create_histogram( "beacon_blobs_sidecar_gossip_verification_seconds", "Full runtime of blob sidecars gossip verification" ); + pub static ref DATA_COLUMN_SIDECAR_GOSSIP_VERIFICATION_TIMES: Result = try_create_histogram( + "beacon_blobs_column_sidecar_gossip_verification_seconds", + "Full runtime of data column sidecars gossip verification" + ); pub static ref BLOB_SIDECAR_INCLUSION_PROOF_VERIFICATION: Result = try_create_histogram( "blob_sidecar_inclusion_proof_verification_seconds", "Time taken to verify blob sidecar inclusion proof" diff --git a/beacon_node/beacon_processor/src/lib.rs b/beacon_node/beacon_processor/src/lib.rs index 045b06a1e72..8b1c127d300 100644 --- a/beacon_node/beacon_processor/src/lib.rs +++ b/beacon_node/beacon_processor/src/lib.rs @@ -109,6 +109,10 @@ const MAX_GOSSIP_BLOCK_QUEUE_LEN: usize = 1_024; /// will be stored before we start dropping them. const MAX_GOSSIP_BLOB_QUEUE_LEN: usize = 1_024; +/// The maximum number of queued `DataColumnSidecar` objects received on gossip that +/// will be stored before we start dropping them. +const MAX_GOSSIP_DATA_COL_QUEUE_LEN: usize = 1_024; + /// The maximum number of queued `SignedBeaconBlock` objects received prior to their slot (but /// within acceptable clock disparity) that will be queued before we start dropping them. const MAX_DELAYED_BLOCK_QUEUE_LEN: usize = 1_024; @@ -224,6 +228,7 @@ pub const GOSSIP_AGGREGATE: &str = "gossip_aggregate"; pub const GOSSIP_AGGREGATE_BATCH: &str = "gossip_aggregate_batch"; pub const GOSSIP_BLOCK: &str = "gossip_block"; pub const GOSSIP_BLOBS_SIDECAR: &str = "gossip_blobs_sidecar"; +pub const GOSSIP_BLOBS_COLUMN_SIDECAR: &str = "gossip_blobs_column_sidecar"; pub const DELAYED_IMPORT_BLOCK: &str = "delayed_import_block"; pub const GOSSIP_VOLUNTARY_EXIT: &str = "gossip_voluntary_exit"; pub const GOSSIP_PROPOSER_SLASHING: &str = "gossip_proposer_slashing"; @@ -590,6 +595,7 @@ pub enum Work { }, GossipBlock(AsyncFn), GossipBlobSidecar(AsyncFn), + GossipDataColumnSidecar(AsyncFn), DelayedImportBlock { beacon_block_slot: Slot, beacon_block_root: Hash256, @@ -640,6 +646,7 @@ impl Work { Work::GossipAggregateBatch { .. } => GOSSIP_AGGREGATE_BATCH, Work::GossipBlock(_) => GOSSIP_BLOCK, Work::GossipBlobSidecar(_) => GOSSIP_BLOBS_SIDECAR, + Work::GossipDataColumnSidecar(_) => GOSSIP_BLOBS_COLUMN_SIDECAR, Work::DelayedImportBlock { .. } => DELAYED_IMPORT_BLOCK, Work::GossipVoluntaryExit(_) => GOSSIP_VOLUNTARY_EXIT, Work::GossipProposerSlashing(_) => GOSSIP_PROPOSER_SLASHING, @@ -809,6 +816,7 @@ impl BeaconProcessor { let mut backfill_chain_segment = FifoQueue::new(MAX_CHAIN_SEGMENT_QUEUE_LEN); let mut gossip_block_queue = FifoQueue::new(MAX_GOSSIP_BLOCK_QUEUE_LEN); let mut gossip_blob_queue = FifoQueue::new(MAX_GOSSIP_BLOB_QUEUE_LEN); + let mut gossip_data_column_queue = FifoQueue::new(MAX_GOSSIP_DATA_COL_QUEUE_LEN); let mut delayed_block_queue = FifoQueue::new(MAX_DELAYED_BLOCK_QUEUE_LEN); let mut status_queue = FifoQueue::new(MAX_STATUS_QUEUE_LEN); @@ -964,6 +972,8 @@ impl BeaconProcessor { self.spawn_worker(item, idle_tx); } else if let Some(item) = gossip_blob_queue.pop() { self.spawn_worker(item, idle_tx); + } else if let Some(item) = gossip_data_column_queue.pop() { + self.spawn_worker(item, idle_tx); // Check the priority 0 API requests after blocks and blobs, but before attestations. } else if let Some(item) = api_request_p0_queue.pop() { self.spawn_worker(item, idle_tx); @@ -1206,6 +1216,9 @@ impl BeaconProcessor { Work::GossipBlobSidecar { .. } => { gossip_blob_queue.push(work, work_id, &self.log) } + Work::GossipDataColumnSidecar { .. } => { + gossip_data_column_queue.push(work, work_id, &self.log) + } Work::DelayedImportBlock { .. } => { delayed_block_queue.push(work, work_id, &self.log) } @@ -1304,6 +1317,10 @@ impl BeaconProcessor { &metrics::BEACON_PROCESSOR_GOSSIP_BLOB_QUEUE_TOTAL, gossip_blob_queue.len() as i64, ); + metrics::set_gauge( + &metrics::BEACON_PROCESSOR_GOSSIP_DATA_COLUMN_QUEUE_TOTAL, + gossip_data_column_queue.len() as i64, + ); metrics::set_gauge( &metrics::BEACON_PROCESSOR_RPC_BLOCK_QUEUE_TOTAL, rpc_block_queue.len() as i64, @@ -1455,11 +1472,11 @@ impl BeaconProcessor { task_spawner.spawn_async(process_fn) } Work::IgnoredRpcBlock { process_fn } => task_spawner.spawn_blocking(process_fn), - Work::GossipBlock(work) | Work::GossipBlobSidecar(work) => { - task_spawner.spawn_async(async move { - work.await; - }) - } + Work::GossipBlock(work) + | Work::GossipBlobSidecar(work) + | Work::GossipDataColumnSidecar(work) => task_spawner.spawn_async(async move { + work.await; + }), Work::BlobsByRangeRequest(process_fn) | Work::BlobsByRootsRequest(process_fn) => { task_spawner.spawn_blocking(process_fn) } diff --git a/beacon_node/beacon_processor/src/metrics.rs b/beacon_node/beacon_processor/src/metrics.rs index fa7d7d7b9a3..bcd422b357d 100644 --- a/beacon_node/beacon_processor/src/metrics.rs +++ b/beacon_node/beacon_processor/src/metrics.rs @@ -51,6 +51,11 @@ lazy_static::lazy_static! { "beacon_processor_gossip_blob_queue_total", "Count of blobs from gossip waiting to be verified." ); + // Gossip data column sidecars. + pub static ref BEACON_PROCESSOR_GOSSIP_DATA_COLUMN_QUEUE_TOTAL: Result = try_create_int_gauge( + "beacon_processor_gossip_data_column_queue_total", + "Count of data column sidecars from gossip waiting to be verified." + ); // Gossip Exits. pub static ref BEACON_PROCESSOR_EXIT_QUEUE_TOTAL: Result = try_create_int_gauge( "beacon_processor_exit_queue_total", diff --git a/beacon_node/http_api/src/publish_blocks.rs b/beacon_node/http_api/src/publish_blocks.rs index 8b85c2ac951..67e5d00f8c6 100644 --- a/beacon_node/http_api/src/publish_blocks.rs +++ b/beacon_node/http_api/src/publish_blocks.rs @@ -19,9 +19,9 @@ use std::time::Duration; use tokio::sync::mpsc::UnboundedSender; use tree_hash::TreeHash; use types::{ - AbstractExecPayload, BeaconBlockRef, BlobSidecarList, EthSpec, ExecPayload, ExecutionBlockHash, - ForkName, FullPayload, FullPayloadMerge, Hash256, SignedBeaconBlock, SignedBlindedBeaconBlock, - VariableList, + AbstractExecPayload, BeaconBlockRef, BlobSidecarList, DataColumnSidecar, DataColumnSubnetId, + EthSpec, ExecPayload, ExecutionBlockHash, ForkName, FullPayload, FullPayloadMerge, Hash256, + SignedBeaconBlock, SignedBlindedBeaconBlock, VariableList, }; use warp::http::StatusCode; use warp::{reply::Response, Rejection, Reply}; @@ -88,6 +88,24 @@ pub async fn publish_block { let mut pubsub_messages = vec![PubsubMessage::BeaconBlock(block.clone())]; if let Some(blob_sidecars) = blobs_opt { + // Build and publish column sidecars + let col_sidecars = DataColumnSidecar::random_from_blob_sidecars(&blob_sidecars) + .map_err(|e| { + BeaconChainError::UnableToBuildColumnSidecar(format!("{e:?}")) + })?; + + for (col_index, col_sidecar) in col_sidecars.into_iter().enumerate() { + let subnet_id = + DataColumnSubnetId::try_from_column_index::(col_index) + .map_err(|e| { + BeaconChainError::UnableToBuildColumnSidecar(format!("{e:?}")) + })?; + pubsub_messages.push(PubsubMessage::DataColumnSidecar(Box::new(( + subnet_id, + Arc::new(col_sidecar), + )))); + } + // Publish blob sidecars for (blob_index, blob) in blob_sidecars.into_iter().enumerate() { pubsub_messages.push(PubsubMessage::BlobSidecar(Box::new(( blob_index as u64, diff --git a/beacon_node/lighthouse_network/src/discovery/mod.rs b/beacon_node/lighthouse_network/src/discovery/mod.rs index 829124e1233..ebb2bfcccfb 100644 --- a/beacon_node/lighthouse_network/src/discovery/mod.rs +++ b/beacon_node/lighthouse_network/src/discovery/mod.rs @@ -513,6 +513,8 @@ impl Discovery { ) .map_err(|e| format!("{:?}", e))?; } + // TODO(das) discovery to be implemented at a later phase. Initially we just use a large peer count. + Subnet::DataColumn(_) => return Ok(()), } // replace the global version @@ -832,6 +834,7 @@ impl Discovery { let query_str = match query.subnet { Subnet::Attestation(_) => "attestation", Subnet::SyncCommittee(_) => "sync_committee", + Subnet::DataColumn(_) => "data_column", }; if let Some(v) = metrics::get_int_counter( diff --git a/beacon_node/lighthouse_network/src/discovery/subnet_predicate.rs b/beacon_node/lighthouse_network/src/discovery/subnet_predicate.rs index f79ff8daf69..0b35465233a 100644 --- a/beacon_node/lighthouse_network/src/discovery/subnet_predicate.rs +++ b/beacon_node/lighthouse_network/src/discovery/subnet_predicate.rs @@ -33,6 +33,8 @@ where Subnet::SyncCommittee(s) => sync_committee_bitfield .as_ref() .map_or(false, |b| b.get(*s.deref() as usize).unwrap_or(false)), + // TODO(das) discovery to be implemented at a later phase. Initially we just use a large peer count. + Subnet::DataColumn(_) => false, }); if !predicate { diff --git a/beacon_node/lighthouse_network/src/peer_manager/mod.rs b/beacon_node/lighthouse_network/src/peer_manager/mod.rs index 4316c0d07e1..76fe7a7d7b3 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/mod.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/mod.rs @@ -1073,6 +1073,10 @@ impl PeerManager { .or_default() .insert(id); } + // TODO(das) to be implemented. We're not pruning data column peers yet + // because data column topics are subscribed as core topics until we + // implement recomputing data column subnets. + Subnet::DataColumn(_) => {} } } } diff --git a/beacon_node/lighthouse_network/src/peer_manager/peerdb/peer_info.rs b/beacon_node/lighthouse_network/src/peer_manager/peerdb/peer_info.rs index 44c54511ddc..6e0c00e42b8 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/peerdb/peer_info.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/peerdb/peer_info.rs @@ -94,6 +94,8 @@ impl PeerInfo { .syncnets() .map_or(false, |s| s.get(**id as usize).unwrap_or(false)) } + // TODO(das) Add data column nets bitfield + Subnet::DataColumn(_) => return false, } } false diff --git a/beacon_node/lighthouse_network/src/service/gossip_cache.rs b/beacon_node/lighthouse_network/src/service/gossip_cache.rs index 5dc0d29ff5b..d0927283da7 100644 --- a/beacon_node/lighthouse_network/src/service/gossip_cache.rs +++ b/beacon_node/lighthouse_network/src/service/gossip_cache.rs @@ -194,6 +194,7 @@ impl GossipCache { let expire_timeout = match topic.kind() { GossipKind::BeaconBlock => self.beacon_block, GossipKind::BlobSidecar(_) => self.blob_sidecar, + GossipKind::DataColumnSidecar(_) => self.blob_sidecar, GossipKind::BeaconAggregateAndProof => self.aggregates, GossipKind::Attestation(_) => self.attestation, GossipKind::VoluntaryExit => self.voluntary_exit, diff --git a/beacon_node/lighthouse_network/src/service/mod.rs b/beacon_node/lighthouse_network/src/service/mod.rs index 2b20c76cf4b..545e58d28ed 100644 --- a/beacon_node/lighthouse_network/src/service/mod.rs +++ b/beacon_node/lighthouse_network/src/service/mod.rs @@ -226,6 +226,7 @@ impl Network { let max_topics = ctx.chain_spec.attestation_subnet_count as usize + SYNC_COMMITTEE_SUBNET_COUNT as usize + ctx.chain_spec.blob_sidecar_subnet_count as usize + + ctx.chain_spec.data_column_sidecar_subnet_count as usize + BASE_CORE_TOPICS.len() + ALTAIR_CORE_TOPICS.len() + CAPELLA_CORE_TOPICS.len() @@ -239,6 +240,7 @@ impl Network { ctx.chain_spec.attestation_subnet_count, SYNC_COMMITTEE_SUBNET_COUNT, ctx.chain_spec.blob_sidecar_subnet_count, + ctx.chain_spec.data_column_sidecar_subnet_count, ), // during a fork we subscribe to both the old and new topics max_subscribed_topics: max_topics * 4, diff --git a/beacon_node/lighthouse_network/src/service/utils.rs b/beacon_node/lighthouse_network/src/service/utils.rs index 34dec1ca6c0..217469eac8a 100644 --- a/beacon_node/lighthouse_network/src/service/utils.rs +++ b/beacon_node/lighthouse_network/src/service/utils.rs @@ -20,7 +20,9 @@ use std::io::prelude::*; use std::path::Path; use std::sync::Arc; use std::time::Duration; -use types::{ChainSpec, EnrForkId, EthSpec, ForkContext, SubnetId, SyncSubnetId}; +use types::{ + ChainSpec, DataColumnSubnetId, EnrForkId, EthSpec, ForkContext, SubnetId, SyncSubnetId, +}; pub const NETWORK_KEY_FILENAME: &str = "key"; /// The maximum simultaneous libp2p connections per peer. @@ -232,6 +234,7 @@ pub(crate) fn create_whitelist_filter( attestation_subnet_count: u64, sync_committee_subnet_count: u64, blob_sidecar_subnet_count: u64, + data_column_subnet_count: u64, ) -> gossipsub::WhitelistSubscriptionFilter { let mut possible_hashes = HashSet::new(); for fork_digest in possible_fork_digests { @@ -260,6 +263,9 @@ pub(crate) fn create_whitelist_filter( for id in 0..blob_sidecar_subnet_count { add(BlobSidecar(id)); } + for id in 0..data_column_subnet_count { + add(DataColumnSidecar(DataColumnSubnetId::new(id))); + } } gossipsub::WhitelistSubscriptionFilter(possible_hashes) } diff --git a/beacon_node/lighthouse_network/src/types/pubsub.rs b/beacon_node/lighthouse_network/src/types/pubsub.rs index 60fe3748265..e9bccf59617 100644 --- a/beacon_node/lighthouse_network/src/types/pubsub.rs +++ b/beacon_node/lighthouse_network/src/types/pubsub.rs @@ -9,12 +9,12 @@ use std::boxed::Box; use std::io::{Error, ErrorKind}; use std::sync::Arc; use types::{ - Attestation, AttesterSlashing, BlobSidecar, EthSpec, ForkContext, ForkName, - LightClientFinalityUpdate, LightClientOptimisticUpdate, ProposerSlashing, - SignedAggregateAndProof, SignedBeaconBlock, SignedBeaconBlockAltair, SignedBeaconBlockBase, - SignedBeaconBlockCapella, SignedBeaconBlockDeneb, SignedBeaconBlockMerge, - SignedBlsToExecutionChange, SignedContributionAndProof, SignedVoluntaryExit, SubnetId, - SyncCommitteeMessage, SyncSubnetId, + Attestation, AttesterSlashing, BlobSidecar, DataColumnSidecar, DataColumnSubnetId, EthSpec, + ForkContext, ForkName, LightClientFinalityUpdate, LightClientOptimisticUpdate, + ProposerSlashing, SignedAggregateAndProof, SignedBeaconBlock, SignedBeaconBlockAltair, + SignedBeaconBlockBase, SignedBeaconBlockCapella, SignedBeaconBlockDeneb, + SignedBeaconBlockMerge, SignedBlsToExecutionChange, SignedContributionAndProof, + SignedVoluntaryExit, SubnetId, SyncCommitteeMessage, SyncSubnetId, }; #[derive(Debug, Clone, PartialEq)] @@ -23,6 +23,8 @@ pub enum PubsubMessage { BeaconBlock(Arc>), /// Gossipsub message providing notification of a [`BlobSidecar`] along with the subnet id where it was received. BlobSidecar(Box<(u64, Arc>)>), + /// Gossipsub message providing notification of a [`DataColumnSidecar`] along with the subnet id where it was received. + DataColumnSidecar(Box<(DataColumnSubnetId, Arc>)>), /// Gossipsub message providing notification of a Aggregate attestation and associated proof. AggregateAndProofAttestation(Box>), /// Gossipsub message providing notification of a raw un-aggregated attestation with its shard id. @@ -119,6 +121,9 @@ impl PubsubMessage { PubsubMessage::BlobSidecar(blob_sidecar_data) => { GossipKind::BlobSidecar(blob_sidecar_data.0) } + PubsubMessage::DataColumnSidecar(column_sidecar_data) => { + GossipKind::DataColumnSidecar(column_sidecar_data.0) + } PubsubMessage::AggregateAndProofAttestation(_) => GossipKind::BeaconAggregateAndProof, PubsubMessage::Attestation(attestation_data) => { GossipKind::Attestation(attestation_data.0) @@ -226,6 +231,30 @@ impl PubsubMessage { )), } } + GossipKind::DataColumnSidecar(subnet_id) => { + match fork_context.from_context_bytes(gossip_topic.fork_digest) { + Some(ForkName::Deneb) => { + let col_sidecar = Arc::new( + DataColumnSidecar::from_ssz_bytes(data) + .map_err(|e| format!("{:?}", e))?, + ); + Ok(PubsubMessage::DataColumnSidecar(Box::new(( + *subnet_id, + col_sidecar, + )))) + } + Some( + ForkName::Base + | ForkName::Altair + | ForkName::Merge + | ForkName::Capella, + ) + | None => Err(format!( + "data_column_sidecar topic invalid for given fork digest {:?}", + gossip_topic.fork_digest + )), + } + } GossipKind::VoluntaryExit => { let voluntary_exit = SignedVoluntaryExit::from_ssz_bytes(data) .map_err(|e| format!("{:?}", e))?; @@ -295,6 +324,7 @@ impl PubsubMessage { match &self { PubsubMessage::BeaconBlock(data) => data.as_ssz_bytes(), PubsubMessage::BlobSidecar(data) => data.1.as_ssz_bytes(), + PubsubMessage::DataColumnSidecar(data) => data.1.as_ssz_bytes(), PubsubMessage::AggregateAndProofAttestation(data) => data.as_ssz_bytes(), PubsubMessage::VoluntaryExit(data) => data.as_ssz_bytes(), PubsubMessage::ProposerSlashing(data) => data.as_ssz_bytes(), @@ -324,6 +354,12 @@ impl std::fmt::Display for PubsubMessage { data.1.slot(), data.1.index, ), + PubsubMessage::DataColumnSidecar(data) => write!( + f, + "DataColumnSidecar: slot: {}, column index: {}", + data.1.slot(), + data.1.index, + ), PubsubMessage::AggregateAndProofAttestation(att) => write!( f, "Aggregate and Proof: slot: {}, index: {}, aggregator_index: {}", diff --git a/beacon_node/lighthouse_network/src/types/subnet.rs b/beacon_node/lighthouse_network/src/types/subnet.rs index 50d28542bec..e814feefc70 100644 --- a/beacon_node/lighthouse_network/src/types/subnet.rs +++ b/beacon_node/lighthouse_network/src/types/subnet.rs @@ -1,6 +1,6 @@ use serde::Serialize; use std::time::Instant; -use types::{SubnetId, SyncSubnetId}; +use types::{DataColumnSubnetId, SubnetId, SyncSubnetId}; /// Represents a subnet on an attestation or sync committee `SubnetId`. /// @@ -12,6 +12,8 @@ pub enum Subnet { Attestation(SubnetId), /// Represents a gossipsub sync committee subnet and the metadata `syncnets` field. SyncCommittee(SyncSubnetId), + /// Represents a gossipsub data column subnet and the metadata `blbcolnets` field. + DataColumn(DataColumnSubnetId), } /// A subnet to discover peers on along with the instant after which it's no longer useful. diff --git a/beacon_node/lighthouse_network/src/types/topics.rs b/beacon_node/lighthouse_network/src/types/topics.rs index b774905174f..dc1479e9f6a 100644 --- a/beacon_node/lighthouse_network/src/types/topics.rs +++ b/beacon_node/lighthouse_network/src/types/topics.rs @@ -1,7 +1,7 @@ use libp2p::gossipsub::{IdentTopic as Topic, TopicHash}; use serde::{Deserialize, Serialize}; use strum::AsRefStr; -use types::{ChainSpec, EthSpec, ForkName, SubnetId, SyncSubnetId}; +use types::{ChainSpec, DataColumnSubnetId, EthSpec, ForkName, SubnetId, SyncSubnetId}; use crate::Subnet; @@ -14,6 +14,7 @@ pub const BEACON_BLOCK_TOPIC: &str = "beacon_block"; pub const BEACON_AGGREGATE_AND_PROOF_TOPIC: &str = "beacon_aggregate_and_proof"; pub const BEACON_ATTESTATION_PREFIX: &str = "beacon_attestation_"; pub const BLOB_SIDECAR_PREFIX: &str = "blob_sidecar_"; +pub const DATA_COLUMN_SIDECAR_PREFIX: &str = "data_column_sidecar_"; pub const VOLUNTARY_EXIT_TOPIC: &str = "voluntary_exit"; pub const PROPOSER_SLASHING_TOPIC: &str = "proposer_slashing"; pub const ATTESTER_SLASHING_TOPIC: &str = "attester_slashing"; @@ -100,6 +101,8 @@ pub enum GossipKind { BeaconAggregateAndProof, /// Topic for publishing BlobSidecars. BlobSidecar(u64), + /// Topic for publishing DataColumnSidecars. + DataColumnSidecar(DataColumnSubnetId), /// Topic for publishing raw attestations on a particular subnet. #[strum(serialize = "beacon_attestation")] Attestation(SubnetId), @@ -132,6 +135,9 @@ impl std::fmt::Display for GossipKind { GossipKind::BlobSidecar(blob_index) => { write!(f, "{}{}", BLOB_SIDECAR_PREFIX, blob_index) } + GossipKind::DataColumnSidecar(column_index) => { + write!(f, "{}{}", DATA_COLUMN_SIDECAR_PREFIX, **column_index) + } x => f.write_str(x.as_ref()), } } @@ -219,6 +225,7 @@ impl GossipTopic { match self.kind() { GossipKind::Attestation(subnet_id) => Some(Subnet::Attestation(*subnet_id)), GossipKind::SyncCommitteeMessage(subnet_id) => Some(Subnet::SyncCommittee(*subnet_id)), + GossipKind::DataColumnSidecar(subnet_id) => Some(Subnet::DataColumn(*subnet_id)), _ => None, } } @@ -257,6 +264,9 @@ impl std::fmt::Display for GossipTopic { GossipKind::BlobSidecar(blob_index) => { format!("{}{}", BLOB_SIDECAR_PREFIX, blob_index) } + GossipKind::DataColumnSidecar(index) => { + format!("{}{}", DATA_COLUMN_SIDECAR_PREFIX, *index) + } GossipKind::BlsToExecutionChange => BLS_TO_EXECUTION_CHANGE_TOPIC.into(), GossipKind::LightClientFinalityUpdate => LIGHT_CLIENT_FINALITY_UPDATE.into(), GossipKind::LightClientOptimisticUpdate => LIGHT_CLIENT_OPTIMISTIC_UPDATE.into(), @@ -277,6 +287,7 @@ impl From for GossipKind { match subnet_id { Subnet::Attestation(s) => GossipKind::Attestation(s), Subnet::SyncCommittee(s) => GossipKind::SyncCommitteeMessage(s), + Subnet::DataColumn(s) => GossipKind::DataColumnSidecar(s), } } } @@ -300,6 +311,10 @@ fn subnet_topic_index(topic: &str) -> Option { ))); } else if let Some(index) = topic.strip_prefix(BLOB_SIDECAR_PREFIX) { return Some(GossipKind::BlobSidecar(index.parse::().ok()?)); + } else if let Some(index) = topic.strip_prefix(DATA_COLUMN_SIDECAR_PREFIX) { + return Some(GossipKind::DataColumnSidecar(DataColumnSubnetId::new( + index.parse::().ok()?, + ))); } None } diff --git a/beacon_node/network/src/metrics.rs b/beacon_node/network/src/metrics.rs index 0509ed1ea7d..11e02f5f3e7 100644 --- a/beacon_node/network/src/metrics.rs +++ b/beacon_node/network/src/metrics.rs @@ -71,6 +71,10 @@ lazy_static! { "beacon_processor_gossip_blob_verified_total", "Total number of gossip blob verified for propagation." ); + pub static ref BEACON_PROCESSOR_GOSSIP_DATA_COLUMN_SIDECAR_VERIFIED_TOTAL: Result = try_create_int_counter( + "beacon_processor_gossip_data_column_verified_total", + "Total number of gossip data column sidecar verified for propagation." + ); // Gossip Exits. pub static ref BEACON_PROCESSOR_EXIT_VERIFIED_TOTAL: Result = try_create_int_counter( "beacon_processor_exit_verified_total", @@ -283,6 +287,12 @@ lazy_static! { // [0.001, 0.002, 0.005, 0.01, 0.02, 0.05, 0.1, 0.2, 0.5] decimal_buckets(-3,-1) ); + pub static ref BEACON_DATA_COLUMN_GOSSIP_PROPAGATION_VERIFICATION_DELAY_TIME: Result = try_create_histogram_with_buckets( + "beacon_data_column_gossip_propagation_verification_delay_time", + "Duration between when the data column sidecar is received over gossip and when it is verified for propagation.", + // [0.001, 0.002, 0.005, 0.01, 0.02, 0.05, 0.1, 0.2, 0.5] + decimal_buckets(-3,-1) + ); pub static ref BEACON_BLOB_GOSSIP_SLOT_START_DELAY_TIME: Result = try_create_histogram_with_buckets( "beacon_blob_gossip_slot_start_delay_time", "Duration between when the blob is received over gossip and the start of the slot it belongs to.", @@ -292,6 +302,15 @@ lazy_static! { // [0.1, 0.2, 0.5, 1, 2, 5, 10, 20, 50] //decimal_buckets(-1,2) ); + pub static ref BEACON_DATA_COLUMN_GOSSIP_SLOT_START_DELAY_TIME: Result = try_create_histogram_with_buckets( + "beacon_data_column_gossip_slot_start_delay_time", + "Duration between when the data column sidecar is received over gossip and the start of the slot it belongs to.", + // Create a custom bucket list for greater granularity in block delay + Ok(vec![0.1, 0.2, 0.3,0.4,0.5,0.75,1.0,1.25,1.5,1.75,2.0,2.5,3.0,3.5,4.0,5.0,6.0,7.0,8.0,9.0,10.0,15.0,20.0]) + // NOTE: Previous values, which we may want to switch back to. + // [0.1, 0.2, 0.5, 1, 2, 5, 10, 20, 50] + //decimal_buckets(-1,2) + ); pub static ref BEACON_BLOB_RPC_SLOT_START_DELAY_TIME: Result = try_create_histogram_with_buckets( "beacon_blob_rpc_slot_start_delay_time", "Duration between when a blob is received over rpc and the start of the slot it belongs to.", @@ -306,6 +325,10 @@ lazy_static! { "beacon_blob_last_delay", "Keeps track of the last blob's delay from the start of the slot" ); + pub static ref BEACON_DATA_COLUMN_LAST_DELAY: Result = try_create_int_gauge( + "beacon_data_column_last_delay", + "Keeps track of the last data column sidecar's delay from the start of the slot" + ); pub static ref BEACON_BLOB_GOSSIP_ARRIVED_LATE_TOTAL: Result = try_create_int_counter( "beacon_blob_gossip_arrived_late_total", 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 9d9b196e9be..2362b3d672f 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -4,7 +4,9 @@ use crate::{ service::NetworkMessage, sync::SyncMessage, }; -use beacon_chain::blob_verification::{GossipBlobError, GossipVerifiedBlob}; +use beacon_chain::blob_verification::{ + GossipBlobError, GossipVerifiedBlob, GossipVerifiedDataColumnSidecar, +}; use beacon_chain::block_verification_types::AsBlock; use beacon_chain::store::Error; use beacon_chain::{ @@ -31,9 +33,9 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH}; use store::hot_cold_store::HotColdDBError; use tokio::sync::mpsc; use types::{ - Attestation, AttesterSlashing, BlobSidecar, EthSpec, Hash256, IndexedAttestation, - LightClientFinalityUpdate, LightClientOptimisticUpdate, ProposerSlashing, - SignedAggregateAndProof, SignedBeaconBlock, SignedBlsToExecutionChange, + Attestation, AttesterSlashing, BlobSidecar, DataColumnSidecar, DataColumnSubnetId, EthSpec, + Hash256, IndexedAttestation, LightClientFinalityUpdate, LightClientOptimisticUpdate, + ProposerSlashing, SignedAggregateAndProof, SignedBeaconBlock, SignedBlsToExecutionChange, SignedContributionAndProof, SignedVoluntaryExit, Slot, SubnetId, SyncCommitteeMessage, SyncSubnetId, }; @@ -599,6 +601,75 @@ impl NetworkBeaconProcessor { } } + pub async fn process_gossip_data_column_sidecar( + self: &Arc, + message_id: MessageId, + peer_id: PeerId, + _peer_client: Client, + subnet_id: DataColumnSubnetId, + column_sidecar: Arc>, + seen_duration: Duration, + ) { + let slot = column_sidecar.slot(); + let root = column_sidecar.block_root(); + let index = column_sidecar.index; + let delay = get_slot_delay_ms(seen_duration, slot, &self.chain.slot_clock); + // Log metrics to track delay from other nodes on the network. + metrics::observe_duration( + &metrics::BEACON_DATA_COLUMN_GOSSIP_SLOT_START_DELAY_TIME, + delay, + ); + metrics::set_gauge( + &metrics::BEACON_DATA_COLUMN_LAST_DELAY, + delay.as_millis() as i64, + ); + match self + .chain + .verify_data_column_sidecar_for_gossip(column_sidecar, *subnet_id) + { + Ok(gossip_verified_data_column) => { + metrics::inc_counter( + &metrics::BEACON_PROCESSOR_GOSSIP_DATA_COLUMN_SIDECAR_VERIFIED_TOTAL, + ); + + debug!( + self.log, + "Successfully verified gossip data column sidecar"; + "slot" => %slot, + "root" => %root, + "index" => %index, + ); + + self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Accept); + + // Log metrics to keep track of propagation delay times. + if let Some(duration) = SystemTime::now() + .duration_since(UNIX_EPOCH) + .ok() + .and_then(|now| now.checked_sub(seen_duration)) + { + metrics::observe_duration( + &metrics::BEACON_DATA_COLUMN_GOSSIP_PROPAGATION_VERIFICATION_DELAY_TIME, + duration, + ); + } + self.process_gossip_verified_data_column( + peer_id, + gossip_verified_data_column, + seen_duration, + ) + .await + } + Err(err) => { + error!( + self.log, + "Internal error when verifying data column sidecar"; + "error" => ?err, + ) + } + } + } + #[allow(clippy::too_many_arguments)] pub async fn process_gossip_blob( self: &Arc, @@ -796,6 +867,16 @@ impl NetworkBeaconProcessor { } } + pub async fn process_gossip_verified_data_column( + self: &Arc, + _peer_id: PeerId, + verified_data_column: GossipVerifiedDataColumnSidecar, + // This value is not used presently, but it might come in handy for debugging. + _seen_duration: Duration, + ) { + self.chain.process_gossip_data_column(verified_data_column); + } + /// Process the beacon block received from the gossip network and: /// /// - If it passes gossip propagation criteria, tell the network thread to forward it. diff --git a/beacon_node/network/src/network_beacon_processor/mod.rs b/beacon_node/network/src/network_beacon_processor/mod.rs index 67fc2fabb1e..42cab254412 100644 --- a/beacon_node/network/src/network_beacon_processor/mod.rs +++ b/beacon_node/network/src/network_beacon_processor/mod.rs @@ -229,6 +229,36 @@ impl NetworkBeaconProcessor { }) } + /// Create a new `Work` event for some data column sidecar. + pub fn send_gossip_data_column_sidecar( + self: &Arc, + message_id: MessageId, + peer_id: PeerId, + peer_client: Client, + subnet_id: DataColumnSubnetId, + column_sidecar: Arc>, + seen_timestamp: Duration, + ) -> Result<(), Error> { + let processor = self.clone(); + let process_fn = async move { + processor + .process_gossip_data_column_sidecar( + message_id, + peer_id, + peer_client, + subnet_id, + column_sidecar, + seen_timestamp, + ) + .await + }; + + self.try_send(BeaconWorkEvent { + drop_during_sync: false, + work: Work::GossipDataColumnSidecar(Box::pin(process_fn)), + }) + } + /// Create a new `Work` event for some sync committee signature. pub fn send_gossip_sync_signature( self: &Arc, diff --git a/beacon_node/network/src/router.rs b/beacon_node/network/src/router.rs index f56a3b7445e..924e9355d8c 100644 --- a/beacon_node/network/src/router.rs +++ b/beacon_node/network/src/router.rs @@ -308,6 +308,20 @@ impl Router { ), ) } + PubsubMessage::DataColumnSidecar(data) => { + let (subnet_id, column_sidecar) = *data; + self.handle_beacon_processor_send_result( + self.network_beacon_processor + .send_gossip_data_column_sidecar( + message_id, + peer_id, + self.network_globals.client(&peer_id), + subnet_id, + column_sidecar, + timestamp_now(), + ), + ) + } PubsubMessage::VoluntaryExit(exit) => { debug!(self.log, "Received a voluntary exit"; "peer_id" => %peer_id); self.handle_beacon_processor_send_result( diff --git a/beacon_node/network/src/service.rs b/beacon_node/network/src/service.rs index 01a7e1f9896..2186f8ac896 100644 --- a/beacon_node/network/src/service.rs +++ b/beacon_node/network/src/service.rs @@ -34,8 +34,8 @@ use task_executor::ShutdownReason; use tokio::sync::mpsc; use tokio::time::Sleep; use types::{ - ChainSpec, EthSpec, ForkContext, Slot, SubnetId, SyncCommitteeSubscription, SyncSubnetId, - Unsigned, ValidatorSubscription, + ChainSpec, DataColumnSubnetId, EthSpec, ForkContext, Slot, SubnetId, SyncCommitteeSubscription, + SyncSubnetId, Unsigned, ValidatorSubscription, }; mod tests; @@ -753,6 +753,29 @@ impl NetworkService { } } + if !self.subscribe_all_subnets { + for column_subnet in + DataColumnSubnetId::compute_subnets_for_data_column::( + self.network_globals.local_enr().node_id().raw().into(), + &self.beacon_chain.spec, + ) + { + for fork_digest in self.required_gossip_fork_digests() { + let gossip_kind = Subnet::DataColumn(column_subnet).into(); + let topic = GossipTopic::new( + gossip_kind, + GossipEncoding::default(), + fork_digest, + ); + if self.libp2p.subscribe(topic.clone()) { + subscribed_topics.push(topic); + } else { + warn!(self.log, "Could not subscribe to topic"; "topic" => %topic); + } + } + } + } + // If we are to subscribe to all subnets we do it here if self.subscribe_all_subnets { for subnet_id in 0..<::EthSpec as EthSpec>::SubnetBitfieldLength::to_u64() { @@ -786,6 +809,23 @@ impl NetworkService { } } } + // Subscribe to all data column subnets + for column_subnet in 0..T::EthSpec::data_column_subnet_count() as u64 { + for fork_digest in self.required_gossip_fork_digests() { + let gossip_kind = + Subnet::DataColumn(DataColumnSubnetId::new(column_subnet)).into(); + let topic = GossipTopic::new( + gossip_kind, + GossipEncoding::default(), + fork_digest, + ); + if self.libp2p.subscribe(topic.clone()) { + subscribed_topics.push(topic); + } else { + warn!(self.log, "Could not subscribe to topic"; "topic" => %topic); + } + } + } } if !subscribed_topics.is_empty() { diff --git a/consensus/types/presets/gnosis/deneb.yaml b/consensus/types/presets/gnosis/deneb.yaml index d2d7d0abed3..bef51470e89 100644 --- a/consensus/types/presets/gnosis/deneb.yaml +++ b/consensus/types/presets/gnosis/deneb.yaml @@ -12,3 +12,12 @@ MAX_BLOB_COMMITMENTS_PER_BLOCK: 4096 MAX_BLOBS_PER_BLOCK: 6 # `floorlog2(BLOB_KZG_COMMITMENTS_GINDEX) + 1 + ceillog2(MAX_BLOB_COMMITMENTS_PER_BLOCK)` = 4 + 1 + 12 = 17 KZG_COMMITMENT_INCLUSION_PROOF_DEPTH: 17 + +# EIP-7594 (temporary in Deneb for the purpose of prototyping) +# --------------------------------------------------------------- +# `uint64(2**6)` (= 64) +FIELD_ELEMENTS_PER_CELL: 64 +# uint64(floorlog2(get_generalized_index(BeaconBlockBody, 'blob_kzg_commitments')) +KZG_COMMITMENTS_INCLUSION_PROOF_DEPTH: 4 +# `uint64((FIELD_ELEMENTS_PER_BLOB * 2) // FIELD_ELEMENTS_PER_CELL)` (= 128) +NUMBER_OF_COLUMNS: 128 \ No newline at end of file diff --git a/consensus/types/presets/mainnet/deneb.yaml b/consensus/types/presets/mainnet/deneb.yaml index 6d2fb4abde9..0b2f28853ec 100644 --- a/consensus/types/presets/mainnet/deneb.yaml +++ b/consensus/types/presets/mainnet/deneb.yaml @@ -10,3 +10,12 @@ MAX_BLOB_COMMITMENTS_PER_BLOCK: 4096 MAX_BLOBS_PER_BLOCK: 6 # `floorlog2(BLOB_KZG_COMMITMENTS_GINDEX) + 1 + ceillog2(MAX_BLOB_COMMITMENTS_PER_BLOCK)` = 4 + 1 + 12 = 17 KZG_COMMITMENT_INCLUSION_PROOF_DEPTH: 17 + +# EIP-7594 (temporary in Deneb for the purpose of prototyping) +# --------------------------------------------------------------- +# `uint64(2**6)` (= 64) +FIELD_ELEMENTS_PER_CELL: 64 +# uint64(floorlog2(get_generalized_index(BeaconBlockBody, 'blob_kzg_commitments')) +KZG_COMMITMENTS_INCLUSION_PROOF_DEPTH: 4 +# `uint64((FIELD_ELEMENTS_PER_BLOB * 2) // FIELD_ELEMENTS_PER_CELL)` (= 128) +NUMBER_OF_COLUMNS: 128 \ No newline at end of file diff --git a/consensus/types/presets/minimal/deneb.yaml b/consensus/types/presets/minimal/deneb.yaml index be2b9fadfa5..8bb3e0b66bd 100644 --- a/consensus/types/presets/minimal/deneb.yaml +++ b/consensus/types/presets/minimal/deneb.yaml @@ -10,3 +10,12 @@ MAX_BLOB_COMMITMENTS_PER_BLOCK: 16 MAX_BLOBS_PER_BLOCK: 6 # [customized] `floorlog2(BLOB_KZG_COMMITMENTS_GINDEX) + 1 + ceillog2(MAX_BLOB_COMMITMENTS_PER_BLOCK)` = 4 + 1 + 4 = 9 KZG_COMMITMENT_INCLUSION_PROOF_DEPTH: 9 + +# EIP-7594 (temporary in Deneb for the purpose of prototyping) +# --------------------------------------------------------------- +# `uint64(2**6)` (= 64) +FIELD_ELEMENTS_PER_CELL: 64 +# uint64(floorlog2(get_generalized_index(BeaconBlockBody, 'blob_kzg_commitments')) +KZG_COMMITMENTS_INCLUSION_PROOF_DEPTH: 4 +# `uint64((FIELD_ELEMENTS_PER_BLOB * 2) // FIELD_ELEMENTS_PER_CELL)` (= 128) +NUMBER_OF_COLUMNS: 128 \ No newline at end of file diff --git a/consensus/types/src/chain_spec.rs b/consensus/types/src/chain_spec.rs index b2120fb0406..109d706b800 100644 --- a/consensus/types/src/chain_spec.rs +++ b/consensus/types/src/chain_spec.rs @@ -168,6 +168,11 @@ pub struct ChainSpec { pub deneb_fork_version: [u8; 4], pub deneb_fork_epoch: Option, + /* + * DAS params + */ + pub custody_requirement: u64, + /* * Networking */ @@ -197,6 +202,7 @@ pub struct ChainSpec { pub max_request_blob_sidecars: u64, pub min_epochs_for_blob_sidecars_requests: u64, pub blob_sidecar_subnet_count: u64, + pub data_column_sidecar_subnet_count: u64, /* * Networking Derived @@ -681,6 +687,11 @@ impl ChainSpec { deneb_fork_version: [0x04, 0x00, 0x00, 0x00], deneb_fork_epoch: None, + /* + * DAS params + */ + custody_requirement: 1, + /* * Network specific */ @@ -710,6 +721,7 @@ impl ChainSpec { max_request_blob_sidecars: default_max_request_blob_sidecars(), min_epochs_for_blob_sidecars_requests: default_min_epochs_for_blob_sidecars_requests(), blob_sidecar_subnet_count: default_blob_sidecar_subnet_count(), + data_column_sidecar_subnet_count: default_data_column_sidecar_subnet_count(), /* * Derived Deneb Specific @@ -941,7 +953,10 @@ impl ChainSpec { */ deneb_fork_version: [0x04, 0x00, 0x00, 0x64], deneb_fork_epoch: None, - + /* + * DAS params + */ + custody_requirement: 1, /* * Network specific */ @@ -971,6 +986,7 @@ impl ChainSpec { max_request_blob_sidecars: default_max_request_blob_sidecars(), min_epochs_for_blob_sidecars_requests: default_min_epochs_for_blob_sidecars_requests(), blob_sidecar_subnet_count: default_blob_sidecar_subnet_count(), + data_column_sidecar_subnet_count: default_data_column_sidecar_subnet_count(), /* * Derived Deneb Specific @@ -1151,6 +1167,9 @@ pub struct Config { #[serde(default = "default_blob_sidecar_subnet_count")] #[serde(with = "serde_utils::quoted_u64")] blob_sidecar_subnet_count: u64, + #[serde(default = "default_data_column_sidecar_subnet_count")] + #[serde(with = "serde_utils::quoted_u64")] + data_column_sidecar_subnet_count: u64, } fn default_bellatrix_fork_version() -> [u8; 4] { @@ -1256,6 +1275,10 @@ const fn default_blob_sidecar_subnet_count() -> u64 { 6 } +const fn default_data_column_sidecar_subnet_count() -> u64 { + 32 +} + const fn default_epochs_per_subnet_subscription() -> u64 { 256 } @@ -1418,6 +1441,7 @@ impl Config { max_request_blob_sidecars: spec.max_request_blob_sidecars, min_epochs_for_blob_sidecars_requests: spec.min_epochs_for_blob_sidecars_requests, blob_sidecar_subnet_count: spec.blob_sidecar_subnet_count, + data_column_sidecar_subnet_count: spec.data_column_sidecar_subnet_count, } } @@ -1482,6 +1506,7 @@ impl Config { max_request_blob_sidecars, min_epochs_for_blob_sidecars_requests, blob_sidecar_subnet_count, + data_column_sidecar_subnet_count, } = self; if preset_base != T::spec_name().to_string().as_str() { @@ -1539,6 +1564,7 @@ impl Config { max_request_blob_sidecars, min_epochs_for_blob_sidecars_requests, blob_sidecar_subnet_count, + data_column_sidecar_subnet_count, // We need to re-derive any values that might have changed in the config. max_blocks_by_root_request: max_blocks_by_root_request_common(max_request_blocks), diff --git a/consensus/types/src/data_column_sidecar.rs b/consensus/types/src/data_column_sidecar.rs new file mode 100644 index 00000000000..310c13a5e94 --- /dev/null +++ b/consensus/types/src/data_column_sidecar.rs @@ -0,0 +1,216 @@ +use crate::beacon_block_body::KzgCommitments; +use crate::test_utils::TestRandom; +use crate::{BlobSidecarList, EthSpec, Hash256, KzgProofs, SignedBeaconBlockHeader, Slot}; +use derivative::Derivative; +use rand::rngs::StdRng; +use rand::{Rng, SeedableRng}; +use safe_arith::ArithError; +use serde::{Deserialize, Serialize}; +use ssz_derive::{Decode, Encode}; +use ssz_types::Error as SszError; +use ssz_types::{FixedVector, VariableList}; +use test_random_derive::TestRandom; +use tree_hash::TreeHash; +use tree_hash_derive::TreeHash; + +pub type ColumnIndex = u64; +pub type Cell = FixedVector::FieldElementsPerCell>; +pub type DataColumn = VariableList, ::MaxBlobsPerBlock>; + +#[derive( + Debug, + Clone, + Serialize, + Deserialize, + Encode, + Decode, + TreeHash, + TestRandom, + Derivative, + arbitrary::Arbitrary, +)] +#[serde(bound = "T: EthSpec")] +#[arbitrary(bound = "T: EthSpec")] +#[derivative(PartialEq, Eq, Hash(bound = "T: EthSpec"))] +pub struct DataColumnSidecar { + #[serde(with = "serde_utils::quoted_u64")] + pub index: ColumnIndex, + #[serde(with = "ssz_types::serde_utils::list_of_hex_fixed_vec")] + pub column: DataColumn, + /// All of the KZG commitments and proofs associated with the block, used for verifying sample cells. + pub kzg_commitments: KzgCommitments, + pub kzg_proofs: KzgProofs, + pub signed_block_header: SignedBeaconBlockHeader, + /// An inclusion proof, proving the inclusion of `blob_kzg_commitments` in `BeaconBlockBody`. + pub kzg_commitments_inclusion_proof: FixedVector, +} + +impl DataColumnSidecar { + pub fn random_from_blob_sidecars( + blob_sidecars: &BlobSidecarList, + ) -> Result>, DataColumnSidecarError> { + if blob_sidecars.is_empty() { + return Ok(vec![]); + } + + let first_blob_sidecar = blob_sidecars + .first() + .ok_or(DataColumnSidecarError::MissingBlobSidecars)?; + let slot = first_blob_sidecar.slot(); + + // Proof for kzg commitments in `BeaconBlockBody` + let body_proof_start = first_blob_sidecar + .kzg_commitment_inclusion_proof + .len() + .saturating_sub(T::kzg_commitments_inclusion_proof_depth()); + let kzg_commitments_inclusion_proof: FixedVector< + Hash256, + T::KzgCommitmentsInclusionProofDepth, + > = first_blob_sidecar + .kzg_commitment_inclusion_proof + .get(body_proof_start..) + .ok_or(DataColumnSidecarError::KzgCommitmentInclusionProofOutOfBounds)? + .to_vec() + .into(); + + let mut rng = StdRng::seed_from_u64(slot.as_u64()); + let num_of_blobs = blob_sidecars.len(); + + (0..T::number_of_columns()) + .map(|col_index| { + Ok(DataColumnSidecar { + index: col_index as u64, + column: Self::generate_column_data(&mut rng, num_of_blobs, col_index)?, + kzg_commitments: blob_sidecars + .iter() + .map(|b| b.kzg_commitment) + .collect::>() + .into(), + kzg_proofs: blob_sidecars + .iter() + .map(|b| b.kzg_proof) + .collect::>() + .into(), + signed_block_header: first_blob_sidecar.signed_block_header.clone(), + kzg_commitments_inclusion_proof: kzg_commitments_inclusion_proof.clone(), + }) + }) + .collect::, _>>() + } + + fn generate_column_data( + rng: &mut StdRng, + num_of_blobs: usize, + index: usize, + ) -> Result, DataColumnSidecarError> { + let mut dummy_cell_data = Cell::::default(); + // Prefix with column index + let prefix = index.to_le_bytes(); + dummy_cell_data + .get_mut(..prefix.len()) + .ok_or(DataColumnSidecarError::DataColumnIndexOutOfBounds)? + .copy_from_slice(&prefix); + // Fill the rest of the vec with random values + rng.fill( + dummy_cell_data + .get_mut(prefix.len()..) + .ok_or(DataColumnSidecarError::DataColumnIndexOutOfBounds)?, + ); + + let column = DataColumn::::new(vec![dummy_cell_data; num_of_blobs])?; + Ok(column) + } + + pub fn slot(&self) -> Slot { + self.signed_block_header.message.slot + } + + pub fn block_root(&self) -> Hash256 { + self.signed_block_header.message.tree_hash_root() + } +} + +#[derive(Debug)] +pub enum DataColumnSidecarError { + ArithError(ArithError), + MissingBlobSidecars, + KzgCommitmentInclusionProofOutOfBounds, + DataColumnIndexOutOfBounds, + SszError(SszError), +} + +impl From for DataColumnSidecarError { + fn from(e: ArithError) -> Self { + Self::ArithError(e) + } +} + +impl From for DataColumnSidecarError { + fn from(e: SszError) -> Self { + Self::SszError(e) + } +} + +#[cfg(test)] +mod test { + use crate::beacon_block::EmptyBlock; + use crate::beacon_block_body::KzgCommitments; + use crate::eth_spec::EthSpec; + use crate::{ + BeaconBlock, BeaconBlockDeneb, Blob, BlobSidecar, BlobSidecarList, ChainSpec, + DataColumnSidecar, MainnetEthSpec, SignedBeaconBlock, + }; + use bls::Signature; + use kzg::{KzgCommitment, KzgProof}; + use std::sync::Arc; + + #[test] + fn test_random_from_blob_sidecars() { + type E = MainnetEthSpec; + let num_of_blobs = 6; + let spec = E::default_spec(); + let blob_sidecars: BlobSidecarList = create_test_blob_sidecars(num_of_blobs, &spec); + + let column_sidecars = DataColumnSidecar::random_from_blob_sidecars(&blob_sidecars).unwrap(); + + assert_eq!(column_sidecars.len(), E::number_of_columns()); + + for (idx, col_sidecar) in column_sidecars.iter().enumerate() { + assert_eq!(col_sidecar.index, idx as u64); + assert_eq!(col_sidecar.kzg_commitments.len(), num_of_blobs); + // ensure column sidecars are prefixed with column index (for verification purpose in prototype only) + let prefix_len = 8; // column index (u64) is stored as the first 8 bytes + let cell = col_sidecar.column.first().unwrap(); + let col_index_prefix = u64::from_le_bytes(cell[0..prefix_len].try_into().unwrap()); + assert_eq!(col_index_prefix, idx as u64) + } + } + + fn create_test_blob_sidecars( + num_of_blobs: usize, + spec: &ChainSpec, + ) -> BlobSidecarList { + let mut block = BeaconBlock::Deneb(BeaconBlockDeneb::empty(spec)); + let mut body = block.body_mut(); + let blob_kzg_commitments = body.blob_kzg_commitments_mut().unwrap(); + *blob_kzg_commitments = + KzgCommitments::::new(vec![KzgCommitment::empty_for_testing(); num_of_blobs]) + .unwrap(); + + let signed_block = SignedBeaconBlock::from_block(block, Signature::empty()); + + (0..num_of_blobs) + .map(|index| { + BlobSidecar::new( + index, + Blob::::default(), + &signed_block, + KzgProof::empty(), + ) + .map(Arc::new) + }) + .collect::, _>>() + .unwrap() + .into() + } +} diff --git a/consensus/types/src/data_column_subnet_id.rs b/consensus/types/src/data_column_subnet_id.rs new file mode 100644 index 00000000000..5a42b323895 --- /dev/null +++ b/consensus/types/src/data_column_subnet_id.rs @@ -0,0 +1,170 @@ +//! Identifies each data column subnet by an integer identifier. +use crate::{ChainSpec, EthSpec}; +use ethereum_types::U256; +use safe_arith::{ArithError, SafeArith}; +use serde::{Deserialize, Serialize}; +use std::fmt::{self, Display}; +use std::ops::{Deref, DerefMut}; + +const DATA_COLUMN_SUBNET_COUNT: u64 = 64; + +lazy_static! { + static ref DATA_COLUMN_SUBNET_ID_TO_STRING: Vec = { + let mut v = Vec::with_capacity(DATA_COLUMN_SUBNET_COUNT as usize); + + for i in 0..DATA_COLUMN_SUBNET_COUNT { + v.push(i.to_string()); + } + v + }; +} + +#[derive(arbitrary::Arbitrary, Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(transparent)] +pub struct DataColumnSubnetId(#[serde(with = "serde_utils::quoted_u64")] u64); + +pub fn data_column_subnet_id_to_string(i: u64) -> &'static str { + if i < DATA_COLUMN_SUBNET_COUNT { + DATA_COLUMN_SUBNET_ID_TO_STRING + .get(i as usize) + .expect("index below DATA_COLUMN_SUBNET_COUNT") + } else { + "data column subnet id out of range" + } +} + +impl DataColumnSubnetId { + pub fn new(id: u64) -> Self { + id.into() + } + + pub fn try_from_column_index(column_index: usize) -> Result { + let id = column_index.safe_rem(T::data_column_subnet_count())? as u64; + Ok(id.into()) + } + + #[allow(clippy::arithmetic_side_effects)] + /// Compute required subnets to subscribe to given the node id. + /// TODO(das): Add epoch param + /// TODO(das): Add num of subnets (from ENR) + pub fn compute_subnets_for_data_column( + node_id: U256, + spec: &ChainSpec, + ) -> impl Iterator { + let num_of_column_subnets = T::data_column_subnet_count() as u64; + (0..spec.custody_requirement) + .map(move |i| { + let node_offset = (node_id % U256::from(num_of_column_subnets)).as_u64(); + node_offset.saturating_add(i) % num_of_column_subnets + }) + .map(DataColumnSubnetId::new) + } +} + +impl Display for DataColumnSubnetId { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + write!(f, "{}", self.0) + } +} + +impl Deref for DataColumnSubnetId { + type Target = u64; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for DataColumnSubnetId { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl From for DataColumnSubnetId { + fn from(x: u64) -> Self { + Self(x) + } +} + +impl Into for DataColumnSubnetId { + fn into(self) -> u64 { + self.0 + } +} + +impl Into for &DataColumnSubnetId { + fn into(self) -> u64 { + self.0 + } +} + +impl AsRef for DataColumnSubnetId { + fn as_ref(&self) -> &str { + data_column_subnet_id_to_string(self.0) + } +} + +#[derive(Debug)] +pub enum Error { + ArithError(ArithError), +} + +impl From for Error { + fn from(e: ArithError) -> Self { + Error::ArithError(e) + } +} + +#[cfg(test)] +mod test { + use crate::data_column_subnet_id::DataColumnSubnetId; + use crate::ChainSpec; + + #[test] + fn test_compute_subnets_for_data_column() { + let node_ids = [ + "0", + "88752428858350697756262172400162263450541348766581994718383409852729519486397", + "18732750322395381632951253735273868184515463718109267674920115648614659369468", + "27726842142488109545414954493849224833670205008410190955613662332153332462900", + "39755236029158558527862903296867805548949739810920318269566095185775868999998", + "31899136003441886988955119620035330314647133604576220223892254902004850516297", + "58579998103852084482416614330746509727562027284701078483890722833654510444626", + "28248042035542126088870192155378394518950310811868093527036637864276176517397", + "60930578857433095740782970114409273483106482059893286066493409689627770333527", + "103822458477361691467064888613019442068586830412598673713899771287914656699997", + ] + .into_iter() + .map(|v| ethereum_types::U256::from_dec_str(v).unwrap()) + .collect::>(); + + let expected_subnets = vec![ + vec![0], + vec![29], + vec![28], + vec![20], + vec![30], + vec![9], + vec![18], + vec![21], + vec![23], + vec![29], + ]; + + let spec = ChainSpec::mainnet(); + + for x in 0..node_ids.len() { + let computed_subnets = DataColumnSubnetId::compute_subnets_for_data_column::< + crate::MainnetEthSpec, + >(node_ids[x], &spec); + + assert_eq!( + expected_subnets[x], + computed_subnets + .map(DataColumnSubnetId::into) + .collect::>() + ); + } + } +} diff --git a/consensus/types/src/eth_spec.rs b/consensus/types/src/eth_spec.rs index 17baad9c4c7..00bf41e8a73 100644 --- a/consensus/types/src/eth_spec.rs +++ b/consensus/types/src/eth_spec.rs @@ -109,8 +109,16 @@ pub trait EthSpec: type MaxBlobsPerBlock: Unsigned + Clone + Sync + Send + Debug + PartialEq + Unpin; type MaxBlobCommitmentsPerBlock: Unsigned + Clone + Sync + Send + Debug + PartialEq + Unpin; type FieldElementsPerBlob: Unsigned + Clone + Sync + Send + Debug + PartialEq; + type FieldElementsPerCell: Unsigned + Clone + Sync + Send + Debug + PartialEq; type BytesPerFieldElement: Unsigned + Clone + Sync + Send + Debug + PartialEq; type KzgCommitmentInclusionProofDepth: Unsigned + Clone + Sync + Send + Debug + PartialEq; + /* + * New in PeerDAS + */ + type DataColumnSubnetCount: Unsigned + Clone + Sync + Send + Debug + PartialEq; + type DataColumnCount: Unsigned + Clone + Sync + Send + Debug + PartialEq; + type MaxBytesPerColumn: Unsigned + Clone + Sync + Send + Debug + PartialEq; + type KzgCommitmentsInclusionProofDepth: Unsigned + Clone + Sync + Send + Debug + PartialEq; /* * Derived values (set these CAREFULLY) */ @@ -269,14 +277,36 @@ pub trait EthSpec: Self::FieldElementsPerBlob::to_usize() } + /// Returns the `FIELD_ELEMENTS_PER_CELL` constant for this specification. + fn field_elements_per_cell() -> usize { + Self::FieldElementsPerCell::to_usize() + } + /// Returns the `BYTES_PER_BLOB` constant for this specification. fn bytes_per_blob() -> usize { Self::BytesPerBlob::to_usize() } + /// Returns the `KZG_COMMITMENT_INCLUSION_PROOF_DEPTH` preset for this specification. fn kzg_proof_inclusion_proof_depth() -> usize { Self::KzgCommitmentInclusionProofDepth::to_usize() } + + fn number_of_columns() -> usize { + Self::DataColumnCount::to_usize() + } + + fn data_column_subnet_count() -> usize { + Self::DataColumnSubnetCount::to_usize() + } + + fn max_bytes_per_column() -> usize { + Self::MaxBytesPerColumn::to_usize() + } + + fn kzg_commitments_inclusion_proof_depth() -> usize { + Self::KzgCommitmentsInclusionProofDepth::to_usize() + } } /// Macro to inherit some type values from another EthSpec. @@ -320,8 +350,16 @@ impl EthSpec for MainnetEthSpec { type MaxBlobCommitmentsPerBlock = U4096; type BytesPerFieldElement = U32; type FieldElementsPerBlob = U4096; + type FieldElementsPerCell = U64; type BytesPerBlob = U131072; type KzgCommitmentInclusionProofDepth = U17; + type DataColumnSubnetCount = U32; + type DataColumnCount = U128; + // Column samples are entire columns in 1D DAS. + // max data size = extended_blob_bytes * max_blobs_per_block / num_of_columns + // 256kb * 32 / 128 = 64kb + type MaxBytesPerColumn = U65536; + type KzgCommitmentsInclusionProofDepth = U4; // inclusion of the whole list of commitments type SyncSubcommitteeSize = U128; // 512 committee size / 4 sync committee subnet count type MaxPendingAttestations = U4096; // 128 max attestations * 32 slots per epoch type SlotsPerEth1VotingPeriod = U2048; // 64 epochs * 32 slots per epoch @@ -353,9 +391,15 @@ impl EthSpec for MinimalEthSpec { type SlotsPerEth1VotingPeriod = U32; // 4 epochs * 8 slots per epoch type MaxWithdrawalsPerPayload = U4; type FieldElementsPerBlob = U4096; + type FieldElementsPerCell = U64; type BytesPerBlob = U131072; type MaxBlobCommitmentsPerBlock = U16; type KzgCommitmentInclusionProofDepth = U9; + // DAS spec values copied from `MainnetEthSpec` + type DataColumnSubnetCount = U32; + type DataColumnCount = U128; + type MaxBytesPerColumn = U65536; + type KzgCommitmentsInclusionProofDepth = U4; params_from_eth_spec!(MainnetEthSpec { JustificationBitsLength, @@ -427,9 +471,15 @@ impl EthSpec for GnosisEthSpec { type MaxBlobsPerBlock = U6; type MaxBlobCommitmentsPerBlock = U4096; type FieldElementsPerBlob = U4096; + type FieldElementsPerCell = U64; type BytesPerFieldElement = U32; type BytesPerBlob = U131072; type KzgCommitmentInclusionProofDepth = U17; + // DAS spec values copied from `MainnetEthSpec` + type DataColumnSubnetCount = U32; + type DataColumnCount = U128; + type MaxBytesPerColumn = U65536; + type KzgCommitmentsInclusionProofDepth = U4; fn default_spec() -> ChainSpec { ChainSpec::gnosis() diff --git a/consensus/types/src/lib.rs b/consensus/types/src/lib.rs index b07b497a2ae..3c3e18d9297 100644 --- a/consensus/types/src/lib.rs +++ b/consensus/types/src/lib.rs @@ -99,6 +99,8 @@ pub mod slot_data; pub mod sqlite; pub mod blob_sidecar; +pub mod data_column_sidecar; +pub mod data_column_subnet_id; pub mod light_client_header; pub mod non_zero_usize; pub mod runtime_var_list; @@ -127,6 +129,8 @@ pub use crate::chain_spec::{ChainSpec, Config, Domain}; pub use crate::checkpoint::Checkpoint; pub use crate::config_and_preset::{ConfigAndPreset, ConfigAndPresetCapella, ConfigAndPresetDeneb}; pub use crate::contribution_and_proof::ContributionAndProof; +pub use crate::data_column_sidecar::DataColumnSidecar; +pub use crate::data_column_subnet_id::DataColumnSubnetId; pub use crate::deposit::{Deposit, DEPOSIT_TREE_DEPTH}; pub use crate::deposit_data::DepositData; pub use crate::deposit_message::DepositMessage; diff --git a/consensus/types/src/preset.rs b/consensus/types/src/preset.rs index 63a372ea1c9..fe8cc94f818 100644 --- a/consensus/types/src/preset.rs +++ b/consensus/types/src/preset.rs @@ -214,6 +214,13 @@ pub struct DenebPreset { pub max_blob_commitments_per_block: u64, #[serde(with = "serde_utils::quoted_u64")] pub field_elements_per_blob: u64, + // EIP-7594 DAS presets - to be moved to the next fork + #[serde(with = "serde_utils::quoted_u64")] + pub field_elements_per_cell: u64, + #[serde(with = "serde_utils::quoted_u64")] + pub kzg_commitments_inclusion_proof_depth: u64, + #[serde(with = "serde_utils::quoted_u64")] + pub number_of_columns: u64, } impl DenebPreset { @@ -222,6 +229,10 @@ impl DenebPreset { max_blobs_per_block: T::max_blobs_per_block() as u64, max_blob_commitments_per_block: T::max_blob_commitments_per_block() as u64, field_elements_per_blob: T::field_elements_per_blob() as u64, + field_elements_per_cell: T::field_elements_per_cell() as u64, + kzg_commitments_inclusion_proof_depth: T::kzg_commitments_inclusion_proof_depth() + as u64, + number_of_columns: T::number_of_columns() as u64, } } } From ae470d70041339f00de5d404ff75bcc3e0cf31eb Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Mon, 4 Mar 2024 15:46:49 +1100 Subject: [PATCH 02/76] Add `DataColumnSidecarsByRoot ` req/resp protocol (#5196) * Add stub for `DataColumnsByRoot` * Add basic implementation of serving RPC data column from DA checker. * Store data columns in early attester cache and blobs db. * Apply suggestions from code review Co-authored-by: Eitan Seri-Levi Co-authored-by: Jacob Kaufmann * Fix build. * Store `DataColumnInfo` in database and various cleanups. * Update `DataColumnSidecar` ssz max size and remove panic code. --------- Co-authored-by: Eitan Seri-Levi Co-authored-by: Jacob Kaufmann --- beacon_node/beacon_chain/src/beacon_chain.rs | 95 ++++++-- .../beacon_chain/src/blob_verification.rs | 41 +--- .../beacon_chain/src/block_verification.rs | 32 +++ .../src/block_verification_types.rs | 35 ++- beacon_node/beacon_chain/src/builder.rs | 10 + .../src/data_availability_checker.rs | 102 +++++--- .../availability_view.rs | 95 +++++++- .../child_components.rs | 14 +- .../src/data_availability_checker/error.rs | 2 + .../overflow_lru_cache.rs | 168 +++++++++++++- .../processing_cache.rs | 6 + .../src/data_column_verification.rs | 218 ++++++++++++++++++ .../beacon_chain/src/early_attester_cache.rs | 14 +- .../beacon_chain/src/historical_blocks.rs | 25 +- beacon_node/beacon_chain/src/lib.rs | 1 + beacon_node/beacon_chain/tests/store_tests.rs | 4 +- beacon_node/beacon_processor/src/lib.rs | 17 +- .../src/peer_manager/mod.rs | 10 +- .../src/rpc/codec/ssz_snappy.rs | 32 ++- .../lighthouse_network/src/rpc/config.rs | 13 ++ .../lighthouse_network/src/rpc/methods.rs | 33 ++- beacon_node/lighthouse_network/src/rpc/mod.rs | 1 + .../lighthouse_network/src/rpc/outbound.rs | 9 + .../lighthouse_network/src/rpc/protocol.rs | 22 +- .../src/rpc/rate_limiter.rs | 14 ++ .../src/service/api_types.rs | 13 +- .../lighthouse_network/src/service/mod.rs | 15 ++ .../gossip_methods.rs | 58 ++++- .../src/network_beacon_processor/mod.rs | 21 +- .../network_beacon_processor/rpc_methods.rs | 93 +++++++- .../network_beacon_processor/sync_methods.rs | 5 + beacon_node/network/src/router.rs | 19 +- .../sync/block_lookups/single_block_lookup.rs | 2 + .../network/src/sync/block_lookups/tests.rs | 4 +- beacon_node/network/src/sync/manager.rs | 2 +- beacon_node/store/src/errors.rs | 2 + beacon_node/store/src/hot_cold_store.rs | 202 +++++++++++++++- beacon_node/store/src/lib.rs | 6 + beacon_node/store/src/metadata.rs | 28 +++ beacon_node/store/src/metrics.rs | 4 + consensus/types/src/chain_spec.rs | 38 +++ consensus/types/src/data_column_sidecar.rs | 71 +++++- 42 files changed, 1465 insertions(+), 131 deletions(-) create mode 100644 beacon_node/beacon_chain/src/data_column_verification.rs diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index ff856ef7f35..58960ed8078 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -7,9 +7,7 @@ use crate::attester_cache::{AttesterCache, AttesterCacheKey}; use crate::beacon_block_streamer::{BeaconBlockStreamer, CheckCaches}; use crate::beacon_proposer_cache::compute_proposer_duties_from_head; use crate::beacon_proposer_cache::BeaconProposerCache; -use crate::blob_verification::{ - GossipBlobError, GossipVerifiedBlob, GossipVerifiedDataColumnSidecar, -}; +use crate::blob_verification::{GossipBlobError, GossipVerifiedBlob}; use crate::block_times_cache::BlockTimesCache; use crate::block_verification::POS_PANDA_BANNER; use crate::block_verification::{ @@ -25,6 +23,7 @@ use crate::chain_config::ChainConfig; use crate::data_availability_checker::{ Availability, AvailabilityCheckError, AvailableBlock, DataAvailabilityChecker, }; +use crate::data_column_verification::{GossipDataColumnError, GossipVerifiedDataColumn}; use crate::early_attester_cache::EarlyAttesterCache; use crate::errors::{BeaconChainError as Error, BlockProductionError}; use crate::eth1_chain::{Eth1Chain, Eth1ChainBackend}; @@ -124,6 +123,7 @@ use tokio_stream::Stream; use tree_hash::TreeHash; use types::beacon_state::CloneConfig; use types::blob_sidecar::{BlobSidecarList, FixedBlobSidecarList}; +use types::data_column_sidecar::DataColumnSidecarList; use types::payload::BlockProductionVersion; use types::*; @@ -1176,6 +1176,15 @@ impl BeaconChain { .map_or_else(|| self.get_blobs(block_root), Ok) } + pub fn get_data_columns_checking_early_attester_cache( + &self, + block_root: &Hash256, + ) -> Result, Error> { + self.early_attester_cache + .get_data_columns(*block_root) + .map_or_else(|| self.get_data_columns(block_root), Ok) + } + /// Returns the block at the given root, if any. /// /// ## Errors @@ -1251,6 +1260,20 @@ impl BeaconChain { } } + /// Returns the data columns at the given root, if any. + /// + /// ## Errors + /// May return a database error. + pub fn get_data_columns( + &self, + block_root: &Hash256, + ) -> Result, Error> { + match self.store.get_data_columns(block_root)? { + Some(data_columns) => Ok(data_columns), + None => Ok(DataColumnSidecarList::default()), + } + } + pub fn get_blinded_block( &self, block_root: &Hash256, @@ -2088,10 +2111,10 @@ impl BeaconChain { self: &Arc, data_column_sidecar: Arc>, subnet_id: u64, - ) -> Result, GossipBlobError> { + ) -> Result, GossipDataColumnError> { metrics::inc_counter(&metrics::BLOBS_COLUMN_SIDECAR_PROCESSING_REQUESTS); let _timer = metrics::start_timer(&metrics::DATA_COLUMN_SIDECAR_GOSSIP_VERIFICATION_TIMES); - GossipVerifiedDataColumnSidecar::new(data_column_sidecar, subnet_id, self).map(|v| { + GossipVerifiedDataColumn::new(data_column_sidecar, subnet_id, self).map(|v| { metrics::inc_counter(&metrics::DATA_COLUMNS_SIDECAR_PROCESSING_SUCCESSES); v }) @@ -2912,18 +2935,28 @@ impl BeaconChain { self.remove_notified(&block_root, r) } - pub fn process_gossip_data_column( + /// Cache the data column in the processing cache, process it, then evict it from the cache if it was + /// imported or errors. + pub async fn process_gossip_data_column( self: &Arc, - gossip_verified_data_column: GossipVerifiedDataColumnSidecar, - ) { - let data_column = gossip_verified_data_column.as_data_column(); - // TODO(das) send to DA checker - info!( - self.log, - "Processed gossip data column"; - "index" => data_column.index, - "slot" => data_column.slot().as_u64() - ); + data_column: GossipVerifiedDataColumn, + ) -> Result> { + let block_root = data_column.block_root(); + + // 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) + { + return Err(BlockError::BlockIsAlreadyKnown); + } + + let r = self + .check_gossip_data_column_availability_and_import(data_column) + .await; + self.remove_notified(&block_root, r) } /// Cache the blobs in the processing cache, process it, then evict it from the cache if it was @@ -3198,6 +3231,23 @@ impl BeaconChain { self.process_availability(slot, availability).await } + /// Checks if the provided data column can make any cached blocks available, and imports immediately + /// if so, otherwise caches the data column in the data availability checker. + async fn check_gossip_data_column_availability_and_import( + self: &Arc, + data_column: GossipVerifiedDataColumn, + ) -> Result> { + let slot = data_column.slot(); + if let Some(slasher) = self.slasher.as_ref() { + slasher.accept_block_header(data_column.signed_block_header()); + } + let availability = self + .data_availability_checker + .put_gossip_data_column(data_column)?; + + self.process_availability(slot, availability).await + } + /// Checks if the provided blobs can make any cached blocks available, and imports immediately /// if so, otherwise caches the blob in the data availability checker. async fn check_rpc_blob_availability_and_import( @@ -3475,7 +3525,7 @@ impl BeaconChain { // If the write fails, revert fork choice to the version from disk, else we can // end up with blocks in fork choice that are missing from disk. // See https://github.com/sigp/lighthouse/issues/2028 - let (_, signed_block, blobs) = signed_block.deconstruct(); + let (_, signed_block, blobs, data_columns) = signed_block.deconstruct(); let block = signed_block.message(); ops.extend( confirmed_state_roots @@ -3496,6 +3546,17 @@ impl BeaconChain { } } + if let Some(data_columns) = data_columns { + if !data_columns.is_empty() { + debug!( + self.log, "Writing data_columns to store"; + "block_root" => %block_root, + "count" => data_columns.len(), + ); + ops.push(StoreOp::PutDataColumns(block_root, data_columns)); + } + } + let txn_lock = self.store.hot_db.begin_rw_transaction(); if let Err(e) = self.store.do_atomically_with_block_and_blobs_cache(ops) { diff --git a/beacon_node/beacon_chain/src/blob_verification.rs b/beacon_node/beacon_chain/src/blob_verification.rs index ffc64f9d10e..f2d150d72bf 100644 --- a/beacon_node/beacon_chain/src/blob_verification.rs +++ b/beacon_node/beacon_chain/src/blob_verification.rs @@ -17,8 +17,7 @@ use ssz_types::VariableList; use tree_hash::TreeHash; use types::blob_sidecar::BlobIdentifier; use types::{ - BeaconStateError, BlobSidecar, CloneConfig, DataColumnSidecar, EthSpec, Hash256, - SignedBeaconBlockHeader, Slot, + BeaconStateError, BlobSidecar, CloneConfig, EthSpec, Hash256, SignedBeaconBlockHeader, Slot, }; /// An error occurred while validating a gossip blob. @@ -185,33 +184,6 @@ pub type GossipVerifiedBlobList = VariableList< <::EthSpec as EthSpec>::MaxBlobsPerBlock, >; -#[derive(Debug)] -pub struct GossipVerifiedDataColumnSidecar { - data_column_sidecar: Arc>, -} - -impl GossipVerifiedDataColumnSidecar { - pub fn new( - column_sidecar: Arc>, - subnet_id: u64, - chain: &BeaconChain, - ) -> Result> { - let header = column_sidecar.signed_block_header.clone(); - // We only process slashing info if the gossip verification failed - // since we do not process the blob any further in that case. - validate_data_column_sidecar_for_gossip(column_sidecar, subnet_id, chain).map_err(|e| { - process_block_slash_info::<_, GossipBlobError>( - chain, - BlockSlashInfo::from_early_error_blob(header, e), - ) - }) - } - - pub fn as_data_column(&self) -> &Arc> { - &self.data_column_sidecar - } -} - /// A wrapper around a `BlobSidecar` that indicates it has been approved for re-gossiping on /// the p2p network. #[derive(Debug)] @@ -675,17 +647,6 @@ pub fn validate_blob_sidecar_for_gossip( }) } -pub fn validate_data_column_sidecar_for_gossip( - data_column_sidecar: Arc>, - _subnet: u64, - _chain: &BeaconChain, -) -> Result, GossipBlobError> { - // TODO(das): validate kzg commitments, cell proofs etc - Ok(GossipVerifiedDataColumnSidecar { - data_column_sidecar: data_column_sidecar.clone(), - }) -} - /// Returns the canonical root of the given `blob`. /// /// Use this function to ensure that we report the blob hashing time Prometheus metric. diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index ac3d3e3ab80..b896327e06f 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -53,6 +53,7 @@ use crate::block_verification_types::{ AsBlock, BlockContentsError, BlockImportData, GossipVerifiedBlockContents, RpcBlock, }; use crate::data_availability_checker::{AvailabilityCheckError, MaybeAvailableBlock}; +use crate::data_column_verification::GossipDataColumnError; use crate::eth1_finalization_cache::Eth1FinalizationData; use crate::execution_payload::{ is_optimistic_candidate_block, validate_execution_payload_for_gossip, validate_merge_block, @@ -528,6 +529,20 @@ impl BlockSlashInfo> { } } +impl BlockSlashInfo> { + pub fn from_early_error_data_column( + header: SignedBeaconBlockHeader, + e: GossipDataColumnError, + ) -> Self { + match e { + GossipDataColumnError::ProposalSignatureInvalid => BlockSlashInfo::SignatureInvalid(e), + // `InvalidSignature` could indicate any signature in the block, so we want + // to recheck the proposer signature alone. + _ => BlockSlashInfo::SignatureNotChecked(header, e), + } + } +} + /// Process invalid blocks to see if they are suitable for the slasher. /// /// If no slasher is configured, this is a no-op. @@ -2002,6 +2017,23 @@ impl BlockBlobError for GossipBlobError { } } +impl BlockBlobError for GossipDataColumnError { + fn not_later_than_parent_error(data_column_slot: Slot, parent_slot: Slot) -> Self { + GossipDataColumnError::DataColumnIsNotLaterThanParent { + data_column_slot, + parent_slot, + } + } + + fn unknown_validator_error(validator_index: u64) -> Self { + GossipDataColumnError::UnknownValidator(validator_index) + } + + fn proposer_signature_invalid() -> Self { + GossipDataColumnError::ProposalSignatureInvalid + } +} + /// Performs a cheap (time-efficient) state advancement so the committees and proposer shuffling for /// `slot` can be obtained from `state`. /// diff --git a/beacon_node/beacon_chain/src/block_verification_types.rs b/beacon_node/beacon_chain/src/block_verification_types.rs index edba7a211cb..263a6eab074 100644 --- a/beacon_node/beacon_chain/src/block_verification_types.rs +++ b/beacon_node/beacon_chain/src/block_verification_types.rs @@ -9,6 +9,7 @@ use ssz_types::VariableList; use state_processing::ConsensusContext; use std::sync::Arc; use types::blob_sidecar::{BlobIdentifier, BlobSidecarError, FixedBlobSidecarList}; +use types::data_column_sidecar::DataColumnSidecarList; use types::{ BeaconBlockRef, BeaconState, BlindedPayload, BlobSidecarList, Epoch, EthSpec, Hash256, SignedBeaconBlock, SignedBeaconBlockHeader, Slot, @@ -43,6 +44,7 @@ impl RpcBlock { match &self.block { RpcBlockInner::Block(block) => block, RpcBlockInner::BlockAndBlobs(block, _) => block, + RpcBlockInner::BlockAndDataColumns(block, _) => block, } } @@ -50,6 +52,7 @@ impl RpcBlock { match &self.block { RpcBlockInner::Block(block) => block.clone(), RpcBlockInner::BlockAndBlobs(block, _) => block.clone(), + RpcBlockInner::BlockAndDataColumns(block, _) => block.clone(), } } @@ -57,6 +60,7 @@ impl RpcBlock { match &self.block { RpcBlockInner::Block(_) => None, RpcBlockInner::BlockAndBlobs(_, blobs) => Some(blobs), + RpcBlockInner::BlockAndDataColumns(_, _) => None, } } } @@ -72,6 +76,9 @@ enum RpcBlockInner { /// This variant is used with parent lookups and by-range responses. It should have all blobs /// ordered, all block roots matching, and the correct number of blobs for this block. BlockAndBlobs(Arc>, BlobSidecarList), + /// This variant is used with parent lookups and by-range responses. It should have all data columns + /// ordered, all block roots matching, and the correct number of data columns for this block. + BlockAndDataColumns(Arc>, DataColumnSidecarList), } impl RpcBlock { @@ -141,25 +148,36 @@ impl RpcBlock { Self::new(Some(block_root), block, blobs) } + #[allow(clippy::type_complexity)] pub fn deconstruct( self, ) -> ( Hash256, Arc>, Option>, + Option>, ) { let block_root = self.block_root(); match self.block { - RpcBlockInner::Block(block) => (block_root, block, None), - RpcBlockInner::BlockAndBlobs(block, blobs) => (block_root, block, Some(blobs)), + RpcBlockInner::Block(block) => (block_root, block, None, None), + RpcBlockInner::BlockAndBlobs(block, blobs) => (block_root, block, Some(blobs), None), + RpcBlockInner::BlockAndDataColumns(block, data_columns) => { + (block_root, block, None, Some(data_columns)) + } } } pub fn n_blobs(&self) -> usize { match &self.block { - RpcBlockInner::Block(_) => 0, + RpcBlockInner::Block(_) | RpcBlockInner::BlockAndDataColumns(_, _) => 0, RpcBlockInner::BlockAndBlobs(_, blobs) => blobs.len(), } } + pub fn n_data_columns(&self) -> usize { + match &self.block { + RpcBlockInner::Block(_) | RpcBlockInner::BlockAndBlobs(_, _) => 0, + RpcBlockInner::BlockAndDataColumns(_, data_columns) => data_columns.len(), + } + } } /// A block that has gone through all pre-deneb block processing checks including block processing @@ -485,12 +503,13 @@ impl AsBlock for AvailableBlock { } fn into_rpc_block(self) -> RpcBlock { - let (block_root, block, blobs_opt) = self.deconstruct(); + let (block_root, block, blobs_opt, data_columns_opt) = self.deconstruct(); // Circumvent the constructor here, because an Available block will have already had // consistency checks performed. - let inner = match blobs_opt { - None => RpcBlockInner::Block(block), - Some(blobs) => RpcBlockInner::BlockAndBlobs(block, blobs), + let inner = match (blobs_opt, data_columns_opt) { + (None, None) => RpcBlockInner::Block(block), + (Some(blobs), _) => RpcBlockInner::BlockAndBlobs(block, blobs), + (_, Some(data_columns)) => RpcBlockInner::BlockAndDataColumns(block, data_columns), }; RpcBlock { block_root, @@ -522,12 +541,14 @@ impl AsBlock for RpcBlock { match &self.block { RpcBlockInner::Block(block) => block, RpcBlockInner::BlockAndBlobs(block, _) => block, + RpcBlockInner::BlockAndDataColumns(block, _) => block, } } fn block_cloned(&self) -> Arc> { match &self.block { RpcBlockInner::Block(block) => block.clone(), RpcBlockInner::BlockAndBlobs(block, _) => block.clone(), + RpcBlockInner::BlockAndDataColumns(block, _) => block.clone(), } } fn canonical_root(&self) -> Hash256 { diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index c75c3f695b3..a1d2706726b 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -407,6 +407,11 @@ where .init_blob_info(genesis.beacon_block.slot()) .map_err(|e| format!("Failed to initialize genesis blob info: {:?}", e))?, ); + self.pending_io_batch.push( + store + .init_data_column_info(genesis.beacon_block.slot()) + .map_err(|e| format!("Failed to initialize genesis data column info: {:?}", e))?, + ); let fc_store = BeaconForkChoiceStore::get_forkchoice_store(store, &genesis) .map_err(|e| format!("Unable to initialize fork choice store: {e:?}"))?; @@ -564,6 +569,11 @@ where .init_blob_info(weak_subj_block.slot()) .map_err(|e| format!("Failed to initialize blob info: {:?}", e))?, ); + self.pending_io_batch.push( + store + .init_data_column_info(weak_subj_block.slot()) + .map_err(|e| format!("Failed to initialize data column info: {:?}", e))?, + ); // Store pruning checkpoint to prevent attempting to prune before the anchor state. self.pending_io_batch diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index f906032ecd2..9a4f5eea048 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -22,7 +22,9 @@ use std::sync::Arc; use task_executor::TaskExecutor; use types::beacon_block_body::KzgCommitmentOpts; use types::blob_sidecar::{BlobIdentifier, BlobSidecar, FixedBlobSidecarList}; -use types::{BlobSidecarList, ChainSpec, Epoch, EthSpec, Hash256, SignedBeaconBlock, Slot}; +use types::{ + BlobSidecarList, ChainSpec, DataColumnSidecar, Epoch, EthSpec, Hash256, SignedBeaconBlock, Slot, +}; mod availability_view; mod child_components; @@ -31,7 +33,9 @@ mod overflow_lru_cache; mod processing_cache; mod state_lru_cache; +use crate::data_column_verification::{verify_kzg_for_data_column_list, GossipVerifiedDataColumn}; pub use error::{Error as AvailabilityCheckError, ErrorCategory as AvailabilityCheckErrorCategory}; +use types::data_column_sidecar::{DataColumnIdentifier, DataColumnSidecarList}; use types::non_zero_usize::new_non_zero_usize; /// The LRU Cache stores `PendingComponents` which can store up to @@ -200,6 +204,14 @@ impl DataAvailabilityChecker { .and_then(|cached| cached.block.clone()) } + /// Get a data column from the availability cache. + pub fn get_data_column( + &self, + data_column_id: &DataColumnIdentifier, + ) -> Result>>, AvailabilityCheckError> { + self.availability_cache.peek_data_column(data_column_id) + } + /// Put a list of blobs received via RPC into the availability cache. This performs KZG /// verification on the blobs in the list. pub fn put_rpc_blobs( @@ -231,6 +243,21 @@ impl DataAvailabilityChecker { .put_kzg_verified_blobs(gossip_blob.block_root(), vec![gossip_blob.into_inner()]) } + /// 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. + pub fn put_gossip_data_column( + &self, + gossip_data_column: GossipVerifiedDataColumn, + ) -> Result, AvailabilityCheckError> { + self.availability_cache.put_kzg_verified_data_columns( + gossip_data_column.block_root(), + vec![gossip_data_column.into_inner()], + ) + } + /// 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_pending_executed_block( @@ -250,9 +277,9 @@ impl DataAvailabilityChecker { &self, block: RpcBlock, ) -> Result, AvailabilityCheckError> { - let (block_root, block, blobs) = block.deconstruct(); - match blobs { - None => { + let (block_root, block, blobs, data_columns) = block.deconstruct(); + match (blobs, data_columns) { + (None, None) => { if self.blobs_required_for_block(&block) { Ok(MaybeAvailableBlock::AvailabilityPending { block_root, block }) } else { @@ -260,25 +287,35 @@ impl DataAvailabilityChecker { block_root, block, blobs: None, + data_columns: None, })) } } - Some(blob_list) => { - let verified_blobs = if self.blobs_required_for_block(&block) { - let kzg = self - .kzg - .as_ref() - .ok_or(AvailabilityCheckError::KzgNotInitialized)?; - verify_kzg_for_blob_list(blob_list.iter(), kzg) - .map_err(AvailabilityCheckError::Kzg)?; - Some(blob_list) - } else { - None - }; + (maybe_blob_list, maybe_data_column_list) => { + let (verified_blobs, verified_data_column) = + if self.blobs_required_for_block(&block) { + let kzg = self + .kzg + .as_ref() + .ok_or(AvailabilityCheckError::KzgNotInitialized)?; + + if let Some(blob_list) = maybe_blob_list.as_ref() { + verify_kzg_for_blob_list(blob_list.iter(), kzg) + .map_err(AvailabilityCheckError::Kzg)?; + } + if let Some(data_column_list) = maybe_data_column_list.as_ref() { + verify_kzg_for_data_column_list(data_column_list.iter(), kzg) + .map_err(AvailabilityCheckError::Kzg)?; + } + (maybe_blob_list, maybe_data_column_list) + } else { + (None, None) + }; Ok(MaybeAvailableBlock::Available(AvailableBlock { block_root, block, blobs: verified_blobs, + data_columns: verified_data_column, })) } } @@ -314,9 +351,9 @@ impl DataAvailabilityChecker { } for block in blocks { - let (block_root, block, blobs) = block.deconstruct(); - match blobs { - None => { + let (block_root, block, blobs, data_columns) = block.deconstruct(); + match (blobs, data_columns) { + (None, None) => { if self.blobs_required_for_block(&block) { results.push(MaybeAvailableBlock::AvailabilityPending { block_root, block }) } else { @@ -324,20 +361,23 @@ impl DataAvailabilityChecker { block_root, block, blobs: None, + data_columns: None, })) } } - Some(blob_list) => { - let verified_blobs = if self.blobs_required_for_block(&block) { - Some(blob_list) - } else { - None - }; + (maybe_blob_list, maybe_data_column_list) => { + let (verified_blobs, verified_data_columns) = + if self.blobs_required_for_block(&block) { + (maybe_blob_list, maybe_data_column_list) + } else { + (None, None) + }; // already verified kzg for all blobs results.push(MaybeAvailableBlock::Available(AvailableBlock { block_root, block, blobs: verified_blobs, + data_columns: verified_data_columns, })) } } @@ -564,6 +604,7 @@ pub struct AvailableBlock { block_root: Hash256, block: Arc>, blobs: Option>, + data_columns: Option>, } impl AvailableBlock { @@ -571,11 +612,13 @@ impl AvailableBlock { block_root: Hash256, block: Arc>, blobs: Option>, + data_columns: Option>, ) -> Self { Self { block_root, block, blobs, + data_columns, } } @@ -590,19 +633,26 @@ impl AvailableBlock { self.blobs.as_ref() } + pub fn data_columns(&self) -> Option<&DataColumnSidecarList> { + self.data_columns.as_ref() + } + + #[allow(clippy::type_complexity)] pub fn deconstruct( self, ) -> ( Hash256, Arc>, Option>, + Option>, ) { let AvailableBlock { block_root, block, blobs, + data_columns, } = self; - (block_root, block, blobs) + (block_root, block, blobs, data_columns) } } diff --git a/beacon_node/beacon_chain/src/data_availability_checker/availability_view.rs b/beacon_node/beacon_chain/src/data_availability_checker/availability_view.rs index 65093db26bd..f79f28b1cad 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker/availability_view.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker/availability_view.rs @@ -4,11 +4,12 @@ use crate::blob_verification::KzgVerifiedBlob; use crate::block_verification_types::AsBlock; use crate::data_availability_checker::overflow_lru_cache::PendingComponents; use crate::data_availability_checker::ProcessingComponents; +use crate::data_column_verification::KzgVerifiedDataColumn; use kzg::KzgCommitment; use ssz_types::FixedVector; use std::sync::Arc; use types::beacon_block_body::KzgCommitments; -use types::{BlobSidecar, EthSpec, SignedBeaconBlock}; +use types::{BlobSidecar, DataColumnSidecar, EthSpec, SignedBeaconBlock}; /// Defines an interface for managing data availability with two key invariants: /// @@ -26,12 +27,20 @@ pub trait AvailabilityView { /// The type representing a blob in the implementation. Must implement `Clone`. type BlobType: Clone + GetCommitment; + /// The type representing a data column in the implementation. + type DataColumnType: Clone; + /// Returns an immutable reference to the cached block. fn get_cached_block(&self) -> &Option; /// Returns an immutable reference to the fixed vector of cached blobs. fn get_cached_blobs(&self) -> &FixedVector, E::MaxBlobsPerBlock>; + /// Returns an immutable reference to the fixed vector of cached data columns. + fn get_cached_data_columns( + &self, + ) -> &FixedVector, E::DataColumnCount>; + /// Returns a mutable reference to the cached block. fn get_cached_block_mut(&mut self) -> &mut Option; @@ -40,6 +49,11 @@ pub trait AvailabilityView { &mut self, ) -> &mut FixedVector, E::MaxBlobsPerBlock>; + /// Returns a mutable reference to the fixed vector of cached data columns. + fn get_cached_data_columns_mut( + &mut self, + ) -> &mut FixedVector, E::DataColumnCount>; + /// Checks if a block exists in the cache. /// /// Returns: @@ -61,6 +75,18 @@ pub trait AvailabilityView { .unwrap_or(false) } + /// Checks if a data column exists at the given index in the cache. + /// + /// Returns: + /// - `true` if a data column exists at the given index. + /// - `false` otherwise. + fn data_column_exists(&self, data_colum_index: usize) -> bool { + self.get_cached_data_columns() + .get(data_colum_index) + .map(|d| d.is_some()) + .unwrap_or(false) + } + /// Returns the number of blobs that are expected to be present. Returns `None` if we don't have a /// block. /// @@ -90,6 +116,42 @@ pub trait AvailabilityView { } } + /// Inserts a data column at a specific index in the cache. + /// + /// Existing data column at the index will be replaced. + fn insert_data_column_at_index( + &mut self, + data_column_index: usize, + data_column: Self::DataColumnType, + ) { + if let Some(b) = self + .get_cached_data_columns_mut() + .get_mut(data_column_index) + { + *b = Some(data_column); + } + } + + /// Merges a given set of data columns into the cache. + /// + /// Data columns are only inserted if: + /// 1. The data column entry at the index is empty and no block exists. + /// 2. The block exists and its commitments matches the data column's commitments. + fn merge_data_columns( + &mut self, + data_columns: FixedVector, E::DataColumnCount>, + ) { + for (index, data_column) in data_columns.iter().cloned().enumerate() { + let Some(data_column) = data_column else { + continue; + }; + // TODO(das): Add equivalent checks for data columns if necessary + if !self.data_column_exists(index) { + self.insert_data_column_at_index(index, data_column) + } + } + } + /// Merges a given set of blobs into the cache. /// /// Blobs are only inserted if: @@ -148,14 +210,16 @@ pub trait AvailabilityView { /// - `$struct_name`: The name of the struct for which to implement `AvailabilityView`. /// - `$block_type`: The type to use for `BlockType` in the `AvailabilityView` trait. /// - `$blob_type`: The type to use for `BlobType` in the `AvailabilityView` trait. +/// - `$data_column_type`: The type to use for `DataColumnType` in the `AvailabilityView` trait. /// - `$block_field`: The field name in the struct that holds the cached block. -/// - `$blob_field`: The field name in the struct that holds the cached blobs. +/// - `$data_column_field`: The field name in the struct that holds the cached data columns. #[macro_export] macro_rules! impl_availability_view { - ($struct_name:ident, $block_type:ty, $blob_type:ty, $block_field:ident, $blob_field:ident) => { + ($struct_name:ident, $block_type:ty, $blob_type:ty, $data_column_type:ty, $block_field:ident, $blob_field:ident, $data_column_field:ident) => { impl AvailabilityView for $struct_name { type BlockType = $block_type; type BlobType = $blob_type; + type DataColumnType = $data_column_type; fn get_cached_block(&self) -> &Option { &self.$block_field @@ -167,6 +231,12 @@ macro_rules! impl_availability_view { &self.$blob_field } + fn get_cached_data_columns( + &self, + ) -> &FixedVector, E::DataColumnCount> { + &self.$data_column_field + } + fn get_cached_block_mut(&mut self) -> &mut Option { &mut self.$block_field } @@ -176,6 +246,12 @@ macro_rules! impl_availability_view { ) -> &mut FixedVector, E::MaxBlobsPerBlock> { &mut self.$blob_field } + + fn get_cached_data_columns_mut( + &mut self, + ) -> &mut FixedVector, E::DataColumnCount> { + &mut self.$data_column_field + } } }; } @@ -184,24 +260,30 @@ impl_availability_view!( ProcessingComponents, Arc>, KzgCommitment, + (), block, - blob_commitments + blob_commitments, + data_column_opts ); impl_availability_view!( PendingComponents, DietAvailabilityPendingExecutedBlock, KzgVerifiedBlob, + KzgVerifiedDataColumn, executed_block, - verified_blobs + verified_blobs, + verified_data_columns ); impl_availability_view!( ChildComponents, Arc>, Arc>, + Arc>, downloaded_block, - downloaded_blobs + downloaded_blobs, + downloaded_data_columns ); pub trait GetCommitments { @@ -247,6 +329,7 @@ impl GetCommitments for Arc> { .unwrap_or_default() } } + impl GetCommitment for Arc> { fn get_commitment(&self) -> &KzgCommitment { &self.kzg_commitment diff --git a/beacon_node/beacon_chain/src/data_availability_checker/child_components.rs b/beacon_node/beacon_chain/src/data_availability_checker/child_components.rs index 028bf9d67c8..09cc5da9027 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker/child_components.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker/child_components.rs @@ -3,6 +3,7 @@ use crate::data_availability_checker::AvailabilityView; use bls::Hash256; use std::sync::Arc; use types::blob_sidecar::FixedBlobSidecarList; +use types::data_column_sidecar::FixedDataColumnSidecarList; use types::{EthSpec, SignedBeaconBlock}; /// For requests triggered by an `UnknownBlockParent` or `UnknownBlobParent`, this struct @@ -13,15 +14,19 @@ pub struct ChildComponents { pub block_root: Hash256, pub downloaded_block: Option>>, pub downloaded_blobs: FixedBlobSidecarList, + pub downloaded_data_columns: FixedDataColumnSidecarList, } impl From> for ChildComponents { fn from(value: RpcBlock) -> Self { - let (block_root, block, blobs) = value.deconstruct(); + let (block_root, block, blobs, data_columns) = value.deconstruct(); let fixed_blobs = blobs.map(|blobs| { FixedBlobSidecarList::from(blobs.into_iter().map(Some).collect::>()) }); - Self::new(block_root, Some(block), fixed_blobs) + let fixed_data_columns = data_columns.map(|data_columns| { + FixedDataColumnSidecarList::from(data_columns.into_iter().map(Some).collect::>()) + }); + Self::new(block_root, Some(block), fixed_blobs, fixed_data_columns) } } @@ -31,12 +36,14 @@ impl ChildComponents { block_root, downloaded_block: None, downloaded_blobs: <_>::default(), + downloaded_data_columns: <_>::default(), } } pub fn new( block_root: Hash256, block: Option>>, blobs: Option>, + data_columns: Option>, ) -> Self { let mut cache = Self::empty(block_root); if let Some(block) = block { @@ -45,6 +52,9 @@ impl ChildComponents { if let Some(blobs) = blobs { cache.merge_blobs(blobs); } + if let Some(data_columns) = data_columns { + cache.merge_data_columns(data_columns); + } cache } 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 0804fe3b9ab..9e52b34185f 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker/error.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker/error.rs @@ -14,6 +14,7 @@ pub enum Error { SszTypes(ssz_types::Error), MissingBlobs, BlobIndexInvalid(u64), + DataColumnIndexInvalid(u64), StoreError(store::Error), DecodeError(ssz::DecodeError), ParentStateMissing(Hash256), @@ -42,6 +43,7 @@ impl Error { | Error::RebuildingStateCaches(_) => ErrorCategory::Internal, Error::Kzg(_) | Error::BlobIndexInvalid(_) + | Error::DataColumnIndexInvalid(_) | Error::KzgCommitmentMismatch { .. } | Error::KzgVerificationFailed => ErrorCategory::Malicious, } diff --git a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs index 80cbc6c8990..246daf9579d 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs @@ -35,6 +35,7 @@ use crate::block_verification_types::{ }; use crate::data_availability_checker::availability_view::AvailabilityView; use crate::data_availability_checker::{Availability, AvailabilityCheckError}; +use crate::data_column_verification::KzgVerifiedDataColumn; use crate::store::{DBColumn, KeyValueStore}; use crate::BeaconChainTypes; use lru::LruCache; @@ -45,7 +46,8 @@ use ssz_types::{FixedVector, VariableList}; use std::num::NonZeroUsize; use std::{collections::HashSet, sync::Arc}; use types::blob_sidecar::BlobIdentifier; -use types::{BlobSidecar, ChainSpec, Epoch, EthSpec, Hash256}; +use types::data_column_sidecar::DataColumnIdentifier; +use types::{BlobSidecar, ChainSpec, DataColumnSidecar, Epoch, EthSpec, Hash256}; /// This represents the components of a partially available block /// @@ -55,6 +57,7 @@ use types::{BlobSidecar, ChainSpec, Epoch, EthSpec, Hash256}; pub struct PendingComponents { pub block_root: Hash256, pub verified_blobs: FixedVector>, T::MaxBlobsPerBlock>, + pub verified_data_columns: FixedVector>, T::DataColumnCount>, pub executed_block: Option>, } @@ -63,6 +66,7 @@ impl PendingComponents { Self { block_root, verified_blobs: FixedVector::default(), + verified_data_columns: FixedVector::default(), executed_block: None, } } @@ -82,6 +86,7 @@ impl PendingComponents { let Self { block_root, verified_blobs, + verified_data_columns, executed_block, } = self; @@ -100,6 +105,14 @@ impl PendingComponents { }; let verified_blobs = VariableList::new(verified_blobs)?; + // TODO(das) Do we need a check here for number of expected custody columns? + let verified_data_columns = verified_data_columns + .into_iter() + .cloned() + .filter_map(|d| d.map(|d| d.to_data_column())) + .collect::>() + .into(); + let executed_block = recover(diet_executed_block)?; let AvailabilityPendingExecutedBlock { @@ -112,6 +125,7 @@ impl PendingComponents { block_root, block, blobs: Some(verified_blobs), + data_columns: Some(verified_data_columns), }; Ok(Availability::Available(Box::new( AvailableExecutedBlock::new(available_block, import_data, payload_verification_outcome), @@ -133,6 +147,16 @@ impl PendingComponents { }); } } + for maybe_data_column in self.verified_data_columns.iter() { + if maybe_data_column.is_some() { + return maybe_data_column.as_ref().map(|kzg_verified_data_column| { + kzg_verified_data_column + .as_data_column() + .slot() + .epoch(T::slots_per_epoch()) + }); + } + } None }) } @@ -144,6 +168,7 @@ impl PendingComponents { enum OverflowKey { Block(Hash256), Blob(Hash256, u8), + DataColumn(Hash256, u8), } impl OverflowKey { @@ -160,10 +185,27 @@ impl OverflowKey { Ok(Self::Blob(blob_id.block_root, blob_id.index as u8)) } + pub fn from_data_column_id( + data_column_id: DataColumnIdentifier, + ) -> Result { + if data_column_id.index >= E::number_of_columns() as u64 + || data_column_id.index > u8::MAX as u64 + { + return Err(AvailabilityCheckError::DataColumnIndexInvalid( + data_column_id.index, + )); + } + Ok(Self::DataColumn( + data_column_id.block_root, + data_column_id.index as u8, + )) + } + pub fn root(&self) -> &Hash256 { match self { Self::Block(root) => root, Self::Blob(root, _) => root, + Self::DataColumn(root, _) => root, } } } @@ -203,6 +245,22 @@ impl OverflowStore { .put_bytes(col.as_str(), &key.as_ssz_bytes(), &blob.as_ssz_bytes())? } + for data_column in Vec::from(pending_components.verified_data_columns) + .into_iter() + .flatten() + { + let key = OverflowKey::from_data_column_id::(DataColumnIdentifier { + block_root, + index: data_column.data_column_index(), + })?; + + self.0.hot_db.put_bytes( + col.as_str(), + &key.as_ssz_bytes(), + &data_column.as_ssz_bytes(), + )? + } + Ok(()) } @@ -236,6 +294,16 @@ impl OverflowStore { .ok_or(AvailabilityCheckError::BlobIndexInvalid(index as u64))? = Some(KzgVerifiedBlob::from_ssz_bytes(value_bytes.as_slice())?); } + OverflowKey::DataColumn(_, index) => { + *maybe_pending_components + .get_or_insert_with(|| PendingComponents::empty(block_root)) + .verified_data_columns + .get_mut(index as usize) + .ok_or(AvailabilityCheckError::DataColumnIndexInvalid(index as u64))? = + Some(KzgVerifiedDataColumn::from_ssz_bytes( + value_bytes.as_slice(), + )?); + } } } @@ -267,6 +335,23 @@ impl OverflowStore { .map_err(|e| e.into()) } + /// Load a single data column from the database + pub fn load_data_column( + &self, + data_column_id: &DataColumnIdentifier, + ) -> Result>>, AvailabilityCheckError> { + let key = OverflowKey::from_data_column_id::(*data_column_id)?; + + self.0 + .hot_db + .get_bytes(DBColumn::OverflowLRUCache.as_str(), &key.as_ssz_bytes())? + .map(|data_column_bytes| { + Arc::>::from_ssz_bytes(data_column_bytes.as_slice()) + }) + .transpose() + .map_err(|e| e.into()) + } + /// Delete a set of keys from the database pub fn delete_keys(&self, keys: &Vec) -> Result<(), AvailabilityCheckError> { for key in keys { @@ -322,6 +407,25 @@ impl Critical { } } + /// This only checks for the data columns in memory + pub fn peek_data_column( + &self, + data_column_id: &DataColumnIdentifier, + ) -> Result>>, AvailabilityCheckError> { + if let Some(pending_components) = self.in_memory.peek(&data_column_id.block_root) { + Ok(pending_components + .verified_data_columns + .get(data_column_id.index as usize) + .ok_or(AvailabilityCheckError::DataColumnIndexInvalid( + data_column_id.index, + ))? + .as_ref() + .map(|data_column| data_column.clone_data_column())) + } else { + Ok(None) + } + } + /// Puts the pending components in the LRU cache. If the cache /// is at capacity, the LRU entry is written to the store first pub fn put_pending_components( @@ -425,6 +529,55 @@ impl OverflowLRUCache { } } + /// Fetch a data column from the cache without affecting the LRU ordering + pub fn peek_data_column( + &self, + data_column_id: &DataColumnIdentifier, + ) -> Result>>, AvailabilityCheckError> { + let read_lock = self.critical.read(); + if let Some(data_column) = read_lock.peek_data_column(data_column_id)? { + Ok(Some(data_column)) + } else if read_lock.store_keys.contains(&data_column_id.block_root) { + drop(read_lock); + self.overflow_store.load_data_column(data_column_id) + } else { + Ok(None) + } + } + + pub fn put_kzg_verified_data_columns< + I: IntoIterator>, + >( + &self, + block_root: Hash256, + kzg_verified_data_columns: I, + ) -> Result, AvailabilityCheckError> { + let mut fixed_data_columns = FixedVector::default(); + + for data_column in kzg_verified_data_columns { + if let Some(data_column_opt) = + fixed_data_columns.get_mut(data_column.data_column_index() as usize) + { + *data_column_opt = Some(data_column); + } + } + + let mut write_lock = self.critical.write(); + + // Grab existing entry or create a new entry. + let mut pending_components = write_lock + .pop_pending_components(block_root, &self.overflow_store)? + .unwrap_or_else(|| PendingComponents::empty(block_root)); + + // Merge in the data columns. + pending_components.merge_data_columns(fixed_data_columns); + + write_lock.put_pending_components(block_root, pending_components, &self.overflow_store)?; + + // TODO(das): Currently this does not change availability status and nor import yet. + Ok(Availability::MissingComponents(block_root)) + } + pub fn put_kzg_verified_blobs>>( &self, block_root: Hash256, @@ -660,6 +813,14 @@ impl OverflowLRUCache { .slot() .epoch(T::EthSpec::slots_per_epoch()) } + OverflowKey::DataColumn(_, _) => { + KzgVerifiedDataColumn::::from_ssz_bytes( + value_bytes.as_slice(), + )? + .as_data_column() + .slot() + .epoch(T::EthSpec::slots_per_epoch()) + } }; current_block_data = Some(BlockData { keys: vec![overflow_key], @@ -713,6 +874,10 @@ impl ssz::Encode for OverflowKey { block_hash.ssz_append(buf); buf.push(*index + 1) } + OverflowKey::DataColumn(block_hash, index) => { + block_hash.ssz_append(buf); + buf.push(*index + 1) + } } } @@ -724,6 +889,7 @@ impl ssz::Encode for OverflowKey { match self { Self::Block(root) => root.ssz_bytes_len() + 1, Self::Blob(root, _) => root.ssz_bytes_len() + 1, + Self::DataColumn(root, _) => root.ssz_bytes_len() + 1, } } } diff --git a/beacon_node/beacon_chain/src/data_availability_checker/processing_cache.rs b/beacon_node/beacon_chain/src/data_availability_checker/processing_cache.rs index af94803dcfb..7abbd700104 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker/processing_cache.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker/processing_cache.rs @@ -1,4 +1,5 @@ use crate::data_availability_checker::AvailabilityView; +use ssz_types::FixedVector; use std::collections::hash_map::Entry; use std::collections::HashMap; use std::sync::Arc; @@ -53,6 +54,9 @@ pub struct ProcessingComponents { /// `KzgCommitments` for blobs are always known, even if we haven't seen the block. See /// `AvailabilityView`'s trait definition for more details. pub blob_commitments: KzgCommitmentOpts, + // TODO(das): `KzgCommitments` are available in every data column sidecar, hence it may not be useful to store them + // again here and a `()` may be sufficient to indicate what we have. + pub data_column_opts: FixedVector, E::DataColumnCount>, } impl ProcessingComponents { @@ -61,6 +65,7 @@ impl ProcessingComponents { slot, block: None, blob_commitments: KzgCommitmentOpts::::default(), + data_column_opts: FixedVector::default(), } } } @@ -73,6 +78,7 @@ impl ProcessingComponents { slot: Slot::new(0), block: None, blob_commitments: KzgCommitmentOpts::::default(), + data_column_opts: FixedVector::default(), } } } diff --git a/beacon_node/beacon_chain/src/data_column_verification.rs b/beacon_node/beacon_chain/src/data_column_verification.rs new file mode 100644 index 00000000000..2ea8fe1ae4f --- /dev/null +++ b/beacon_node/beacon_chain/src/data_column_verification.rs @@ -0,0 +1,218 @@ +use crate::block_verification::{process_block_slash_info, BlockSlashInfo}; +use crate::{BeaconChain, BeaconChainError, BeaconChainTypes}; +use derivative::Derivative; +use kzg::{Error as KzgError, Kzg}; +use ssz_derive::{Decode, Encode}; +use std::sync::Arc; +use types::data_column_sidecar::{ColumnIndex, DataColumnIdentifier}; +use types::{BeaconStateError, DataColumnSidecar, EthSpec, Hash256, SignedBeaconBlockHeader, Slot}; + +/// An error occurred while validating a gossip data column. +#[derive(Debug)] +pub enum GossipDataColumnError { + /// There was an error whilst processing the data column. It is not known if it is + /// valid or invalid. + /// + /// ## Peer scoring + /// + /// We were unable to process this data column due to an internal error. It's + /// unclear if the data column is valid. + BeaconChainError(BeaconChainError), + + /// The proposal signature in invalid. + /// + /// ## Peer scoring + /// + /// The data column is invalid and the peer is faulty. + ProposalSignatureInvalid, + + /// The proposal_index corresponding to data column.beacon_block_root is not known. + /// + /// ## Peer scoring + /// + /// The data column is invalid and the peer is faulty. + UnknownValidator(u64), + + /// The provided data column is not from a later slot than its parent. + /// + /// ## Peer scoring + /// + /// The data column is invalid and the peer is faulty. + DataColumnIsNotLaterThanParent { + data_column_slot: Slot, + parent_slot: Slot, + }, + + /// `Kzg` struct hasn't been initialized. This is an internal error. + /// + /// ## Peer scoring + /// + /// The peer isn't faulty, This is an internal error. + KzgNotInitialized, + + /// The kzg verification failed. + /// + /// ## Peer scoring + /// + /// The data column sidecar is invalid and the peer is faulty. + KzgError(kzg::Error), + + /// The provided data column's parent block is unknown. + /// + /// ## Peer scoring + /// + /// We cannot process the data column without validating its parent, the peer isn't necessarily faulty. + DataColumnParentUnknown(Arc>), +} + +impl From for GossipDataColumnError { + fn from(e: BeaconChainError) -> Self { + GossipDataColumnError::BeaconChainError(e) + } +} + +impl From for GossipDataColumnError { + fn from(e: BeaconStateError) -> Self { + GossipDataColumnError::BeaconChainError(BeaconChainError::BeaconStateError(e)) + } +} + +/// A wrapper around a `DataColumnSidecar` that indicates it has been approved for re-gossiping on +/// the p2p network. +#[derive(Debug)] +pub struct GossipVerifiedDataColumn { + block_root: Hash256, + data_column: KzgVerifiedDataColumn, +} + +impl GossipVerifiedDataColumn { + pub fn new( + column_sidecar: Arc>, + subnet_id: u64, + chain: &BeaconChain, + ) -> Result> { + let header = column_sidecar.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(column_sidecar, subnet_id, chain).map_err(|e| { + process_block_slash_info::<_, GossipDataColumnError>( + chain, + BlockSlashInfo::from_early_error_data_column(header, e), + ) + }) + } + + pub fn id(&self) -> DataColumnIdentifier { + DataColumnIdentifier { + block_root: self.block_root, + index: self.data_column.data_column_index(), + } + } + + pub fn as_data_column(&self) -> &DataColumnSidecar { + self.data_column.as_data_column() + } + + pub fn block_root(&self) -> Hash256 { + self.block_root + } + + pub fn slot(&self) -> Slot { + self.data_column.data_column.slot() + } + + pub fn index(&self) -> ColumnIndex { + self.data_column.data_column.index + } + + pub fn signed_block_header(&self) -> SignedBeaconBlockHeader { + self.data_column.data_column.signed_block_header.clone() + } + + pub fn into_inner(self) -> KzgVerifiedDataColumn { + self.data_column + } +} + +/// Wrapper over a `DataColumnSidecar` for which we have completed kzg verification. +#[derive(Debug, Derivative, Clone, Encode, Decode)] +#[derivative(PartialEq, Eq)] +#[ssz(struct_behaviour = "transparent")] +pub struct KzgVerifiedDataColumn { + data_column: Arc>, +} + +impl KzgVerifiedDataColumn { + pub fn new(data_column: Arc>, kzg: &Kzg) -> Result { + verify_kzg_for_data_column(data_column, kzg) + } + pub fn to_data_column(self) -> Arc> { + self.data_column + } + pub fn as_data_column(&self) -> &DataColumnSidecar { + &self.data_column + } + /// This is cheap as we're calling clone on an Arc + pub fn clone_data_column(&self) -> Arc> { + self.data_column.clone() + } + + pub fn data_column_index(&self) -> u64 { + self.data_column.index + } +} + +/// Complete kzg verification for a `DataColumnSidecar`. +/// +/// Returns an error if the kzg verification check fails. +pub fn verify_kzg_for_data_column( + data_column: Arc>, + _kzg: &Kzg, +) -> Result, KzgError> { + // TODO(das): validate data column + // validate_blob::( + // kzg, + // &data_column.blob, + // data_column.kzg_commitment, + // data_column.kzg_proof, + // )?; + Ok(KzgVerifiedDataColumn { data_column }) +} + +/// Complete kzg verification for a list of `DataColumnSidecar`s. +/// Returns an error if any of the `DataColumnSidecar`s fails kzg verification. +/// +/// Note: This function should be preferred over calling `verify_kzg_for_data_column` +/// in a loop since this function kzg verifies a list of data columns more efficiently. +pub fn verify_kzg_for_data_column_list<'a, T: EthSpec, I>( + _data_column_iter: I, + _kzg: &'a Kzg, +) -> Result<(), KzgError> +where + I: Iterator>>, +{ + // TODO(das): implement kzg verification + Ok(()) +} + +pub fn validate_data_column_sidecar_for_gossip( + data_column: Arc>, + _subnet: u64, + chain: &BeaconChain, +) -> Result, GossipDataColumnError> { + // TODO(das): validate gossip rules + let block_root = data_column.block_root(); + + // Kzg verification for gossip data column sidecar + let kzg = chain + .kzg + .as_ref() + .ok_or(GossipDataColumnError::KzgNotInitialized)?; + let kzg_verified_data_column = + KzgVerifiedDataColumn::new(data_column, kzg).map_err(GossipDataColumnError::KzgError)?; + + Ok(GossipVerifiedDataColumn { + block_root, + data_column: kzg_verified_data_column, + }) +} diff --git a/beacon_node/beacon_chain/src/early_attester_cache.rs b/beacon_node/beacon_chain/src/early_attester_cache.rs index da3c2c8a1e9..50e5578c5fc 100644 --- a/beacon_node/beacon_chain/src/early_attester_cache.rs +++ b/beacon_node/beacon_chain/src/early_attester_cache.rs @@ -7,6 +7,7 @@ use parking_lot::RwLock; use proto_array::Block as ProtoBlock; use std::sync::Arc; use types::blob_sidecar::BlobSidecarList; +use types::data_column_sidecar::DataColumnSidecarList; use types::*; pub struct CacheItem { @@ -23,6 +24,7 @@ pub struct CacheItem { */ block: Arc>, blobs: Option>, + data_columns: Option>, proto_block: ProtoBlock, } @@ -70,7 +72,7 @@ impl EarlyAttesterCache { }, }; - let (_, block, blobs) = block.deconstruct(); + let (_, block, blobs, data_columns) = block.deconstruct(); let item = CacheItem { epoch, committee_lengths, @@ -79,6 +81,7 @@ impl EarlyAttesterCache { target, block, blobs, + data_columns, proto_block, }; @@ -167,6 +170,15 @@ impl EarlyAttesterCache { .and_then(|item| item.blobs.clone()) } + /// Returns the data columns, if `block_root` matches the cached item. + pub fn get_data_columns(&self, block_root: Hash256) -> Option> { + self.item + .read() + .as_ref() + .filter(|item| item.beacon_block_root == block_root) + .and_then(|item| item.data_columns.clone()) + } + /// Returns the proto-array block, if `block_root` matches the cached item. pub fn get_proto_block(&self, block_root: Hash256) -> Option { self.item diff --git a/beacon_node/beacon_chain/src/historical_blocks.rs b/beacon_node/beacon_chain/src/historical_blocks.rs index 85208c8ad6f..526027b998f 100644 --- a/beacon_node/beacon_chain/src/historical_blocks.rs +++ b/beacon_node/beacon_chain/src/historical_blocks.rs @@ -9,6 +9,7 @@ use state_processing::{ use std::borrow::Cow; use std::iter; use std::time::Duration; +use store::metadata::DataColumnInfo; use store::{chunked_vector::BlockRoots, AnchorInfo, BlobInfo, ChunkWriter, KeyValueStore}; use types::{Hash256, Slot}; @@ -66,6 +67,7 @@ impl BeaconChain { .get_anchor_info() .ok_or(HistoricalBlockError::NoAnchorInfo)?; let blob_info = self.store.get_blob_info(); + let data_column_info = self.store.get_data_column_info(); // Take all blocks with slots less than the oldest block slot. let num_relevant = blocks.partition_point(|available_block| { @@ -100,6 +102,7 @@ impl BeaconChain { let mut chunk_writer = ChunkWriter::::new(&self.store.cold_db, prev_block_slot.as_usize())?; let mut new_oldest_blob_slot = blob_info.oldest_blob_slot; + let mut new_oldest_data_column_slot = data_column_info.oldest_data_column_slot; let mut blob_batch = Vec::with_capacity(n_blobs_lists_to_import); let mut cold_batch = Vec::with_capacity(blocks_to_import.len()); @@ -107,7 +110,8 @@ impl BeaconChain { let mut signed_blocks = Vec::with_capacity(blocks_to_import.len()); for available_block in blocks_to_import.into_iter().rev() { - let (block_root, block, maybe_blobs) = available_block.deconstruct(); + let (block_root, block, maybe_blobs, maybe_data_columns) = + available_block.deconstruct(); if block_root != expected_block_root { return Err(HistoricalBlockError::MismatchedBlockRoot { @@ -127,6 +131,12 @@ impl BeaconChain { self.store .blobs_as_kv_store_ops(&block_root, blobs, &mut blob_batch); } + // Store the data columns too + if let Some(data_columns) = maybe_data_columns { + new_oldest_data_column_slot = Some(block.slot()); + self.store + .data_columns_as_kv_store_ops(&block_root, data_columns, &mut blob_batch); + } // Store block roots, including at all skip slots in the freezer DB. for slot in (block.slot().as_usize()..prev_block_slot.as_usize()).rev() { @@ -220,6 +230,19 @@ impl BeaconChain { } } + // Update the data column info. + if new_oldest_data_column_slot != data_column_info.oldest_data_column_slot { + if let Some(oldest_data_column_slot) = new_oldest_data_column_slot { + let new_data_column_info = DataColumnInfo { + oldest_data_column_slot: Some(oldest_data_column_slot), + }; + anchor_and_blob_batch.push( + self.store + .compare_and_set_data_column_info(data_column_info, new_data_column_info)?, + ); + } + } + // Update the anchor. let new_anchor = AnchorInfo { oldest_block_slot: prev_block_slot, diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index 522009b1b27..20023b2f299 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -18,6 +18,7 @@ pub mod canonical_head; pub mod capella_readiness; pub mod chain_config; pub mod data_availability_checker; +pub mod data_column_verification; pub mod deneb_readiness; mod early_attester_cache; mod errors; diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index ff201729821..cec5f22af5a 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -2569,10 +2569,10 @@ async fn weak_subjectivity_sync_test(slots: Vec, checkpoint_slot: Slot) { // signatures correctly. Regression test for https://github.com/sigp/lighthouse/pull/5120. let mut batch_with_invalid_first_block = available_blocks.clone(); batch_with_invalid_first_block[0] = { - let (block_root, block, blobs) = available_blocks[0].clone().deconstruct(); + let (block_root, block, blobs, data_columns) = available_blocks[0].clone().deconstruct(); let mut corrupt_block = (*block).clone(); *corrupt_block.signature_mut() = Signature::empty(); - AvailableBlock::__new_for_testing(block_root, Arc::new(corrupt_block), blobs) + AvailableBlock::__new_for_testing(block_root, Arc::new(corrupt_block), blobs, data_columns) }; // Importing the invalid batch should error. diff --git a/beacon_node/beacon_processor/src/lib.rs b/beacon_node/beacon_processor/src/lib.rs index 8b1c127d300..5ac1aaac4c7 100644 --- a/beacon_node/beacon_processor/src/lib.rs +++ b/beacon_node/beacon_processor/src/lib.rs @@ -181,6 +181,10 @@ const MAX_BLOCKS_BY_ROOTS_QUEUE_LEN: usize = 1_024; /// will be stored before we start dropping them. const MAX_BLOBS_BY_ROOTS_QUEUE_LEN: usize = 1_024; +/// The maximum number of queued `DataColumnsByRootRequest` objects received from the network RPC that +/// will be stored before we start dropping them. +const MAX_DATA_COLUMNS_BY_ROOTS_QUEUE_LEN: usize = 2_048; + /// Maximum number of `SignedBlsToExecutionChange` messages to queue before dropping them. /// /// This value is set high to accommodate the large spike that is expected immediately after Capella @@ -247,6 +251,7 @@ pub const BLOCKS_BY_RANGE_REQUEST: &str = "blocks_by_range_request"; pub const BLOCKS_BY_ROOTS_REQUEST: &str = "blocks_by_roots_request"; pub const BLOBS_BY_RANGE_REQUEST: &str = "blobs_by_range_request"; pub const BLOBS_BY_ROOTS_REQUEST: &str = "blobs_by_roots_request"; +pub const DATA_COLUMNS_BY_ROOTS_REQUEST: &str = "data_columns_by_roots_request"; pub const LIGHT_CLIENT_BOOTSTRAP_REQUEST: &str = "light_client_bootstrap"; pub const UNKNOWN_BLOCK_ATTESTATION: &str = "unknown_block_attestation"; pub const UNKNOWN_BLOCK_AGGREGATE: &str = "unknown_block_aggregate"; @@ -624,6 +629,7 @@ pub enum Work { BlocksByRootsRequest(BlockingFnWithManualSendOnIdle), BlobsByRangeRequest(BlockingFn), BlobsByRootsRequest(BlockingFn), + DataColumnsByRootsRequest(BlockingFn), GossipBlsToExecutionChange(BlockingFn), LightClientBootstrapRequest(BlockingFn), ApiRequestP0(BlockingOrAsync), @@ -665,6 +671,7 @@ impl Work { Work::BlocksByRootsRequest(_) => BLOCKS_BY_ROOTS_REQUEST, Work::BlobsByRangeRequest(_) => BLOBS_BY_RANGE_REQUEST, Work::BlobsByRootsRequest(_) => BLOBS_BY_ROOTS_REQUEST, + Work::DataColumnsByRootsRequest(_) => DATA_COLUMNS_BY_ROOTS_REQUEST, Work::LightClientBootstrapRequest(_) => LIGHT_CLIENT_BOOTSTRAP_REQUEST, Work::UnknownBlockAttestation { .. } => UNKNOWN_BLOCK_ATTESTATION, Work::UnknownBlockAggregate { .. } => UNKNOWN_BLOCK_AGGREGATE, @@ -824,6 +831,7 @@ impl BeaconProcessor { let mut bbroots_queue = FifoQueue::new(MAX_BLOCKS_BY_ROOTS_QUEUE_LEN); let mut blbroots_queue = FifoQueue::new(MAX_BLOBS_BY_ROOTS_QUEUE_LEN); let mut blbrange_queue = FifoQueue::new(MAX_BLOBS_BY_RANGE_QUEUE_LEN); + let mut dcbroots_queue = FifoQueue::new(MAX_DATA_COLUMNS_BY_ROOTS_QUEUE_LEN); let mut gossip_bls_to_execution_change_queue = FifoQueue::new(MAX_BLS_TO_EXECUTION_CHANGE_QUEUE_LEN); @@ -1123,6 +1131,8 @@ impl BeaconProcessor { self.spawn_worker(item, idle_tx); } else if let Some(item) = blbroots_queue.pop() { self.spawn_worker(item, idle_tx); + } else if let Some(item) = dcbroots_queue.pop() { + self.spawn_worker(item, idle_tx); // Check slashings after all other consensus messages so we prioritize // following head. // @@ -1276,6 +1286,9 @@ impl BeaconProcessor { Work::BlobsByRootsRequest { .. } => { blbroots_queue.push(work, work_id, &self.log) } + Work::DataColumnsByRootsRequest { .. } => { + dcbroots_queue.push(work, work_id, &self.log) + } Work::UnknownLightClientOptimisticUpdate { .. } => { unknown_light_client_update_queue.push(work, work_id, &self.log) } @@ -1477,7 +1490,9 @@ impl BeaconProcessor { | Work::GossipDataColumnSidecar(work) => task_spawner.spawn_async(async move { work.await; }), - Work::BlobsByRangeRequest(process_fn) | Work::BlobsByRootsRequest(process_fn) => { + Work::BlobsByRangeRequest(process_fn) + | Work::BlobsByRootsRequest(process_fn) + | Work::DataColumnsByRootsRequest(process_fn) => { task_spawner.spawn_blocking(process_fn) } Work::BlocksByRangeRequest(work) | Work::BlocksByRootsRequest(work) => { diff --git a/beacon_node/lighthouse_network/src/peer_manager/mod.rs b/beacon_node/lighthouse_network/src/peer_manager/mod.rs index 7c126fe1b9d..32a7e06868c 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/mod.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/mod.rs @@ -530,7 +530,10 @@ impl PeerManager { RPCResponseErrorCode::Unknown => PeerAction::HighToleranceError, RPCResponseErrorCode::ResourceUnavailable => { // Don't ban on this because we want to retry with a block by root request. - if matches!(protocol, Protocol::BlobsByRoot) { + if matches!( + protocol, + Protocol::BlobsByRoot | Protocol::DataColumnsByRoot + ) { return; } @@ -563,8 +566,9 @@ impl PeerManager { Protocol::BlocksByRange => PeerAction::MidToleranceError, Protocol::BlocksByRoot => PeerAction::MidToleranceError, Protocol::BlobsByRange => PeerAction::MidToleranceError, - Protocol::LightClientBootstrap => PeerAction::LowToleranceError, Protocol::BlobsByRoot => PeerAction::MidToleranceError, + Protocol::DataColumnsByRoot => PeerAction::MidToleranceError, + Protocol::LightClientBootstrap => PeerAction::LowToleranceError, Protocol::Goodbye => PeerAction::LowToleranceError, Protocol::MetaData => PeerAction::LowToleranceError, Protocol::Status => PeerAction::LowToleranceError, @@ -583,6 +587,7 @@ impl PeerManager { Protocol::BlocksByRoot => return, Protocol::BlobsByRange => return, Protocol::BlobsByRoot => return, + Protocol::DataColumnsByRoot => return, Protocol::Goodbye => return, Protocol::LightClientBootstrap => return, Protocol::MetaData => PeerAction::Fatal, @@ -601,6 +606,7 @@ impl PeerManager { Protocol::BlocksByRoot => PeerAction::MidToleranceError, Protocol::BlobsByRange => PeerAction::MidToleranceError, Protocol::BlobsByRoot => PeerAction::MidToleranceError, + Protocol::DataColumnsByRoot => PeerAction::MidToleranceError, Protocol::LightClientBootstrap => return, Protocol::Goodbye => return, Protocol::MetaData => return, diff --git a/beacon_node/lighthouse_network/src/rpc/codec/ssz_snappy.rs b/beacon_node/lighthouse_network/src/rpc/codec/ssz_snappy.rs index 7a7f2969f16..0fb90c5d36d 100644 --- a/beacon_node/lighthouse_network/src/rpc/codec/ssz_snappy.rs +++ b/beacon_node/lighthouse_network/src/rpc/codec/ssz_snappy.rs @@ -15,12 +15,12 @@ use std::io::{Read, Write}; use std::marker::PhantomData; use std::sync::Arc; use tokio_util::codec::{Decoder, Encoder}; -use types::ChainSpec; use types::{ BlobSidecar, EthSpec, ForkContext, ForkName, Hash256, LightClientBootstrap, RuntimeVariableList, SignedBeaconBlock, SignedBeaconBlockAltair, SignedBeaconBlockBase, SignedBeaconBlockCapella, SignedBeaconBlockDeneb, SignedBeaconBlockMerge, }; +use types::{ChainSpec, DataColumnSidecar}; use unsigned_varint::codec::Uvi; const CONTEXT_BYTES_LEN: usize = 4; @@ -74,6 +74,7 @@ impl Encoder> for SSZSnappyInboundCodec< RPCResponse::BlocksByRoot(res) => res.as_ssz_bytes(), RPCResponse::BlobsByRange(res) => res.as_ssz_bytes(), RPCResponse::BlobsByRoot(res) => res.as_ssz_bytes(), + RPCResponse::DataColumnsByRoot(res) => res.as_ssz_bytes(), RPCResponse::LightClientBootstrap(res) => res.as_ssz_bytes(), RPCResponse::Pong(res) => res.data.as_ssz_bytes(), RPCResponse::MetaData(res) => @@ -230,6 +231,7 @@ impl Encoder> for SSZSnappyOutboundCodec< }, OutboundRequest::BlobsByRange(req) => req.as_ssz_bytes(), OutboundRequest::BlobsByRoot(req) => req.blob_ids.as_ssz_bytes(), + OutboundRequest::DataColumnsByRoot(req) => req.data_column_ids.as_ssz_bytes(), OutboundRequest::Ping(req) => req.as_ssz_bytes(), OutboundRequest::MetaData(_) => return Ok(()), // no metadata to encode }; @@ -498,6 +500,14 @@ fn handle_rpc_request( )?, }))) } + SupportedProtocol::DataColumnsByRootV1 => Ok(Some(InboundRequest::DataColumnsByRoot( + DataColumnsByRootRequest { + data_column_ids: RuntimeVariableList::from_ssz_bytes( + decoded_buffer, + spec.max_request_data_column_sidecars as usize, + )?, + }, + ))), SupportedProtocol::PingV1 => Ok(Some(InboundRequest::Ping(Ping { data: u64::from_ssz_bytes(decoded_buffer)?, }))), @@ -584,6 +594,23 @@ fn handle_rpc_response( ), )), }, + SupportedProtocol::DataColumnsByRootV1 => match fork_name { + // TODO(das): update fork name + Some(ForkName::Deneb) => Ok(Some(RPCResponse::DataColumnsByRoot(Arc::new( + DataColumnSidecar::from_ssz_bytes(decoded_buffer)?, + )))), + Some(_) => Err(RPCError::ErrorResponse( + RPCResponseErrorCode::InvalidRequest, + "Invalid fork name for data columns by root".to_string(), + )), + None => Err(RPCError::ErrorResponse( + RPCResponseErrorCode::InvalidRequest, + format!( + "No context bytes provided for {:?} response", + versioned_protocol + ), + )), + }, SupportedProtocol::PingV1 => Ok(Some(RPCResponse::Pong(Ping { data: u64::from_ssz_bytes(decoded_buffer)?, }))), @@ -945,6 +972,9 @@ mod tests { OutboundRequest::BlobsByRoot(bbroot) => { assert_eq!(decoded, InboundRequest::BlobsByRoot(bbroot)) } + OutboundRequest::DataColumnsByRoot(dcbroot) => { + assert_eq!(decoded, InboundRequest::DataColumnsByRoot(dcbroot)) + } OutboundRequest::Ping(ping) => { assert_eq!(decoded, InboundRequest::Ping(ping)) } diff --git a/beacon_node/lighthouse_network/src/rpc/config.rs b/beacon_node/lighthouse_network/src/rpc/config.rs index 9895149198a..ea780e1dff7 100644 --- a/beacon_node/lighthouse_network/src/rpc/config.rs +++ b/beacon_node/lighthouse_network/src/rpc/config.rs @@ -91,6 +91,7 @@ pub struct RateLimiterConfig { pub(super) blocks_by_root_quota: Quota, pub(super) blobs_by_range_quota: Quota, pub(super) blobs_by_root_quota: Quota, + pub(super) data_columns_by_root_quota: Quota, pub(super) light_client_bootstrap_quota: Quota, } @@ -103,6 +104,7 @@ impl RateLimiterConfig { pub const DEFAULT_BLOCKS_BY_ROOT_QUOTA: Quota = Quota::n_every(128, 10); pub const DEFAULT_BLOBS_BY_RANGE_QUOTA: Quota = Quota::n_every(768, 10); pub const DEFAULT_BLOBS_BY_ROOT_QUOTA: Quota = Quota::n_every(128, 10); + pub const DEFAULT_DATA_COLUMNS_BY_ROOT_QUOTA: Quota = Quota::n_every(128, 10); pub const DEFAULT_LIGHT_CLIENT_BOOTSTRAP_QUOTA: Quota = Quota::one_every(10); } @@ -117,6 +119,7 @@ impl Default for RateLimiterConfig { blocks_by_root_quota: Self::DEFAULT_BLOCKS_BY_ROOT_QUOTA, blobs_by_range_quota: Self::DEFAULT_BLOBS_BY_RANGE_QUOTA, blobs_by_root_quota: Self::DEFAULT_BLOBS_BY_ROOT_QUOTA, + data_columns_by_root_quota: Self::DEFAULT_DATA_COLUMNS_BY_ROOT_QUOTA, light_client_bootstrap_quota: Self::DEFAULT_LIGHT_CLIENT_BOOTSTRAP_QUOTA, } } @@ -143,6 +146,10 @@ impl Debug for RateLimiterConfig { .field("blocks_by_root", fmt_q!(&self.blocks_by_root_quota)) .field("blobs_by_range", fmt_q!(&self.blobs_by_range_quota)) .field("blobs_by_root", fmt_q!(&self.blobs_by_root_quota)) + .field( + "data_columns_by_root", + fmt_q!(&self.data_columns_by_root_quota), + ) .finish() } } @@ -163,6 +170,7 @@ impl FromStr for RateLimiterConfig { let mut blocks_by_root_quota = None; let mut blobs_by_range_quota = None; let mut blobs_by_root_quota = None; + let mut data_columns_by_root_quota = None; let mut light_client_bootstrap_quota = None; for proto_def in s.split(';') { @@ -175,6 +183,9 @@ impl FromStr for RateLimiterConfig { Protocol::BlocksByRoot => blocks_by_root_quota = blocks_by_root_quota.or(quota), Protocol::BlobsByRange => blobs_by_range_quota = blobs_by_range_quota.or(quota), Protocol::BlobsByRoot => blobs_by_root_quota = blobs_by_root_quota.or(quota), + Protocol::DataColumnsByRoot => { + data_columns_by_root_quota = data_columns_by_root_quota.or(quota) + } Protocol::Ping => ping_quota = ping_quota.or(quota), Protocol::MetaData => meta_data_quota = meta_data_quota.or(quota), Protocol::LightClientBootstrap => { @@ -194,6 +205,8 @@ impl FromStr for RateLimiterConfig { blobs_by_range_quota: blobs_by_range_quota .unwrap_or(Self::DEFAULT_BLOBS_BY_RANGE_QUOTA), blobs_by_root_quota: blobs_by_root_quota.unwrap_or(Self::DEFAULT_BLOBS_BY_ROOT_QUOTA), + data_columns_by_root_quota: data_columns_by_root_quota + .unwrap_or(Self::DEFAULT_DATA_COLUMNS_BY_ROOT_QUOTA), light_client_bootstrap_quota: light_client_bootstrap_quota .unwrap_or(Self::DEFAULT_LIGHT_CLIENT_BOOTSTRAP_QUOTA), }) diff --git a/beacon_node/lighthouse_network/src/rpc/methods.rs b/beacon_node/lighthouse_network/src/rpc/methods.rs index cd3579ad6e3..efc80d55faa 100644 --- a/beacon_node/lighthouse_network/src/rpc/methods.rs +++ b/beacon_node/lighthouse_network/src/rpc/methods.rs @@ -12,9 +12,10 @@ use std::sync::Arc; use strum::IntoStaticStr; use superstruct::superstruct; use types::blob_sidecar::BlobIdentifier; +use types::data_column_sidecar::DataColumnIdentifier; use types::{ - blob_sidecar::BlobSidecar, ChainSpec, Epoch, EthSpec, Hash256, LightClientBootstrap, - RuntimeVariableList, SignedBeaconBlock, Slot, + blob_sidecar::BlobSidecar, ChainSpec, DataColumnSidecar, Epoch, EthSpec, Hash256, + LightClientBootstrap, RuntimeVariableList, SignedBeaconBlock, Slot, }; /// Maximum length of error message. @@ -366,6 +367,13 @@ impl BlobsByRootRequest { } } +/// Request a number of data columns from a peer. +#[derive(Clone, Debug, PartialEq)] +pub struct DataColumnsByRootRequest { + /// The list of beacon block roots and column indices being requested. + pub data_column_ids: RuntimeVariableList, +} + /* RPC Handling and Grouping */ // Collection of enums and structs used by the Codecs to encode/decode RPC messages @@ -390,6 +398,9 @@ pub enum RPCResponse { /// A response to a get BLOBS_BY_ROOT request. BlobsByRoot(Arc>), + /// A response to a get DATA_COLUMN_SIDECARS_BY_ROOT request. + DataColumnsByRoot(Arc>), + /// A PONG response to a PING request. Pong(Ping), @@ -411,6 +422,9 @@ pub enum ResponseTermination { /// Blobs by root stream termination. BlobsByRoot, + + /// Data column sidecars by root stream termination. + DataColumnsByRoot, } /// The structured response containing a result/code indicating success or failure @@ -482,6 +496,7 @@ impl RPCCodedResponse { RPCResponse::BlocksByRoot(_) => true, RPCResponse::BlobsByRange(_) => true, RPCResponse::BlobsByRoot(_) => true, + RPCResponse::DataColumnsByRoot(_) => true, RPCResponse::Pong(_) => false, RPCResponse::MetaData(_) => false, RPCResponse::LightClientBootstrap(_) => false, @@ -520,6 +535,7 @@ impl RPCResponse { RPCResponse::BlocksByRoot(_) => Protocol::BlocksByRoot, RPCResponse::BlobsByRange(_) => Protocol::BlobsByRange, RPCResponse::BlobsByRoot(_) => Protocol::BlobsByRoot, + RPCResponse::DataColumnsByRoot(_) => Protocol::DataColumnsByRoot, RPCResponse::Pong(_) => Protocol::Ping, RPCResponse::MetaData(_) => Protocol::MetaData, RPCResponse::LightClientBootstrap(_) => Protocol::LightClientBootstrap, @@ -563,6 +579,9 @@ impl std::fmt::Display for RPCResponse { RPCResponse::BlobsByRoot(sidecar) => { write!(f, "BlobsByRoot: Blob slot: {}", sidecar.slot()) } + RPCResponse::DataColumnsByRoot(sidecar) => { + write!(f, "DataColumnsByRoot: Data column slot: {}", sidecar.slot()) + } RPCResponse::Pong(ping) => write!(f, "Pong: {}", ping.data), RPCResponse::MetaData(metadata) => write!(f, "Metadata: {}", metadata.seq_number()), RPCResponse::LightClientBootstrap(bootstrap) => { @@ -645,6 +664,16 @@ impl std::fmt::Display for BlobsByRangeRequest { } } +impl std::fmt::Display for DataColumnsByRootRequest { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Request: DataColumnsByRoot: Number of Requested Data Column Ids: {}", + self.data_column_ids.len() + ) + } +} + impl slog::KV for StatusMessage { fn serialize( &self, diff --git a/beacon_node/lighthouse_network/src/rpc/mod.rs b/beacon_node/lighthouse_network/src/rpc/mod.rs index e22e5273866..a32db20441a 100644 --- a/beacon_node/lighthouse_network/src/rpc/mod.rs +++ b/beacon_node/lighthouse_network/src/rpc/mod.rs @@ -414,6 +414,7 @@ where ResponseTermination::BlocksByRoot => Protocol::BlocksByRoot, ResponseTermination::BlobsByRange => Protocol::BlobsByRange, ResponseTermination::BlobsByRoot => Protocol::BlobsByRoot, + ResponseTermination::DataColumnsByRoot => Protocol::DataColumnsByRoot, }, ), }; diff --git a/beacon_node/lighthouse_network/src/rpc/outbound.rs b/beacon_node/lighthouse_network/src/rpc/outbound.rs index 713e9e0ec9d..89762fc623d 100644 --- a/beacon_node/lighthouse_network/src/rpc/outbound.rs +++ b/beacon_node/lighthouse_network/src/rpc/outbound.rs @@ -37,6 +37,7 @@ pub enum OutboundRequest { BlocksByRoot(BlocksByRootRequest), BlobsByRange(BlobsByRangeRequest), BlobsByRoot(BlobsByRootRequest), + DataColumnsByRoot(DataColumnsByRootRequest), Ping(Ping), MetaData(MetadataRequest), } @@ -80,6 +81,10 @@ impl OutboundRequest { SupportedProtocol::BlobsByRootV1, Encoding::SSZSnappy, )], + OutboundRequest::DataColumnsByRoot(_) => vec![ProtocolId::new( + SupportedProtocol::DataColumnsByRootV1, + Encoding::SSZSnappy, + )], OutboundRequest::Ping(_) => vec![ProtocolId::new( SupportedProtocol::PingV1, Encoding::SSZSnappy, @@ -101,6 +106,7 @@ impl OutboundRequest { OutboundRequest::BlocksByRoot(req) => req.block_roots().len() as u64, OutboundRequest::BlobsByRange(req) => req.max_blobs_requested::(), OutboundRequest::BlobsByRoot(req) => req.blob_ids.len() as u64, + OutboundRequest::DataColumnsByRoot(req) => req.data_column_ids.len() as u64, OutboundRequest::Ping(_) => 1, OutboundRequest::MetaData(_) => 1, } @@ -121,6 +127,7 @@ impl OutboundRequest { }, OutboundRequest::BlobsByRange(_) => SupportedProtocol::BlobsByRangeV1, OutboundRequest::BlobsByRoot(_) => SupportedProtocol::BlobsByRootV1, + OutboundRequest::DataColumnsByRoot(_) => SupportedProtocol::DataColumnsByRootV1, OutboundRequest::Ping(_) => SupportedProtocol::PingV1, OutboundRequest::MetaData(req) => match req { MetadataRequest::V1(_) => SupportedProtocol::MetaDataV1, @@ -139,6 +146,7 @@ impl OutboundRequest { OutboundRequest::BlocksByRoot(_) => ResponseTermination::BlocksByRoot, OutboundRequest::BlobsByRange(_) => ResponseTermination::BlobsByRange, OutboundRequest::BlobsByRoot(_) => ResponseTermination::BlobsByRoot, + OutboundRequest::DataColumnsByRoot(_) => ResponseTermination::DataColumnsByRoot, OutboundRequest::Status(_) => unreachable!(), OutboundRequest::Goodbye(_) => unreachable!(), OutboundRequest::Ping(_) => unreachable!(), @@ -196,6 +204,7 @@ impl std::fmt::Display for OutboundRequest { OutboundRequest::BlocksByRoot(req) => write!(f, "Blocks by root: {:?}", req), OutboundRequest::BlobsByRange(req) => write!(f, "Blobs by range: {:?}", req), OutboundRequest::BlobsByRoot(req) => write!(f, "Blobs by root: {:?}", req), + OutboundRequest::DataColumnsByRoot(req) => write!(f, "Data columns by root: {:?}", req), OutboundRequest::Ping(ping) => write!(f, "Ping: {}", ping.data), OutboundRequest::MetaData(_) => write!(f, "MetaData request"), } diff --git a/beacon_node/lighthouse_network/src/rpc/protocol.rs b/beacon_node/lighthouse_network/src/rpc/protocol.rs index 9c174b8e425..d03f45211f1 100644 --- a/beacon_node/lighthouse_network/src/rpc/protocol.rs +++ b/beacon_node/lighthouse_network/src/rpc/protocol.rs @@ -21,8 +21,8 @@ use tokio_util::{ }; use types::{ BeaconBlock, BeaconBlockAltair, BeaconBlockBase, BeaconBlockCapella, BeaconBlockMerge, - BlobSidecar, ChainSpec, EmptyBlock, EthSpec, ForkContext, ForkName, MainnetEthSpec, Signature, - SignedBeaconBlock, + BlobSidecar, ChainSpec, DataColumnSidecar, EmptyBlock, EthSpec, ForkContext, ForkName, + MainnetEthSpec, Signature, SignedBeaconBlock, }; lazy_static! { @@ -166,6 +166,9 @@ pub enum Protocol { /// The `BlobsByRoot` protocol name. #[strum(serialize = "blob_sidecars_by_root")] BlobsByRoot, + /// The `DataColumnSidecarsByRoot` protocol name. + #[strum(serialize = "data_column_sidecars_by_root")] + DataColumnsByRoot, /// The `Ping` protocol name. Ping, /// The `MetaData` protocol name. @@ -185,6 +188,7 @@ impl Protocol { Protocol::BlocksByRoot => Some(ResponseTermination::BlocksByRoot), Protocol::BlobsByRange => Some(ResponseTermination::BlobsByRange), Protocol::BlobsByRoot => Some(ResponseTermination::BlobsByRoot), + Protocol::DataColumnsByRoot => Some(ResponseTermination::DataColumnsByRoot), Protocol::Ping => None, Protocol::MetaData => None, Protocol::LightClientBootstrap => None, @@ -209,6 +213,7 @@ pub enum SupportedProtocol { BlocksByRootV2, BlobsByRangeV1, BlobsByRootV1, + DataColumnsByRootV1, PingV1, MetaDataV1, MetaDataV2, @@ -226,6 +231,7 @@ impl SupportedProtocol { SupportedProtocol::BlocksByRootV2 => "2", SupportedProtocol::BlobsByRangeV1 => "1", SupportedProtocol::BlobsByRootV1 => "1", + SupportedProtocol::DataColumnsByRootV1 => "1", SupportedProtocol::PingV1 => "1", SupportedProtocol::MetaDataV1 => "1", SupportedProtocol::MetaDataV2 => "2", @@ -243,6 +249,7 @@ impl SupportedProtocol { SupportedProtocol::BlocksByRootV2 => Protocol::BlocksByRoot, SupportedProtocol::BlobsByRangeV1 => Protocol::BlobsByRange, SupportedProtocol::BlobsByRootV1 => Protocol::BlobsByRoot, + SupportedProtocol::DataColumnsByRootV1 => Protocol::DataColumnsByRoot, SupportedProtocol::PingV1 => Protocol::Ping, SupportedProtocol::MetaDataV1 => Protocol::MetaData, SupportedProtocol::MetaDataV2 => Protocol::MetaData, @@ -369,6 +376,7 @@ impl ProtocolId { ::ssz_fixed_len(), ), Protocol::BlobsByRoot => RpcLimits::new(0, spec.max_blobs_by_root_request), + Protocol::DataColumnsByRoot => RpcLimits::new(0, spec.max_data_columns_by_root_request), Protocol::Ping => RpcLimits::new( ::ssz_fixed_len(), ::ssz_fixed_len(), @@ -393,6 +401,10 @@ impl ProtocolId { Protocol::BlocksByRoot => rpc_block_limits_by_fork(fork_context.current_fork()), Protocol::BlobsByRange => rpc_blob_limits::(), Protocol::BlobsByRoot => rpc_blob_limits::(), + Protocol::DataColumnsByRoot => RpcLimits::new( + DataColumnSidecar::::min_size(), + DataColumnSidecar::::max_size(), + ), Protocol::Ping => RpcLimits::new( ::ssz_fixed_len(), ::ssz_fixed_len(), @@ -416,6 +428,7 @@ impl ProtocolId { | SupportedProtocol::BlocksByRootV2 | SupportedProtocol::BlobsByRangeV1 | SupportedProtocol::BlobsByRootV1 + | SupportedProtocol::DataColumnsByRootV1 | SupportedProtocol::LightClientBootstrapV1 => true, SupportedProtocol::StatusV1 | SupportedProtocol::BlocksByRootV1 @@ -527,6 +540,7 @@ pub enum InboundRequest { BlocksByRoot(BlocksByRootRequest), BlobsByRange(BlobsByRangeRequest), BlobsByRoot(BlobsByRootRequest), + DataColumnsByRoot(DataColumnsByRootRequest), LightClientBootstrap(LightClientBootstrapRequest), Ping(Ping), MetaData(MetadataRequest), @@ -545,6 +559,7 @@ impl InboundRequest { InboundRequest::BlocksByRoot(req) => req.block_roots().len() as u64, InboundRequest::BlobsByRange(req) => req.max_blobs_requested::(), InboundRequest::BlobsByRoot(req) => req.blob_ids.len() as u64, + InboundRequest::DataColumnsByRoot(req) => req.data_column_ids.len() as u64, InboundRequest::Ping(_) => 1, InboundRequest::MetaData(_) => 1, InboundRequest::LightClientBootstrap(_) => 1, @@ -566,6 +581,7 @@ impl InboundRequest { }, InboundRequest::BlobsByRange(_) => SupportedProtocol::BlobsByRangeV1, InboundRequest::BlobsByRoot(_) => SupportedProtocol::BlobsByRootV1, + InboundRequest::DataColumnsByRoot(_) => SupportedProtocol::DataColumnsByRootV1, InboundRequest::Ping(_) => SupportedProtocol::PingV1, InboundRequest::MetaData(req) => match req { MetadataRequest::V1(_) => SupportedProtocol::MetaDataV1, @@ -585,6 +601,7 @@ impl InboundRequest { InboundRequest::BlocksByRoot(_) => ResponseTermination::BlocksByRoot, InboundRequest::BlobsByRange(_) => ResponseTermination::BlobsByRange, InboundRequest::BlobsByRoot(_) => ResponseTermination::BlobsByRoot, + InboundRequest::DataColumnsByRoot(_) => ResponseTermination::DataColumnsByRoot, InboundRequest::Status(_) => unreachable!(), InboundRequest::Goodbye(_) => unreachable!(), InboundRequest::Ping(_) => unreachable!(), @@ -693,6 +710,7 @@ impl std::fmt::Display for InboundRequest { InboundRequest::BlocksByRoot(req) => write!(f, "Blocks by root: {:?}", req), InboundRequest::BlobsByRange(req) => write!(f, "Blobs by range: {:?}", req), InboundRequest::BlobsByRoot(req) => write!(f, "Blobs by root: {:?}", req), + InboundRequest::DataColumnsByRoot(req) => write!(f, "Data columns by root: {:?}", req), InboundRequest::Ping(ping) => write!(f, "Ping: {}", ping.data), InboundRequest::MetaData(_) => write!(f, "MetaData request"), InboundRequest::LightClientBootstrap(bootstrap) => { diff --git a/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs b/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs index 0b57374e8b6..b9ada25c1de 100644 --- a/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs +++ b/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs @@ -98,6 +98,8 @@ pub struct RPCRateLimiter { blbrange_rl: Limiter, /// BlobsByRoot rate limiter. blbroot_rl: Limiter, + /// DataColumnssByRoot rate limiter. + dcbroot_rl: Limiter, /// LightClientBootstrap rate limiter. lcbootstrap_rl: Limiter, } @@ -130,6 +132,8 @@ pub struct RPCRateLimiterBuilder { blbrange_quota: Option, /// Quota for the BlobsByRoot protocol. blbroot_quota: Option, + /// Quota for the DataColumnsByRoot protocol. + dcbroot_quota: Option, /// Quota for the LightClientBootstrap protocol. lcbootstrap_quota: Option, } @@ -147,6 +151,7 @@ impl RPCRateLimiterBuilder { Protocol::BlocksByRoot => self.bbroots_quota = q, Protocol::BlobsByRange => self.blbrange_quota = q, Protocol::BlobsByRoot => self.blbroot_quota = q, + Protocol::DataColumnsByRoot => self.dcbroot_quota = q, Protocol::LightClientBootstrap => self.lcbootstrap_quota = q, } self @@ -176,6 +181,10 @@ impl RPCRateLimiterBuilder { .blbroot_quota .ok_or("BlobsByRoot quota not specified")?; + let dcbroot_quota = self + .dcbroot_quota + .ok_or("DataColumnsByRoot quota not specified")?; + // create the rate limiters let ping_rl = Limiter::from_quota(ping_quota)?; let metadata_rl = Limiter::from_quota(metadata_quota)?; @@ -185,6 +194,7 @@ impl RPCRateLimiterBuilder { let bbrange_rl = Limiter::from_quota(bbrange_quota)?; let blbrange_rl = Limiter::from_quota(blbrange_quota)?; let blbroot_rl = Limiter::from_quota(blbroots_quota)?; + let dcbroot_rl = Limiter::from_quota(dcbroot_quota)?; let lcbootstrap_rl = Limiter::from_quota(lcbootstrap_quote)?; // check for peers to prune every 30 seconds, starting in 30 seconds @@ -201,6 +211,7 @@ impl RPCRateLimiterBuilder { bbrange_rl, blbrange_rl, blbroot_rl, + dcbroot_rl, lcbootstrap_rl, init_time: Instant::now(), }) @@ -243,6 +254,7 @@ impl RPCRateLimiter { blocks_by_root_quota, blobs_by_range_quota, blobs_by_root_quota, + data_columns_by_root_quota, light_client_bootstrap_quota, } = config; @@ -255,6 +267,7 @@ impl RPCRateLimiter { .set_quota(Protocol::BlocksByRoot, blocks_by_root_quota) .set_quota(Protocol::BlobsByRange, blobs_by_range_quota) .set_quota(Protocol::BlobsByRoot, blobs_by_root_quota) + .set_quota(Protocol::DataColumnsByRoot, data_columns_by_root_quota) .set_quota(Protocol::LightClientBootstrap, light_client_bootstrap_quota) .build() } @@ -283,6 +296,7 @@ impl RPCRateLimiter { Protocol::BlocksByRoot => &mut self.bbroots_rl, Protocol::BlobsByRange => &mut self.blbrange_rl, Protocol::BlobsByRoot => &mut self.blbroot_rl, + Protocol::DataColumnsByRoot => &mut self.dcbroot_rl, Protocol::LightClientBootstrap => &mut self.lcbootstrap_rl, }; check(limiter) diff --git a/beacon_node/lighthouse_network/src/service/api_types.rs b/beacon_node/lighthouse_network/src/service/api_types.rs index 96c9d283327..e12904a0a5e 100644 --- a/beacon_node/lighthouse_network/src/service/api_types.rs +++ b/beacon_node/lighthouse_network/src/service/api_types.rs @@ -1,9 +1,9 @@ use std::sync::Arc; use libp2p::swarm::ConnectionId; -use types::{BlobSidecar, EthSpec, LightClientBootstrap, SignedBeaconBlock}; +use types::{BlobSidecar, DataColumnSidecar, EthSpec, LightClientBootstrap, SignedBeaconBlock}; -use crate::rpc::methods::{BlobsByRangeRequest, BlobsByRootRequest}; +use crate::rpc::methods::{BlobsByRangeRequest, BlobsByRootRequest, DataColumnsByRootRequest}; use crate::rpc::{ methods::{ BlocksByRangeRequest, BlocksByRootRequest, LightClientBootstrapRequest, @@ -42,6 +42,8 @@ pub enum Request { LightClientBootstrap(LightClientBootstrapRequest), /// A request blobs root request. BlobsByRoot(BlobsByRootRequest), + /// A request data columns root request. + DataColumnsByRoot(DataColumnsByRootRequest), } impl std::convert::From for OutboundRequest { @@ -69,6 +71,7 @@ impl std::convert::From for OutboundRequest { } Request::BlobsByRange(r) => OutboundRequest::BlobsByRange(r), Request::BlobsByRoot(r) => OutboundRequest::BlobsByRoot(r), + Request::DataColumnsByRoot(r) => OutboundRequest::DataColumnsByRoot(r), Request::Status(s) => OutboundRequest::Status(s), } } @@ -92,6 +95,8 @@ pub enum Response { BlocksByRoot(Option>>), /// A response to a get BLOBS_BY_ROOT request. BlobsByRoot(Option>>), + /// A response to a get DATA_COLUMN_SIDECARS_BY_ROOT request. + DataColumnsByRoot(Option>>), /// A response to a LightClientUpdate request. LightClientBootstrap(LightClientBootstrap), } @@ -115,6 +120,10 @@ impl std::convert::From> for RPCCodedResponse RPCCodedResponse::Success(RPCResponse::BlobsByRange(b)), None => RPCCodedResponse::StreamTermination(ResponseTermination::BlobsByRange), }, + Response::DataColumnsByRoot(r) => match r { + Some(d) => RPCCodedResponse::Success(RPCResponse::DataColumnsByRoot(d)), + None => RPCCodedResponse::StreamTermination(ResponseTermination::DataColumnsByRoot), + }, Response::Status(s) => RPCCodedResponse::Success(RPCResponse::Status(s)), Response::LightClientBootstrap(b) => { RPCCodedResponse::Success(RPCResponse::LightClientBootstrap(b)) diff --git a/beacon_node/lighthouse_network/src/service/mod.rs b/beacon_node/lighthouse_network/src/service/mod.rs index bc1b60189e7..89c4ce6df1f 100644 --- a/beacon_node/lighthouse_network/src/service/mod.rs +++ b/beacon_node/lighthouse_network/src/service/mod.rs @@ -1137,6 +1137,9 @@ impl Network { Request::BlobsByRoot { .. } => { metrics::inc_counter_vec(&metrics::TOTAL_RPC_REQUESTS, &["blobs_by_root"]) } + Request::DataColumnsByRoot { .. } => { + metrics::inc_counter_vec(&metrics::TOTAL_RPC_REQUESTS, &["data_columns_by_root"]) + } } NetworkEvent::RequestReceived { peer_id, @@ -1458,6 +1461,14 @@ impl Network { self.build_request(peer_request_id, peer_id, Request::BlobsByRoot(req)); Some(event) } + InboundRequest::DataColumnsByRoot(req) => { + let event = self.build_request( + peer_request_id, + peer_id, + Request::DataColumnsByRoot(req), + ); + Some(event) + } InboundRequest::LightClientBootstrap(req) => { let event = self.build_request( peer_request_id, @@ -1499,6 +1510,9 @@ impl Network { RPCResponse::BlobsByRoot(resp) => { self.build_response(id, peer_id, Response::BlobsByRoot(Some(resp))) } + RPCResponse::DataColumnsByRoot(resp) => { + self.build_response(id, peer_id, Response::DataColumnsByRoot(Some(resp))) + } // Should never be reached RPCResponse::LightClientBootstrap(bootstrap) => { self.build_response(id, peer_id, Response::LightClientBootstrap(bootstrap)) @@ -1511,6 +1525,7 @@ impl Network { ResponseTermination::BlocksByRoot => Response::BlocksByRoot(None), ResponseTermination::BlobsByRange => Response::BlobsByRange(None), ResponseTermination::BlobsByRoot => Response::BlobsByRoot(None), + ResponseTermination::DataColumnsByRoot => Response::DataColumnsByRoot(None), }; self.build_response(id, peer_id, response) } 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 fb76f2a2677..62a1216f13b 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -4,10 +4,9 @@ use crate::{ service::NetworkMessage, sync::SyncMessage, }; -use beacon_chain::blob_verification::{ - GossipBlobError, GossipVerifiedBlob, GossipVerifiedDataColumnSidecar, -}; +use beacon_chain::blob_verification::{GossipBlobError, GossipVerifiedBlob}; use beacon_chain::block_verification_types::AsBlock; +use beacon_chain::data_column_verification::GossipVerifiedDataColumn; use beacon_chain::store::Error; use beacon_chain::{ attestation_verification::{self, Error as AttnError, VerifiedAttestation}, @@ -869,12 +868,59 @@ impl NetworkBeaconProcessor { pub async fn process_gossip_verified_data_column( self: &Arc, - _peer_id: PeerId, - verified_data_column: GossipVerifiedDataColumnSidecar, + peer_id: PeerId, + verified_data_column: GossipVerifiedDataColumn, // This value is not used presently, but it might come in handy for debugging. _seen_duration: Duration, ) { - self.chain.process_gossip_data_column(verified_data_column); + let block_root = verified_data_column.block_root(); + let data_column_slot = verified_data_column.slot(); + let data_column_index = verified_data_column.id().index; + + match self + .chain + .process_gossip_data_column(verified_data_column) + .await + { + Ok(AvailabilityProcessingStatus::Imported(block_root)) => { + // Note: Reusing block imported metric here + metrics::inc_counter(&metrics::BEACON_PROCESSOR_GOSSIP_BLOCK_IMPORTED_TOTAL); + info!( + self.log, + "Gossipsub data column processed, imported fully available block"; + "block_root" => %block_root + ); + self.chain.recompute_head_at_current_slot().await; + } + Ok(AvailabilityProcessingStatus::MissingComponents(slot, block_root)) => { + trace!( + self.log, + "Processed data column, waiting for other components"; + "slot" => %slot, + "data_column_index" => %data_column_index, + "block_root" => %block_root, + ); + } + Err(err) => { + debug!( + self.log, + "Invalid gossip data column"; + "outcome" => ?err, + "block root" => ?block_root, + "block slot" => data_column_slot, + "data column index" => data_column_index, + ); + self.gossip_penalize_peer( + peer_id, + PeerAction::MidToleranceError, + "bad_gossip_data_column_ssz", + ); + trace!( + self.log, + "Invalid gossip data column ssz"; + ); + } + } } /// 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 8361554fa9d..7c444b8b52e 100644 --- a/beacon_node/network/src/network_beacon_processor/mod.rs +++ b/beacon_node/network/src/network_beacon_processor/mod.rs @@ -13,7 +13,9 @@ use beacon_processor::{ WorkEvent as BeaconWorkEvent, }; use environment::null_logger; -use lighthouse_network::rpc::methods::{BlobsByRangeRequest, BlobsByRootRequest}; +use lighthouse_network::rpc::methods::{ + BlobsByRangeRequest, BlobsByRootRequest, DataColumnsByRootRequest, +}; use lighthouse_network::{ rpc::{BlocksByRangeRequest, BlocksByRootRequest, LightClientBootstrapRequest, StatusMessage}, Client, MessageId, NetworkGlobals, PeerId, PeerRequestId, @@ -618,6 +620,23 @@ impl NetworkBeaconProcessor { }) } + /// Create a new work event to process `DataColumnsByRootRequest`s from the RPC network. + pub fn send_data_columns_by_roots_request( + self: &Arc, + peer_id: PeerId, + request_id: PeerRequestId, + request: DataColumnsByRootRequest, + ) -> Result<(), Error> { + let processor = self.clone(); + let process_fn = + move || processor.handle_data_columns_by_root_request(peer_id, request_id, request); + + self.try_send(BeaconWorkEvent { + drop_during_sync: false, + work: Work::DataColumnsByRootsRequest(Box::new(process_fn)), + }) + } + /// Create a new work event to process `LightClientBootstrap`s from the RPC network. pub fn send_light_client_bootstrap_request( self: &Arc, 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 66c98ff3b84..167601afbe9 100644 --- a/beacon_node/network/src/network_beacon_processor/rpc_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/rpc_methods.rs @@ -5,7 +5,9 @@ use crate::sync::SyncMessage; use beacon_chain::{BeaconChainError, BeaconChainTypes, HistoricalBlockError, WhenSlotSkipped}; use beacon_processor::SendOnDrop; use itertools::process_results; -use lighthouse_network::rpc::methods::{BlobsByRangeRequest, BlobsByRootRequest}; +use lighthouse_network::rpc::methods::{ + BlobsByRangeRequest, BlobsByRootRequest, DataColumnsByRootRequest, +}; use lighthouse_network::rpc::StatusMessage; use lighthouse_network::rpc::*; use lighthouse_network::{PeerId, PeerRequestId, ReportSource, Response, SyncInfo}; @@ -16,6 +18,7 @@ use std::sync::Arc; use task_executor::TaskExecutor; use tokio_stream::StreamExt; use types::blob_sidecar::BlobIdentifier; +use types::data_column_sidecar::DataColumnIdentifier; use types::{Epoch, EthSpec, ForkName, Hash256, Slot}; impl NetworkBeaconProcessor { @@ -294,6 +297,94 @@ impl NetworkBeaconProcessor { } } + /// Handle a `DataColumnsByRoot` request from the peer. + pub fn handle_data_columns_by_root_request( + self: Arc, + peer_id: PeerId, + request_id: PeerRequestId, + request: DataColumnsByRootRequest, + ) { + let Some(requested_root) = request + .data_column_ids + .as_slice() + .first() + .map(|id| id.block_root) + else { + // No data column ids requested. + return; + }; + let requested_indices = request + .data_column_ids + .as_slice() + .iter() + .map(|id| id.index) + .collect::>(); + let mut send_data_column_count = 0; + + let mut data_column_list_results = HashMap::new(); + for id in request.data_column_ids.as_slice() { + // Attempt to get the data columns from the RPC cache. + if let Ok(Some(data_column)) = self.chain.data_availability_checker.get_data_column(id) + { + self.send_response( + peer_id, + Response::DataColumnsByRoot(Some(data_column)), + request_id, + ); + send_data_column_count += 1; + } else { + let DataColumnIdentifier { + block_root: root, + index, + } = id; + + let data_column_list_result = match data_column_list_results.entry(root) { + Entry::Vacant(entry) => entry.insert( + self.chain + .get_data_columns_checking_early_attester_cache(root), + ), + Entry::Occupied(entry) => entry.into_mut(), + }; + + match data_column_list_result.as_ref() { + Ok(data_columns_sidecar_list) => { + 'inner: for data_column_sidecar in data_columns_sidecar_list.iter() { + if data_column_sidecar.index == *index { + self.send_response( + peer_id, + Response::DataColumnsByRoot(Some(data_column_sidecar.clone())), + request_id, + ); + send_data_column_count += 1; + break 'inner; + } + } + } + Err(e) => { + debug!( + self.log, + "Error fetching data column for peer"; + "peer" => %peer_id, + "request_root" => ?root, + "error" => ?e, + ); + } + } + } + } + debug!( + self.log, + "Received DataColumnsByRoot Request"; + "peer" => %peer_id, + "request_root" => %requested_root, + "request_indices" => ?requested_indices, + "returned" => send_data_column_count + ); + + // send stream termination + self.send_response(peer_id, Response::DataColumnsByRoot(None), request_id); + } + /// Handle a `BlocksByRoot` request from the peer. pub fn handle_light_client_bootstrap( self: &Arc, 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 8894d5d9fd9..7acb99a616e 100644 --- a/beacon_node/network/src/network_beacon_processor/sync_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/sync_methods.rs @@ -361,6 +361,10 @@ impl NetworkBeaconProcessor { .iter() .map(|wrapped| wrapped.n_blobs()) .sum::(); + let n_data_columns = downloaded_blocks + .iter() + .map(|wrapped| wrapped.n_data_columns()) + .sum::(); match self.process_backfill_blocks(downloaded_blocks) { (_, Ok(_)) => { @@ -370,6 +374,7 @@ impl NetworkBeaconProcessor { "last_block_slot" => end_slot, "processed_blocks" => sent_blocks, "processed_blobs" => n_blobs, + "processed_data_columns" => n_data_columns, "service"=> "sync"); BatchProcessResult::Success { was_non_empty: sent_blocks > 0, diff --git a/beacon_node/network/src/router.rs b/beacon_node/network/src/router.rs index 3d88f764e0e..23b14ac1439 100644 --- a/beacon_node/network/src/router.rs +++ b/beacon_node/network/src/router.rs @@ -27,7 +27,7 @@ use std::sync::Arc; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use tokio::sync::mpsc; use tokio_stream::wrappers::UnboundedReceiverStream; -use types::{BlobSidecar, EthSpec, SignedBeaconBlock}; +use types::{BlobSidecar, DataColumnSidecar, EthSpec, SignedBeaconBlock}; /// Handles messages from the network and routes them to the appropriate service to be handled. pub struct Router { @@ -216,6 +216,10 @@ impl Router { self.network_beacon_processor .send_blobs_by_roots_request(peer_id, request_id, request), ), + Request::DataColumnsByRoot(request) => self.handle_beacon_processor_send_result( + self.network_beacon_processor + .send_data_columns_by_roots_request(peer_id, request_id, request), + ), Request::LightClientBootstrap(request) => self.handle_beacon_processor_send_result( self.network_beacon_processor .send_light_client_bootstrap_request(peer_id, request_id, request), @@ -250,6 +254,9 @@ impl Router { Response::BlobsByRoot(blob) => { self.on_blobs_by_root_response(peer_id, request_id, blob); } + Response::DataColumnsByRoot(data_column) => { + self.on_data_columns_by_root_response(peer_id, request_id, data_column); + } Response::LightClientBootstrap(_) => unreachable!(), } } @@ -637,6 +644,16 @@ impl Router { }); } + /// Handle a `DataColumnsByRoot` response from the peer. + pub fn on_data_columns_by_root_response( + &mut self, + _peer_id: PeerId, + _request_id: RequestId, + _data_column_sidecar: Option>>, + ) { + // TODO(das) implement `DataColumnsByRoot` response handling + } + fn handle_beacon_processor_send_result( &mut self, result: Result<(), crate::network_beacon_processor::Error>, 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 8c60621f1c7..989bfab00f0 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 @@ -179,11 +179,13 @@ impl SingleBlockLookup { block_root: _, downloaded_block, downloaded_blobs, + downloaded_data_columns, } = components; if let Some(block) = downloaded_block { existing_components.merge_block(block); } existing_components.merge_blobs(downloaded_blobs); + existing_components.merge_data_columns(downloaded_data_columns); } else { self.child_components = Some(components); } diff --git a/beacon_node/network/src/sync/block_lookups/tests.rs b/beacon_node/network/src/sync/block_lookups/tests.rs index c506696b9d3..f81f16dfb57 100644 --- a/beacon_node/network/src/sync/block_lookups/tests.rs +++ b/beacon_node/network/src/sync/block_lookups/tests.rs @@ -1249,7 +1249,7 @@ mod deneb_only { RequestTrigger::GossipUnknownParentBlock { .. } => { bl.search_child_block( block_root, - ChildComponents::new(block_root, Some(block.clone()), None), + ChildComponents::new(block_root, Some(block.clone()), None, None), &[peer_id], &mut cx, ); @@ -1274,7 +1274,7 @@ mod deneb_only { *lookup_blobs.index_mut(0) = Some(single_blob); bl.search_child_block( child_root, - ChildComponents::new(child_root, None, Some(lookup_blobs)), + ChildComponents::new(child_root, None, Some(lookup_blobs), None), &[peer_id], &mut cx, ); diff --git a/beacon_node/network/src/sync/manager.rs b/beacon_node/network/src/sync/manager.rs index acb735ea442..7fff76dd9eb 100644 --- a/beacon_node/network/src/sync/manager.rs +++ b/beacon_node/network/src/sync/manager.rs @@ -644,7 +644,7 @@ impl SyncManager { block_root, parent_root, blob_slot, - ChildComponents::new(block_root, None, Some(blobs)), + ChildComponents::new(block_root, None, Some(blobs), None), ); } SyncMessage::UnknownBlockHashFromAttestation(peer_id, block_hash) => { diff --git a/beacon_node/store/src/errors.rs b/beacon_node/store/src/errors.rs index 96e02b80ff8..7e5b539406c 100644 --- a/beacon_node/store/src/errors.rs +++ b/beacon_node/store/src/errors.rs @@ -27,6 +27,8 @@ pub enum Error { AnchorInfoConcurrentMutation, /// The store's `blob_info` was mutated concurrently, the latest modification wasn't applied. BlobInfoConcurrentMutation, + /// The store's `data_column_info` was mutated concurrently, the latest modification wasn't applied. + DataColumnInfoConcurrentMutation, /// The block or state is unavailable due to weak subjectivity sync. HistoryUnavailable, /// State reconstruction cannot commence because not all historic blocks are known. diff --git a/beacon_node/store/src/hot_cold_store.rs b/beacon_node/store/src/hot_cold_store.rs index 4bdb0deca33..5acd8ff8445 100644 --- a/beacon_node/store/src/hot_cold_store.rs +++ b/beacon_node/store/src/hot_cold_store.rs @@ -12,9 +12,10 @@ use crate::leveldb_store::BytesKey; use crate::leveldb_store::LevelDB; use crate::memory_store::MemoryStore; use crate::metadata::{ - AnchorInfo, BlobInfo, CompactionTimestamp, PruningCheckpoint, SchemaVersion, ANCHOR_INFO_KEY, - BLOB_INFO_KEY, COMPACTION_TIMESTAMP_KEY, CONFIG_KEY, CURRENT_SCHEMA_VERSION, - PRUNING_CHECKPOINT_KEY, SCHEMA_VERSION_KEY, SPLIT_KEY, STATE_UPPER_LIMIT_NO_RETAIN, + AnchorInfo, BlobInfo, CompactionTimestamp, DataColumnInfo, PruningCheckpoint, SchemaVersion, + ANCHOR_INFO_KEY, BLOB_INFO_KEY, COMPACTION_TIMESTAMP_KEY, CONFIG_KEY, CURRENT_SCHEMA_VERSION, + DATA_COLUMN_INFO_KEY, PRUNING_CHECKPOINT_KEY, SCHEMA_VERSION_KEY, SPLIT_KEY, + STATE_UPPER_LIMIT_NO_RETAIN, }; use crate::metrics; use crate::{ @@ -40,6 +41,7 @@ use std::path::Path; use std::sync::Arc; use std::time::Duration; use types::blob_sidecar::BlobSidecarList; +use types::data_column_sidecar::DataColumnSidecarList; use types::*; /// On-disk database that stores finalized states efficiently. @@ -57,6 +59,8 @@ pub struct HotColdDB, Cold: ItemStore> { anchor_info: RwLock>, /// The starting slots for the range of blobs stored in the database. blob_info: RwLock, + /// The starting slots for the range of data columns stored in the database. + data_column_info: RwLock, pub(crate) config: StoreConfig, /// Cold database containing compact historical data. pub cold_db: Cold, @@ -82,6 +86,7 @@ pub struct HotColdDB, Cold: ItemStore> { struct BlockCache { block_cache: LruCache>, blob_cache: LruCache>, + data_column_cache: LruCache>, } impl BlockCache { @@ -89,6 +94,7 @@ impl BlockCache { Self { block_cache: LruCache::new(size), blob_cache: LruCache::new(size), + data_column_cache: LruCache::new(size), } } pub fn put_block(&mut self, block_root: Hash256, block: SignedBeaconBlock) { @@ -97,12 +103,25 @@ impl BlockCache { pub fn put_blobs(&mut self, block_root: Hash256, blobs: BlobSidecarList) { self.blob_cache.put(block_root, blobs); } + pub fn put_data_columns( + &mut self, + block_root: Hash256, + data_columns: DataColumnSidecarList, + ) { + self.data_column_cache.put(block_root, data_columns); + } pub fn get_block<'a>(&'a mut self, block_root: &Hash256) -> Option<&'a SignedBeaconBlock> { self.block_cache.get(block_root) } pub fn get_blobs<'a>(&'a mut self, block_root: &Hash256) -> Option<&'a BlobSidecarList> { self.blob_cache.get(block_root) } + pub fn get_data_columns<'a>( + &'a mut self, + block_root: &Hash256, + ) -> Option<&'a DataColumnSidecarList> { + self.data_column_cache.get(block_root) + } pub fn delete_block(&mut self, block_root: &Hash256) { let _ = self.block_cache.pop(block_root); } @@ -176,6 +195,7 @@ impl HotColdDB, MemoryStore> { split: RwLock::new(Split::default()), anchor_info: RwLock::new(None), blob_info: RwLock::new(BlobInfo::default()), + data_column_info: RwLock::new(DataColumnInfo::default()), cold_db: MemoryStore::open(), blobs_db: MemoryStore::open(), hot_db: MemoryStore::open(), @@ -213,6 +233,7 @@ impl HotColdDB, LevelDB> { split: RwLock::new(Split::default()), anchor_info: RwLock::new(None), blob_info: RwLock::new(BlobInfo::default()), + data_column_info: RwLock::new(DataColumnInfo::default()), cold_db: LevelDB::open(cold_path)?, blobs_db: LevelDB::open(blobs_db_path)?, hot_db: LevelDB::open(hot_path)?, @@ -290,11 +311,35 @@ impl HotColdDB, LevelDB> { }, }; db.compare_and_set_blob_info_with_write(<_>::default(), new_blob_info.clone())?; + + let data_column_info = db.load_data_column_info()?; + let new_data_column_info = match &data_column_info { + // TODO[das]: update to EIP-7594 fork + Some(data_column_info) => { + // Set the oldest data column slot to the Deneb fork slot if it is not yet set. + let oldest_data_column_slot = + data_column_info.oldest_data_column_slot.or(deneb_fork_slot); + DataColumnInfo { + oldest_data_column_slot, + } + } + // First start. + None => DataColumnInfo { + // Set the oldest data column slot to the Deneb fork slot if it is not yet set. + oldest_data_column_slot: deneb_fork_slot, + }, + }; + db.compare_and_set_data_column_info_with_write( + <_>::default(), + new_data_column_info.clone(), + )?; + info!( db.log, "Blob DB initialized"; "path" => ?blobs_db_path, "oldest_blob_slot" => ?new_blob_info.oldest_blob_slot, + "oldest_data_column_slot" => ?new_data_column_info.oldest_data_column_slot, ); // Ensure that the schema version of the on-disk database matches the software. @@ -607,6 +652,19 @@ impl, Cold: ItemStore> HotColdDB ops.push(KeyValueStoreOp::PutKeyValue(db_key, blobs.as_ssz_bytes())); } + pub fn data_columns_as_kv_store_ops( + &self, + key: &Hash256, + data_columns: DataColumnSidecarList, + ops: &mut Vec, + ) { + let db_key = get_key_for_col(DBColumn::BeaconDataColumn.into(), key.as_bytes()); + ops.push(KeyValueStoreOp::PutKeyValue( + db_key, + data_columns.as_ssz_bytes(), + )); + } + pub fn put_state_summary( &self, state_root: &Hash256, @@ -896,6 +954,14 @@ impl, Cold: ItemStore> HotColdDB self.blobs_as_kv_store_ops(&block_root, blobs, &mut key_value_batch); } + StoreOp::PutDataColumns(block_root, data_columns) => { + self.data_columns_as_kv_store_ops( + &block_root, + data_columns, + &mut key_value_batch, + ); + } + StoreOp::PutStateSummary(state_root, summary) => { key_value_batch.push(summary.as_kv_store_op(state_root)); } @@ -920,6 +986,12 @@ impl, Cold: ItemStore> HotColdDB key_value_batch.push(KeyValueStoreOp::DeleteKey(key)); } + StoreOp::DeleteDataColumns(block_root) => { + let key = + get_key_for_col(DBColumn::BeaconDataColumn.into(), block_root.as_bytes()); + key_value_batch.push(KeyValueStoreOp::DeleteKey(key)); + } + StoreOp::DeleteState(state_root, slot) => { let state_summary_key = get_key_for_col(DBColumn::BeaconStateSummary.into(), state_root.as_bytes()); @@ -950,9 +1022,10 @@ impl, Cold: ItemStore> HotColdDB batch: Vec>, ) -> Result<(), Error> { let mut blobs_to_delete = Vec::new(); + let mut data_columns_to_delete = Vec::new(); let (blobs_ops, hot_db_ops): (Vec>, Vec>) = batch.into_iter().partition(|store_op| match store_op { - StoreOp::PutBlobs(_, _) => true, + StoreOp::PutBlobs(_, _) | StoreOp::PutDataColumns(_, _) => true, StoreOp::DeleteBlobs(block_root) => { match self.get_blobs(block_root) { Ok(Some(blob_sidecar_list)) => { @@ -969,6 +1042,22 @@ impl, Cold: ItemStore> HotColdDB } true } + StoreOp::DeleteDataColumns(block_root) => { + match self.get_data_columns(block_root) { + Ok(Some(data_column_sidecar_list)) => { + data_columns_to_delete.push((*block_root, data_column_sidecar_list)); + } + Err(e) => { + error!( + self.log, "Error getting data columns"; + "block_root" => %block_root, + "error" => ?e + ); + } + _ => (), + } + true + } StoreOp::PutBlock(_, _) | StoreOp::DeleteBlock(_) => false, _ => false, }); @@ -1000,10 +1089,19 @@ impl, Cold: ItemStore> HotColdDB for op in blob_cache_ops.iter_mut() { let reverse_op = match op { StoreOp::PutBlobs(block_root, _) => StoreOp::DeleteBlobs(*block_root), + StoreOp::PutDataColumns(block_root, _) => { + StoreOp::DeleteDataColumns(*block_root) + } StoreOp::DeleteBlobs(_) => match blobs_to_delete.pop() { Some((block_root, blobs)) => StoreOp::PutBlobs(block_root, blobs), None => return Err(HotColdDBError::Rollback.into()), }, + StoreOp::DeleteDataColumns(_) => match data_columns_to_delete.pop() { + Some((block_root, data_columns)) => { + StoreOp::PutDataColumns(block_root, data_columns) + } + None => return Err(HotColdDBError::Rollback.into()), + }, _ => return Err(HotColdDBError::Rollback.into()), }; *op = reverse_op; @@ -1021,6 +1119,8 @@ impl, Cold: ItemStore> HotColdDB StoreOp::PutBlobs(_, _) => (), + StoreOp::PutDataColumns(_, _) => (), + StoreOp::PutState(_, _) => (), StoreOp::PutStateSummary(_, _) => (), @@ -1035,6 +1135,8 @@ impl, Cold: ItemStore> HotColdDB StoreOp::DeleteBlobs(_) => (), + StoreOp::DeleteDataColumns(_) => (), + StoreOp::DeleteState(_, _) => (), StoreOp::DeleteExecutionPayload(_) => (), @@ -1448,6 +1550,32 @@ impl, Cold: ItemStore> HotColdDB } } + /// Fetch data_columns for a given block from the store. + pub fn get_data_columns( + &self, + block_root: &Hash256, + ) -> Result>, Error> { + // Check the cache. + if let Some(data_columns) = self.block_cache.lock().get_data_columns(block_root) { + metrics::inc_counter(&metrics::BEACON_DATA_COLUMNS_CACHE_HIT_COUNT); + return Ok(Some(data_columns.clone())); + } + + match self + .blobs_db + .get_bytes(DBColumn::BeaconDataColumn.into(), block_root.as_bytes())? + { + Some(ref data_columns_bytes) => { + let data_columns = DataColumnSidecarList::from_ssz_bytes(data_columns_bytes)?; + self.block_cache + .lock() + .put_data_columns(*block_root, data_columns.clone()); + Ok(Some(data_columns)) + } + None => Ok(None), + } + } + /// Get a reference to the `ChainSpec` used by the database. pub fn get_chain_spec(&self) -> &ChainSpec { &self.spec @@ -1644,6 +1772,24 @@ impl, Cold: ItemStore> HotColdDB self.blob_info.read_recursive().clone() } + /// Initialize the `DataColumnInfo` when starting from genesis or a checkpoint. + pub fn init_data_column_info(&self, anchor_slot: Slot) -> Result { + let oldest_data_column_slot = self.spec.deneb_fork_epoch.map(|fork_epoch| { + std::cmp::max(anchor_slot, fork_epoch.start_slot(E::slots_per_epoch())) + }); + let data_column_info = DataColumnInfo { + oldest_data_column_slot, + }; + self.compare_and_set_data_column_info(self.get_data_column_info(), data_column_info) + } + + /// Get a clone of the store's data column info. + /// + /// To do mutations, use `compare_and_set_data_column_info`. + pub fn get_data_column_info(&self) -> DataColumnInfo { + self.data_column_info.read_recursive().clone() + } + /// Atomically update the blob info from `prev_value` to `new_value`. /// /// Return a `KeyValueStoreOp` which should be written to disk, possibly atomically with other @@ -1689,6 +1835,54 @@ impl, Cold: ItemStore> HotColdDB blob_info.as_kv_store_op(BLOB_INFO_KEY) } + /// Atomically update the data column info from `prev_value` to `new_value`. + /// + /// Return a `KeyValueStoreOp` which should be written to disk, possibly atomically with other + /// values. + /// + /// Return an `DataColumnInfoConcurrentMutation` error if the `prev_value` provided + /// is not correct. + pub fn compare_and_set_data_column_info( + &self, + prev_value: DataColumnInfo, + new_value: DataColumnInfo, + ) -> Result { + let mut data_column_info = self.data_column_info.write(); + if *data_column_info == prev_value { + let kv_op = self.store_data_column_info_in_batch(&new_value); + *data_column_info = new_value; + Ok(kv_op) + } else { + Err(Error::DataColumnInfoConcurrentMutation) + } + } + + /// As for `compare_and_set_data_column_info`, but also writes the blob info to disk immediately. + pub fn compare_and_set_data_column_info_with_write( + &self, + prev_value: DataColumnInfo, + new_value: DataColumnInfo, + ) -> Result<(), Error> { + let kv_store_op = self.compare_and_set_data_column_info(prev_value, new_value)?; + self.hot_db.do_atomically(vec![kv_store_op]) + } + + /// Load the blob info from disk, but do not set `self.data_column_info`. + fn load_data_column_info(&self) -> Result, Error> { + self.hot_db.get(&DATA_COLUMN_INFO_KEY) + } + + /// Store the given `data_column_info` to disk. + /// + /// The argument is intended to be `self.data_column_info`, but is passed manually to avoid issues + /// with recursive locking. + fn store_data_column_info_in_batch( + &self, + data_column_info: &DataColumnInfo, + ) -> KeyValueStoreOp { + data_column_info.as_kv_store_op(DATA_COLUMN_INFO_KEY) + } + /// Return the slot-window describing the available historic states. /// /// Returns `(lower_limit, upper_limit)`. diff --git a/beacon_node/store/src/lib.rs b/beacon_node/store/src/lib.rs index e86689b0cf1..31b0bd5658e 100644 --- a/beacon_node/store/src/lib.rs +++ b/beacon_node/store/src/lib.rs @@ -42,6 +42,7 @@ pub use metrics::scrape_for_metrics; use parking_lot::MutexGuard; use std::sync::Arc; use strum::{EnumString, IntoStaticStr}; +use types::data_column_sidecar::DataColumnSidecarList; pub use types::*; pub type ColumnIter<'a, K> = Box), Error>> + 'a>; @@ -203,11 +204,13 @@ pub enum StoreOp<'a, E: EthSpec> { PutBlock(Hash256, Arc>), PutState(Hash256, &'a BeaconState), PutBlobs(Hash256, BlobSidecarList), + PutDataColumns(Hash256, DataColumnSidecarList), PutStateSummary(Hash256, HotStateSummary), PutStateTemporaryFlag(Hash256), DeleteStateTemporaryFlag(Hash256), DeleteBlock(Hash256), DeleteBlobs(Hash256), + DeleteDataColumns(Hash256), DeleteState(Hash256, Option), DeleteExecutionPayload(Hash256), KeyValueOp(KeyValueStoreOp), @@ -223,6 +226,8 @@ pub enum DBColumn { BeaconBlock, #[strum(serialize = "blb")] BeaconBlob, + #[strum(serialize = "bdc")] + BeaconDataColumn, /// For full `BeaconState`s in the hot database (finalized or fork-boundary states). #[strum(serialize = "ste")] BeaconState, @@ -294,6 +299,7 @@ impl DBColumn { | Self::BeaconBlock | Self::BeaconState | Self::BeaconBlob + | Self::BeaconDataColumn | Self::BeaconStateSummary | Self::BeaconStateTemporary | Self::ExecPayload diff --git a/beacon_node/store/src/metadata.rs b/beacon_node/store/src/metadata.rs index 1675051bd80..5aada7c95a5 100644 --- a/beacon_node/store/src/metadata.rs +++ b/beacon_node/store/src/metadata.rs @@ -16,6 +16,7 @@ pub const PRUNING_CHECKPOINT_KEY: Hash256 = Hash256::repeat_byte(3); pub const COMPACTION_TIMESTAMP_KEY: Hash256 = Hash256::repeat_byte(4); pub const ANCHOR_INFO_KEY: Hash256 = Hash256::repeat_byte(5); pub const BLOB_INFO_KEY: Hash256 = Hash256::repeat_byte(6); +pub const DATA_COLUMN_INFO_KEY: Hash256 = Hash256::repeat_byte(7); /// State upper limit value used to indicate that a node is not storing historic states. pub const STATE_UPPER_LIMIT_NO_RETAIN: Slot = Slot::new(u64::MAX); @@ -152,3 +153,30 @@ impl StoreItem for BlobInfo { Ok(Self::from_ssz_bytes(bytes)?) } } + +/// Database parameters relevant to data column sync. +#[derive(Debug, PartialEq, Eq, Clone, Encode, Decode, Serialize, Deserialize, Default)] +pub struct DataColumnInfo { + /// The slot after which data columns are or *will be* available (>=). + /// + /// If this slot is in the future, then it is the first slot of the EIP-7594 fork, from which + /// data columns will be available. + /// + /// If the `oldest_data_column_slot` is `None` then this means that the EIP-7594 fork epoch is + /// not yet known. + pub oldest_data_column_slot: Option, +} + +impl StoreItem for DataColumnInfo { + fn db_column() -> DBColumn { + DBColumn::BeaconMeta + } + + 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/metrics.rs b/beacon_node/store/src/metrics.rs index 2d901fdd932..a5aba47830c 100644 --- a/beacon_node/store/src/metrics.rs +++ b/beacon_node/store/src/metrics.rs @@ -105,6 +105,10 @@ lazy_static! { "store_beacon_blobs_cache_hit_total", "Number of hits to the store's blob cache" ); + pub static ref BEACON_DATA_COLUMNS_CACHE_HIT_COUNT: Result = try_create_int_counter( + "store_beacon_data_columns_cache_hit_total", + "Number of hits to the store's data column cache" + ); pub static ref BEACON_BLOCK_READ_TIMES: Result = try_create_histogram( "store_beacon_block_read_overhead_seconds", "Overhead on reading a beacon block from the DB (e.g., decoding)" diff --git a/consensus/types/src/chain_spec.rs b/consensus/types/src/chain_spec.rs index ac22c8e7613..e5f94bfe3ae 100644 --- a/consensus/types/src/chain_spec.rs +++ b/consensus/types/src/chain_spec.rs @@ -1,5 +1,6 @@ use crate::application_domain::{ApplicationDomain, APPLICATION_DOMAIN_BUILDER}; use crate::blob_sidecar::BlobIdentifier; +use crate::data_column_sidecar::DataColumnIdentifier; use crate::*; use int_to_bytes::int_to_bytes4; use serde::Deserialize; @@ -202,6 +203,7 @@ pub struct ChainSpec { */ pub max_request_blocks_deneb: u64, pub max_request_blob_sidecars: u64, + pub max_request_data_column_sidecars: u64, pub min_epochs_for_blob_sidecars_requests: u64, pub blob_sidecar_subnet_count: u64, pub data_column_sidecar_subnet_count: u64, @@ -214,6 +216,7 @@ pub struct ChainSpec { pub max_blocks_by_root_request: usize, pub max_blocks_by_root_request_deneb: usize, pub max_blobs_by_root_request: usize, + pub max_data_columns_by_root_request: usize, /* * Application params @@ -723,6 +726,7 @@ impl ChainSpec { */ max_request_blocks_deneb: default_max_request_blocks_deneb(), max_request_blob_sidecars: default_max_request_blob_sidecars(), + max_request_data_column_sidecars: default_max_request_data_column_sidecars(), min_epochs_for_blob_sidecars_requests: default_min_epochs_for_blob_sidecars_requests(), blob_sidecar_subnet_count: default_blob_sidecar_subnet_count(), data_column_sidecar_subnet_count: default_data_column_sidecar_subnet_count(), @@ -733,6 +737,7 @@ impl ChainSpec { max_blocks_by_root_request: default_max_blocks_by_root_request(), max_blocks_by_root_request_deneb: default_max_blocks_by_root_request_deneb(), max_blobs_by_root_request: default_max_blobs_by_root_request(), + max_data_columns_by_root_request: default_data_columns_by_root_request(), /* * Application specific @@ -990,6 +995,7 @@ impl ChainSpec { */ max_request_blocks_deneb: default_max_request_blocks_deneb(), max_request_blob_sidecars: default_max_request_blob_sidecars(), + max_request_data_column_sidecars: default_max_request_data_column_sidecars(), min_epochs_for_blob_sidecars_requests: 16384, blob_sidecar_subnet_count: default_blob_sidecar_subnet_count(), data_column_sidecar_subnet_count: default_data_column_sidecar_subnet_count(), @@ -1000,6 +1006,7 @@ impl ChainSpec { max_blocks_by_root_request: default_max_blocks_by_root_request(), max_blocks_by_root_request_deneb: default_max_blocks_by_root_request_deneb(), max_blobs_by_root_request: default_max_blobs_by_root_request(), + max_data_columns_by_root_request: default_data_columns_by_root_request(), /* * Application specific @@ -1170,6 +1177,9 @@ pub struct Config { #[serde(default = "default_max_request_blob_sidecars")] #[serde(with = "serde_utils::quoted_u64")] max_request_blob_sidecars: u64, + #[serde(default = "default_max_request_data_column_sidecars")] + #[serde(with = "serde_utils::quoted_u64")] + max_request_data_column_sidecars: u64, #[serde(default = "default_min_epochs_for_blob_sidecars_requests")] #[serde(with = "serde_utils::quoted_u64")] min_epochs_for_blob_sidecars_requests: u64, @@ -1280,6 +1290,10 @@ const fn default_max_request_blob_sidecars() -> u64 { 768 } +const fn default_max_request_data_column_sidecars() -> u64 { + 16384 +} + const fn default_min_epochs_for_blob_sidecars_requests() -> u64 { 4096 } @@ -1329,6 +1343,20 @@ fn max_blobs_by_root_request_common(max_request_blob_sidecars: u64) -> usize { .len() } +fn max_data_columns_by_root_request_common(max_request_data_column_sidecars: u64) -> usize { + let max_request_data_column_sidecars = max_request_data_column_sidecars as usize; + let empty_data_column_id = DataColumnIdentifier { + block_root: Hash256::zero(), + index: 0, + }; + RuntimeVariableList::from_vec( + vec![empty_data_column_id; max_request_data_column_sidecars], + max_request_data_column_sidecars, + ) + .as_ssz_bytes() + .len() +} + fn default_max_blocks_by_root_request() -> usize { max_blocks_by_root_request_common(default_max_request_blocks()) } @@ -1341,6 +1369,10 @@ fn default_max_blobs_by_root_request() -> usize { max_blobs_by_root_request_common(default_max_request_blob_sidecars()) } +fn default_data_columns_by_root_request() -> usize { + max_data_columns_by_root_request_common(default_max_request_data_column_sidecars()) +} + impl Default for Config { fn default() -> Self { let chain_spec = MainnetEthSpec::default_spec(); @@ -1458,6 +1490,7 @@ impl Config { attestation_subnet_shuffling_prefix_bits: spec.attestation_subnet_shuffling_prefix_bits, max_request_blocks_deneb: spec.max_request_blocks_deneb, max_request_blob_sidecars: spec.max_request_blob_sidecars, + max_request_data_column_sidecars: spec.max_request_data_column_sidecars, min_epochs_for_blob_sidecars_requests: spec.min_epochs_for_blob_sidecars_requests, blob_sidecar_subnet_count: spec.blob_sidecar_subnet_count, data_column_sidecar_subnet_count: spec.data_column_sidecar_subnet_count, @@ -1524,6 +1557,7 @@ impl Config { maximum_gossip_clock_disparity_millis, max_request_blocks_deneb, max_request_blob_sidecars, + max_request_data_column_sidecars, min_epochs_for_blob_sidecars_requests, blob_sidecar_subnet_count, data_column_sidecar_subnet_count, @@ -1583,6 +1617,7 @@ impl Config { maximum_gossip_clock_disparity_millis, max_request_blocks_deneb, max_request_blob_sidecars, + max_request_data_column_sidecars, min_epochs_for_blob_sidecars_requests, blob_sidecar_subnet_count, data_column_sidecar_subnet_count, @@ -1593,6 +1628,9 @@ impl Config { max_request_blocks_deneb, ), max_blobs_by_root_request: max_blobs_by_root_request_common(max_request_blob_sidecars), + max_data_columns_by_root_request: max_data_columns_by_root_request_common( + max_request_data_column_sidecars, + ), ..chain_spec.clone() }) diff --git a/consensus/types/src/data_column_sidecar.rs b/consensus/types/src/data_column_sidecar.rs index 310c13a5e94..a6fc4c56745 100644 --- a/consensus/types/src/data_column_sidecar.rs +++ b/consensus/types/src/data_column_sidecar.rs @@ -1,21 +1,37 @@ use crate::beacon_block_body::KzgCommitments; use crate::test_utils::TestRandom; -use crate::{BlobSidecarList, EthSpec, Hash256, KzgProofs, SignedBeaconBlockHeader, Slot}; +use crate::{ + BeaconBlockHeader, BlobSidecarList, EthSpec, Hash256, KzgProofs, SignedBeaconBlockHeader, Slot, +}; +use bls::Signature; use derivative::Derivative; +use kzg::{KzgCommitment, KzgProof}; use rand::rngs::StdRng; use rand::{Rng, SeedableRng}; use safe_arith::ArithError; use serde::{Deserialize, Serialize}; +use ssz::Encode; use ssz_derive::{Decode, Encode}; +use ssz_types::typenum::Unsigned; use ssz_types::Error as SszError; use ssz_types::{FixedVector, VariableList}; +use std::sync::Arc; use test_random_derive::TestRandom; use tree_hash::TreeHash; use tree_hash_derive::TreeHash; pub type ColumnIndex = u64; pub type Cell = FixedVector::FieldElementsPerCell>; -pub type DataColumn = VariableList, ::MaxBlobsPerBlock>; +pub type DataColumn = VariableList, ::MaxBlobCommitmentsPerBlock>; + +/// Container of the data that identifies an individual data column. +#[derive( + Serialize, Deserialize, Encode, Decode, TreeHash, Copy, Clone, Debug, PartialEq, Eq, Hash, +)] +pub struct DataColumnIdentifier { + pub block_root: Hash256, + pub index: ColumnIndex, +} #[derive( Debug, @@ -121,6 +137,13 @@ impl DataColumnSidecar { Ok(column) } + pub fn id(&self) -> DataColumnIdentifier { + DataColumnIdentifier { + block_root: self.block_root(), + index: self.index, + } + } + pub fn slot(&self) -> Slot { self.signed_block_header.message.slot } @@ -128,6 +151,45 @@ impl DataColumnSidecar { pub fn block_root(&self) -> Hash256 { self.signed_block_header.message.tree_hash_root() } + + pub fn min_size() -> usize { + // min size is one cell + Self { + index: 0, + column: VariableList::new(vec![Cell::::default()]).unwrap(), + kzg_commitments: VariableList::new(vec![KzgCommitment::empty_for_testing()]).unwrap(), + kzg_proofs: VariableList::new(vec![KzgProof::empty()]).unwrap(), + signed_block_header: SignedBeaconBlockHeader { + message: BeaconBlockHeader::empty(), + signature: Signature::empty(), + }, + kzg_commitments_inclusion_proof: Default::default(), + } + .as_ssz_bytes() + .len() + } + + pub fn max_size() -> usize { + Self { + index: 0, + column: VariableList::new(vec![Cell::::default(); T::MaxBlobsPerBlock::to_usize()]) + .unwrap(), + kzg_commitments: VariableList::new(vec![ + KzgCommitment::empty_for_testing(); + T::MaxBlobsPerBlock::to_usize() + ]) + .unwrap(), + kzg_proofs: VariableList::new(vec![KzgProof::empty(); T::MaxBlobsPerBlock::to_usize()]) + .unwrap(), + signed_block_header: SignedBeaconBlockHeader { + message: BeaconBlockHeader::empty(), + signature: Signature::empty(), + }, + kzg_commitments_inclusion_proof: Default::default(), + } + .as_ssz_bytes() + .len() + } } #[derive(Debug)] @@ -151,6 +213,11 @@ impl From for DataColumnSidecarError { } } +pub type DataColumnSidecarList = + VariableList>, ::DataColumnCount>; +pub type FixedDataColumnSidecarList = + FixedVector>>, ::DataColumnCount>; + #[cfg(test)] mod test { use crate::beacon_block::EmptyBlock; From ab427a04fbfb05190adf6cf8dbdf1170828e7cf8 Mon Sep 17 00:00:00 2001 From: Jacob Kaufmann Date: Tue, 12 Mar 2024 01:12:07 -0600 Subject: [PATCH 03/76] feat: add DAS KZG in data col construction (#5210) * feat: add DAS KZG in data col construction * refactor data col sidecar construction * refactor: add data cols to GossipVerifiedBlockContents * Disable windows tests for `das` branch. (c-kzg doesn't build on windows) * Formatting and lint changes only. * refactor: remove iters in construction of data cols * Update vec capacity and error handling. * Add `data_column_sidecar_computation_seconds` metric. --------- Co-authored-by: Jimmy Chen --- .github/workflows/test-suite.yml | 488 +++++++++--------- Cargo.lock | 3 +- .../beacon_chain/src/block_verification.rs | 59 ++- .../src/block_verification_types.rs | 38 +- .../src/data_column_verification.rs | 15 +- beacon_node/beacon_chain/src/metrics.rs | 4 + beacon_node/http_api/src/publish_blocks.rs | 52 +- consensus/types/Cargo.toml | 1 + consensus/types/src/beacon_block_body.rs | 33 ++ consensus/types/src/data_column_sidecar.rs | 242 +++++---- crypto/kzg/Cargo.toml | 2 +- crypto/kzg/src/lib.rs | 48 ++ 12 files changed, 605 insertions(+), 380 deletions(-) diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index 28b8ec29e33..ce69f8e233d 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -34,257 +34,258 @@ jobs: runs-on: ubuntu-latest if: github.event_name == 'pull_request' || github.event_name == 'merge_group' steps: - - name: Check that the pull request is not targeting the stable branch - run: test ${{ github.base_ref }} != "stable" + - name: Check that the pull request is not targeting the stable branch + run: test ${{ github.base_ref }} != "stable" release-tests-ubuntu: name: release-tests-ubuntu # Use self-hosted runners only on the sigp repo. runs-on: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "linux", "CI", "large"]') || 'ubuntu-latest' }} steps: - - uses: actions/checkout@v3 - - name: Get latest version of stable Rust - if: env.SELF_HOSTED_RUNNERS == 'false' - uses: moonrepo/setup-rust@v1 - with: - channel: stable - cache-target: release - bins: cargo-nextest - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Install Foundry (anvil) - if: env.SELF_HOSTED_RUNNERS == 'false' - uses: foundry-rs/foundry-toolchain@v1 - with: - version: nightly-ca67d15f4abd46394b324c50e21e66f306a1162d - - name: Run tests in release - run: make nextest-release - - name: Show cache stats - if: env.SELF_HOSTED_RUNNERS == 'true' - run: sccache --show-stats - release-tests-windows: - name: release-tests-windows - runs-on: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "windows", "CI"]') || 'windows-2019' }} - steps: - - uses: actions/checkout@v3 - - name: Get latest version of stable Rust - if: env.SELF_HOSTED_RUNNERS == 'false' - uses: moonrepo/setup-rust@v1 - with: + - uses: actions/checkout@v3 + - name: Get latest version of stable Rust + if: env.SELF_HOSTED_RUNNERS == 'false' + uses: moonrepo/setup-rust@v1 + with: channel: stable cache-target: release bins: cargo-nextest - env: + env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Install Foundry (anvil) - if: env.SELF_HOSTED_RUNNERS == 'false' - uses: foundry-rs/foundry-toolchain@v1 - with: - version: nightly-ca67d15f4abd46394b324c50e21e66f306a1162d - - name: Install make - if: env.SELF_HOSTED_RUNNERS == 'false' - run: choco install -y make -# - uses: KyleMayes/install-llvm-action@v1 -# if: env.SELF_HOSTED_RUNNERS == 'false' -# with: -# version: "16.0" -# directory: ${{ runner.temp }}/llvm - - name: Set LIBCLANG_PATH - run: echo "LIBCLANG_PATH=$((gcm clang).source -replace "clang.exe")" >> $env:GITHUB_ENV - - name: Run tests in release - run: make nextest-release - - name: Show cache stats - if: env.SELF_HOSTED_RUNNERS == 'true' - run: sccache --show-stats + - name: Install Foundry (anvil) + if: env.SELF_HOSTED_RUNNERS == 'false' + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly-ca67d15f4abd46394b324c50e21e66f306a1162d + - name: Run tests in release + run: make nextest-release + - name: Show cache stats + if: env.SELF_HOSTED_RUNNERS == 'true' + run: sccache --show-stats + # FIXME(das): disabled for now as the c-kzg-4844 `das` branch doesn't build on windows. + # release-tests-windows: + # name: release-tests-windows + # runs-on: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "windows", "CI"]') || 'windows-2019' }} + # steps: + # - uses: actions/checkout@v3 + # - name: Get latest version of stable Rust + # if: env.SELF_HOSTED_RUNNERS == 'false' + # uses: moonrepo/setup-rust@v1 + # with: + # channel: stable + # cache-target: release + # bins: cargo-nextest + # env: + # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # - name: Install Foundry (anvil) + # if: env.SELF_HOSTED_RUNNERS == 'false' + # uses: foundry-rs/foundry-toolchain@v1 + # with: + # version: nightly-ca67d15f4abd46394b324c50e21e66f306a1162d + # - name: Install make + # if: env.SELF_HOSTED_RUNNERS == 'false' + # run: choco install -y make + ## - uses: KyleMayes/install-llvm-action@v1 + ## if: env.SELF_HOSTED_RUNNERS == 'false' + ## with: + ## version: "16.0" + ## directory: ${{ runner.temp }}/llvm + # - name: Set LIBCLANG_PATH + # run: echo "LIBCLANG_PATH=$((gcm clang).source -replace "clang.exe")" >> $env:GITHUB_ENV + # - name: Run tests in release + # run: make nextest-release + # - name: Show cache stats + # if: env.SELF_HOSTED_RUNNERS == 'true' + # run: sccache --show-stats beacon-chain-tests: name: beacon-chain-tests # Use self-hosted runners only on the sigp repo. runs-on: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "linux", "CI", "large"]') || 'ubuntu-latest' }} env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@v3 - - name: Get latest version of stable Rust - if: env.SELF_HOSTED_RUNNERS == 'false' - uses: moonrepo/setup-rust@v1 - with: + - uses: actions/checkout@v3 + - name: Get latest version of stable Rust + if: env.SELF_HOSTED_RUNNERS == 'false' + uses: moonrepo/setup-rust@v1 + with: channel: stable cache-target: release bins: cargo-nextest - - name: Run beacon_chain tests for all known forks - run: make test-beacon-chain - - name: Show cache stats - if: env.SELF_HOSTED_RUNNERS == 'true' - run: sccache --show-stats + - name: Run beacon_chain tests for all known forks + run: make test-beacon-chain + - name: Show cache stats + if: env.SELF_HOSTED_RUNNERS == 'true' + run: sccache --show-stats op-pool-tests: name: op-pool-tests runs-on: ubuntu-latest env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@v3 - - name: Get latest version of stable Rust - uses: moonrepo/setup-rust@v1 - with: + - uses: actions/checkout@v3 + - name: Get latest version of stable Rust + uses: moonrepo/setup-rust@v1 + with: channel: stable cache-target: release bins: cargo-nextest - - name: Run operation_pool tests for all known forks - run: make test-op-pool + - name: Run operation_pool tests for all known forks + run: make test-op-pool network-tests: name: network-tests runs-on: ubuntu-latest env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@v3 - - name: Get latest version of stable Rust - uses: moonrepo/setup-rust@v1 - with: - channel: stable - cache-target: release - bins: cargo-nextest - - name: Run network tests for all known forks - run: make test-network + - uses: actions/checkout@v3 + - name: Get latest version of stable Rust + uses: moonrepo/setup-rust@v1 + with: + channel: stable + cache-target: release + bins: cargo-nextest + - name: Run network tests for all known forks + run: make test-network slasher-tests: name: slasher-tests runs-on: ubuntu-latest env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@v3 - - name: Get latest version of stable Rust - uses: moonrepo/setup-rust@v1 - with: + - uses: actions/checkout@v3 + - name: Get latest version of stable Rust + uses: moonrepo/setup-rust@v1 + with: channel: stable cache-target: release bins: cargo-nextest - - name: Run slasher tests for all supported backends - run: make test-slasher + - name: Run slasher tests for all supported backends + run: make test-slasher debug-tests-ubuntu: name: debug-tests-ubuntu # Use self-hosted runners only on the sigp repo. runs-on: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "linux", "CI", "large"]') || 'ubuntu-latest' }} env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@v3 - - name: Get latest version of stable Rust - if: env.SELF_HOSTED_RUNNERS == 'false' - uses: moonrepo/setup-rust@v1 - with: + - uses: actions/checkout@v3 + - name: Get latest version of stable Rust + if: env.SELF_HOSTED_RUNNERS == 'false' + uses: moonrepo/setup-rust@v1 + with: channel: stable bins: cargo-nextest - - name: Install Foundry (anvil) - if: env.SELF_HOSTED_RUNNERS == 'false' - uses: foundry-rs/foundry-toolchain@v1 - with: - version: nightly-ca67d15f4abd46394b324c50e21e66f306a1162d - - name: Run tests in debug - run: make nextest-debug - - name: Show cache stats - if: env.SELF_HOSTED_RUNNERS == 'true' - run: sccache --show-stats + - name: Install Foundry (anvil) + if: env.SELF_HOSTED_RUNNERS == 'false' + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly-ca67d15f4abd46394b324c50e21e66f306a1162d + - name: Run tests in debug + run: make nextest-debug + - name: Show cache stats + if: env.SELF_HOSTED_RUNNERS == 'true' + run: sccache --show-stats state-transition-vectors-ubuntu: name: state-transition-vectors-ubuntu runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - name: Get latest version of stable Rust - uses: moonrepo/setup-rust@v1 - with: + - uses: actions/checkout@v3 + - name: Get latest version of stable Rust + uses: moonrepo/setup-rust@v1 + with: channel: stable cache-target: release - - name: Run state_transition_vectors in release. - run: make run-state-transition-tests + - name: Run state_transition_vectors in release. + run: make run-state-transition-tests ef-tests-ubuntu: name: ef-tests-ubuntu # Use self-hosted runners only on the sigp repo. runs-on: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "linux", "CI", "small"]') || 'ubuntu-latest' }} env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@v3 - - name: Get latest version of stable Rust - if: env.SELF_HOSTED_RUNNERS == 'false' - uses: moonrepo/setup-rust@v1 - with: + - uses: actions/checkout@v3 + - name: Get latest version of stable Rust + if: env.SELF_HOSTED_RUNNERS == 'false' + uses: moonrepo/setup-rust@v1 + with: channel: stable cache-target: release bins: cargo-nextest - - name: Run consensus-spec-tests with blst, milagro and fake_crypto - run: make nextest-ef - - name: Show cache stats - if: env.SELF_HOSTED_RUNNERS == 'true' - run: sccache --show-stats + - name: Run consensus-spec-tests with blst, milagro and fake_crypto + run: make nextest-ef + - name: Show cache stats + if: env.SELF_HOSTED_RUNNERS == 'true' + run: sccache --show-stats dockerfile-ubuntu: name: dockerfile-ubuntu runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - name: Build the root Dockerfile - run: docker build --build-arg FEATURES=portable -t lighthouse:local . - - name: Test the built image - run: docker run -t lighthouse:local lighthouse --version + - uses: actions/checkout@v3 + - name: Build the root Dockerfile + run: docker build --build-arg FEATURES=portable -t lighthouse:local . + - name: Test the built image + run: docker run -t lighthouse:local lighthouse --version eth1-simulator-ubuntu: name: eth1-simulator-ubuntu runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - name: Get latest version of stable Rust - uses: moonrepo/setup-rust@v1 - with: + - uses: actions/checkout@v3 + - name: Get latest version of stable Rust + uses: moonrepo/setup-rust@v1 + with: channel: stable cache-target: release - - name: Install Foundry (anvil) - uses: foundry-rs/foundry-toolchain@v1 - with: - version: nightly-ca67d15f4abd46394b324c50e21e66f306a1162d - - name: Run the beacon chain sim that starts from an eth1 contract - run: cargo run --release --bin simulator eth1-sim + - name: Install Foundry (anvil) + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly-ca67d15f4abd46394b324c50e21e66f306a1162d + - name: Run the beacon chain sim that starts from an eth1 contract + run: cargo run --release --bin simulator eth1-sim merge-transition-ubuntu: name: merge-transition-ubuntu runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - name: Get latest version of stable Rust - uses: moonrepo/setup-rust@v1 - with: + - uses: actions/checkout@v3 + - name: Get latest version of stable Rust + uses: moonrepo/setup-rust@v1 + with: channel: stable cache-target: release - - name: Install Foundry (anvil) - uses: foundry-rs/foundry-toolchain@v1 - with: - version: nightly-ca67d15f4abd46394b324c50e21e66f306a1162d - - name: Run the beacon chain sim and go through the merge transition - run: cargo run --release --bin simulator eth1-sim --post-merge + - name: Install Foundry (anvil) + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly-ca67d15f4abd46394b324c50e21e66f306a1162d + - name: Run the beacon chain sim and go through the merge transition + run: cargo run --release --bin simulator eth1-sim --post-merge no-eth1-simulator-ubuntu: name: no-eth1-simulator-ubuntu runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - name: Get latest version of stable Rust - uses: moonrepo/setup-rust@v1 - with: + - uses: actions/checkout@v3 + - name: Get latest version of stable Rust + uses: moonrepo/setup-rust@v1 + with: channel: stable cache-target: release - - name: Run the beacon chain sim without an eth1 connection - run: cargo run --release --bin simulator no-eth1-sim + - name: Run the beacon chain sim without an eth1 connection + run: cargo run --release --bin simulator no-eth1-sim syncing-simulator-ubuntu: name: syncing-simulator-ubuntu runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - name: Get latest version of stable Rust - uses: moonrepo/setup-rust@v1 - with: + - uses: actions/checkout@v3 + - name: Get latest version of stable Rust + uses: moonrepo/setup-rust@v1 + with: channel: stable cache-target: release - - name: Install Foundry (anvil) - uses: foundry-rs/foundry-toolchain@v1 - with: - version: nightly-ca67d15f4abd46394b324c50e21e66f306a1162d - - name: Run the syncing simulator - run: cargo run --release --bin simulator syncing-sim + - name: Install Foundry (anvil) + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly-ca67d15f4abd46394b324c50e21e66f306a1162d + - name: Run the syncing simulator + run: cargo run --release --bin simulator syncing-sim doppelganger-protection-test: name: doppelganger-protection-test runs-on: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "linux", "CI", "small"]') || 'ubuntu-latest' }} @@ -292,113 +293,113 @@ jobs: # Enable portable to prevent issues with caching `blst` for the wrong CPU type FEATURES: jemalloc,portable steps: - - uses: actions/checkout@v3 - - name: Get latest version of stable Rust - if: env.SELF_HOSTED_RUNNERS == 'false' - uses: moonrepo/setup-rust@v1 - with: + - uses: actions/checkout@v3 + - name: Get latest version of stable Rust + if: env.SELF_HOSTED_RUNNERS == 'false' + uses: moonrepo/setup-rust@v1 + with: channel: stable cache-target: release - - name: Install geth - if: env.SELF_HOSTED_RUNNERS == 'false' - run: | + - name: Install geth + if: env.SELF_HOSTED_RUNNERS == 'false' + run: | sudo add-apt-repository -y ppa:ethereum/ethereum sudo apt-get update sudo apt-get install ethereum - - name: Install lighthouse - run: | + - name: Install lighthouse + run: | make - - name: Install lcli - # TODO: uncomment after the version of lcli in https://github.com/sigp/lighthouse/pull/5137 - # is installed on the runners - # if: env.SELF_HOSTED_RUNNERS == 'false' - run: make install-lcli - - name: Run the doppelganger protection failure test script - run: | + - name: Install lcli + # TODO: uncomment after the version of lcli in https://github.com/sigp/lighthouse/pull/5137 + # is installed on the runners + # if: env.SELF_HOSTED_RUNNERS == 'false' + run: make install-lcli + - name: Run the doppelganger protection failure test script + run: | cd scripts/tests ./doppelganger_protection.sh failure genesis.json - - name: Run the doppelganger protection success test script - run: | + - name: Run the doppelganger protection success test script + run: | cd scripts/tests ./doppelganger_protection.sh success genesis.json execution-engine-integration-ubuntu: name: execution-engine-integration-ubuntu runs-on: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "linux", "CI", "small"]') || 'ubuntu-latest' }} steps: - - uses: actions/checkout@v3 - - name: Get latest version of stable Rust - if: env.SELF_HOSTED_RUNNERS == 'false' - uses: moonrepo/setup-rust@v1 - with: + - uses: actions/checkout@v3 + - name: Get latest version of stable Rust + if: env.SELF_HOSTED_RUNNERS == 'false' + uses: moonrepo/setup-rust@v1 + with: channel: stable cache-target: release cache: false - env: + env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Add go compiler to $PATH - if: env.SELF_HOSTED_RUNNERS == 'true' - run: echo "/usr/local/go/bin" >> $GITHUB_PATH - - name: Run exec engine integration tests in release - run: make test-exec-engine + - name: Add go compiler to $PATH + if: env.SELF_HOSTED_RUNNERS == 'true' + run: echo "/usr/local/go/bin" >> $GITHUB_PATH + - name: Run exec engine integration tests in release + run: make test-exec-engine check-code: name: check-code runs-on: ubuntu-latest env: CARGO_INCREMENTAL: 1 steps: - - uses: actions/checkout@v3 - - name: Get latest version of stable Rust - uses: moonrepo/setup-rust@v1 - with: + - uses: actions/checkout@v3 + - name: Get latest version of stable Rust + uses: moonrepo/setup-rust@v1 + with: channel: stable cache-target: release components: rustfmt,clippy bins: cargo-audit - - name: Check formatting with cargo fmt - run: make cargo-fmt - - name: Lint code for quality and style with Clippy - run: make lint - - name: Certify Cargo.lock freshness - run: git diff --exit-code Cargo.lock - - name: Typecheck benchmark code without running it - run: make check-benches - - name: Validate state_processing feature arbitrary-fuzz - run: make arbitrary-fuzz - - name: Run cargo audit - run: make audit-CI - - name: Run cargo vendor to make sure dependencies can be vendored for packaging, reproducibility and archival purpose - run: CARGO_HOME=$(readlink -f $HOME) make vendor + - name: Check formatting with cargo fmt + run: make cargo-fmt + - name: Lint code for quality and style with Clippy + run: make lint + - name: Certify Cargo.lock freshness + run: git diff --exit-code Cargo.lock + - name: Typecheck benchmark code without running it + run: make check-benches + - name: Validate state_processing feature arbitrary-fuzz + run: make arbitrary-fuzz + - name: Run cargo audit + run: make audit-CI + - name: Run cargo vendor to make sure dependencies can be vendored for packaging, reproducibility and archival purpose + run: CARGO_HOME=$(readlink -f $HOME) make vendor check-msrv: name: check-msrv runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - name: Install Rust at Minimum Supported Rust Version (MSRV) - run: | - metadata=$(cargo metadata --no-deps --format-version 1) - msrv=$(echo $metadata | jq -r '.packages | map(select(.name == "lighthouse")) | .[0].rust_version') - rustup override set $msrv - - name: Run cargo check - run: cargo check --workspace + - uses: actions/checkout@v3 + - name: Install Rust at Minimum Supported Rust Version (MSRV) + run: | + metadata=$(cargo metadata --no-deps --format-version 1) + msrv=$(echo $metadata | jq -r '.packages | map(select(.name == "lighthouse")) | .[0].rust_version') + rustup override set $msrv + - name: Run cargo check + run: cargo check --workspace cargo-udeps: name: cargo-udeps runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - name: Get latest version of nightly Rust - uses: moonrepo/setup-rust@v1 - with: + - uses: actions/checkout@v3 + - name: Get latest version of nightly Rust + uses: moonrepo/setup-rust@v1 + with: channel: nightly bins: cargo-udeps cache: false - env: + env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Create Cargo config dir - run: mkdir -p .cargo - - name: Install custom Cargo config - run: cp -f .github/custom/config.toml .cargo/config.toml - - name: Run cargo udeps to identify unused crates in the dependency graph - run: make udeps + - name: Create Cargo config dir + run: mkdir -p .cargo + - name: Install custom Cargo config + run: cp -f .github/custom/config.toml .cargo/config.toml + - name: Run cargo udeps to identify unused crates in the dependency graph + run: make udeps env: # Allow warnings on Nightly RUSTFLAGS: "" @@ -406,25 +407,25 @@ jobs: name: compile-with-beta-compiler runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - name: Install dependencies - run: sudo apt install -y git gcc g++ make cmake pkg-config llvm-dev libclang-dev clang - - name: Use Rust beta - run: rustup override set beta - - name: Run make - run: make + - uses: actions/checkout@v3 + - name: Install dependencies + run: sudo apt install -y git gcc g++ make cmake pkg-config llvm-dev libclang-dev clang + - name: Use Rust beta + run: rustup override set beta + - name: Run make + run: make cli-check: name: cli-check runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - name: Get latest version of stable Rust - uses: moonrepo/setup-rust@v1 - with: - channel: stable - cache-target: release - - name: Run Makefile to trigger the bash script - run: make cli + - uses: actions/checkout@v3 + - name: Get latest version of stable Rust + uses: moonrepo/setup-rust@v1 + with: + channel: stable + cache-target: release + - name: Run Makefile to trigger the bash script + run: make cli # This job succeeds ONLY IF all others succeed. It is used by the merge queue to determine whether # a PR is safe to merge. New jobs should be added here. test-suite-success: @@ -433,7 +434,8 @@ jobs: needs: [ 'target-branch-check', 'release-tests-ubuntu', - 'release-tests-windows', + # FIXME(das): disabled for now as the c-kzg-4844 `das` branch doesn't build on windows. + # 'release-tests-windows', 'beacon-chain-tests', 'op-pool-tests', 'network-tests', diff --git a/Cargo.lock b/Cargo.lock index 46fd981aa77..be905910acd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1295,7 +1295,7 @@ dependencies = [ [[package]] name = "c-kzg" version = "0.1.0" -source = "git+https://github.com/ethereum/c-kzg-4844?rev=748283cced543c486145d5f3f38684becdfe3e1b#748283cced543c486145d5f3f38684becdfe3e1b" +source = "git+https://github.com/ethereum/c-kzg-4844?branch=das#5a2656d9409713fdb5112ed5c1c9ec808093de8f" dependencies = [ "bindgen 0.66.1", "blst", @@ -8985,6 +8985,7 @@ dependencies = [ "criterion", "derivative", "eth2_interop_keypairs", + "eth2_network_config", "ethereum-types 0.14.1", "ethereum_hashing", "ethereum_serde_utils", diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index b896327e06f..c0308e0b808 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -53,7 +53,9 @@ use crate::block_verification_types::{ AsBlock, BlockContentsError, BlockImportData, GossipVerifiedBlockContents, RpcBlock, }; use crate::data_availability_checker::{AvailabilityCheckError, MaybeAvailableBlock}; -use crate::data_column_verification::GossipDataColumnError; +use crate::data_column_verification::{ + GossipDataColumnError, GossipVerifiedDataColumn, GossipVerifiedDataColumnList, +}; use crate::eth1_finalization_cache::Eth1FinalizationData; use crate::execution_payload::{ is_optimistic_candidate_block, validate_execution_payload_for_gossip, validate_merge_block, @@ -99,10 +101,12 @@ use std::time::Duration; use store::{Error as DBError, HotStateSummary, KeyValueStore, StoreOp}; use task_executor::JoinHandle; use tree_hash::TreeHash; +use types::data_column_sidecar::DataColumnSidecarError; use types::{ - BeaconBlockRef, BeaconState, BeaconStateError, ChainSpec, CloneConfig, Epoch, EthSpec, - ExecutionBlockHash, Hash256, InconsistentFork, PublicKey, PublicKeyBytes, RelativeEpoch, - SignedBeaconBlock, SignedBeaconBlockHeader, Slot, + BeaconBlockRef, BeaconState, BeaconStateError, BlobSidecarList, ChainSpec, CloneConfig, + DataColumnSidecar, DataColumnSubnetId, Epoch, EthSpec, ExecutionBlockHash, Hash256, + InconsistentFork, PublicKey, PublicKeyBytes, RelativeEpoch, SignedBeaconBlock, + SignedBeaconBlockHeader, Slot, }; use types::{BlobSidecar, ExecPayload}; @@ -727,7 +731,7 @@ impl IntoGossipVerifiedBlockContents for PublishBlockReq let _timer = metrics::start_timer(&metrics::BLOB_SIDECAR_INCLUSION_PROOF_COMPUTATION); let blob = BlobSidecar::new(i, blob, &block, *kzg_proof) - .map_err(BlockContentsError::SidecarError)?; + .map_err(BlockContentsError::BlobSidecarError)?; drop(_timer); let gossip_verified_blob = GossipVerifiedBlob::new(Arc::new(blob), i as u64, chain)?; @@ -737,9 +741,52 @@ impl IntoGossipVerifiedBlockContents for PublishBlockReq Ok::<_, BlockContentsError>(gossip_verified_blobs) }) .transpose()?; + + let gossip_verified_data_columns = gossip_verified_blobs + .as_ref() + .map(|blobs| { + // NOTE: we expect KZG to be initialized if the blobs are present + let kzg = chain + .kzg + .as_ref() + .ok_or(BlockContentsError::DataColumnError( + GossipDataColumnError::::KzgNotInitialized, + ))?; + + let blob_sidecar_list: Vec<_> = + blobs.iter().map(|blob| blob.clone_blob()).collect(); + let blob_sidecar_list = BlobSidecarList::new(blob_sidecar_list) + .map_err(DataColumnSidecarError::SszError)?; + let timer = metrics::start_timer(&metrics::DATA_COLUMN_SIDECAR_COMPUTATION); + let sidecars = DataColumnSidecar::build_sidecars(&blob_sidecar_list, &block, kzg)?; + drop(timer); + let mut gossip_verified_data_columns = vec![]; + for sidecar in sidecars { + let subnet = DataColumnSubnetId::try_from_column_index::( + sidecar.index as usize, + ) + .map_err(|_| { + BlockContentsError::::DataColumnSidecarError( + DataColumnSidecarError::DataColumnIndexOutOfBounds, + ) + })?; + let column = GossipVerifiedDataColumn::new(sidecar, subnet.into(), chain)?; + gossip_verified_data_columns.push(column); + } + let gossip_verified_data_columns = + GossipVerifiedDataColumnList::new(gossip_verified_data_columns) + .map_err(DataColumnSidecarError::SszError)?; + Ok::<_, BlockContentsError>(gossip_verified_data_columns) + }) + .transpose()?; + let gossip_verified_block = GossipVerifiedBlock::new(block, chain)?; - Ok((gossip_verified_block, gossip_verified_blobs)) + Ok(( + gossip_verified_block, + gossip_verified_blobs, + gossip_verified_data_columns, + )) } fn inner_block(&self) -> &SignedBeaconBlock { diff --git a/beacon_node/beacon_chain/src/block_verification_types.rs b/beacon_node/beacon_chain/src/block_verification_types.rs index 263a6eab074..d509ba565a8 100644 --- a/beacon_node/beacon_chain/src/block_verification_types.rs +++ b/beacon_node/beacon_chain/src/block_verification_types.rs @@ -2,14 +2,15 @@ use crate::blob_verification::{GossipBlobError, GossipVerifiedBlobList}; use crate::block_verification::BlockError; use crate::data_availability_checker::AvailabilityCheckError; pub use crate::data_availability_checker::{AvailableBlock, MaybeAvailableBlock}; +use crate::data_column_verification::{GossipDataColumnError, GossipVerifiedDataColumnList}; use crate::eth1_finalization_cache::Eth1FinalizationData; use crate::{get_block_root, GossipVerifiedBlock, PayloadVerificationOutcome}; use derivative::Derivative; use ssz_types::VariableList; use state_processing::ConsensusContext; use std::sync::Arc; -use types::blob_sidecar::{BlobIdentifier, BlobSidecarError, FixedBlobSidecarList}; -use types::data_column_sidecar::DataColumnSidecarList; +use types::blob_sidecar::{self, BlobIdentifier, FixedBlobSidecarList}; +use types::data_column_sidecar::{self, DataColumnSidecarList}; use types::{ BeaconBlockRef, BeaconState, BlindedPayload, BlobSidecarList, Epoch, EthSpec, Hash256, SignedBeaconBlock, SignedBeaconBlockHeader, Slot, @@ -320,14 +321,19 @@ pub struct BlockImportData { pub consensus_context: ConsensusContext, } -pub type GossipVerifiedBlockContents = - (GossipVerifiedBlock, Option>); +pub type GossipVerifiedBlockContents = ( + GossipVerifiedBlock, + Option>, + Option>, +); #[derive(Debug)] pub enum BlockContentsError { BlockError(BlockError), BlobError(GossipBlobError), - SidecarError(BlobSidecarError), + BlobSidecarError(blob_sidecar::BlobSidecarError), + DataColumnError(GossipDataColumnError), + DataColumnSidecarError(data_column_sidecar::DataColumnSidecarError), } impl From> for BlockContentsError { @@ -342,6 +348,18 @@ impl From> for BlockContentsError { } } +impl From> for BlockContentsError { + fn from(value: GossipDataColumnError) -> Self { + Self::DataColumnError(value) + } +} + +impl From for BlockContentsError { + fn from(value: data_column_sidecar::DataColumnSidecarError) -> Self { + Self::DataColumnSidecarError(value) + } +} + impl std::fmt::Display for BlockContentsError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -351,8 +369,14 @@ impl std::fmt::Display for BlockContentsError { BlockContentsError::BlobError(err) => { write!(f, "BlobError({})", err) } - BlockContentsError::SidecarError(err) => { - write!(f, "SidecarError({:?})", err) + BlockContentsError::BlobSidecarError(err) => { + write!(f, "BlobSidecarError({:?})", err) + } + BlockContentsError::DataColumnError(err) => { + write!(f, "DataColumnError({:?})", err) + } + BlockContentsError::DataColumnSidecarError(err) => { + write!(f, "DataColumnSidecarError({:?})", err) } } } diff --git a/beacon_node/beacon_chain/src/data_column_verification.rs b/beacon_node/beacon_chain/src/data_column_verification.rs index 2ea8fe1ae4f..8e045b7f29f 100644 --- a/beacon_node/beacon_chain/src/data_column_verification.rs +++ b/beacon_node/beacon_chain/src/data_column_verification.rs @@ -5,7 +5,10 @@ use kzg::{Error as KzgError, Kzg}; use ssz_derive::{Decode, Encode}; use std::sync::Arc; use types::data_column_sidecar::{ColumnIndex, DataColumnIdentifier}; -use types::{BeaconStateError, DataColumnSidecar, EthSpec, Hash256, SignedBeaconBlockHeader, Slot}; +use types::{ + BeaconStateError, DataColumnSidecar, EthSpec, Hash256, SignedBeaconBlockHeader, Slot, + VariableList, +}; /// An error occurred while validating a gossip data column. #[derive(Debug)] @@ -77,6 +80,11 @@ impl From for GossipDataColumnError { } } +pub type GossipVerifiedDataColumnList = VariableList< + GossipVerifiedDataColumn, + <::EthSpec as EthSpec>::DataColumnCount, +>; + /// A wrapper around a `DataColumnSidecar` that indicates it has been approved for re-gossiping on /// the p2p network. #[derive(Debug)] @@ -113,6 +121,11 @@ impl GossipVerifiedDataColumn { self.data_column.as_data_column() } + /// This is cheap as we're calling clone on an Arc + pub fn clone_data_column(&self) -> Arc> { + self.data_column.clone_data_column() + } + pub fn block_root(&self) -> Hash256 { self.block_root } diff --git a/beacon_node/beacon_chain/src/metrics.rs b/beacon_node/beacon_chain/src/metrics.rs index 9ab2a0d395e..ffc13bd7cae 100644 --- a/beacon_node/beacon_chain/src/metrics.rs +++ b/beacon_node/beacon_chain/src/metrics.rs @@ -1042,6 +1042,10 @@ lazy_static! { "blob_sidecar_inclusion_proof_computation_seconds", "Time taken to compute blob sidecar inclusion proof" ); + pub static ref DATA_COLUMN_SIDECAR_COMPUTATION: Result = try_create_histogram( + "data_column_sidecar_computation_seconds", + "Time taken to compute data column sidecar, including cells, proofs and inclusion proof" + ); } // Fifth lazy-static block is used to account for macro recursion limit. diff --git a/beacon_node/http_api/src/publish_blocks.rs b/beacon_node/http_api/src/publish_blocks.rs index 67e5d00f8c6..7a5c384a4c8 100644 --- a/beacon_node/http_api/src/publish_blocks.rs +++ b/beacon_node/http_api/src/publish_blocks.rs @@ -18,10 +18,11 @@ use std::sync::Arc; use std::time::Duration; use tokio::sync::mpsc::UnboundedSender; use tree_hash::TreeHash; +use types::data_column_sidecar::DataColumnSidecarList; use types::{ - AbstractExecPayload, BeaconBlockRef, BlobSidecarList, DataColumnSidecar, DataColumnSubnetId, - EthSpec, ExecPayload, ExecutionBlockHash, ForkName, FullPayload, FullPayloadMerge, Hash256, - SignedBeaconBlock, SignedBlindedBeaconBlock, VariableList, + AbstractExecPayload, BeaconBlockRef, BlobSidecarList, DataColumnSubnetId, EthSpec, ExecPayload, + ExecutionBlockHash, ForkName, FullPayload, FullPayloadMerge, Hash256, SignedBeaconBlock, + SignedBlindedBeaconBlock, VariableList, }; use warp::http::StatusCode; use warp::{reply::Response, Rejection, Reply}; @@ -67,6 +68,7 @@ pub async fn publish_block>, blobs_opt: Option>, + data_cols_opt: Option>, sender, log, seen_timestamp| { @@ -88,23 +90,6 @@ pub async fn publish_block { let mut pubsub_messages = vec![PubsubMessage::BeaconBlock(block.clone())]; if let Some(blob_sidecars) = blobs_opt { - // Build and publish column sidecars - let col_sidecars = DataColumnSidecar::random_from_blob_sidecars(&blob_sidecars) - .map_err(|e| { - BeaconChainError::UnableToBuildColumnSidecar(format!("{e:?}")) - })?; - - for (col_index, col_sidecar) in col_sidecars.into_iter().enumerate() { - let subnet_id = - DataColumnSubnetId::try_from_column_index::(col_index) - .map_err(|e| { - BeaconChainError::UnableToBuildColumnSidecar(format!("{e:?}")) - })?; - pubsub_messages.push(PubsubMessage::DataColumnSidecar(Box::new(( - subnet_id, - Arc::new(col_sidecar), - )))); - } // Publish blob sidecars for (blob_index, blob) in blob_sidecars.into_iter().enumerate() { pubsub_messages.push(PubsubMessage::BlobSidecar(Box::new(( @@ -113,6 +98,19 @@ pub async fn publish_block( + data_col.index as usize, + ) + .map_err(|e| { + BeaconChainError::UnableToBuildColumnSidecar(format!("{e:?}")) + })?; + pubsub_messages.push(PubsubMessage::DataColumnSidecar(Box::new(( + subnet, data_col, + )))); + } + } crate::publish_pubsub_messages(&sender, pubsub_messages) .map_err(|_| BlockError::BeaconChainError(BeaconChainError::UnableToPublish))?; } @@ -128,7 +126,7 @@ pub async fn publish_block b, Err(BlockContentsError::BlockError(BlockError::BlockIsAlreadyKnown)) @@ -167,6 +165,15 @@ pub async fn publish_block>(); VariableList::from(blobs) }); + let data_cols_opt = gossip_verified_data_columns + .as_ref() + .map(|gossip_verified_data_columns| { + let data_columns = gossip_verified_data_columns + .into_iter() + .map(|col| col.clone_data_column()) + .collect::>(); + VariableList::from(data_columns) + }); let block_root = block_root.unwrap_or(gossip_verified_block.block_root); @@ -174,6 +181,7 @@ pub async fn publish_block publish_block( block_clone, blobs_opt, + data_cols_opt, sender_clone, log_clone, seen_timestamp, @@ -203,6 +212,7 @@ pub async fn publish_block> BeaconBlockBodyRef<'a, T, } } + /// Produces the proof of inclusion for `self.blob_kzg_commitments`. + pub fn kzg_commitments_merkle_proof( + &self, + ) -> Result, Error> { + match self { + Self::Base(_) | Self::Altair(_) | Self::Merge(_) | Self::Capella(_) => { + Err(Error::IncorrectStateVariant) + } + Self::Deneb(body) => { + let leaves = [ + body.randao_reveal.tree_hash_root(), + body.eth1_data.tree_hash_root(), + body.graffiti.tree_hash_root(), + body.proposer_slashings.tree_hash_root(), + body.attester_slashings.tree_hash_root(), + body.attestations.tree_hash_root(), + body.deposits.tree_hash_root(), + body.voluntary_exits.tree_hash_root(), + body.sync_aggregate.tree_hash_root(), + body.execution_payload.tree_hash_root(), + body.bls_to_execution_changes.tree_hash_root(), + body.blob_kzg_commitments.tree_hash_root(), + ]; + let beacon_block_body_depth = leaves.len().next_power_of_two().ilog2() as usize; + let tree = MerkleTree::create(&leaves, beacon_block_body_depth); + let (_, proof) = tree + .generate_proof(BLOB_KZG_COMMITMENTS_INDEX, beacon_block_body_depth) + .map_err(Error::MerkleTreeError)?; + Ok(proof.into()) + } + } + } + /// Return `true` if this block body has a non-zero number of blobs. pub fn has_blobs(self) -> bool { self.blob_kzg_commitments() diff --git a/consensus/types/src/data_column_sidecar.rs b/consensus/types/src/data_column_sidecar.rs index a6fc4c56745..7ff371cee93 100644 --- a/consensus/types/src/data_column_sidecar.rs +++ b/consensus/types/src/data_column_sidecar.rs @@ -1,13 +1,14 @@ use crate::beacon_block_body::KzgCommitments; use crate::test_utils::TestRandom; +use crate::BeaconStateError; use crate::{ - BeaconBlockHeader, BlobSidecarList, EthSpec, Hash256, KzgProofs, SignedBeaconBlockHeader, Slot, + BeaconBlockHeader, BlobSidecarList, EthSpec, Hash256, KzgProofs, SignedBeaconBlock, + SignedBeaconBlockHeader, Slot, }; use bls::Signature; use derivative::Derivative; +use kzg::{Blob as KzgBlob, Error as KzgError, Kzg}; use kzg::{KzgCommitment, KzgProof}; -use rand::rngs::StdRng; -use rand::{Rng, SeedableRng}; use safe_arith::ArithError; use serde::{Deserialize, Serialize}; use ssz::Encode; @@ -62,94 +63,96 @@ pub struct DataColumnSidecar { } impl DataColumnSidecar { - pub fn random_from_blob_sidecars( - blob_sidecars: &BlobSidecarList, - ) -> Result>, DataColumnSidecarError> { - if blob_sidecars.is_empty() { - return Ok(vec![]); - } - - let first_blob_sidecar = blob_sidecars - .first() - .ok_or(DataColumnSidecarError::MissingBlobSidecars)?; - let slot = first_blob_sidecar.slot(); - - // Proof for kzg commitments in `BeaconBlockBody` - let body_proof_start = first_blob_sidecar - .kzg_commitment_inclusion_proof - .len() - .saturating_sub(T::kzg_commitments_inclusion_proof_depth()); - let kzg_commitments_inclusion_proof: FixedVector< - Hash256, - T::KzgCommitmentsInclusionProofDepth, - > = first_blob_sidecar - .kzg_commitment_inclusion_proof - .get(body_proof_start..) - .ok_or(DataColumnSidecarError::KzgCommitmentInclusionProofOutOfBounds)? - .to_vec() - .into(); - - let mut rng = StdRng::seed_from_u64(slot.as_u64()); - let num_of_blobs = blob_sidecars.len(); - - (0..T::number_of_columns()) - .map(|col_index| { - Ok(DataColumnSidecar { - index: col_index as u64, - column: Self::generate_column_data(&mut rng, num_of_blobs, col_index)?, - kzg_commitments: blob_sidecars - .iter() - .map(|b| b.kzg_commitment) - .collect::>() - .into(), - kzg_proofs: blob_sidecars - .iter() - .map(|b| b.kzg_proof) - .collect::>() - .into(), - signed_block_header: first_blob_sidecar.signed_block_header.clone(), - kzg_commitments_inclusion_proof: kzg_commitments_inclusion_proof.clone(), - }) - }) - .collect::, _>>() + pub fn slot(&self) -> Slot { + self.signed_block_header.message.slot } - fn generate_column_data( - rng: &mut StdRng, - num_of_blobs: usize, - index: usize, - ) -> Result, DataColumnSidecarError> { - let mut dummy_cell_data = Cell::::default(); - // Prefix with column index - let prefix = index.to_le_bytes(); - dummy_cell_data - .get_mut(..prefix.len()) - .ok_or(DataColumnSidecarError::DataColumnIndexOutOfBounds)? - .copy_from_slice(&prefix); - // Fill the rest of the vec with random values - rng.fill( - dummy_cell_data - .get_mut(prefix.len()..) - .ok_or(DataColumnSidecarError::DataColumnIndexOutOfBounds)?, - ); - - let column = DataColumn::::new(vec![dummy_cell_data; num_of_blobs])?; - Ok(column) + pub fn block_root(&self) -> Hash256 { + self.signed_block_header.message.tree_hash_root() } - pub fn id(&self) -> DataColumnIdentifier { - DataColumnIdentifier { - block_root: self.block_root(), - index: self.index, + pub fn build_sidecars( + blobs: &BlobSidecarList, + block: &SignedBeaconBlock, + kzg: &Kzg, + ) -> Result, DataColumnSidecarError> { + let kzg_commitments = block + .message() + .body() + .blob_kzg_commitments() + .map_err(|_err| DataColumnSidecarError::PreDeneb)?; + let kzg_commitments_inclusion_proof = + block.message().body().kzg_commitments_merkle_proof()?; + let signed_block_header = block.signed_block_header(); + + let mut columns = + vec![Vec::with_capacity(T::max_blobs_per_block()); T::number_of_columns()]; + let mut column_kzg_proofs = + vec![Vec::with_capacity(T::max_blobs_per_block()); T::number_of_columns()]; + + // NOTE: assumes blob sidecars are ordered by index + for blob in blobs { + let blob = KzgBlob::from_bytes(&blob.blob).map_err(KzgError::from)?; + let (blob_cells, blob_cell_proofs) = kzg.compute_cells_and_proofs(&blob)?; + + // we iterate over each column, and we construct the column from "top to bottom", + // pushing on the cell and the corresponding proof at each column index. we do this for + // each blob (i.e. the outer loop). + for col in 0..T::number_of_columns() { + let cell = + blob_cells + .get(col) + .ok_or(DataColumnSidecarError::InconsistentArrayLength(format!( + "Missing blob cell at index {col}" + )))?; + let cell: Vec = cell + .into_inner() + .into_iter() + .flat_map(|data| (*data).into_iter()) + .collect(); + let cell = Cell::::from(cell); + + let proof = blob_cell_proofs.get(col).ok_or( + DataColumnSidecarError::InconsistentArrayLength(format!( + "Missing blob cell KZG proof at index {col}" + )), + )?; + + let column = + columns + .get_mut(col) + .ok_or(DataColumnSidecarError::InconsistentArrayLength(format!( + "Missing data column at index {col}" + )))?; + let column_proofs = column_kzg_proofs.get_mut(col).ok_or( + DataColumnSidecarError::InconsistentArrayLength(format!( + "Missing data column proofs at index {col}" + )), + )?; + + column.push(cell); + column_proofs.push(*proof); + } } - } - pub fn slot(&self) -> Slot { - self.signed_block_header.message.slot - } + let sidecars: Vec>> = columns + .into_iter() + .zip(column_kzg_proofs) + .enumerate() + .map(|(index, (col, proofs))| { + Arc::new(DataColumnSidecar { + index: index as u64, + column: DataColumn::::from(col), + kzg_commitments: kzg_commitments.clone(), + kzg_proofs: KzgProofs::::from(proofs), + signed_block_header: signed_block_header.clone(), + kzg_commitments_inclusion_proof: kzg_commitments_inclusion_proof.clone(), + }) + }) + .collect(); + let sidecars = DataColumnSidecarList::from(sidecars); - pub fn block_root(&self) -> Hash256 { - self.signed_block_header.message.tree_hash_root() + Ok(sidecars) } pub fn min_size() -> usize { @@ -195,10 +198,14 @@ impl DataColumnSidecar { #[derive(Debug)] pub enum DataColumnSidecarError { ArithError(ArithError), - MissingBlobSidecars, - KzgCommitmentInclusionProofOutOfBounds, + BeaconStateError(BeaconStateError), DataColumnIndexOutOfBounds, + KzgCommitmentInclusionProofOutOfBounds, + KzgError(KzgError), + MissingBlobSidecars, + PreDeneb, SszError(SszError), + InconsistentArrayLength(String), } impl From for DataColumnSidecarError { @@ -207,6 +214,18 @@ impl From for DataColumnSidecarError { } } +impl From for DataColumnSidecarError { + fn from(e: BeaconStateError) -> Self { + Self::BeaconStateError(e) + } +} + +impl From for DataColumnSidecarError { + fn from(e: KzgError) -> Self { + Self::KzgError(e) + } +} + impl From for DataColumnSidecarError { fn from(e: SszError) -> Self { Self::SszError(e) @@ -228,35 +247,56 @@ mod test { DataColumnSidecar, MainnetEthSpec, SignedBeaconBlock, }; use bls::Signature; - use kzg::{KzgCommitment, KzgProof}; + use eth2_network_config::TRUSTED_SETUP_BYTES; + use kzg::{Kzg, KzgCommitment, KzgProof, TrustedSetup}; use std::sync::Arc; #[test] - fn test_random_from_blob_sidecars() { + fn test_build_sidecars() { type E = MainnetEthSpec; let num_of_blobs = 6; let spec = E::default_spec(); - let blob_sidecars: BlobSidecarList = create_test_blob_sidecars(num_of_blobs, &spec); + let (signed_block, blob_sidecars) = + create_test_block_and_blob_sidecars::(num_of_blobs, &spec); - let column_sidecars = DataColumnSidecar::random_from_blob_sidecars(&blob_sidecars).unwrap(); + let trusted_setup: TrustedSetup = serde_json::from_reader(TRUSTED_SETUP_BYTES).unwrap(); + let kzg = Arc::new(Kzg::new_from_trusted_setup(trusted_setup).unwrap()); - assert_eq!(column_sidecars.len(), E::number_of_columns()); + let column_sidecars = + DataColumnSidecar::build_sidecars(&blob_sidecars, &signed_block, &kzg).unwrap(); + + let block_kzg_commitments = signed_block + .message() + .body() + .blob_kzg_commitments() + .unwrap() + .clone(); + let block_kzg_commitments_inclusion_proof = signed_block + .message() + .body() + .kzg_commitments_merkle_proof() + .unwrap(); + assert_eq!(column_sidecars.len(), E::number_of_columns()); for (idx, col_sidecar) in column_sidecars.iter().enumerate() { assert_eq!(col_sidecar.index, idx as u64); + assert_eq!(col_sidecar.kzg_commitments.len(), num_of_blobs); - // ensure column sidecars are prefixed with column index (for verification purpose in prototype only) - let prefix_len = 8; // column index (u64) is stored as the first 8 bytes - let cell = col_sidecar.column.first().unwrap(); - let col_index_prefix = u64::from_le_bytes(cell[0..prefix_len].try_into().unwrap()); - assert_eq!(col_index_prefix, idx as u64) + assert_eq!(col_sidecar.column.len(), num_of_blobs); + assert_eq!(col_sidecar.kzg_proofs.len(), num_of_blobs); + + assert_eq!(col_sidecar.kzg_commitments, block_kzg_commitments); + assert_eq!( + col_sidecar.kzg_commitments_inclusion_proof, + block_kzg_commitments_inclusion_proof + ); } } - fn create_test_blob_sidecars( + fn create_test_block_and_blob_sidecars( num_of_blobs: usize, spec: &ChainSpec, - ) -> BlobSidecarList { + ) -> (SignedBeaconBlock, BlobSidecarList) { let mut block = BeaconBlock::Deneb(BeaconBlockDeneb::empty(spec)); let mut body = block.body_mut(); let blob_kzg_commitments = body.blob_kzg_commitments_mut().unwrap(); @@ -266,7 +306,7 @@ mod test { let signed_block = SignedBeaconBlock::from_block(block, Signature::empty()); - (0..num_of_blobs) + let sidecars = (0..num_of_blobs) .map(|index| { BlobSidecar::new( index, @@ -278,6 +318,8 @@ mod test { }) .collect::, _>>() .unwrap() - .into() + .into(); + + (signed_block, sidecars) } } diff --git a/crypto/kzg/Cargo.toml b/crypto/kzg/Cargo.toml index 7b70166f921..34ec99bbd34 100644 --- a/crypto/kzg/Cargo.toml +++ b/crypto/kzg/Cargo.toml @@ -16,4 +16,4 @@ serde = { workspace = true } ethereum_serde_utils = { workspace = true } hex = { workspace = true } ethereum_hashing = { workspace = true } -c-kzg = { git = "https://github.com/ethereum/c-kzg-4844", rev = "748283cced543c486145d5f3f38684becdfe3e1b"} \ No newline at end of file +c-kzg = { git = "https://github.com/ethereum/c-kzg-4844", branch = "das" } \ No newline at end of file diff --git a/crypto/kzg/src/lib.rs b/crypto/kzg/src/lib.rs index 0e096ba55c2..cd87514943f 100644 --- a/crypto/kzg/src/lib.rs +++ b/crypto/kzg/src/lib.rs @@ -13,6 +13,7 @@ pub use c_kzg::{ Blob, Bytes32, Bytes48, KzgSettings, BYTES_PER_BLOB, BYTES_PER_COMMITMENT, BYTES_PER_FIELD_ELEMENT, BYTES_PER_PROOF, FIELD_ELEMENTS_PER_BLOB, }; +use c_kzg::{Cell, CELLS_PER_BLOB}; #[derive(Debug)] pub enum Error { /// An error from the underlying kzg library. @@ -141,4 +142,51 @@ impl Kzg { ) .map_err(Into::into) } + + /// Computes the cells and associated proofs for a given `blob` at index `index`. + #[allow(clippy::type_complexity)] + pub fn compute_cells_and_proofs( + &self, + blob: &Blob, + ) -> Result<(Box<[Cell; CELLS_PER_BLOB]>, Box<[KzgProof; CELLS_PER_BLOB]>), Error> { + let (cells, proofs) = c_kzg::Cell::compute_cells_and_proofs(blob, &self.trusted_setup) + .map_err(Into::::into)?; + let proofs = Box::new(proofs.map(|proof| KzgProof::from(proof.to_bytes().into_inner()))); + Ok((cells, proofs)) + } + + /// Verifies a batch of cell-proof-commitment triplets. + /// + /// Here, `coordinates` correspond to the (row, col) coordinate of the cell in the extended + /// blob "matrix". In the 1D extension, row corresponds to the blob index, and col corresponds + /// to the data column index. + pub fn verify_cell_proof_batch( + &self, + cells: &[Cell], + kzg_proofs: &[KzgProof], + coordinates: &[(u64, u64)], + kzg_commitments: &[KzgCommitment], + ) -> Result<(), Error> { + let commitments_bytes: Vec = kzg_commitments + .iter() + .map(|comm| Bytes48::from(*comm)) + .collect(); + let proofs_bytes: Vec = kzg_proofs + .iter() + .map(|proof| Bytes48::from(*proof)) + .collect(); + let (rows, columns): (Vec, Vec) = coordinates.iter().cloned().unzip(); + if !c_kzg::KzgProof::verify_cell_proof_batch( + &commitments_bytes, + &rows, + &columns, + cells, + &proofs_bytes, + &self.trusted_setup, + )? { + Err(Error::KzgVerificationFailed) + } else { + Ok(()) + } + } } From 1317f7053570955b80f6c2a128c8b9b5cdfd3638 Mon Sep 17 00:00:00 2001 From: Jacob Kaufmann Date: Thu, 14 Mar 2024 17:20:27 -0600 Subject: [PATCH 04/76] fix: update data col subnet count from 64 to 32 (#5413) --- consensus/types/src/data_column_subnet_id.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/consensus/types/src/data_column_subnet_id.rs b/consensus/types/src/data_column_subnet_id.rs index 5a42b323895..8113db24eff 100644 --- a/consensus/types/src/data_column_subnet_id.rs +++ b/consensus/types/src/data_column_subnet_id.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; use std::fmt::{self, Display}; use std::ops::{Deref, DerefMut}; -const DATA_COLUMN_SUBNET_COUNT: u64 = 64; +const DATA_COLUMN_SUBNET_COUNT: u64 = 32; lazy_static! { static ref DATA_COLUMN_SUBNET_ID_TO_STRING: Vec = { From 7108c747a2760b56f50e7981b39a23be4189eaf2 Mon Sep 17 00:00:00 2001 From: Jacob Kaufmann Date: Tue, 9 Apr 2024 03:37:27 -0600 Subject: [PATCH 05/76] feat: add peerdas custody field to ENR (#5409) * feat: add peerdas custody field to ENR * add hash prefix step in subnet computation * refactor test and fix possible u64 overflow * default to min custody value if not present in ENR --- Cargo.lock | 1 + beacon_node/lighthouse_network/Cargo.toml | 1 + .../lighthouse_network/src/discovery/enr.rs | 30 +++++- .../src/discovery/subnet_predicate.rs | 19 +++- beacon_node/network/src/service.rs | 14 +-- consensus/types/src/data_column_subnet_id.rs | 97 ++++++++++++------- consensus/types/src/eth_spec.rs | 10 +- 7 files changed, 126 insertions(+), 46 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 78ddf185dd3..1050faf12da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5150,6 +5150,7 @@ dependencies = [ "hex", "hex_fmt", "instant", + "itertools", "lazy_static", "libp2p", "libp2p-mplex", diff --git a/beacon_node/lighthouse_network/Cargo.toml b/beacon_node/lighthouse_network/Cargo.toml index 17114180729..24f01a6b8ab 100644 --- a/beacon_node/lighthouse_network/Cargo.toml +++ b/beacon_node/lighthouse_network/Cargo.toml @@ -47,6 +47,7 @@ tracing = { workspace = true } byteorder = { workspace = true } bytes = { workspace = true } either = { workspace = true } +itertools = { workspace = true } # Local dependencies futures-ticker = "0.0.3" diff --git a/beacon_node/lighthouse_network/src/discovery/enr.rs b/beacon_node/lighthouse_network/src/discovery/enr.rs index b0e0a01eecc..c95cbe8f61d 100644 --- a/beacon_node/lighthouse_network/src/discovery/enr.rs +++ b/beacon_node/lighthouse_network/src/discovery/enr.rs @@ -24,6 +24,8 @@ pub const ETH2_ENR_KEY: &str = "eth2"; pub const ATTESTATION_BITFIELD_ENR_KEY: &str = "attnets"; /// The ENR field specifying the sync committee subnet bitfield. pub const SYNC_COMMITTEE_BITFIELD_ENR_KEY: &str = "syncnets"; +/// The ENR field specifying the peerdas custody subnet count. +pub const PEERDAS_CUSTODY_SUBNET_COUNT_ENR_KEY: &str = "custody_subnet_count"; /// Extension trait for ENR's within Eth2. pub trait Eth2Enr { @@ -37,6 +39,9 @@ pub trait Eth2Enr { &self, ) -> Result, &'static str>; + /// The peerdas custody subnet count associated with the ENR. + fn custody_subnet_count(&self) -> Result; + fn eth2(&self) -> Result; } @@ -63,6 +68,17 @@ impl Eth2Enr for Enr { .map_err(|_| "Could not decode the ENR syncnets bitfield") } + fn custody_subnet_count(&self) -> Result { + // NOTE: if the custody value is non-existent in the ENR, then we assume the minimum + // custody value defined in the spec. + let min_custody_bytes = TSpec::min_custody_requirement().as_ssz_bytes(); + let custody_bytes = self + .get(PEERDAS_CUSTODY_SUBNET_COUNT_ENR_KEY) + .unwrap_or(&min_custody_bytes); + u64::from_ssz_bytes(custody_bytes) + .map_err(|_| "Could not decode the ENR custody subnet count") + } + fn eth2(&self) -> Result { let eth2_bytes = self.get(ETH2_ENR_KEY).ok_or("ENR has no eth2 field")?; @@ -225,6 +241,14 @@ pub fn build_enr( builder.add_value(SYNC_COMMITTEE_BITFIELD_ENR_KEY, &bitfield.as_ssz_bytes()); + // set the "custody_subnet_count" field on our ENR + let custody_subnet_count = T::min_custody_requirement() as u64; + + builder.add_value( + PEERDAS_CUSTODY_SUBNET_COUNT_ENR_KEY, + &custody_subnet_count.as_ssz_bytes(), + ); + builder .build(enr_key) .map_err(|e| format!("Could not build Local ENR: {:?}", e)) @@ -248,10 +272,12 @@ fn compare_enr(local_enr: &Enr, disk_enr: &Enr) -> bool { // take preference over disk udp port if one is not specified && (local_enr.udp4().is_none() || local_enr.udp4() == disk_enr.udp4()) && (local_enr.udp6().is_none() || local_enr.udp6() == disk_enr.udp6()) - // we need the ATTESTATION_BITFIELD_ENR_KEY and SYNC_COMMITTEE_BITFIELD_ENR_KEY key to match, - // otherwise we use a new ENR. This will likely only be true for non-validating nodes + // we need the ATTESTATION_BITFIELD_ENR_KEY and SYNC_COMMITTEE_BITFIELD_ENR_KEY and + // PEERDAS_CUSTODY_SUBNET_COUNT_ENR_KEY key to match, otherwise we use a new ENR. This will + // likely only be true for non-validating nodes. && local_enr.get(ATTESTATION_BITFIELD_ENR_KEY) == disk_enr.get(ATTESTATION_BITFIELD_ENR_KEY) && local_enr.get(SYNC_COMMITTEE_BITFIELD_ENR_KEY) == disk_enr.get(SYNC_COMMITTEE_BITFIELD_ENR_KEY) + && local_enr.get(PEERDAS_CUSTODY_SUBNET_COUNT_ENR_KEY) == disk_enr.get(PEERDAS_CUSTODY_SUBNET_COUNT_ENR_KEY) } /// Loads enr from the given directory diff --git a/beacon_node/lighthouse_network/src/discovery/subnet_predicate.rs b/beacon_node/lighthouse_network/src/discovery/subnet_predicate.rs index 0b35465233a..c161abc39db 100644 --- a/beacon_node/lighthouse_network/src/discovery/subnet_predicate.rs +++ b/beacon_node/lighthouse_network/src/discovery/subnet_predicate.rs @@ -1,8 +1,10 @@ //! The subnet predicate used for searching for a particular subnet. use super::*; use crate::types::{EnrAttestationBitfield, EnrSyncCommitteeBitfield}; +use itertools::Itertools; use slog::trace; use std::ops::Deref; +use types::DataColumnSubnetId; /// Returns the predicate for a given subnet. pub fn subnet_predicate( @@ -22,10 +24,16 @@ where }; // Pre-fork/fork-boundary enrs may not contain a syncnets field. - // Don't return early here + // Don't return early here. let sync_committee_bitfield: Result, _> = enr.sync_committee_bitfield::(); + // Pre-fork/fork-boundary enrs may not contain a peerdas custody field. + // Don't return early here. + // + // NOTE: we could map to minimum custody requirement here. + let custody_subnet_count: Result = enr.custody_subnet_count::(); + let predicate = subnets.iter().any(|subnet| match subnet { Subnet::Attestation(s) => attestation_bitfield .get(*s.deref() as usize) @@ -33,8 +41,13 @@ where Subnet::SyncCommittee(s) => sync_committee_bitfield .as_ref() .map_or(false, |b| b.get(*s.deref() as usize).unwrap_or(false)), - // TODO(das) discovery to be implemented at a later phase. Initially we just use a large peer count. - Subnet::DataColumn(_) => false, + Subnet::DataColumn(s) => custody_subnet_count.map_or(false, |count| { + let mut subnets = DataColumnSubnetId::compute_custody_subnets::( + enr.node_id().raw().into(), + count, + ); + subnets.contains(s) + }), }); if !predicate { diff --git a/beacon_node/network/src/service.rs b/beacon_node/network/src/service.rs index 226c5f7bade..47f0696a785 100644 --- a/beacon_node/network/src/service.rs +++ b/beacon_node/network/src/service.rs @@ -17,6 +17,7 @@ use futures::prelude::*; use futures::StreamExt; use lighthouse_network::service::Network; use lighthouse_network::types::GossipKind; +use lighthouse_network::Eth2Enr; use lighthouse_network::{prometheus_client::registry::Registry, MessageAcceptance}; use lighthouse_network::{ rpc::{GoodbyeReason, RPCResponseErrorCode}, @@ -733,12 +734,13 @@ impl NetworkService { } if !self.subscribe_all_subnets { - for column_subnet in - DataColumnSubnetId::compute_subnets_for_data_column::( - self.network_globals.local_enr().node_id().raw().into(), - &self.beacon_chain.spec, - ) - { + for column_subnet in DataColumnSubnetId::compute_custody_subnets::( + self.network_globals.local_enr().node_id().raw().into(), + self.network_globals + .local_enr() + .custody_subnet_count::<::EthSpec>() + .unwrap_or(self.beacon_chain.spec.custody_requirement), + ) { for fork_digest in self.required_gossip_fork_digests() { let gossip_kind = Subnet::DataColumn(column_subnet).into(); let topic = GossipTopic::new( diff --git a/consensus/types/src/data_column_subnet_id.rs b/consensus/types/src/data_column_subnet_id.rs index 8113db24eff..fdac89f6ffa 100644 --- a/consensus/types/src/data_column_subnet_id.rs +++ b/consensus/types/src/data_column_subnet_id.rs @@ -1,8 +1,9 @@ //! Identifies each data column subnet by an integer identifier. -use crate::{ChainSpec, EthSpec}; +use crate::EthSpec; use ethereum_types::U256; use safe_arith::{ArithError, SafeArith}; use serde::{Deserialize, Serialize}; +use smallvec::SmallVec; use std::fmt::{self, Display}; use std::ops::{Deref, DerefMut}; @@ -44,20 +45,50 @@ impl DataColumnSubnetId { } #[allow(clippy::arithmetic_side_effects)] + pub fn columns(&self) -> impl Iterator { + let subnet = self.0; + let data_column_subnet_count = T::data_column_subnet_count() as u64; + let columns_per_subnet = (T::number_of_columns() as u64) / data_column_subnet_count; + (0..columns_per_subnet).map(move |i| data_column_subnet_count * i + subnet) + } + /// Compute required subnets to subscribe to given the node id. /// TODO(das): Add epoch param - /// TODO(das): Add num of subnets (from ENR) - pub fn compute_subnets_for_data_column( + #[allow(clippy::arithmetic_side_effects)] + pub fn compute_custody_subnets( node_id: U256, - spec: &ChainSpec, + custody_subnet_count: u64, ) -> impl Iterator { - let num_of_column_subnets = T::data_column_subnet_count() as u64; - (0..spec.custody_requirement) - .map(move |i| { - let node_offset = (node_id % U256::from(num_of_column_subnets)).as_u64(); - node_offset.saturating_add(i) % num_of_column_subnets - }) - .map(DataColumnSubnetId::new) + // NOTE: we could perform check on `custody_subnet_count` here to ensure that it is a valid + // value, but here we assume it is valid. + + let mut subnets = SmallVec::<[u64; 32]>::new(); + let mut offset = 0; + while (subnets.len() as u64) < custody_subnet_count { + let offset_node_id = node_id + U256::from(offset); + let offset_node_id = offset_node_id.low_u64().to_le_bytes(); + let hash: [u8; 32] = ethereum_hashing::hash_fixed(&offset_node_id); + let hash_prefix = [ + hash[0], hash[1], hash[2], hash[3], hash[4], hash[5], hash[6], hash[7], + ]; + let hash_prefix_u64 = u64::from_le_bytes(hash_prefix); + let subnet = hash_prefix_u64 % (T::data_column_subnet_count() as u64); + + if !subnets.contains(&subnet) { + subnets.push(subnet); + } + + offset += 1 + } + subnets.into_iter().map(DataColumnSubnetId::new) + } + + pub fn compute_custody_columns( + node_id: U256, + custody_subnet_count: u64, + ) -> impl Iterator { + Self::compute_custody_subnets::(node_id, custody_subnet_count) + .flat_map(|subnet| subnet.columns::()) } } @@ -120,6 +151,7 @@ impl From for Error { mod test { use crate::data_column_subnet_id::DataColumnSubnetId; use crate::ChainSpec; + use crate::EthSpec; #[test] fn test_compute_subnets_for_data_column() { @@ -139,32 +171,29 @@ mod test { .map(|v| ethereum_types::U256::from_dec_str(v).unwrap()) .collect::>(); - let expected_subnets = vec![ - vec![0], - vec![29], - vec![28], - vec![20], - vec![30], - vec![9], - vec![18], - vec![21], - vec![23], - vec![29], - ]; - let spec = ChainSpec::mainnet(); - for x in 0..node_ids.len() { - let computed_subnets = DataColumnSubnetId::compute_subnets_for_data_column::< + for node_id in node_ids { + let computed_subnets = DataColumnSubnetId::compute_custody_subnets::< crate::MainnetEthSpec, - >(node_ids[x], &spec); - - assert_eq!( - expected_subnets[x], - computed_subnets - .map(DataColumnSubnetId::into) - .collect::>() - ); + >(node_id, spec.custody_requirement); + let computed_subnets: Vec<_> = computed_subnets.collect(); + + // the number of subnets is equal to the custody requirement + assert_eq!(computed_subnets.len() as u64, spec.custody_requirement); + + let subnet_count = crate::MainnetEthSpec::data_column_subnet_count(); + let columns_per_subnet = crate::MainnetEthSpec::number_of_columns() / subnet_count; + for subnet in computed_subnets { + let columns: Vec<_> = subnet.columns::().collect(); + // the number of columns is equal to the specified number of columns per subnet + assert_eq!(columns.len(), columns_per_subnet); + + for pair in columns.windows(2) { + // each successive column index is offset by the number of subnets + assert_eq!(pair[1] - pair[0], subnet_count as u64); + } + } } } } diff --git a/consensus/types/src/eth_spec.rs b/consensus/types/src/eth_spec.rs index 00bf41e8a73..79c4376b5c7 100644 --- a/consensus/types/src/eth_spec.rs +++ b/consensus/types/src/eth_spec.rs @@ -6,7 +6,7 @@ use ssz_types::typenum::{ bit::B0, UInt, Unsigned, U0, U1024, U1048576, U1073741824, U1099511627776, U128, U131072, U16, U16777216, U2, U2048, U256, U32, U4, U4096, U512, U6, U625, U64, U65536, U8, U8192, }; -use ssz_types::typenum::{U17, U9}; +use ssz_types::typenum::{U1, U17, U9}; use std::fmt::{self, Debug}; use std::str::FromStr; @@ -115,6 +115,7 @@ pub trait EthSpec: /* * New in PeerDAS */ + type MinCustodyRequirement: Unsigned + Clone + Sync + Send + Debug + PartialEq; type DataColumnSubnetCount: Unsigned + Clone + Sync + Send + Debug + PartialEq; type DataColumnCount: Unsigned + Clone + Sync + Send + Debug + PartialEq; type MaxBytesPerColumn: Unsigned + Clone + Sync + Send + Debug + PartialEq; @@ -296,6 +297,10 @@ pub trait EthSpec: Self::DataColumnCount::to_usize() } + fn min_custody_requirement() -> usize { + Self::MinCustodyRequirement::to_usize() + } + fn data_column_subnet_count() -> usize { Self::DataColumnSubnetCount::to_usize() } @@ -353,6 +358,7 @@ impl EthSpec for MainnetEthSpec { type FieldElementsPerCell = U64; type BytesPerBlob = U131072; type KzgCommitmentInclusionProofDepth = U17; + type MinCustodyRequirement = U1; type DataColumnSubnetCount = U32; type DataColumnCount = U128; // Column samples are entire columns in 1D DAS. @@ -396,6 +402,7 @@ impl EthSpec for MinimalEthSpec { type MaxBlobCommitmentsPerBlock = U16; type KzgCommitmentInclusionProofDepth = U9; // DAS spec values copied from `MainnetEthSpec` + type MinCustodyRequirement = U1; type DataColumnSubnetCount = U32; type DataColumnCount = U128; type MaxBytesPerColumn = U65536; @@ -476,6 +483,7 @@ impl EthSpec for GnosisEthSpec { type BytesPerBlob = U131072; type KzgCommitmentInclusionProofDepth = U17; // DAS spec values copied from `MainnetEthSpec` + type MinCustodyRequirement = U1; type DataColumnSubnetCount = U32; type DataColumnCount = U128; type MaxBytesPerColumn = U65536; From d71dc588e0a3ce50e9229a9911098e3301b0b4d8 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Wed, 24 Apr 2024 15:15:35 +1000 Subject: [PATCH 06/76] Fix merge conflicts. --- .github/workflows/test-suite.yml | 66 ++------------------------------ 1 file changed, 4 insertions(+), 62 deletions(-) diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index 8ffd8bb7b40..038f7c0e095 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -221,23 +221,13 @@ jobs: name: dockerfile-ubuntu runs-on: ubuntu-latest steps: -<<<<<<< HEAD - uses: actions/checkout@v4 - name: Build the root Dockerfile run: docker build --build-arg FEATURES=portable -t lighthouse:local . - name: Test the built image run: docker run -t lighthouse:local lighthouse --version - eth1-simulator-ubuntu: - name: eth1-simulator-ubuntu -======= - - uses: actions/checkout@v4 - - name: Build the root Dockerfile - run: docker build --build-arg FEATURES=portable -t lighthouse:local . - - name: Test the built image - run: docker run -t lighthouse:local lighthouse --version basic-simulator-ubuntu: name: basic-simulator-ubuntu ->>>>>>> sigp/unstable runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -246,50 +236,10 @@ jobs: with: channel: stable cache-target: release -<<<<<<< HEAD - - name: Install Foundry (anvil) - uses: foundry-rs/foundry-toolchain@v1 - with: - version: nightly-ca67d15f4abd46394b324c50e21e66f306a1162d - - name: Run the beacon chain sim that starts from an eth1 contract - run: cargo run --release --bin simulator eth1-sim - merge-transition-ubuntu: - name: merge-transition-ubuntu -======= - - name: Run a basic beacon chain sim that starts from Bellatrix - run: cargo run --release --bin simulator basic-sim + - name: Run a basic beacon chain sim that starts from Bellatrix + run: cargo run --release --bin simulator basic-sim fallback-simulator-ubuntu: name: fallback-simulator-ubuntu ->>>>>>> sigp/unstable - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Get latest version of stable Rust - uses: moonrepo/setup-rust@v1 - with: - channel: stable - cache-target: release -<<<<<<< HEAD - - name: Install Foundry (anvil) - uses: foundry-rs/foundry-toolchain@v1 - with: - version: nightly-ca67d15f4abd46394b324c50e21e66f306a1162d - - name: Run the beacon chain sim and go through the merge transition - run: cargo run --release --bin simulator eth1-sim --post-merge - no-eth1-simulator-ubuntu: - name: no-eth1-simulator-ubuntu - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Get latest version of stable Rust - uses: moonrepo/setup-rust@v1 - with: - channel: stable - cache-target: release - - name: Run the beacon chain sim without an eth1 connection - run: cargo run --release --bin simulator no-eth1-sim - syncing-simulator-ubuntu: - name: syncing-simulator-ubuntu runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -298,16 +248,8 @@ jobs: with: channel: stable cache-target: release - - name: Install Foundry (anvil) - uses: foundry-rs/foundry-toolchain@v1 - with: - version: nightly-ca67d15f4abd46394b324c50e21e66f306a1162d - - name: Run the syncing simulator - run: cargo run --release --bin simulator syncing-sim -======= - - name: Run a beacon chain sim which tests VC fallback behaviour - run: cargo run --release --bin simulator fallback-sim ->>>>>>> sigp/unstable + - name: Run a beacon chain sim which tests VC fallback behaviour + run: cargo run --release --bin simulator fallback-sim doppelganger-protection-test: name: doppelganger-protection-test runs-on: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "linux", "CI", "small"]') || 'ubuntu-latest' }} From c5bab04f5abfa763d5a502080f765fc091228ce7 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Wed, 24 Apr 2024 17:06:55 +1000 Subject: [PATCH 07/76] Send custody data column to `DataAvailabilityChecker` for determining block importability (#5570) * Only import custody data columns after publishing a block. * Add `subscribe-all-data-column-subnets` and pass custody column count to `availability_cache`. * Add custody requirement checks to `availability_cache`. * Fix config not being passed to DAChecker and add more logging. * Introduce `peer_das_epoch` and make blobs and columns mutually exclusive. * Add DA filter for PeerDAS. * Fix data availability check and use test_logger in tests. * Fix subscribe to all data column subnets not working correctly. * Fix tests. * Only publish column sidecars if PeerDAS is activated. Add `PEER_DAS_EPOCH` chain spec serialization. * Remove unused data column index in `OverflowKey`. * Fix column sidecars incorrectly produced when there are no blobs. * Re-instate index to `OverflowKey::DataColumn` and downgrade noisy debug log to `trace`. --- Cargo.lock | 87 ++++++ Cargo.toml | 2 + .../beacon_chain/src/block_verification.rs | 96 ++++--- .../src/block_verification_types.rs | 4 +- beacon_node/beacon_chain/src/builder.rs | 19 +- .../src/data_availability_checker.rs | 176 +++++++----- .../src/data_availability_checker/error.rs | 2 + .../overflow_lru_cache.rs | 257 +++++++++++------- beacon_node/client/src/builder.rs | 1 + beacon_node/http_api/src/lib.rs | 24 ++ beacon_node/http_api/src/publish_blocks.rs | 35 ++- .../tests/broadcast_validation_tests.rs | 6 + beacon_node/lighthouse_network/Cargo.toml | 1 + beacon_node/lighthouse_network/src/config.rs | 4 + .../lighthouse_network/src/discovery/enr.rs | 6 +- .../lighthouse_network/src/types/globals.rs | 33 ++- beacon_node/network/src/service.rs | 40 +-- .../sync/block_lookups/single_block_lookup.rs | 4 +- beacon_node/src/cli.rs | 8 + beacon_node/src/config.rs | 4 + book/src/help_bn.md | 3 + .../mainnet/config.yaml | 2 + common/task_executor/Cargo.toml | 1 + common/task_executor/src/test_utils.rs | 3 +- consensus/types/Cargo.toml | 1 + consensus/types/src/chain_spec.rs | 16 ++ consensus/types/src/data_column_sidecar.rs | 36 ++- consensus/types/src/data_column_subnet_id.rs | 23 +- consensus/types/src/eth_spec.rs | 38 ++- crypto/kzg/Cargo.toml | 1 + crypto/kzg/src/lib.rs | 21 ++ lighthouse/tests/beacon_node.rs | 7 + 32 files changed, 701 insertions(+), 260 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0d35dc71a70..fae1a803492 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2220,6 +2220,12 @@ dependencies = [ "syn 2.0.60", ] +[[package]] +name = "downcast" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" + [[package]] name = "dtoa" version = "1.0.9" @@ -3263,6 +3269,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fragile" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" + [[package]] name = "fs2" version = "0.4.3" @@ -4474,6 +4486,7 @@ dependencies = [ "ethereum_ssz", "ethereum_ssz_derive", "hex", + "mockall", "serde", "tree_hash", ] @@ -5150,6 +5163,7 @@ dependencies = [ "libp2p-mplex", "lighthouse_metrics", "lighthouse_version", + "logging", "lru", "lru_cache", "parking_lot 0.12.1", @@ -5504,6 +5518,45 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9366861eb2a2c436c20b12c8dbec5f798cea6b47ad99216be0282942e2c81ea0" +[[package]] +name = "mockall" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43766c2b5203b10de348ffe19f7e54564b64f3d6018ff7648d1e2d6d3a0f0a48" +dependencies = [ + "cfg-if", + "downcast", + "fragile", + "lazy_static", + "mockall_derive", + "predicates", + "predicates-tree", +] + +[[package]] +name = "mockall_derive" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af7cbce79ec385a1d4f54baa90a76401eb15d9cab93685f62e7e9f942aa00ae2" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn 2.0.60", +] + +[[package]] +name = "mockall_double" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1ca96e5ac35256ae3e13536edd39b172b88f41615e1d7b653c8ad24524113e8" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn 2.0.60", +] + [[package]] name = "monitoring_api" version = "0.1.0" @@ -6461,6 +6514,32 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "predicates" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b87bfd4605926cdfefc1c3b5f8fe560e3feca9d5552cf68c466d3d8236c7e8" +dependencies = [ + "anstyle", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" + +[[package]] +name = "predicates-tree" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "pretty_reqwest_error" version = "0.1.0" @@ -8415,6 +8494,7 @@ dependencies = [ "futures", "lazy_static", "lighthouse_metrics", + "logging", "slog", "sloggers", "tokio", @@ -8452,6 +8532,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "termtree" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" + [[package]] name = "test-test_logger" version = "0.1.0" @@ -9025,6 +9111,7 @@ dependencies = [ "maplit", "merkle_proof", "metastruct", + "mockall_double", "parking_lot 0.12.1", "paste", "rand", diff --git a/Cargo.toml b/Cargo.toml index 4d3e77f83d7..3e0631ec09e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -119,6 +119,8 @@ libsecp256k1 = "0.7" log = "0.4" lru = "0.12" maplit = "1" +mockall = "0.12" +mockall_double = "0.3" num_cpus = "1" parking_lot = "0.12" paste = "1" diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 2ce996c69af..e1a1828d81d 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -48,7 +48,7 @@ // returned alongside. #![allow(clippy::result_large_err)] -use crate::blob_verification::{GossipBlobError, GossipVerifiedBlob}; +use crate::blob_verification::{GossipBlobError, GossipVerifiedBlob, GossipVerifiedBlobList}; use crate::block_verification_types::{ AsBlock, BlockContentsError, BlockImportData, GossipVerifiedBlockContents, RpcBlock, }; @@ -104,8 +104,8 @@ use tree_hash::TreeHash; use types::data_column_sidecar::DataColumnSidecarError; use types::{ BeaconBlockRef, BeaconState, BeaconStateError, BlobSidecarList, ChainSpec, CloneConfig, - DataColumnSidecar, DataColumnSubnetId, Epoch, EthSpec, ExecutionBlockHash, Hash256, - InconsistentFork, PublicKey, PublicKeyBytes, RelativeEpoch, SignedBeaconBlock, + DataColumnSidecar, DataColumnSubnetId, Epoch, EthSpec, ExecutionBlockHash, FullPayload, + Hash256, InconsistentFork, PublicKey, PublicKeyBytes, RelativeEpoch, SignedBeaconBlock, SignedBeaconBlockHeader, Slot, }; use types::{BlobSidecar, ExecPayload}; @@ -741,43 +741,16 @@ impl IntoGossipVerifiedBlockContents for PublishBlockReq }) .transpose()?; - let gossip_verified_data_columns = gossip_verified_blobs - .as_ref() - .map(|blobs| { - // NOTE: we expect KZG to be initialized if the blobs are present - let kzg = chain - .kzg - .as_ref() - .ok_or(BlockContentsError::DataColumnError( - GossipDataColumnError::::KzgNotInitialized, - ))?; - - let blob_sidecar_list: Vec<_> = - blobs.iter().map(|blob| blob.clone_blob()).collect(); - let blob_sidecar_list = BlobSidecarList::new(blob_sidecar_list) - .map_err(DataColumnSidecarError::SszError)?; - let timer = metrics::start_timer(&metrics::DATA_COLUMN_SIDECAR_COMPUTATION); - let sidecars = DataColumnSidecar::build_sidecars(&blob_sidecar_list, &block, kzg)?; - drop(timer); - let mut gossip_verified_data_columns = vec![]; - for sidecar in sidecars { - let subnet = DataColumnSubnetId::try_from_column_index::( - sidecar.index as usize, - ) - .map_err(|_| { - BlockContentsError::::DataColumnSidecarError( - DataColumnSidecarError::DataColumnIndexOutOfBounds, - ) - })?; - let column = GossipVerifiedDataColumn::new(sidecar, subnet.into(), chain)?; - gossip_verified_data_columns.push(column); - } - let gossip_verified_data_columns = - GossipVerifiedDataColumnList::new(gossip_verified_data_columns) - .map_err(DataColumnSidecarError::SszError)?; - Ok::<_, BlockContentsError>(gossip_verified_data_columns) - }) - .transpose()?; + let peer_das_enabled = chain + .spec + .peer_das_epoch + .map_or(false, |peer_das_epoch| block.epoch() >= peer_das_epoch); + + let gossip_verified_data_columns = if peer_das_enabled { + build_gossip_verified_data_columns(chain, &block, gossip_verified_blobs.as_ref())? + } else { + None + }; let gossip_verified_block = GossipVerifiedBlock::new(block, chain)?; @@ -793,6 +766,49 @@ impl IntoGossipVerifiedBlockContents for PublishBlockReq } } +fn build_gossip_verified_data_columns( + chain: &BeaconChain, + block: &SignedBeaconBlock>, + gossip_verified_blobs: Option<&GossipVerifiedBlobList>, +) -> Result>, BlockContentsError> { + gossip_verified_blobs + // Only attempt to build data columns if blobs is non empty to avoid skewing the metrics. + .filter(|b| !b.is_empty()) + .map(|blobs| { + // NOTE: we expect KZG to be initialized if the blobs are present + let kzg = chain + .kzg + .as_ref() + .ok_or(BlockContentsError::DataColumnError( + GossipDataColumnError::::KzgNotInitialized, + ))?; + + let blob_sidecar_list: Vec<_> = blobs.iter().map(|blob| blob.clone_blob()).collect(); + let blob_sidecar_list = BlobSidecarList::new(blob_sidecar_list) + .map_err(DataColumnSidecarError::SszError)?; + let timer = metrics::start_timer(&metrics::DATA_COLUMN_SIDECAR_COMPUTATION); + let sidecars = DataColumnSidecar::build_sidecars(&blob_sidecar_list, block, kzg)?; + drop(timer); + let mut gossip_verified_data_columns = vec![]; + for sidecar in sidecars { + let subnet = + DataColumnSubnetId::try_from_column_index::(sidecar.index as usize) + .map_err(|_| { + BlockContentsError::::DataColumnSidecarError( + DataColumnSidecarError::DataColumnIndexOutOfBounds, + ) + })?; + let column = GossipVerifiedDataColumn::new(sidecar, subnet.into(), chain)?; + gossip_verified_data_columns.push(column); + } + let gossip_verified_data_columns = + GossipVerifiedDataColumnList::new(gossip_verified_data_columns) + .map_err(DataColumnSidecarError::SszError)?; + Ok::<_, BlockContentsError>(gossip_verified_data_columns) + }) + .transpose() +} + /// Implemented on types that can be converted into a `ExecutionPendingBlock`. /// /// Used to allow functions to accept blocks at various stages of verification. diff --git a/beacon_node/beacon_chain/src/block_verification_types.rs b/beacon_node/beacon_chain/src/block_verification_types.rs index 813eb64d903..f6935fc90e1 100644 --- a/beacon_node/beacon_chain/src/block_verification_types.rs +++ b/beacon_node/beacon_chain/src/block_verification_types.rs @@ -84,8 +84,8 @@ enum RpcBlockInner { /// This variant is used with parent lookups and by-range responses. It should have all blobs /// ordered, all block roots matching, and the correct number of blobs for this block. BlockAndBlobs(Arc>, BlobSidecarList), - /// This variant is used with parent lookups and by-range responses. It should have all data columns - /// ordered, all block roots matching, and the correct number of data columns for this block. + /// This variant is used with parent lookups and by-range responses. It should have all + /// requested data columns, all block roots matching for this block. BlockAndDataColumns(Arc>, DataColumnSidecarList), } diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index 9f99cad8fb6..c5d94555096 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -104,6 +104,7 @@ pub struct BeaconChainBuilder { kzg: Option>, task_executor: Option, validator_monitor_config: Option, + import_all_data_columns: bool, } impl @@ -145,6 +146,7 @@ where kzg: None, task_executor: None, validator_monitor_config: None, + import_all_data_columns: false, } } @@ -618,6 +620,12 @@ where self } + /// Sets whether to require and import all data columns when importing block. + pub fn import_all_data_columns(mut self, import_all_data_columns: bool) -> Self { + self.import_all_data_columns = import_all_data_columns; + self + } + /// Sets the `BeaconChain` event handler backend. /// /// For example, provide `ServerSentEventHandler` as a `handler`. @@ -968,8 +976,15 @@ where validator_monitor: RwLock::new(validator_monitor), genesis_backfill_slot, data_availability_checker: Arc::new( - DataAvailabilityChecker::new(slot_clock, self.kzg.clone(), store, &log, self.spec) - .map_err(|e| format!("Error initializing DataAvailabiltyChecker: {:?}", e))?, + DataAvailabilityChecker::new( + slot_clock, + self.kzg.clone(), + store, + self.import_all_data_columns, + &log, + self.spec, + ) + .map_err(|e| format!("Error initializing DataAvailabilityChecker: {:?}", e))?, ), kzg: self.kzg.clone(), block_production_state: Arc::new(Mutex::new(None)), diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index d1f5cbd3b17..65533531423 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -7,7 +7,7 @@ use crate::data_availability_checker::overflow_lru_cache::OverflowLRUCache; use crate::{BeaconChain, BeaconChainTypes, BeaconStore}; use kzg::Kzg; use slasher::test_utils::E; -use slog::{debug, error, Logger}; +use slog::{debug, error, o, Logger}; use slot_clock::SlotClock; use ssz_types::FixedVector; use std::fmt; @@ -79,10 +79,26 @@ impl DataAvailabilityChecker { slot_clock: T::SlotClock, kzg: Option>, store: BeaconStore, + import_all_data_columns: bool, log: &Logger, spec: ChainSpec, ) -> Result { - let overflow_cache = OverflowLRUCache::new(OVERFLOW_LRU_CAPACITY, store, spec.clone())?; + let custody_subnet_count = if import_all_data_columns { + E::data_column_subnet_count() + } else { + E::min_custody_requirement() + }; + + let custody_column_count = + custody_subnet_count.saturating_mul(E::data_columns_per_subnet()); + let overflow_cache = OverflowLRUCache::new( + OVERFLOW_LRU_CAPACITY, + store, + custody_column_count, + log.new(o!("service" => "availability_cache")), + spec.clone(), + )?; + Ok(Self { availability_cache: Arc::new(overflow_cache), slot_clock, @@ -250,49 +266,52 @@ impl DataAvailabilityChecker { block: RpcBlock, ) -> Result, AvailabilityCheckError> { let (block_root, block, blobs, data_columns) = block.deconstruct(); - match (blobs, data_columns) { - (None, None) => { - if self.blobs_required_for_block(&block) { - Ok(MaybeAvailableBlock::AvailabilityPending { block_root, block }) - } else { - Ok(MaybeAvailableBlock::Available(AvailableBlock { - block_root, - block, - blobs: None, - blobs_available_timestamp: None, - data_columns: None, - })) - } - } - (maybe_blob_list, maybe_data_column_list) => { - let (verified_blobs, verified_data_column) = - if self.blobs_required_for_block(&block) { - let kzg = self - .kzg - .as_ref() - .ok_or(AvailabilityCheckError::KzgNotInitialized)?; - - if let Some(blob_list) = maybe_blob_list.as_ref() { - verify_kzg_for_blob_list(blob_list.iter(), kzg) - .map_err(AvailabilityCheckError::Kzg)?; - } - if let Some(data_column_list) = maybe_data_column_list.as_ref() { - verify_kzg_for_data_column_list(data_column_list.iter(), kzg) - .map_err(AvailabilityCheckError::Kzg)?; - } - (maybe_blob_list, maybe_data_column_list) - } else { - (None, None) - }; + if self.blobs_required_for_block(&block) { + return if let Some(blob_list) = blobs.as_ref() { + let kzg = self + .kzg + .as_ref() + .ok_or(AvailabilityCheckError::KzgNotInitialized)?; + verify_kzg_for_blob_list(blob_list.iter(), kzg) + .map_err(AvailabilityCheckError::Kzg)?; Ok(MaybeAvailableBlock::Available(AvailableBlock { block_root, block, - blobs: verified_blobs, + blobs, blobs_available_timestamp: None, - data_columns: verified_data_column, + data_columns: None, })) - } + } else { + Ok(MaybeAvailableBlock::AvailabilityPending { block_root, block }) + }; + } + if self.data_columns_required_for_block(&block) { + return if let Some(data_column_list) = data_columns.as_ref() { + let kzg = self + .kzg + .as_ref() + .ok_or(AvailabilityCheckError::KzgNotInitialized)?; + verify_kzg_for_data_column_list(data_column_list.iter(), kzg) + .map_err(AvailabilityCheckError::Kzg)?; + Ok(MaybeAvailableBlock::Available(AvailableBlock { + block_root, + block, + blobs: None, + blobs_available_timestamp: None, + data_columns, + })) + } else { + Ok(MaybeAvailableBlock::AvailabilityPending { block_root, block }) + }; } + + Ok(MaybeAvailableBlock::Available(AvailableBlock { + block_root, + block, + blobs: None, + blobs_available_timestamp: None, + data_columns: None, + })) } /// Checks if a vector of blocks are available. Returns a vector of `MaybeAvailableBlock` @@ -324,39 +343,46 @@ impl DataAvailabilityChecker { verify_kzg_for_blob_list(all_blobs.iter(), kzg)?; } + // TODO(das) verify kzg for all data columns + for block in blocks { let (block_root, block, blobs, data_columns) = block.deconstruct(); - match (blobs, data_columns) { - (None, None) => { - if self.blobs_required_for_block(&block) { - results.push(MaybeAvailableBlock::AvailabilityPending { block_root, block }) - } else { - results.push(MaybeAvailableBlock::Available(AvailableBlock { - block_root, - block, - blobs: None, - blobs_available_timestamp: None, - data_columns: None, - })) - } + + let maybe_available_block = if self.blobs_required_for_block(&block) { + if blobs.is_some() { + MaybeAvailableBlock::Available(AvailableBlock { + block_root, + block, + blobs, + blobs_available_timestamp: None, + data_columns: None, + }) + } else { + MaybeAvailableBlock::AvailabilityPending { block_root, block } } - (maybe_blob_list, maybe_data_column_list) => { - let (verified_blobs, verified_data_columns) = - if self.blobs_required_for_block(&block) { - (maybe_blob_list, maybe_data_column_list) - } else { - (None, None) - }; - // already verified kzg for all blobs - results.push(MaybeAvailableBlock::Available(AvailableBlock { + } else if self.data_columns_required_for_block(&block) { + if data_columns.is_some() { + MaybeAvailableBlock::Available(AvailableBlock { block_root, block, - blobs: verified_blobs, + blobs: None, blobs_available_timestamp: None, - data_columns: verified_data_columns, - })) + data_columns, + }) + } else { + MaybeAvailableBlock::AvailabilityPending { block_root, block } } - } + } else { + MaybeAvailableBlock::Available(AvailableBlock { + block_root, + block, + blobs: None, + blobs_available_timestamp: None, + data_columns: None, + }) + }; + + results.push(maybe_available_block); } Ok(results) @@ -365,7 +391,25 @@ impl DataAvailabilityChecker { /// Determines the blob requirements for a block. If the block is pre-deneb, no blobs are required. /// If the block's epoch is from prior to the data availability boundary, no blobs are required. fn blobs_required_for_block(&self, block: &SignedBeaconBlock) -> bool { - block.num_expected_blobs() > 0 && self.da_check_required_for_epoch(block.epoch()) + block.num_expected_blobs() > 0 + && self.da_check_required_for_epoch(block.epoch()) + && !self.is_peer_das_enabled_for_epoch(block.epoch()) + } + + /// Determines the data column requirements for a block. + /// - If the block is pre-peerdas, no data columns are required. + /// - If the block's epoch is from prior to the data availability boundary, no data columns are required. + fn data_columns_required_for_block(&self, block: &SignedBeaconBlock) -> bool { + block.num_expected_blobs() > 0 + && self.da_check_required_for_epoch(block.epoch()) + && self.is_peer_das_enabled_for_epoch(block.epoch()) + } + + /// Returns true if the given epoch is greater than or equal to the `PEER_DAS_EPOCH`. + fn is_peer_das_enabled_for_epoch(&self, block_epoch: Epoch) -> bool { + self.spec + .peer_das_epoch + .map_or(false, |peer_das_epoch| block_epoch >= peer_das_epoch) } /// The epoch at which we require a data availability check in block processing. 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 58aedc73ab1..e4843889bbf 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker/error.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker/error.rs @@ -10,6 +10,7 @@ pub enum Error { blob_commitment: KzgCommitment, block_commitment: KzgCommitment, }, + UnableToDetermineImportRequirement, Unexpected, SszTypes(ssz_types::Error), MissingBlobs, @@ -41,6 +42,7 @@ impl Error { | Error::Unexpected | Error::ParentStateMissing(_) | Error::BlockReplayError(_) + | Error::UnableToDetermineImportRequirement | Error::RebuildingStateCaches(_) | Error::SlotClockError => ErrorCategory::Internal, Error::Kzg(_) diff --git a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs index dc29388bf9a..8db5ebdbad7 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs @@ -39,6 +39,7 @@ use crate::store::{DBColumn, KeyValueStore}; use crate::BeaconChainTypes; use lru::LruCache; use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard}; +use slog::{debug, trace, Logger}; use ssz::{Decode, Encode}; use ssz_derive::{Decode, Encode}; use ssz_types::{FixedVector, VariableList}; @@ -52,14 +53,21 @@ use types::{BlobSidecar, ChainSpec, DataColumnSidecar, Epoch, EthSpec, Hash256}; /// /// The blobs are all gossip and kzg verified. /// The block has completed all verifications except the availability check. +/// TODO(das): this struct can potentially be reafactored as blobs and data columns are mutually +/// exclusive and this could simplify `is_importable`. #[derive(Encode, Decode, Clone)] pub struct PendingComponents { pub block_root: Hash256, pub verified_blobs: FixedVector>, E::MaxBlobsPerBlock>, - pub verified_data_columns: FixedVector>, E::DataColumnCount>, + pub verified_data_columns: VariableList, E::DataColumnCount>, pub executed_block: Option>, } +pub enum BlockImportRequirement { + AllBlobs, + CustodyColumns(usize), +} + impl PendingComponents { /// Returns an immutable reference to the cached block. pub fn get_cached_block(&self) -> &Option> { @@ -73,10 +81,20 @@ impl PendingComponents { &self.verified_blobs } - /// Returns an immutable reference to the fixed vector of cached data columns. + /// Returns a mutable reference to the cached data column. + pub fn get_cached_data_column( + &self, + data_column_index: u64, + ) -> Option<&KzgVerifiedDataColumn> { + self.verified_data_columns + .iter() + .find(|d| d.data_column_index() == data_column_index) + } + + /// Returns an immutable reference to the list of cached data columns. pub fn get_cached_data_columns( &self, - ) -> &FixedVector>, E::DataColumnCount> { + ) -> &VariableList, E::DataColumnCount> { &self.verified_data_columns } @@ -95,7 +113,7 @@ impl PendingComponents { /// Returns a mutable reference to the fixed vector of cached data columns. pub fn get_cached_data_columns_mut( &mut self, - ) -> &mut FixedVector>, E::DataColumnCount> { + ) -> &mut VariableList, E::DataColumnCount> { &mut self.verified_data_columns } @@ -111,16 +129,15 @@ impl PendingComponents { .unwrap_or(false) } - /// Checks if a data column exists at the given index in the cache. + /// Checks if a data column of a given index exists in the cache. /// /// Returns: - /// - `true` if a data column exists at the given index. + /// - `true` if a data column for the given index exists. /// - `false` otherwise. - fn data_column_exists(&self, data_colum_index: usize) -> bool { + fn data_column_exists(&self, data_colum_index: u64) -> bool { self.get_cached_data_columns() - .get(data_colum_index) - .map(|d| d.is_some()) - .unwrap_or(false) + .iter() + .any(|d| d.data_column_index() == data_colum_index) } /// Returns the number of blobs that are expected to be present. Returns `None` if we don't have a @@ -138,6 +155,11 @@ impl PendingComponents { self.get_cached_blobs().iter().flatten().count() } + /// Returns the number of data columns that have been received and are stored in the cache. + pub fn num_received_data_columns(&self) -> usize { + self.get_cached_data_columns().iter().count() + } + /// Inserts a block into the cache. pub fn insert_block(&mut self, block: DietAvailabilityPendingExecutedBlock) { *self.get_cached_block_mut() = Some(block) @@ -185,40 +207,18 @@ impl PendingComponents { } } - /// Inserts a data column at a specific index in the cache. - /// - /// Existing data column at the index will be replaced. - fn insert_data_column_at_index( - &mut self, - data_column_index: usize, - data_column: KzgVerifiedDataColumn, - ) { - if let Some(b) = self - .get_cached_data_columns_mut() - .get_mut(data_column_index) - { - *b = Some(data_column); - } - } - /// Merges a given set of data columns into the cache. - /// - /// Data columns are only inserted if: - /// 1. The data column entry at the index is empty and no block exists. - /// 2. The block exists and its commitments matches the data column's commitments. - fn merge_data_columns( + fn merge_data_columns>>( &mut self, - data_columns: FixedVector>, E::DataColumnCount>, - ) { - for (index, data_column) in data_columns.iter().cloned().enumerate() { - let Some(data_column) = data_column else { - continue; - }; + kzg_verified_data_columns: I, + ) -> Result<(), AvailabilityCheckError> { + for data_column in kzg_verified_data_columns { // TODO(das): Add equivalent checks for data columns if necessary - if !self.data_column_exists(index) { - self.insert_data_column_at_index(index, data_column) + if !self.data_column_exists(data_column.data_column_index()) { + self.verified_data_columns.push(data_column)?; } } + Ok(()) } /// Inserts a new block and revalidates the existing blobs against it. @@ -230,15 +230,48 @@ impl PendingComponents { self.merge_blobs(reinsert); } - /// Checks if the block and all of its expected blobs are available in the cache. + /// Checks if the block and all of its expected blobs or custody columns (post-PeerDAS) are + /// available in the cache. /// - /// Returns `true` if both the block exists and the number of received blobs matches the number - /// of expected blobs. - pub fn is_available(&self) -> bool { - if let Some(num_expected_blobs) = self.num_expected_blobs() { - num_expected_blobs == self.num_received_blobs() - } else { - false + /// Returns `true` if both the block exists and the number of received blobs / custody columns + /// matches the number of expected blobs / custody columns. + pub fn is_available( + &self, + block_import_requirement: BlockImportRequirement, + log: &Logger, + ) -> bool { + match block_import_requirement { + BlockImportRequirement::AllBlobs => { + debug!( + log, + "Checking block and blob importability"; + "block_root" => %self.block_root, + "num_expected_blobs" => ?self.num_expected_blobs(), + "num_received_blobs" => self.num_received_blobs(), + ); + if let Some(num_expected_blobs) = self.num_expected_blobs() { + num_expected_blobs == self.num_received_blobs() + } else { + false + } + } + BlockImportRequirement::CustodyColumns(num_expected_columns) => { + let num_received_data_columns = self.num_received_data_columns(); + + trace!( + log, + "Checking block and data column importability"; + "block_root" => %self.block_root, + "num_received_data_columns" => num_received_data_columns, + ); + + if let Some(num_expected_blobs) = self.num_expected_blobs() { + // No data columns when there are 0 blobs + num_expected_blobs == 0 || num_expected_columns == num_received_data_columns + } else { + false + } + } } } @@ -247,7 +280,7 @@ impl PendingComponents { Self { block_root, verified_blobs: FixedVector::default(), - verified_data_columns: FixedVector::default(), + verified_data_columns: VariableList::default(), executed_block: None, } } @@ -295,8 +328,7 @@ impl PendingComponents { // TODO(das) Do we need a check here for number of expected custody columns? let verified_data_columns = verified_data_columns .into_iter() - .cloned() - .filter_map(|d| d.map(|d| d.to_data_column())) + .map(|d| d.to_data_column()) .collect::>() .into(); @@ -336,16 +368,15 @@ impl PendingComponents { }); } } - for maybe_data_column in self.verified_data_columns.iter() { - if maybe_data_column.is_some() { - return maybe_data_column.as_ref().map(|kzg_verified_data_column| { - kzg_verified_data_column - .as_data_column() - .slot() - .epoch(E::slots_per_epoch()) - }); - } + + if let Some(kzg_verified_data_column) = self.verified_data_columns.first() { + let epoch = kzg_verified_data_column + .as_data_column() + .slot() + .epoch(E::slots_per_epoch()); + return Some(epoch); } + None }) } @@ -434,10 +465,7 @@ impl OverflowStore { .put_bytes(col.as_str(), &key.as_ssz_bytes(), &blob.as_ssz_bytes())? } - for data_column in Vec::from(pending_components.verified_data_columns) - .into_iter() - .flatten() - { + for data_column in pending_components.verified_data_columns.into_iter() { let key = OverflowKey::from_data_column_id::(DataColumnIdentifier { block_root, index: data_column.data_column_index(), @@ -483,15 +511,13 @@ impl OverflowStore { .ok_or(AvailabilityCheckError::BlobIndexInvalid(index as u64))? = Some(KzgVerifiedBlob::from_ssz_bytes(value_bytes.as_slice())?); } - OverflowKey::DataColumn(_, index) => { - *maybe_pending_components + OverflowKey::DataColumn(_, _index) => { + let data_column = + KzgVerifiedDataColumn::from_ssz_bytes(value_bytes.as_slice())?; + maybe_pending_components .get_or_insert_with(|| PendingComponents::empty(block_root)) - .verified_data_columns - .get_mut(index as usize) - .ok_or(AvailabilityCheckError::DataColumnIndexInvalid(index as u64))? = - Some(KzgVerifiedDataColumn::from_ssz_bytes( - value_bytes.as_slice(), - )?); + .get_cached_data_columns_mut() + .push(data_column)?; } } } @@ -608,12 +634,7 @@ impl Critical { ) -> Result>>, AvailabilityCheckError> { if let Some(pending_components) = self.in_memory.peek(&data_column_id.block_root) { Ok(pending_components - .verified_data_columns - .get(data_column_id.index as usize) - .ok_or(AvailabilityCheckError::DataColumnIndexInvalid( - data_column_id.index, - ))? - .as_ref() + .get_cached_data_column(data_column_id.index) .map(|data_column| data_column.clone_data_column())) } else { Ok(None) @@ -694,12 +715,18 @@ pub struct OverflowLRUCache { maintenance_lock: Mutex<()>, /// The capacity of the LRU cache capacity: NonZeroUsize, + /// The number of data columns the node is custodying. + custody_column_count: usize, + log: Logger, + spec: ChainSpec, } impl OverflowLRUCache { pub fn new( capacity: NonZeroUsize, beacon_store: BeaconStore, + custody_column_count: usize, + log: Logger, spec: ChainSpec, ) -> Result { let overflow_store = OverflowStore(beacon_store.clone()); @@ -708,9 +735,12 @@ impl OverflowLRUCache { Ok(Self { critical: RwLock::new(critical), overflow_store, - state_cache: StateLRUCache::new(beacon_store, spec), + state_cache: StateLRUCache::new(beacon_store, spec.clone()), maintenance_lock: Mutex::new(()), capacity, + custody_column_count, + log, + spec, }) } @@ -759,6 +789,27 @@ impl OverflowLRUCache { } } + fn block_import_requirement( + &self, + pending_components: &PendingComponents, + ) -> Result { + let epoch = pending_components + .epoch() + .ok_or(AvailabilityCheckError::UnableToDetermineImportRequirement)?; + + let peer_das_enabled = self + .spec + .peer_das_epoch + .map_or(false, |peer_das_epoch| epoch >= peer_das_epoch); + if peer_das_enabled { + Ok(BlockImportRequirement::CustodyColumns( + self.custody_column_count, + )) + } else { + Ok(BlockImportRequirement::AllBlobs) + } + } + pub fn put_kzg_verified_data_columns< I: IntoIterator>, >( @@ -766,16 +817,6 @@ impl OverflowLRUCache { block_root: Hash256, kzg_verified_data_columns: I, ) -> Result, AvailabilityCheckError> { - let mut fixed_data_columns = FixedVector::default(); - - for data_column in kzg_verified_data_columns { - if let Some(data_column_opt) = - fixed_data_columns.get_mut(data_column.data_column_index() as usize) - { - *data_column_opt = Some(data_column); - } - } - let mut write_lock = self.critical.write(); // Grab existing entry or create a new entry. @@ -784,12 +825,23 @@ impl OverflowLRUCache { .unwrap_or_else(|| PendingComponents::empty(block_root)); // Merge in the data columns. - pending_components.merge_data_columns(fixed_data_columns); - - write_lock.put_pending_components(block_root, pending_components, &self.overflow_store)?; + pending_components.merge_data_columns(kzg_verified_data_columns)?; - // TODO(das): Currently this does not change availability status and nor import yet. - Ok(Availability::MissingComponents(block_root)) + let block_import_requirement = self.block_import_requirement(&pending_components)?; + if pending_components.is_available(block_import_requirement, &self.log) { + // No need to hold the write lock anymore + drop(write_lock); + pending_components.make_available(|diet_block| { + self.state_cache.recover_pending_executed_block(diet_block) + }) + } else { + write_lock.put_pending_components( + block_root, + pending_components, + &self.overflow_store, + )?; + Ok(Availability::MissingComponents(block_root)) + } } pub fn put_kzg_verified_blobs>>( @@ -815,7 +867,8 @@ impl OverflowLRUCache { // Merge in the blobs. pending_components.merge_blobs(fixed_blobs); - if pending_components.is_available() { + let block_import_requirement = self.block_import_requirement(&pending_components)?; + if pending_components.is_available(block_import_requirement, &self.log) { // No need to hold the write lock anymore drop(write_lock); pending_components.make_available(|diet_block| { @@ -854,7 +907,8 @@ impl OverflowLRUCache { pending_components.merge_block(diet_executed_block); // Check if we have all components and entire set is consistent. - if pending_components.is_available() { + let block_import_requirement = self.block_import_requirement(&pending_components)?; + if pending_components.is_available(block_import_requirement, &self.log) { // No need to hold the write lock anymore drop(write_lock); pending_components.make_available(|diet_block| { @@ -1163,6 +1217,7 @@ mod test { use types::{ExecPayload, MinimalEthSpec}; const LOW_VALIDATOR_COUNT: usize = 32; + const DEFAULT_TEST_CUSTODY_COLUMN_COUNT: usize = 4; fn get_store_with_spec( db_path: &TempDir, @@ -1383,8 +1438,14 @@ mod test { let test_store = harness.chain.store.clone(); let capacity_non_zero = new_non_zero_usize(capacity); let cache = Arc::new( - OverflowLRUCache::::new(capacity_non_zero, test_store, spec.clone()) - .expect("should create cache"), + OverflowLRUCache::::new( + capacity_non_zero, + test_store, + DEFAULT_TEST_CUSTODY_COLUMN_COUNT, + harness.logger().clone(), + spec.clone(), + ) + .expect("should create cache"), ); (harness, cache, chain_db_path) } @@ -1887,6 +1948,8 @@ mod test { let recovered_cache = OverflowLRUCache::::new( new_non_zero_usize(capacity), harness.chain.store.clone(), + DEFAULT_TEST_CUSTODY_COLUMN_COUNT, + harness.logger().clone(), harness.chain.spec.clone(), ) .expect("should recover cache"); diff --git a/beacon_node/client/src/builder.rs b/beacon_node/client/src/builder.rs index e7f201b8521..701ea689006 100644 --- a/beacon_node/client/src/builder.rs +++ b/beacon_node/client/src/builder.rs @@ -206,6 +206,7 @@ where .graffiti(graffiti) .event_handler(event_handler) .execution_layer(execution_layer) + .import_all_data_columns(config.network.subscribe_all_data_column_subnets) .validator_monitor_config(config.validator_monitor.clone()); let builder = if let Some(slasher) = self.slasher.clone() { diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index cc117c3fb92..4f5f6bd6948 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -1261,12 +1261,14 @@ pub fn serve( .and(task_spawner_filter.clone()) .and(chain_filter.clone()) .and(network_tx_filter.clone()) + .and(network_globals.clone()) .and(log_filter.clone()) .then( move |block_contents: PublishBlockRequest, task_spawner: TaskSpawner, chain: Arc>, network_tx: UnboundedSender>, + network_globals: Arc>, log: Logger| { task_spawner.spawn_async_with_rejection(Priority::P0, async move { publish_blocks::publish_block( @@ -1277,6 +1279,7 @@ pub fn serve( log, BroadcastValidation::default(), duplicate_block_status_code, + network_globals, ) .await }) @@ -1292,6 +1295,7 @@ pub fn serve( .and(task_spawner_filter.clone()) .and(chain_filter.clone()) .and(network_tx_filter.clone()) + .and(network_globals.clone()) .and(log_filter.clone()) .then( move |block_bytes: Bytes, @@ -1299,6 +1303,7 @@ pub fn serve( task_spawner: TaskSpawner, chain: Arc>, network_tx: UnboundedSender>, + network_globals: Arc>, log: Logger| { task_spawner.spawn_async_with_rejection(Priority::P0, async move { let block_contents = PublishBlockRequest::::from_ssz_bytes( @@ -1316,6 +1321,7 @@ pub fn serve( log, BroadcastValidation::default(), duplicate_block_status_code, + network_globals, ) .await }) @@ -1331,6 +1337,7 @@ pub fn serve( .and(task_spawner_filter.clone()) .and(chain_filter.clone()) .and(network_tx_filter.clone()) + .and(network_globals.clone()) .and(log_filter.clone()) .then( move |validation_level: api_types::BroadcastValidationQuery, @@ -1338,6 +1345,7 @@ pub fn serve( task_spawner: TaskSpawner, chain: Arc>, network_tx: UnboundedSender>, + network_globals: Arc>, log: Logger| { task_spawner.spawn_async_with_rejection(Priority::P0, async move { publish_blocks::publish_block( @@ -1348,6 +1356,7 @@ pub fn serve( log, validation_level.broadcast_validation, duplicate_block_status_code, + network_globals, ) .await }) @@ -1364,6 +1373,7 @@ pub fn serve( .and(task_spawner_filter.clone()) .and(chain_filter.clone()) .and(network_tx_filter.clone()) + .and(network_globals.clone()) .and(log_filter.clone()) .then( move |validation_level: api_types::BroadcastValidationQuery, @@ -1372,6 +1382,7 @@ pub fn serve( task_spawner: TaskSpawner, chain: Arc>, network_tx: UnboundedSender>, + network_globals: Arc>, log: Logger| { task_spawner.spawn_async_with_rejection(Priority::P0, async move { let block_contents = PublishBlockRequest::::from_ssz_bytes( @@ -1389,6 +1400,7 @@ pub fn serve( log, validation_level.broadcast_validation, duplicate_block_status_code, + network_globals, ) .await }) @@ -1408,12 +1420,14 @@ pub fn serve( .and(task_spawner_filter.clone()) .and(chain_filter.clone()) .and(network_tx_filter.clone()) + .and(network_globals.clone()) .and(log_filter.clone()) .then( move |block_contents: Arc>, task_spawner: TaskSpawner, chain: Arc>, network_tx: UnboundedSender>, + network_globals: Arc>, log: Logger| { task_spawner.spawn_async_with_rejection(Priority::P0, async move { publish_blocks::publish_blinded_block( @@ -1423,6 +1437,7 @@ pub fn serve( log, BroadcastValidation::default(), duplicate_block_status_code, + network_globals, ) .await }) @@ -1438,12 +1453,14 @@ pub fn serve( .and(task_spawner_filter.clone()) .and(chain_filter.clone()) .and(network_tx_filter.clone()) + .and(network_globals.clone()) .and(log_filter.clone()) .then( move |block_bytes: Bytes, task_spawner: TaskSpawner, chain: Arc>, network_tx: UnboundedSender>, + network_globals: Arc>, log: Logger| { task_spawner.spawn_async_with_rejection(Priority::P0, async move { let block = SignedBlindedBeaconBlock::::from_ssz_bytes( @@ -1461,6 +1478,7 @@ pub fn serve( log, BroadcastValidation::default(), duplicate_block_status_code, + network_globals, ) .await }) @@ -1476,6 +1494,7 @@ pub fn serve( .and(task_spawner_filter.clone()) .and(chain_filter.clone()) .and(network_tx_filter.clone()) + .and(network_globals.clone()) .and(log_filter.clone()) .then( move |validation_level: api_types::BroadcastValidationQuery, @@ -1483,6 +1502,7 @@ pub fn serve( task_spawner: TaskSpawner, chain: Arc>, network_tx: UnboundedSender>, + network_globals: Arc>, log: Logger| { task_spawner.spawn_async_with_rejection(Priority::P0, async move { publish_blocks::publish_blinded_block( @@ -1492,6 +1512,7 @@ pub fn serve( log, validation_level.broadcast_validation, duplicate_block_status_code, + network_globals, ) .await }) @@ -1507,6 +1528,7 @@ pub fn serve( .and(task_spawner_filter.clone()) .and(chain_filter.clone()) .and(network_tx_filter.clone()) + .and(network_globals.clone()) .and(log_filter.clone()) .then( move |validation_level: api_types::BroadcastValidationQuery, @@ -1514,6 +1536,7 @@ pub fn serve( task_spawner: TaskSpawner, chain: Arc>, network_tx: UnboundedSender>, + network_globals: Arc>, log: Logger| { task_spawner.spawn_async_with_rejection(Priority::P0, async move { let block = SignedBlindedBeaconBlock::::from_ssz_bytes( @@ -1531,6 +1554,7 @@ pub fn serve( log, validation_level.broadcast_validation, duplicate_block_status_code, + network_globals, ) .await }) diff --git a/beacon_node/http_api/src/publish_blocks.rs b/beacon_node/http_api/src/publish_blocks.rs index 59ab3388d8f..7155f6c4889 100644 --- a/beacon_node/http_api/src/publish_blocks.rs +++ b/beacon_node/http_api/src/publish_blocks.rs @@ -9,7 +9,7 @@ use beacon_chain::{ use eth2::types::{into_full_block_and_blobs, BroadcastValidation, ErrorMessage}; use eth2::types::{FullPayloadContents, PublishBlockRequest}; use execution_layer::ProvenancedPayload; -use lighthouse_network::PubsubMessage; +use lighthouse_network::{NetworkGlobals, PubsubMessage}; use network::NetworkMessage; use slog::{debug, error, info, warn, Logger}; use slot_clock::SlotClock; @@ -46,6 +46,7 @@ impl> ProvenancedBloc } /// Handles a request from the HTTP API for full blocks. +#[allow(clippy::too_many_arguments)] pub async fn publish_block>( block_root: Option, provenanced_block: ProvenancedBlock, @@ -54,6 +55,7 @@ pub async fn publish_block>, ) -> Result { let seen_timestamp = timestamp_now(); @@ -238,6 +240,35 @@ pub async fn publish_block &msg + ); + Err(warp_utils::reject::custom_bad_request(msg)) + }; + } + } + } + } + match Box::pin(chain.process_block( block_root, gossip_verified_block, @@ -324,6 +355,7 @@ pub async fn publish_blinded_block( log: Logger, validation_level: BroadcastValidation, duplicate_status_code: StatusCode, + network_globals: Arc>, ) -> Result { let block_root = blinded_block.canonical_root(); let full_block: ProvenancedBlock> = @@ -336,6 +368,7 @@ pub async fn publish_blinded_block( log, validation_level, duplicate_status_code, + network_globals, ) .await } diff --git a/beacon_node/http_api/tests/broadcast_validation_tests.rs b/beacon_node/http_api/tests/broadcast_validation_tests.rs index 6a3f7947e6b..702b636ff9c 100644 --- a/beacon_node/http_api/tests/broadcast_validation_tests.rs +++ b/beacon_node/http_api/tests/broadcast_validation_tests.rs @@ -370,6 +370,7 @@ pub async fn consensus_partial_pass_only_consensus() { /* submit `block_b` which should induce equivocation */ let channel = tokio::sync::mpsc::unbounded_channel(); + let network_globals = tester.ctx.network_globals.clone().unwrap(); let publication_result = publish_block( None, @@ -379,6 +380,7 @@ pub async fn consensus_partial_pass_only_consensus() { test_logger, validation_level.unwrap(), StatusCode::ACCEPTED, + network_globals, ) .await; @@ -659,6 +661,7 @@ pub async fn equivocation_consensus_late_equivocation() { assert!(gossip_block_contents_a.is_err()); let channel = tokio::sync::mpsc::unbounded_channel(); + let network_globals = tester.ctx.network_globals.clone().unwrap(); let publication_result = publish_block( None, @@ -668,6 +671,7 @@ pub async fn equivocation_consensus_late_equivocation() { test_logger, validation_level.unwrap(), StatusCode::ACCEPTED, + network_globals, ) .await; @@ -1305,6 +1309,7 @@ pub async fn blinded_equivocation_consensus_late_equivocation() { assert!(gossip_block_a.is_err()); let channel = tokio::sync::mpsc::unbounded_channel(); + let network_globals = tester.ctx.network_globals.clone().unwrap(); let publication_result = publish_blinded_block( block_b, @@ -1313,6 +1318,7 @@ pub async fn blinded_equivocation_consensus_late_equivocation() { test_logger, validation_level.unwrap(), StatusCode::ACCEPTED, + network_globals, ) .await; diff --git a/beacon_node/lighthouse_network/Cargo.toml b/beacon_node/lighthouse_network/Cargo.toml index 494fd6892a9..a8e8e13668a 100644 --- a/beacon_node/lighthouse_network/Cargo.toml +++ b/beacon_node/lighthouse_network/Cargo.toml @@ -70,6 +70,7 @@ tempfile = { workspace = true } quickcheck = { workspace = true } quickcheck_macros = { workspace = true } async-channel = { workspace = true } +logging = { workspace = true } [features] libp2p-websocket = [] diff --git a/beacon_node/lighthouse_network/src/config.rs b/beacon_node/lighthouse_network/src/config.rs index 03f530db4d1..5189706a3bc 100644 --- a/beacon_node/lighthouse_network/src/config.rs +++ b/beacon_node/lighthouse_network/src/config.rs @@ -100,6 +100,9 @@ pub struct Config { /// Attempt to construct external port mappings with UPnP. pub upnp_enabled: bool, + /// Subscribe to all data column subnets for the duration of the runtime. + pub subscribe_all_data_column_subnets: bool, + /// Subscribe to all subnets for the duration of the runtime. pub subscribe_all_subnets: bool, @@ -338,6 +341,7 @@ impl Default for Config { upnp_enabled: true, network_load: 4, private: false, + subscribe_all_data_column_subnets: false, subscribe_all_subnets: false, import_all_attestations: false, shutdown_after_sync: false, diff --git a/beacon_node/lighthouse_network/src/discovery/enr.rs b/beacon_node/lighthouse_network/src/discovery/enr.rs index a2aa640eb2b..00ce790da28 100644 --- a/beacon_node/lighthouse_network/src/discovery/enr.rs +++ b/beacon_node/lighthouse_network/src/discovery/enr.rs @@ -238,7 +238,11 @@ pub fn build_enr( builder.add_value(SYNC_COMMITTEE_BITFIELD_ENR_KEY, &bitfield.as_ssz_bytes()); // set the "custody_subnet_count" field on our ENR - let custody_subnet_count = E::min_custody_requirement() as u64; + let custody_subnet_count = if config.subscribe_all_data_column_subnets { + E::data_column_subnet_count() as u64 + } else { + E::min_custody_requirement() as u64 + }; builder.add_value( PEERDAS_CUSTODY_SUBNET_COUNT_ENR_KEY, diff --git a/beacon_node/lighthouse_network/src/types/globals.rs b/beacon_node/lighthouse_network/src/types/globals.rs index f9ed2c9f740..ff2cb97d057 100644 --- a/beacon_node/lighthouse_network/src/types/globals.rs +++ b/beacon_node/lighthouse_network/src/types/globals.rs @@ -2,12 +2,13 @@ use crate::peer_manager::peerdb::PeerDB; use crate::rpc::{MetaData, MetaDataV2}; use crate::types::{BackFillState, SyncState}; -use crate::Client; use crate::EnrExt; +use crate::{Client, Eth2Enr}; use crate::{Enr, GossipTopic, Multiaddr, PeerId}; use parking_lot::RwLock; use std::collections::HashSet; -use types::EthSpec; +use types::data_column_sidecar::ColumnIndex; +use types::{DataColumnSubnetId, Epoch, EthSpec}; pub struct NetworkGlobals { /// The current local ENR. @@ -110,6 +111,17 @@ impl NetworkGlobals { std::mem::replace(&mut *self.sync_state.write(), new_state) } + /// Compute custody data columns the node is assigned to custody. + pub fn custody_columns(&self, _epoch: Epoch) -> Result, &'static str> { + let enr = self.local_enr(); + let node_id = enr.node_id().raw().into(); + let custody_subnet_count = enr.custody_subnet_count::()?; + Ok( + DataColumnSubnetId::compute_custody_columns::(node_id, custody_subnet_count) + .collect(), + ) + } + /// TESTING ONLY. Build a dummy NetworkGlobals instance. pub fn new_test_globals(trusted_peers: Vec, log: &slog::Logger) -> NetworkGlobals { use crate::CombinedKeyExt; @@ -129,3 +141,20 @@ impl NetworkGlobals { ) } } + +#[cfg(test)] +mod test { + use crate::NetworkGlobals; + use types::{Epoch, EthSpec, MainnetEthSpec as E}; + + #[test] + fn test_custody_count_default() { + let log = logging::test_logger(); + let default_custody_requirement_column_count = + E::number_of_columns() / E::data_column_subnet_count(); + let globals = NetworkGlobals::::new_test_globals(vec![], &log); + let any_epoch = Epoch::new(0); + let columns = globals.custody_columns(any_epoch).unwrap(); + assert_eq!(columns.len(), default_custody_requirement_column_count); + } +} diff --git a/beacon_node/network/src/service.rs b/beacon_node/network/src/service.rs index 19f0fad6d95..4a1bd2c6621 100644 --- a/beacon_node/network/src/service.rs +++ b/beacon_node/network/src/service.rs @@ -191,6 +191,8 @@ pub struct NetworkService { next_fork_subscriptions: Pin>>, /// A delay that expires when we need to unsubscribe from old fork topics. next_unsubscribe: Pin>>, + /// Subscribe to all the data column subnets. + subscribe_all_data_column_subnets: bool, /// Subscribe to all the subnets once synced. subscribe_all_subnets: bool, /// Shutdown beacon node after sync is complete. @@ -357,6 +359,7 @@ impl NetworkService { next_fork_update, next_fork_subscriptions, next_unsubscribe, + subscribe_all_data_column_subnets: config.subscribe_all_data_column_subnets, subscribe_all_subnets: config.subscribe_all_subnets, shutdown_after_sync: config.shutdown_after_sync, metrics_enabled: config.metrics_enabled, @@ -733,7 +736,25 @@ impl NetworkService { } } - if !self.subscribe_all_subnets { + if self.subscribe_all_data_column_subnets { + for column_subnet in 0..T::EthSpec::data_column_subnet_count() as u64 { + for fork_digest in self.required_gossip_fork_digests() { + let gossip_kind = + Subnet::DataColumn(DataColumnSubnetId::new(column_subnet)).into(); + let topic = GossipTopic::new( + gossip_kind, + GossipEncoding::default(), + fork_digest, + ); + if self.libp2p.subscribe(topic.clone()) { + subscribed_topics.push(topic); + } else { + warn!(self.log, "Could not subscribe to topic"; "topic" => %topic); + } + } + } + } else { + // TODO(das): subscribe after `PEER_DAS_EPOCH` for column_subnet in DataColumnSubnetId::compute_custody_subnets::( self.network_globals.local_enr().node_id().raw().into(), self.network_globals @@ -790,23 +811,6 @@ impl NetworkService { } } } - // Subscribe to all data column subnets - for column_subnet in 0..T::EthSpec::data_column_subnet_count() as u64 { - for fork_digest in self.required_gossip_fork_digests() { - let gossip_kind = - Subnet::DataColumn(DataColumnSubnetId::new(column_subnet)).into(); - let topic = GossipTopic::new( - gossip_kind, - GossipEncoding::default(), - fork_digest, - ); - if self.libp2p.subscribe(topic.clone()) { - subscribed_topics.push(topic); - } else { - warn!(self.log, "Could not subscribe to topic"; "topic" => %topic); - } - } - } } if !subscribed_topics.is_empty() { 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 59cb6636d57..c2094e7b930 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 @@ -652,7 +652,7 @@ mod tests { HotColdDB::open_ephemeral(StoreConfig::default(), ChainSpec::minimal(), log.clone()) .expect("store"); let da_checker = Arc::new( - DataAvailabilityChecker::new(slot_clock, None, store.into(), &log, spec.clone()) + DataAvailabilityChecker::new(slot_clock, None, store.into(), false, &log, spec.clone()) .expect("data availability checker"), ); let mut sl = SingleBlockLookup::::new( @@ -685,7 +685,7 @@ mod tests { .expect("store"); let da_checker = Arc::new( - DataAvailabilityChecker::new(slot_clock, None, store.into(), &log, spec.clone()) + DataAvailabilityChecker::new(slot_clock, None, store.into(), false, &log, spec.clone()) .expect("data availability checker"), ); diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index 818cdbd460f..19633cc0b0e 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -38,6 +38,14 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { /* * Network parameters. */ + .arg( + Arg::with_name("subscribe-all-data-column-subnets") + .long("subscribe-all-data-column-subnets") + .help("Subscribe to all data column subnets and participate in data custody for \ + all columns. This will also advertise the beacon node as being long-lived \ + subscribed to all data column subnets.") + .takes_value(false), + ) .arg( Arg::with_name("subscribe-all-subnets") .long("subscribe-all-subnets") diff --git a/beacon_node/src/config.rs b/beacon_node/src/config.rs index fd2cf473cb3..5493386bd0e 100644 --- a/beacon_node/src/config.rs +++ b/beacon_node/src/config.rs @@ -1129,6 +1129,10 @@ pub fn set_network_config( config.network_dir = data_dir.join(DEFAULT_NETWORK_DIR); }; + if cli_args.is_present("subscribe-all-data-column-subnets") { + config.subscribe_all_data_column_subnets = true; + } + if cli_args.is_present("subscribe-all-subnets") { config.subscribe_all_subnets = true; } diff --git a/book/src/help_bn.md b/book/src/help_bn.md index e437925a0e8..b628cae145a 100644 --- a/book/src/help_bn.md +++ b/book/src/help_bn.md @@ -108,6 +108,9 @@ FLAGS: server on localhost:5052 and import deposit logs from the execution node. This is equivalent to `--http` on merge-ready networks, or `--http --eth1` pre-merge + --subscribe-all-data-column-subnets Subscribe to all data column subnets and participate in data custody for + all columns. This will also advertise the beacon node as being long-lived + subscribed to all data column subnets. --subscribe-all-subnets Subscribe to all subnets regardless of validator count. This will also advertise the beacon node as being long-lived subscribed to all subnets. --validator-monitor-auto Enables the automatic detection and monitoring of validators connected to diff --git a/common/eth2_network_config/built_in_network_configs/mainnet/config.yaml b/common/eth2_network_config/built_in_network_configs/mainnet/config.yaml index c8695123ab0..8c3d71d6748 100644 --- a/common/eth2_network_config/built_in_network_configs/mainnet/config.yaml +++ b/common/eth2_network_config/built_in_network_configs/mainnet/config.yaml @@ -53,6 +53,8 @@ DENEB_FORK_EPOCH: 269568 # March 13, 2024, 01:55:35pm UTC # Electra ELECTRA_FORK_VERSION: 0x05000000 ELECTRA_FORK_EPOCH: 18446744073709551615 +# PeerDAS +PEER_DAS_EPOCH: 18446744073709551615 # Time parameters diff --git a/common/task_executor/Cargo.toml b/common/task_executor/Cargo.toml index b3d58fa5ea8..cc9a2c5097b 100644 --- a/common/task_executor/Cargo.toml +++ b/common/task_executor/Cargo.toml @@ -12,3 +12,4 @@ futures = { workspace = true } lazy_static = { workspace = true } lighthouse_metrics = { workspace = true } sloggers = { workspace = true } +logging = { workspace = true } diff --git a/common/task_executor/src/test_utils.rs b/common/task_executor/src/test_utils.rs index 6e372d97575..ec8f45d850e 100644 --- a/common/task_executor/src/test_utils.rs +++ b/common/task_executor/src/test_utils.rs @@ -1,4 +1,5 @@ use crate::TaskExecutor; +use logging::test_logger; use slog::Logger; use sloggers::{null::NullLoggerBuilder, Build}; use std::sync::Arc; @@ -26,7 +27,7 @@ impl Default for TestRuntime { fn default() -> Self { let (runtime_shutdown, exit) = async_channel::bounded(1); let (shutdown_tx, _) = futures::channel::mpsc::channel(1); - let log = null_logger().unwrap(); + let log = test_logger(); let (runtime, handle) = if let Ok(handle) = runtime::Handle::try_current() { (None, handle) diff --git a/consensus/types/Cargo.toml b/consensus/types/Cargo.toml index e77cd67ed15..a0abae8a7b3 100644 --- a/consensus/types/Cargo.toml +++ b/consensus/types/Cargo.toml @@ -60,6 +60,7 @@ eth2_network_config = { workspace = true } state_processing = { workspace = true } tokio = { workspace = true } paste = { workspace = true } +mockall_double = { workspace = true } [features] default = ["sqlite", "legacy-arith"] diff --git a/consensus/types/src/chain_spec.rs b/consensus/types/src/chain_spec.rs index a9e9e4ab138..e8a375a8591 100644 --- a/consensus/types/src/chain_spec.rs +++ b/consensus/types/src/chain_spec.rs @@ -194,6 +194,7 @@ pub struct ChainSpec { /* * DAS params */ + pub peer_das_epoch: Option, pub custody_requirement: u64, /* @@ -763,6 +764,7 @@ impl ChainSpec { /* * DAS params */ + peer_das_epoch: None, custody_requirement: 1, /* @@ -867,6 +869,8 @@ impl ChainSpec { // Electra electra_fork_version: [0x05, 0x00, 0x00, 0x01], electra_fork_epoch: None, + // PeerDAS + peer_das_epoch: None, // Other network_id: 2, // lighthouse testnet network id deposit_chain_id: 5, @@ -1071,6 +1075,7 @@ impl ChainSpec { /* * DAS params */ + peer_das_epoch: None, custody_requirement: 1, /* * Network specific @@ -1206,6 +1211,11 @@ pub struct Config { #[serde(deserialize_with = "deserialize_fork_epoch")] pub electra_fork_epoch: Option>, + #[serde(default)] + #[serde(serialize_with = "serialize_fork_epoch")] + #[serde(deserialize_with = "deserialize_fork_epoch")] + pub peer_das_epoch: Option>, + #[serde(with = "serde_utils::quoted_u64")] seconds_per_slot: u64, #[serde(with = "serde_utils::quoted_u64")] @@ -1596,6 +1606,10 @@ impl Config { .electra_fork_epoch .map(|epoch| MaybeQuoted { value: epoch }), + peer_das_epoch: spec + .peer_das_epoch + .map(|epoch| MaybeQuoted { value: epoch }), + seconds_per_slot: spec.seconds_per_slot, seconds_per_eth1_block: spec.seconds_per_eth1_block, min_validator_withdrawability_delay: spec.min_validator_withdrawability_delay, @@ -1673,6 +1687,7 @@ impl Config { deneb_fork_version, electra_fork_epoch, electra_fork_version, + peer_das_epoch, seconds_per_slot, seconds_per_eth1_block, min_validator_withdrawability_delay, @@ -1734,6 +1749,7 @@ impl Config { deneb_fork_version, electra_fork_epoch: electra_fork_epoch.map(|q| q.value), electra_fork_version, + peer_das_epoch: peer_das_epoch.map(|q| q.value), seconds_per_slot, seconds_per_eth1_block, min_validator_withdrawability_delay, diff --git a/consensus/types/src/data_column_sidecar.rs b/consensus/types/src/data_column_sidecar.rs index 885dc243cfd..6b1925f3c4c 100644 --- a/consensus/types/src/data_column_sidecar.rs +++ b/consensus/types/src/data_column_sidecar.rs @@ -7,8 +7,12 @@ use crate::{ }; use bls::Signature; use derivative::Derivative; -use kzg::{Blob as KzgBlob, Error as KzgError, Kzg}; +#[cfg_attr(test, double)] +use kzg::Kzg; +use kzg::{Blob as KzgBlob, Error as KzgError}; use kzg::{KzgCommitment, KzgProof}; +#[cfg(test)] +use mockall_double::double; use safe_arith::ArithError; use serde::{Deserialize, Serialize}; use ssz::Encode; @@ -76,6 +80,9 @@ impl DataColumnSidecar { block: &SignedBeaconBlock, kzg: &Kzg, ) -> Result, DataColumnSidecarError> { + if blobs.is_empty() { + return Ok(DataColumnSidecarList::empty()); + } let kzg_commitments = block .message() .body() @@ -239,6 +246,7 @@ pub type FixedDataColumnSidecarList = #[cfg(test)] mod test { + use super::*; use crate::beacon_block::EmptyBlock; use crate::beacon_block_body::KzgCommitments; use crate::eth_spec::EthSpec; @@ -247,10 +255,24 @@ mod test { DataColumnSidecar, MainnetEthSpec, SignedBeaconBlock, }; use bls::Signature; - use eth2_network_config::TRUSTED_SETUP_BYTES; - use kzg::{Kzg, KzgCommitment, KzgProof, TrustedSetup}; + use kzg::{KzgCommitment, KzgProof}; use std::sync::Arc; + #[test] + fn test_build_sidecars_empty() { + type E = MainnetEthSpec; + let num_of_blobs = 0; + let spec = E::default_spec(); + let (signed_block, blob_sidecars) = + create_test_block_and_blob_sidecars::(num_of_blobs, &spec); + + let mock_kzg = Arc::new(Kzg::default()); + let column_sidecars = + DataColumnSidecar::build_sidecars(&blob_sidecars, &signed_block, &mock_kzg).unwrap(); + + assert!(column_sidecars.is_empty()); + } + #[test] fn test_build_sidecars() { type E = MainnetEthSpec; @@ -259,11 +281,13 @@ mod test { let (signed_block, blob_sidecars) = create_test_block_and_blob_sidecars::(num_of_blobs, &spec); - let trusted_setup: TrustedSetup = serde_json::from_reader(TRUSTED_SETUP_BYTES).unwrap(); - let kzg = Arc::new(Kzg::new_from_trusted_setup(trusted_setup).unwrap()); + let mut mock_kzg = Kzg::default(); + mock_kzg + .expect_compute_cells_and_proofs() + .returning(kzg::mock::compute_cells_and_proofs); let column_sidecars = - DataColumnSidecar::build_sidecars(&blob_sidecars, &signed_block, &kzg).unwrap(); + DataColumnSidecar::build_sidecars(&blob_sidecars, &signed_block, &mock_kzg).unwrap(); let block_kzg_commitments = signed_block .message() diff --git a/consensus/types/src/data_column_subnet_id.rs b/consensus/types/src/data_column_subnet_id.rs index b35abf2bbd6..94f06317360 100644 --- a/consensus/types/src/data_column_subnet_id.rs +++ b/consensus/types/src/data_column_subnet_id.rs @@ -1,4 +1,5 @@ //! Identifies each data column subnet by an integer identifier. +use crate::data_column_sidecar::ColumnIndex; use crate::EthSpec; use ethereum_types::U256; use safe_arith::{ArithError, SafeArith}; @@ -45,10 +46,10 @@ impl DataColumnSubnetId { } #[allow(clippy::arithmetic_side_effects)] - pub fn columns(&self) -> impl Iterator { + pub fn columns(&self) -> impl Iterator { let subnet = self.0; let data_column_subnet_count = E::data_column_subnet_count() as u64; - let columns_per_subnet = (E::number_of_columns() as u64) / data_column_subnet_count; + let columns_per_subnet = E::data_columns_per_subnet() as u64; (0..columns_per_subnet).map(move |i| data_column_subnet_count * i + subnet) } @@ -86,7 +87,7 @@ impl DataColumnSubnetId { pub fn compute_custody_columns( node_id: U256, custody_subnet_count: u64, - ) -> impl Iterator { + ) -> impl Iterator { Self::compute_custody_subnets::(node_id, custody_subnet_count) .flat_map(|subnet| subnet.columns::()) } @@ -150,8 +151,10 @@ impl From for Error { #[cfg(test)] mod test { use crate::data_column_subnet_id::DataColumnSubnetId; - use crate::ChainSpec; use crate::EthSpec; + use crate::{ChainSpec, MainnetEthSpec}; + + type E = MainnetEthSpec; #[test] fn test_compute_subnets_for_data_column() { @@ -174,20 +177,18 @@ mod test { let spec = ChainSpec::mainnet(); for node_id in node_ids { - let computed_subnets = DataColumnSubnetId::compute_custody_subnets::< - crate::MainnetEthSpec, - >(node_id, spec.custody_requirement); + let computed_subnets = + DataColumnSubnetId::compute_custody_subnets::(node_id, spec.custody_requirement); let computed_subnets: Vec<_> = computed_subnets.collect(); // the number of subnets is equal to the custody requirement assert_eq!(computed_subnets.len() as u64, spec.custody_requirement); - let subnet_count = crate::MainnetEthSpec::data_column_subnet_count(); - let columns_per_subnet = crate::MainnetEthSpec::number_of_columns() / subnet_count; + let subnet_count = E::data_column_subnet_count(); for subnet in computed_subnets { - let columns: Vec<_> = subnet.columns::().collect(); + let columns: Vec<_> = subnet.columns::().collect(); // the number of columns is equal to the specified number of columns per subnet - assert_eq!(columns.len(), columns_per_subnet); + assert_eq!(columns.len(), E::data_columns_per_subnet()); for pair in columns.windows(2) { // each successive column index is offset by the number of subnets diff --git a/consensus/types/src/eth_spec.rs b/consensus/types/src/eth_spec.rs index 01a6c005f0e..1cb5c8d11b8 100644 --- a/consensus/types/src/eth_spec.rs +++ b/consensus/types/src/eth_spec.rs @@ -119,7 +119,7 @@ pub trait EthSpec: type MinCustodyRequirement: Unsigned + Clone + Sync + Send + Debug + PartialEq; type DataColumnSubnetCount: Unsigned + Clone + Sync + Send + Debug + PartialEq; type DataColumnCount: Unsigned + Clone + Sync + Send + Debug + PartialEq; - type MaxBytesPerColumn: Unsigned + Clone + Sync + Send + Debug + PartialEq; + type DataColumnsPerSubnet: Unsigned + Clone + Sync + Send + Debug + PartialEq; type KzgCommitmentsInclusionProofDepth: Unsigned + Clone + Sync + Send + Debug + PartialEq; /* * Derived values (set these CAREFULLY) @@ -350,6 +350,10 @@ pub trait EthSpec: Self::DataColumnCount::to_usize() } + fn data_columns_per_subnet() -> usize { + Self::DataColumnsPerSubnet::to_usize() + } + fn min_custody_requirement() -> usize { Self::MinCustodyRequirement::to_usize() } @@ -358,10 +362,6 @@ pub trait EthSpec: Self::DataColumnSubnetCount::to_usize() } - fn max_bytes_per_column() -> usize { - Self::MaxBytesPerColumn::to_usize() - } - fn kzg_commitments_inclusion_proof_depth() -> usize { Self::KzgCommitmentsInclusionProofDepth::to_usize() } @@ -414,10 +414,7 @@ impl EthSpec for MainnetEthSpec { type MinCustodyRequirement = U1; type DataColumnSubnetCount = U32; type DataColumnCount = U128; - // Column samples are entire columns in 1D DAS. - // max data size = extended_blob_bytes * max_blobs_per_block / num_of_columns - // 256kb * 32 / 128 = 64kb - type MaxBytesPerColumn = U65536; + type DataColumnsPerSubnet = U4; type KzgCommitmentsInclusionProofDepth = U4; // inclusion of the whole list of commitments type SyncSubcommitteeSize = U128; // 512 committee size / 4 sync committee subnet count type MaxPendingAttestations = U4096; // 128 max attestations * 32 slots per epoch @@ -466,7 +463,7 @@ impl EthSpec for MinimalEthSpec { type MinCustodyRequirement = U1; type DataColumnSubnetCount = U32; type DataColumnCount = U128; - type MaxBytesPerColumn = U65536; + type DataColumnsPerSubnet = U4; type KzgCommitmentsInclusionProofDepth = U4; params_from_eth_spec!(MainnetEthSpec { @@ -563,7 +560,7 @@ impl EthSpec for GnosisEthSpec { type MinCustodyRequirement = U1; type DataColumnSubnetCount = U32; type DataColumnCount = U128; - type MaxBytesPerColumn = U65536; + type DataColumnsPerSubnet = U4; type KzgCommitmentsInclusionProofDepth = U4; fn default_spec() -> ChainSpec { @@ -574,3 +571,22 @@ impl EthSpec for GnosisEthSpec { EthSpecId::Gnosis } } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_peer_das_config_all_specs() { + test_peer_das_config::(); + test_peer_das_config::(); + test_peer_das_config::(); + } + + fn test_peer_das_config() { + assert_eq!( + E::data_columns_per_subnet(), + E::number_of_columns() / E::data_column_subnet_count() + ); + } +} diff --git a/crypto/kzg/Cargo.toml b/crypto/kzg/Cargo.toml index d26dfe4992a..2c3c894b49c 100644 --- a/crypto/kzg/Cargo.toml +++ b/crypto/kzg/Cargo.toml @@ -17,3 +17,4 @@ ethereum_serde_utils = { workspace = true } hex = { workspace = true } ethereum_hashing = { workspace = true } c-kzg = { workspace = true } +mockall = { workspace = true } diff --git a/crypto/kzg/src/lib.rs b/crypto/kzg/src/lib.rs index 60eb0dfe0e7..bf63390d108 100644 --- a/crypto/kzg/src/lib.rs +++ b/crypto/kzg/src/lib.rs @@ -14,6 +14,8 @@ pub use c_kzg::{ BYTES_PER_FIELD_ELEMENT, BYTES_PER_PROOF, FIELD_ELEMENTS_PER_BLOB, }; use c_kzg::{Cell, CELLS_PER_BLOB}; +use mockall::automock; + #[derive(Debug)] pub enum Error { /// An error from the underlying kzg library. @@ -34,6 +36,7 @@ pub struct Kzg { trusted_setup: KzgSettings, } +#[automock] impl Kzg { /// Load the kzg trusted setup parameters from a vec of G1 and G2 points. pub fn new_from_trusted_setup(trusted_setup: TrustedSetup) -> Result { @@ -191,6 +194,24 @@ impl Kzg { } } +pub mod mock { + use crate::{Error, KzgProof}; + use c_kzg::{Blob, Cell, CELLS_PER_BLOB}; + + pub const MOCK_KZG_BYTES_PER_CELL: usize = 2048; + + #[allow(clippy::type_complexity)] + pub fn compute_cells_and_proofs( + _blob: &Blob, + ) -> Result<(Box<[Cell; CELLS_PER_BLOB]>, Box<[KzgProof; CELLS_PER_BLOB]>), Error> { + let empty_cell = Cell::new([0; MOCK_KZG_BYTES_PER_CELL]); + Ok(( + Box::new([empty_cell; CELLS_PER_BLOB]), + Box::new([KzgProof::empty(); CELLS_PER_BLOB]), + )) + } +} + impl TryFrom for Kzg { type Error = Error; diff --git a/lighthouse/tests/beacon_node.rs b/lighthouse/tests/beacon_node.rs index ec10ff4429d..bff39154240 100644 --- a/lighthouse/tests/beacon_node.rs +++ b/lighthouse/tests/beacon_node.rs @@ -778,6 +778,13 @@ fn network_target_peers_flag() { }); } #[test] +fn network_subscribe_all_data_column_subnets_flag() { + CommandLineTest::new() + .flag("subscribe-all-data-column-subnets", None) + .run_with_zero_port() + .with_config(|config| assert!(config.network.subscribe_all_data_column_subnets)); +} +#[test] fn network_subscribe_all_subnets_flag() { CommandLineTest::new() .flag("subscribe-all-subnets", None) From 75eab79345fbbbf1176afdd537e624f5351cae9d Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Mon, 29 Apr 2024 17:15:00 +0900 Subject: [PATCH 08/76] DAS sampling on sync (#5616) * Data availability sampling on sync * Address @jimmygchen review * Trigger sampling * Address some review comments and only send `SamplingBlock` sync message after PEER_DAS_EPOCH. --------- Co-authored-by: Jimmy Chen --- beacon_node/beacon_chain/src/beacon_chain.rs | 11 + beacon_node/beacon_chain/src/test_utils.rs | 30 + beacon_node/beacon_processor/src/lib.rs | 38 +- beacon_node/beacon_processor/src/metrics.rs | 10 + .../lighthouse_network/src/rpc/methods.rs | 10 + beacon_node/network/src/metrics.rs | 29 + .../gossip_methods.rs | 20 +- .../src/network_beacon_processor/mod.rs | 38 +- .../network_beacon_processor/sync_methods.rs | 21 +- beacon_node/network/src/router.rs | 22 +- .../network/src/sync/block_lookups/tests.rs | 247 +++++++- beacon_node/network/src/sync/manager.rs | 167 +++++- beacon_node/network/src/sync/mod.rs | 2 + .../network/src/sync/network_context.rs | 160 +++++- .../src/sync/network_context/requests.rs | 95 ++- beacon_node/network/src/sync/sampling.rs | 543 ++++++++++++++++++ consensus/types/src/data_column_sidecar.rs | 7 + 17 files changed, 1402 insertions(+), 48 deletions(-) create mode 100644 beacon_node/network/src/sync/sampling.rs diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 6823a82f405..d08e3833967 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -2855,6 +2855,17 @@ impl BeaconChain { ChainSegmentResult::Successful { imported_blocks } } + /// Updates fork-choice node into a permanent `available` state so it can become a viable head. + /// Only completed sampling results are received. Blocks are unavailable by default and should + /// be pruned on finalization, on a timeout or by a max count. + pub async fn process_sampling_completed(self: &Arc, block_root: Hash256) { + // TODO(das): update fork-choice + // NOTE: It is possible that sampling complets before block is imported into fork choice, + // in that case we may need to update availability cache. + // TODO(das): These log levels are too high, reduce once DAS matures + info!(self.log, "Sampling completed"; "block_root" => %block_root); + } + /// Returns `Ok(GossipVerifiedBlock)` if the supplied `block` should be forwarded onto the /// gossip network. The block is not imported into the chain, it is just partially verified. /// diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index debc4881a60..9372606ffed 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -2584,3 +2584,33 @@ pub fn generate_rand_block_and_blobs( } (block, blob_sidecars) } + +pub fn generate_rand_block_and_data_columns( + fork_name: ForkName, + num_blobs: NumBlobs, + rng: &mut impl Rng, +) -> ( + SignedBeaconBlock>, + Vec>, +) { + let (block, blobs) = generate_rand_block_and_blobs(fork_name, num_blobs, rng); + let blob = blobs.first().expect("should have at least 1 blob"); + + let data_columns = (0..E::number_of_columns()) + .map(|index| DataColumnSidecar { + index: index as u64, + column: <_>::default(), + kzg_commitments: block + .message() + .body() + .blob_kzg_commitments() + .unwrap() + .clone(), + kzg_proofs: (vec![]).into(), + signed_block_header: blob.signed_block_header.clone(), + kzg_commitments_inclusion_proof: <_>::default(), + }) + .collect::>(); + + (block, data_columns) +} diff --git a/beacon_node/beacon_processor/src/lib.rs b/beacon_node/beacon_processor/src/lib.rs index 478b6d72cd0..118a043addc 100644 --- a/beacon_node/beacon_processor/src/lib.rs +++ b/beacon_node/beacon_processor/src/lib.rs @@ -157,6 +157,10 @@ const MAX_RPC_BLOCK_QUEUE_LEN: usize = 1_024; /// will be stored before we start dropping them. const MAX_RPC_BLOB_QUEUE_LEN: usize = 1_024; +/// TODO(das): Placeholder number +const MAX_RPC_VERIFY_DATA_COLUMN_QUEUE_LEN: usize = 1000; +const MAX_SAMPLING_RESULT_QUEUE_LEN: usize = 1000; + /// The maximum number of queued `Vec` objects received during syncing that will /// be stored before we start dropping them. const MAX_CHAIN_SEGMENT_QUEUE_LEN: usize = 64; @@ -252,6 +256,8 @@ pub const GOSSIP_LIGHT_CLIENT_OPTIMISTIC_UPDATE: &str = "light_client_optimistic pub const RPC_BLOCK: &str = "rpc_block"; pub const IGNORED_RPC_BLOCK: &str = "ignored_rpc_block"; pub const RPC_BLOBS: &str = "rpc_blob"; +pub const RPC_VERIFY_DATA_COLUMNS: &str = "rpc_verify_data_columns"; +pub const SAMPLING_RESULT: &str = "sampling_result"; pub const CHAIN_SEGMENT: &str = "chain_segment"; pub const CHAIN_SEGMENT_BACKFILL: &str = "chain_segment_backfill"; pub const STATUS_PROCESSING: &str = "status_processing"; @@ -629,6 +635,8 @@ pub enum Work { RpcBlobs { process_fn: AsyncFn, }, + RpcVerifyDataColumn(AsyncFn), + SamplingResult(AsyncFn), IgnoredRpcBlock { process_fn: BlockingFn, }, @@ -675,6 +683,8 @@ impl Work { Work::GossipLightClientOptimisticUpdate(_) => GOSSIP_LIGHT_CLIENT_OPTIMISTIC_UPDATE, Work::RpcBlock { .. } => RPC_BLOCK, Work::RpcBlobs { .. } => RPC_BLOBS, + Work::RpcVerifyDataColumn(_) => RPC_VERIFY_DATA_COLUMNS, + Work::SamplingResult(_) => SAMPLING_RESULT, Work::IgnoredRpcBlock { .. } => IGNORED_RPC_BLOCK, Work::ChainSegment { .. } => CHAIN_SEGMENT, Work::ChainSegmentBackfill(_) => CHAIN_SEGMENT_BACKFILL, @@ -833,6 +843,8 @@ impl BeaconProcessor { // Using a FIFO queue since blocks need to be imported sequentially. let mut rpc_block_queue = FifoQueue::new(MAX_RPC_BLOCK_QUEUE_LEN); let mut rpc_blob_queue = FifoQueue::new(MAX_RPC_BLOB_QUEUE_LEN); + let mut rpc_verify_data_column_queue = FifoQueue::new(MAX_RPC_VERIFY_DATA_COLUMN_QUEUE_LEN); + let mut sampling_result_queue = FifoQueue::new(MAX_SAMPLING_RESULT_QUEUE_LEN); let mut chain_segment_queue = FifoQueue::new(MAX_CHAIN_SEGMENT_QUEUE_LEN); let mut backfill_chain_segment = FifoQueue::new(MAX_CHAIN_SEGMENT_QUEUE_LEN); let mut gossip_block_queue = FifoQueue::new(MAX_GOSSIP_BLOCK_QUEUE_LEN); @@ -988,6 +1000,11 @@ impl BeaconProcessor { self.spawn_worker(item, idle_tx); } else if let Some(item) = rpc_blob_queue.pop() { self.spawn_worker(item, idle_tx); + // TODO(das): decide proper priorization for sampling columns + } else if let Some(item) = rpc_verify_data_column_queue.pop() { + self.spawn_worker(item, idle_tx); + } else if let Some(item) = sampling_result_queue.pop() { + self.spawn_worker(item, idle_tx); // Check delayed blocks before gossip blocks, the gossip blocks might rely // on the delayed ones. } else if let Some(item) = delayed_block_queue.pop() { @@ -1278,6 +1295,12 @@ impl BeaconProcessor { rpc_block_queue.push(work, work_id, &self.log) } Work::RpcBlobs { .. } => rpc_blob_queue.push(work, work_id, &self.log), + Work::RpcVerifyDataColumn(_) => { + rpc_verify_data_column_queue.push(work, work_id, &self.log) + } + Work::SamplingResult(_) => { + sampling_result_queue.push(work, work_id, &self.log) + } Work::ChainSegment { .. } => { chain_segment_queue.push(work, work_id, &self.log) } @@ -1371,6 +1394,14 @@ impl BeaconProcessor { &metrics::BEACON_PROCESSOR_RPC_BLOB_QUEUE_TOTAL, rpc_blob_queue.len() as i64, ); + metrics::set_gauge( + &metrics::BEACON_PROCESSOR_RPC_VERIFY_DATA_COLUMN_QUEUE_TOTAL, + rpc_verify_data_column_queue.len() as i64, + ); + metrics::set_gauge( + &metrics::BEACON_PROCESSOR_SAMPLING_RESULT_QUEUE_TOTAL, + sampling_result_queue.len() as i64, + ); metrics::set_gauge( &metrics::BEACON_PROCESSOR_CHAIN_SEGMENT_QUEUE_TOTAL, chain_segment_queue.len() as i64, @@ -1510,9 +1541,10 @@ impl BeaconProcessor { beacon_block_root: _, process_fn, } => task_spawner.spawn_async(process_fn), - Work::RpcBlock { process_fn } | Work::RpcBlobs { process_fn } => { - task_spawner.spawn_async(process_fn) - } + Work::RpcBlock { process_fn } + | Work::RpcBlobs { process_fn } + | Work::RpcVerifyDataColumn(process_fn) + | Work::SamplingResult(process_fn) => task_spawner.spawn_async(process_fn), Work::IgnoredRpcBlock { process_fn } => task_spawner.spawn_blocking(process_fn), Work::GossipBlock(work) | Work::GossipBlobSidecar(work) diff --git a/beacon_node/beacon_processor/src/metrics.rs b/beacon_node/beacon_processor/src/metrics.rs index bcd422b357d..503d29dd699 100644 --- a/beacon_node/beacon_processor/src/metrics.rs +++ b/beacon_node/beacon_processor/src/metrics.rs @@ -86,6 +86,16 @@ lazy_static::lazy_static! { "beacon_processor_rpc_blob_queue_total", "Count of blobs from the rpc waiting to be verified." ); + // Rpc verify data columns + pub static ref BEACON_PROCESSOR_RPC_VERIFY_DATA_COLUMN_QUEUE_TOTAL: Result = try_create_int_gauge( + "beacon_processor_rpc_verify_data_column_queue_total", + "Count of data columns from the rpc waiting to be verified." + ); + // Sampling result + pub static ref BEACON_PROCESSOR_SAMPLING_RESULT_QUEUE_TOTAL: Result = try_create_int_gauge( + "beacon_processor_sampling_result_queue_total", + "Count of sampling results waiting to be processed." + ); // Chain segments. pub static ref BEACON_PROCESSOR_CHAIN_SEGMENT_QUEUE_TOTAL: Result = try_create_int_gauge( "beacon_processor_chain_segment_queue_total", diff --git a/beacon_node/lighthouse_network/src/rpc/methods.rs b/beacon_node/lighthouse_network/src/rpc/methods.rs index 7df20eee280..831e3efc117 100644 --- a/beacon_node/lighthouse_network/src/rpc/methods.rs +++ b/beacon_node/lighthouse_network/src/rpc/methods.rs @@ -378,6 +378,16 @@ pub struct DataColumnsByRootRequest { pub data_column_ids: RuntimeVariableList, } +impl DataColumnsByRootRequest { + pub fn new(data_column_ids: Vec, spec: &ChainSpec) -> Self { + let data_column_ids = RuntimeVariableList::from_vec( + data_column_ids, + spec.max_request_data_column_sidecars as usize, + ); + Self { data_column_ids } + } +} + /* RPC Handling and Grouping */ // Collection of enums and structs used by the Codecs to encode/decode RPC messages diff --git a/beacon_node/network/src/metrics.rs b/beacon_node/network/src/metrics.rs index b81851ff356..abf72748414 100644 --- a/beacon_node/network/src/metrics.rs +++ b/beacon_node/network/src/metrics.rs @@ -14,6 +14,9 @@ use std::sync::Arc; use strum::IntoEnumIterator; use types::EthSpec; +pub const SUCCESS: &str = "SUCCESS"; +pub const FAILURE: &str = "FAILURE"; + lazy_static! { pub static ref BEACON_BLOCK_MESH_PEERS_PER_CLIENT: Result = @@ -344,6 +347,25 @@ lazy_static! { "beacon_processor_reprocessing_queue_sent_optimistic_updates", "Number of queued light client optimistic updates where as matching block has been imported." ); + + /* + * Sampling + */ + pub static ref SAMPLE_DOWNLOAD_RESULT: Result = try_create_int_counter_vec( + "beacon_sampling_sample_verify_result_total", + "Total count of individual sample download results", + &["result"] + ); + pub static ref SAMPLE_VERIFY_RESULT: Result = try_create_int_counter_vec( + "beacon_sampling_sample_verify_result_total", + "Total count of individual sample verify results", + &["result"] + ); + pub static ref SAMPLING_REQUEST_RESULT: Result = try_create_int_counter_vec( + "beacon_sampling_request_result_total", + "Total count of sample request results", + &["result"] + ); } pub fn register_finality_update_error(error: &LightClientFinalityUpdateError) { @@ -362,6 +384,13 @@ pub fn register_sync_committee_error(error: &SyncCommitteeError) { inc_counter_vec(&GOSSIP_SYNC_COMMITTEE_ERRORS_PER_TYPE, &[error.as_ref()]); } +pub fn from_result(result: &std::result::Result) -> &str { + match result { + Ok(_) => SUCCESS, + Err(_) => FAILURE, + } +} + pub fn update_gossip_metrics( gossipsub: &Gossipsub, network_globals: &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 64cbd27a6f4..d6ee2cb4055 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -610,7 +610,7 @@ impl NetworkBeaconProcessor { seen_duration: Duration, ) { let slot = column_sidecar.slot(); - let root = column_sidecar.block_root(); + let block_root = column_sidecar.block_root(); let index = column_sidecar.index; let delay = get_slot_delay_ms(seen_duration, slot, &self.chain.slot_clock); // Log metrics to track delay from other nodes on the network. @@ -635,7 +635,7 @@ impl NetworkBeaconProcessor { self.log, "Successfully verified gossip data column sidecar"; "slot" => %slot, - "root" => %root, + "block_root" => %block_root, "index" => %index, ); @@ -1264,6 +1264,22 @@ impl NetworkBeaconProcessor { let block = verified_block.block.block_cloned(); let block_root = verified_block.block_root; + if block.num_expected_blobs() > 0 { + // Trigger sampling for block not yet execution valid. At this point column custodials are + // unlikely to have received their columns. Triggering sampling so early is only viable with + // either: + // - Sync delaying sampling until some latter window + // - Re-processing early sampling requests: https://github.com/sigp/lighthouse/pull/5569 + if self + .chain + .spec + .peer_das_epoch + .map_or(false, |peer_das_epoch| block.epoch() >= peer_das_epoch) + { + self.send_sync_message(SyncMessage::SampleBlock(block_root, block.slot())); + } + } + let result = self .chain .process_block_with_early_caching(block_root, verified_block, NotifyExecutionLayer::Yes) diff --git a/beacon_node/network/src/network_beacon_processor/mod.rs b/beacon_node/network/src/network_beacon_processor/mod.rs index 8bec17f502c..e9ee06f045f 100644 --- a/beacon_node/network/src/network_beacon_processor/mod.rs +++ b/beacon_node/network/src/network_beacon_processor/mod.rs @@ -1,6 +1,6 @@ use crate::{ service::NetworkMessage, - sync::{manager::BlockProcessType, SyncMessage}, + sync::{manager::BlockProcessType, SamplingId, SyncMessage}, }; use beacon_chain::block_verification_types::RpcBlock; use beacon_chain::{builder::Witness, eth1_chain::CachingEth1Backend, BeaconChain}; @@ -478,6 +478,42 @@ impl NetworkBeaconProcessor { }) } + /// Create a new `Work` event for some data_columns from ReqResp + pub fn send_rpc_data_columns( + self: &Arc, + block_root: Hash256, + data_columns: Vec>>, + seen_timestamp: Duration, + id: SamplingId, + ) -> Result<(), Error> { + let s = self.clone(); + self.try_send(BeaconWorkEvent { + drop_during_sync: false, + work: Work::RpcVerifyDataColumn(Box::pin(async move { + let result = s + .clone() + .validate_rpc_data_columns(block_root, data_columns, seen_timestamp) + .await; + // Sync handles these results + s.send_sync_message(SyncMessage::SampleVerified { id, result }); + })), + }) + } + + /// Create a new `Work` event with a block sampling completed result + pub fn send_sampling_completed( + self: &Arc, + block_root: Hash256, + ) -> Result<(), Error> { + let nbp = self.clone(); + self.try_send(BeaconWorkEvent { + drop_during_sync: false, + work: Work::SamplingResult(Box::pin(async move { + nbp.process_sampling_completed(block_root).await; + })), + }) + } + /// Create a new work event to import `blocks` as a beacon chain segment. pub fn send_chain_segment( self: &Arc, 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 2e5f1216fd7..880eaa1ed0f 100644 --- a/beacon_node/network/src/network_beacon_processor/sync_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/sync_methods.rs @@ -24,7 +24,7 @@ use store::KzgCommitment; use tokio::sync::mpsc; use types::beacon_block_body::format_kzg_commitments; use types::blob_sidecar::FixedBlobSidecarList; -use types::{Epoch, Hash256}; +use types::{DataColumnSidecar, Epoch, Hash256}; /// Id associated to a batch processing request, either a sync batch or a parent lookup. #[derive(Clone, Debug, PartialEq)] @@ -305,6 +305,25 @@ impl NetworkBeaconProcessor { }); } + /// Validate a list of data columns received from RPC requests + pub async fn validate_rpc_data_columns( + self: Arc>, + _block_root: Hash256, + _data_columns: Vec>>, + _seen_timestamp: Duration, + ) -> Result<(), String> { + // TODO(das): validate data column sidecar KZG commitment + Ok(()) + } + + /// Process a sampling completed event, inserting it into fork-choice + pub async fn process_sampling_completed( + self: Arc>, + block_root: Hash256, + ) { + self.chain.process_sampling_completed(block_root).await; + } + /// Attempt to import the chain segment (`blocks`) to the beacon chain, informing the sync /// thread if more blocks are needed to process it. pub async fn process_chain_segment( diff --git a/beacon_node/network/src/router.rs b/beacon_node/network/src/router.rs index 57939f8dcf5..caa9c38af3a 100644 --- a/beacon_node/network/src/router.rs +++ b/beacon_node/network/src/router.rs @@ -514,11 +514,11 @@ impl Router { ) { let request_id = match request_id { RequestId::Sync(sync_id) => match sync_id { - SyncId::SingleBlock { .. } | SyncId::SingleBlob { .. } => { - crit!(self.log, "Block lookups do not request BBRange requests"; "peer_id" => %peer_id); + id @ SyncId::RangeBlockAndBlobs { .. } => id, + other => { + crit!(self.log, "BlocksByRange response on incorrect request"; "request" => ?other); return; } - id @ SyncId::RangeBlockAndBlobs { .. } => id, }, RequestId::Router => { crit!(self.log, "All BBRange requests belong to sync"; "peer_id" => %peer_id); @@ -577,12 +577,8 @@ impl Router { let request_id = match request_id { RequestId::Sync(sync_id) => match sync_id { id @ SyncId::SingleBlock { .. } => id, - SyncId::RangeBlockAndBlobs { .. } => { - crit!(self.log, "Batch syncing do not request BBRoot requests"; "peer_id" => %peer_id); - return; - } - SyncId::SingleBlob { .. } => { - crit!(self.log, "Blob response to block by roots request"; "peer_id" => %peer_id); + other => { + crit!(self.log, "BlocksByRoot response on incorrect request"; "request" => ?other); return; } }, @@ -615,12 +611,8 @@ impl Router { let request_id = match request_id { RequestId::Sync(sync_id) => match sync_id { id @ SyncId::SingleBlob { .. } => id, - SyncId::SingleBlock { .. } => { - crit!(self.log, "Block response to blobs by roots request"; "peer_id" => %peer_id); - return; - } - SyncId::RangeBlockAndBlobs { .. } => { - crit!(self.log, "Batch syncing does not request BBRoot requests"; "peer_id" => %peer_id); + other => { + crit!(self.log, "BlobsByRoot response on incorrect request"; "request" => ?other); return; } }, diff --git a/beacon_node/network/src/sync/block_lookups/tests.rs b/beacon_node/network/src/sync/block_lookups/tests.rs index 8e3b35ee5d3..eeb6b2e719c 100644 --- a/beacon_node/network/src/sync/block_lookups/tests.rs +++ b/beacon_node/network/src/sync/block_lookups/tests.rs @@ -2,7 +2,8 @@ use crate::network_beacon_processor::NetworkBeaconProcessor; use crate::service::RequestId; use crate::sync::manager::{RequestId as SyncRequestId, SingleLookupReqId, SyncManager}; -use crate::sync::SyncMessage; +use crate::sync::sampling::{SamplingConfig, SamplingRequester}; +use crate::sync::{SamplingId, SyncMessage}; use crate::NetworkMessage; use std::sync::Arc; @@ -12,7 +13,8 @@ use crate::sync::block_lookups::common::ResponseType; use beacon_chain::builder::Witness; use beacon_chain::eth1_chain::CachingEth1Backend; use beacon_chain::test_utils::{ - build_log, generate_rand_block_and_blobs, BeaconChainHarness, EphemeralHarnessType, NumBlobs, + build_log, generate_rand_block_and_blobs, generate_rand_block_and_data_columns, + BeaconChainHarness, EphemeralHarnessType, NumBlobs, }; use beacon_processor::WorkEvent; use lighthouse_network::rpc::{RPCError, RPCResponseErrorCode}; @@ -22,6 +24,8 @@ use slog::info; use slot_clock::{ManualSlotClock, SlotClock, TestingSlotClock}; use store::MemoryStore; use tokio::sync::mpsc; +use types::data_column_sidecar::ColumnIndex; +use types::DataColumnSidecar; use types::{ test_utils::{SeedableRng, XorShiftRng}, BlobSidecar, ForkName, MinimalEthSpec as E, SignedBeaconBlock, @@ -57,6 +61,7 @@ type T = Witness, E, MemoryStore, Memo struct TestRig { /// Receiver for `BeaconProcessor` events (e.g. block processing results). beacon_processor_rx: mpsc::Receiver>, + beacon_processor_rx_queue: Vec>, /// Receiver for `NetworkMessage` (e.g. outgoing RPC requests from sync) network_rx: mpsc::UnboundedReceiver>, /// Stores all `NetworkMessage`s received from `network_recv`. (e.g. outgoing RPC requests) @@ -72,6 +77,9 @@ struct TestRig { } const D: Duration = Duration::new(0, 0); +const SAMPLING_REQUIRED_SUCCESSES: usize = 2; + +type SamplingIds = Vec<(Id, ColumnIndex)>; impl TestRig { fn test_setup() -> Self { @@ -114,6 +122,7 @@ impl TestRig { let rng = XorShiftRng::from_seed([42; 16]); TestRig { beacon_processor_rx, + beacon_processor_rx_queue: vec![], network_rx, network_rx_queue: vec![], rng, @@ -123,6 +132,9 @@ impl TestRig { network_tx, beacon_processor.into(), sync_recv, + SamplingConfig::Custom { + required_successes: vec![SAMPLING_REQUIRED_SUCCESSES], + }, log.clone(), ), fork_name, @@ -166,6 +178,10 @@ impl TestRig { )); } + fn trigger_sample_block(&mut self, block_root: Hash256, block_slot: Slot) { + self.send_sync_message(SyncMessage::SampleBlock(block_root, block_slot)) + } + fn rand_block(&mut self) -> SignedBeaconBlock { self.rand_block_and_blobs(NumBlobs::None).0 } @@ -179,6 +195,11 @@ impl TestRig { generate_rand_block_and_blobs::(fork_name, num_blobs, rng) } + fn rand_block_and_data_columns(&mut self) -> (SignedBeaconBlock, Vec>) { + let num_blobs = NumBlobs::Number(1); + generate_rand_block_and_data_columns::(self.fork_name, num_blobs, &mut self.rng) + } + pub fn rand_block_and_parent( &mut self, ) -> (SignedBeaconBlock, SignedBeaconBlock, Hash256, Hash256) { @@ -210,6 +231,20 @@ impl TestRig { self.sync_manager.failed_chains_contains(chain_hash) } + fn expect_no_active_sampling(&mut self) { + assert_eq!( + self.sync_manager.active_sampling_requests(), + vec![], + "expected no active sampling" + ); + } + + fn expect_clean_finished_sampling(&mut self) { + self.expect_empty_network(); + self.expect_sampling_result_work(); + self.expect_no_active_sampling(); + } + #[track_caller] fn assert_parent_lookups_consistency(&self) { let hashes = self.active_parent_lookups(); @@ -233,6 +268,10 @@ impl TestRig { peer_id } + fn new_connected_peers(&mut self, count: usize) -> Vec { + (0..count).map(|_| self.new_connected_peer()).collect() + } + fn parent_chain_processed(&mut self, chain_hash: Hash256, result: BatchProcessResult) { self.send_sync_message(SyncMessage::BatchProcessed { sync_type: ChainSegmentProcessId::ParentLookup(chain_hash), @@ -379,6 +418,77 @@ impl TestRig { }) } + fn return_empty_sampling_requests(&mut self, sampling_ids: SamplingIds) { + for (id, column_index) in sampling_ids { + self.log(&format!("return empty data column for {column_index}")); + self.return_empty_sampling_request(id) + } + } + + fn return_empty_sampling_request(&mut self, id: Id) { + let peer_id = PeerId::random(); + // Send stream termination + self.send_sync_message(SyncMessage::RpcDataColumn { + request_id: SyncRequestId::DataColumnsByRoot(id), + peer_id, + data_column: None, + seen_timestamp: timestamp_now(), + }); + } + + fn complete_valid_sampling_column_requests( + &mut self, + sampling_ids: SamplingIds, + data_columns: Vec>, + ) { + for (id, column_index) in sampling_ids { + self.log(&format!("return valid data column for {column_index}")); + self.complete_valid_sampling_column_request( + id, + data_columns[column_index as usize].clone(), + ); + } + } + + fn complete_valid_sampling_column_request( + &mut self, + id: Id, + data_column: DataColumnSidecar, + ) { + let peer_id = PeerId::random(); + let block_root = data_column.block_root(); + let column_index = data_column.index; + + // Send chunk + self.send_sync_message(SyncMessage::RpcDataColumn { + request_id: SyncRequestId::DataColumnsByRoot(id), + peer_id, + data_column: Some(Arc::new(data_column)), + seen_timestamp: timestamp_now(), + }); + + // Send stream termination + self.send_sync_message(SyncMessage::RpcDataColumn { + request_id: SyncRequestId::DataColumnsByRoot(id), + peer_id, + data_column: None, + seen_timestamp: timestamp_now(), + }); + + // Expect work event + // TODO(das): worth it to append sender id to the work event for stricter assertion? + self.expect_sample_verify_request(); + + // Respond with valid result + self.send_sync_message(SyncMessage::SampleVerified { + id: SamplingId { + id: SamplingRequester::ImportedBlock(block_root), + column_index, + }, + result: Ok(()), + }) + } + fn peer_disconnected(&mut self, peer_id: PeerId) { self.send_sync_message(SyncMessage::Disconnect(peer_id)); } @@ -409,6 +519,36 @@ impl TestRig { } } + fn drain_beacon_processor_rx(&mut self) { + while let Ok(event) = self.beacon_processor_rx.try_recv() { + self.beacon_processor_rx_queue.push(event); + } + } + + fn pop_received_beacon_processor_event) -> Option>( + &mut self, + predicate_transform: F, + ) -> Result { + self.drain_beacon_processor_rx(); + + if let Some(index) = self + .beacon_processor_rx_queue + .iter() + .position(|x| predicate_transform(x).is_some()) + { + // Transform the item, knowing that it won't be None because we checked it in the position predicate. + let transformed = predicate_transform(&self.beacon_processor_rx_queue[index]).unwrap(); + self.beacon_processor_rx_queue.remove(index); + Ok(transformed) + } else { + Err(format!( + "current beacon processor messages {:?}", + self.beacon_processor_rx_queue + ) + .to_string()) + } + } + #[track_caller] fn expect_block_lookup_request(&mut self, for_block: Hash256) -> SingleLookupReqId { self.pop_received_network_event(|ev| match ev { @@ -485,6 +625,38 @@ impl TestRig { .unwrap_or_else(|e| panic!("Expected blob parent request for {for_block:?}: {e}")) } + fn expect_sampling_requests(&mut self, for_block: Hash256, count: usize) -> SamplingIds { + (0..count) + .map(|i| { + self.pop_received_network_event(|ev| match ev { + NetworkMessage::SendRequest { + peer_id: _, + request: Request::DataColumnsByRoot(request), + request_id: RequestId::Sync(SyncRequestId::DataColumnsByRoot(id)), + } if request + .data_column_ids + .to_vec() + .iter() + .any(|r| r.block_root == for_block) => + { + let index = request.data_column_ids.to_vec().first().unwrap().index; + Some((*id, index)) + } + _ => None, + }) + .unwrap_or_else(|e| { + panic!("Expected sampling request {i}/{count} for {for_block:?}: {e}") + }) + }) + .collect() + } + + fn expect_only_sampling_requests(&mut self, for_block: Hash256, count: usize) -> SamplingIds { + let ids = self.expect_sampling_requests(for_block, count); + self.expect_empty_network(); + ids + } + fn expect_lookup_request_block_and_blobs(&mut self, block_root: Hash256) -> SingleLookupReqId { let id = self.expect_block_lookup_request(block_root); // If we're in deneb, a blob request should have been triggered as well, @@ -523,6 +695,28 @@ impl TestRig { } } + fn expect_sample_verify_request(&mut self) { + self.pop_received_beacon_processor_event(|ev| { + if ev.work_type() == beacon_processor::RPC_VERIFY_DATA_COLUMNS { + Some(()) + } else { + None + } + }) + .unwrap_or_else(|e| panic!("Expected sample verify work: {e}")) + } + + fn expect_sampling_result_work(&mut self) { + self.pop_received_beacon_processor_event(|ev| { + if ev.work_type() == beacon_processor::SAMPLING_RESULT { + Some(()) + } else { + None + } + }) + .unwrap_or_else(|e| panic!("Expected sampling result work: {e}")) + } + fn expect_no_penalty_for(&mut self, peer_id: PeerId) { self.drain_network_rx(); let downscore_events = self @@ -554,7 +748,11 @@ impl TestRig { fn expect_empty_network(&mut self) { self.drain_network_rx(); if !self.network_rx_queue.is_empty() { - panic!("expected no network events: {:#?}", self.network_rx_queue); + let n = self.network_rx_queue.len(); + panic!( + "expected no network events but got {n} events, displaying first 2: {:#?}", + self.network_rx_queue[..n.min(2)].iter().collect::>() + ); } } @@ -1092,6 +1290,49 @@ fn test_same_chain_race_condition() { assert_eq!(rig.active_parent_lookups_count(), 0); } +#[test] +fn sampling_happy_path() { + let Some(mut r) = TestRig::test_setup_after_deneb() else { + return; + }; + r.new_connected_peers(100); // Add enough sampling peers + let (block, data_columns) = r.rand_block_and_data_columns(); + let block_root = block.canonical_root(); + r.trigger_sample_block(block_root, block.slot()); + // Retrieve all outgoing sample requests for random column indexes + let sampling_ids = r.expect_only_sampling_requests(block_root, SAMPLING_REQUIRED_SUCCESSES); + // Resolve all of them one by one + r.complete_valid_sampling_column_requests(sampling_ids, data_columns); + r.expect_clean_finished_sampling(); +} + +#[test] +fn sampling_with_retries() { + let Some(mut r) = TestRig::test_setup_after_deneb() else { + return; + }; + r.new_connected_peers(100); // Add enough sampling peers + let (block, data_columns) = r.rand_block_and_data_columns(); + let block_root = block.canonical_root(); + r.trigger_sample_block(block_root, block.slot()); + // Retrieve all outgoing sample requests for random column indexes, and return empty responses + let sampling_ids = r.expect_only_sampling_requests(block_root, SAMPLING_REQUIRED_SUCCESSES); + r.return_empty_sampling_requests(sampling_ids); + // Expect retries for all of them, and resolve them + let sampling_ids = r.expect_only_sampling_requests(block_root, SAMPLING_REQUIRED_SUCCESSES); + r.complete_valid_sampling_column_requests(sampling_ids, data_columns); + r.expect_clean_finished_sampling(); +} + +// TODO(das): Test retries of DataColumnByRoot: +// - Expect request for column_index +// - Respond with bad data +// - Respond with stream terminator +// ^ The stream terminator should be ignored and not close the next retry + +// TODO(das): Test error early a sampling request and it getting drop + then receiving responses +// from pending requests. + mod deneb_only { use super::*; use beacon_chain::data_availability_checker::AvailabilityCheckError; diff --git a/beacon_node/network/src/sync/manager.rs b/beacon_node/network/src/sync/manager.rs index 112ee705da6..9c3ab5d5ac5 100644 --- a/beacon_node/network/src/sync/manager.rs +++ b/beacon_node/network/src/sync/manager.rs @@ -39,6 +39,7 @@ use super::block_lookups::BlockLookups; use super::network_context::{BlockOrBlob, RangeRequestId, RpcEvent, SyncNetworkContext}; use super::peer_sync_info::{remote_sync_type, PeerSyncType}; use super::range_sync::{RangeSync, RangeSyncType, EPOCHS_PER_BATCH}; +use super::sampling::{Sampling, SamplingConfig, SamplingId, SamplingRequester, SamplingResult}; use crate::network_beacon_processor::{ChainSegmentProcessId, NetworkBeaconProcessor}; use crate::service::NetworkMessage; use crate::status::ToStatusMessage; @@ -63,7 +64,7 @@ use std::sync::Arc; use std::time::Duration; use tokio::sync::mpsc; use types::blob_sidecar::FixedBlobSidecarList; -use types::{BlobSidecar, EthSpec, Hash256, SignedBeaconBlock, Slot}; +use types::{BlobSidecar, DataColumnSidecar, EthSpec, Hash256, SignedBeaconBlock, Slot}; /// The number of slots ahead of us that is allowed before requesting a long-range (batch) Sync /// from a peer. If a peer is within this tolerance (forwards or backwards), it is treated as a @@ -90,10 +91,18 @@ pub enum RequestId { SingleBlock { id: SingleLookupReqId }, /// Request searching for a set of blobs given a hash. SingleBlob { id: SingleLookupReqId }, + /// Request searching for a set of data columns given a hash and list of column indices. + DataColumnsByRoot(Id), /// Range request that is composed by both a block range request and a blob range request. RangeBlockAndBlobs { id: Id }, } +#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] +pub enum DataColumnsByRootRequester { + Sampling(SamplingId), + Custody, +} + #[derive(Debug)] /// A message that can be sent to the sync manager thread. pub enum SyncMessage { @@ -116,6 +125,14 @@ pub enum SyncMessage { seen_timestamp: Duration, }, + /// A data columns has been received from the RPC + RpcDataColumn { + request_id: RequestId, + peer_id: PeerId, + data_column: Option>>, + seen_timestamp: Duration, + }, + /// A block with an unknown parent has been received. UnknownParentBlock(PeerId, RpcBlock, Hash256), @@ -126,6 +143,10 @@ pub enum SyncMessage { /// manager to attempt to find the block matching the unknown hash. UnknownBlockHashFromAttestation(PeerId, Hash256), + /// Request to start sampling a block. Caller should ensure that block has data before sending + /// the request. + SampleBlock(Hash256, Slot), + /// A peer has disconnected. Disconnect(PeerId), @@ -147,6 +168,12 @@ pub enum SyncMessage { process_type: BlockProcessType, result: BlockProcessingResult, }, + + /// Sample data column verified + SampleVerified { + id: SamplingId, + result: Result<(), String>, + }, } /// The type of processing specified for a received block. @@ -201,6 +228,8 @@ pub struct SyncManager { block_lookups: BlockLookups, + sampling: Sampling, + /// The logger for the import manager. log: Logger, } @@ -227,6 +256,7 @@ pub fn spawn( network_send, beacon_processor, sync_recv, + SamplingConfig::Default, log.clone(), ); @@ -241,6 +271,7 @@ impl SyncManager { network_send: mpsc::UnboundedSender>, beacon_processor: Arc>, sync_recv: mpsc::UnboundedReceiver>, + sampling_config: SamplingConfig, log: slog::Logger, ) -> Self { let network_globals = beacon_processor.network_globals.clone(); @@ -259,6 +290,7 @@ impl SyncManager { beacon_chain.data_availability_checker.clone(), log.clone(), ), + sampling: Sampling::new(sampling_config, log.clone()), log: log.clone(), } } @@ -278,6 +310,11 @@ impl SyncManager { self.block_lookups.failed_chains_contains(chain_hash) } + #[cfg(test)] + pub(crate) fn active_sampling_requests(&self) -> Vec { + self.sampling.active_sampling_requests() + } + fn network_globals(&self) -> &NetworkGlobals { self.network.network_globals() } @@ -326,6 +363,9 @@ impl SyncManager { RequestId::SingleBlob { id } => { self.on_single_blob_response(id, peer_id, RpcEvent::RPCError(error)) } + RequestId::DataColumnsByRoot(id) => { + self.on_single_data_column_response(id, peer_id, RpcEvent::RPCError(error)) + } RequestId::RangeBlockAndBlobs { id } => { if let Some(sender_id) = self.network.range_request_failed(id) { match sender_id { @@ -570,6 +610,12 @@ impl SyncManager { blob_sidecar, seen_timestamp, } => self.rpc_blob_received(request_id, peer_id, blob_sidecar, seen_timestamp), + SyncMessage::RpcDataColumn { + request_id, + peer_id, + data_column, + seen_timestamp, + } => self.rpc_data_column_received(request_id, peer_id, data_column, seen_timestamp), SyncMessage::UnknownParentBlock(peer_id, block, block_root) => { let block_slot = block.slot(); let parent_root = block.parent_root(); @@ -606,6 +652,15 @@ impl SyncManager { debug!(self.log, "Received unknown block hash message"; "block_root" => %block_root); self.handle_unknown_block_root(peer_id, block_root); } + SyncMessage::SampleBlock(block_root, block_slot) => { + debug!(self.log, "Received SampleBlock message"; "block_root" => %block_root); + if let Some((requester, result)) = + self.sampling + .on_new_sample_request(block_root, block_slot, &mut self.network) + { + self.on_sampling_result(requester, result) + } + } SyncMessage::Disconnect(peer_id) => { debug!(self.log, "Received disconnected message"; "peer_id" => %peer_id); self.peer_disconnect(&peer_id); @@ -666,6 +721,14 @@ impl SyncManager { .block_lookups .parent_chain_processed(chain_hash, result, &mut self.network), }, + SyncMessage::SampleVerified { id, result } => { + if let Some((requester, result)) = + self.sampling + .on_sample_verified(id, result, &mut self.network) + { + self.on_sampling_result(requester, result) + } + } } } @@ -814,12 +877,12 @@ impl SyncManager { None => RpcEvent::StreamTermination, }, ), - RequestId::SingleBlob { .. } => { - crit!(self.log, "Block received during blob request"; "peer_id" => %peer_id ); - } RequestId::RangeBlockAndBlobs { id } => { self.range_block_and_blobs_response(id, peer_id, block.into()) } + other => { + crit!(self.log, "Single block received on incorrect request"; "request_id" => ?other); + } } } @@ -829,7 +892,7 @@ impl SyncManager { peer_id: PeerId, block: RpcEvent>>, ) { - if let Some(resp) = self.network.on_single_block_response(id, block) { + if let Some((_, resp)) = self.network.on_single_block_response(id, block) { match resp { Ok((block, seen_timestamp)) => match id.lookup_type { LookupType::Current => self @@ -881,9 +944,6 @@ impl SyncManager { seen_timestamp: Duration, ) { match request_id { - RequestId::SingleBlock { .. } => { - crit!(self.log, "Single blob received during block request"; "peer_id" => %peer_id ); - } RequestId::SingleBlob { id } => self.on_single_blob_response( id, peer_id, @@ -895,6 +955,36 @@ impl SyncManager { RequestId::RangeBlockAndBlobs { id } => { self.range_block_and_blobs_response(id, peer_id, blob.into()) } + other => { + crit!(self.log, "Single blob received on incorrect request"; "request_id" => ?other); + } + } + } + + fn rpc_data_column_received( + &mut self, + request_id: RequestId, + peer_id: PeerId, + data_column: Option>>, + seen_timestamp: Duration, + ) { + match request_id { + RequestId::SingleBlock { .. } | RequestId::SingleBlob { .. } => { + crit!(self.log, "bad request id for data_column"; "peer_id" => %peer_id ); + } + RequestId::DataColumnsByRoot(id) => { + self.on_single_data_column_response( + id, + peer_id, + match data_column { + Some(data_column) => RpcEvent::Response(data_column, seen_timestamp), + None => RpcEvent::StreamTermination, + }, + ); + } + RequestId::RangeBlockAndBlobs { id } => { + todo!("TODO(das): handle sampling for range sync based on {id}"); + } } } @@ -904,7 +994,7 @@ impl SyncManager { peer_id: PeerId, blob: RpcEvent>>, ) { - if let Some(resp) = self.network.on_single_blob_response(id, blob) { + if let Some((_, resp)) = self.network.on_single_blob_response(id, blob) { match resp { Ok((blobs, seen_timestamp)) => match id.lookup_type { LookupType::Current => self @@ -949,6 +1039,65 @@ impl SyncManager { } } + fn on_single_data_column_response( + &mut self, + id: Id, + peer_id: PeerId, + data_column: RpcEvent>>, + ) { + if let Some((requester, resp)) = self + .network + .on_data_columns_by_root_response(id, data_column) + { + match requester { + DataColumnsByRootRequester::Sampling(id) => { + if let Some((requester, result)) = + self.sampling + .on_sample_downloaded(id, peer_id, resp, &mut self.network) + { + self.on_sampling_result(requester, result) + } + } + DataColumnsByRootRequester::Custody => { + todo!("TODO(das): handle custody requests"); + } + } + } + } + + fn on_sampling_result(&mut self, requester: SamplingRequester, result: SamplingResult) { + // TODO(das): How is a consumer of sampling results? + // - Fork-choice for trailing DA + // - Single lookups to complete import requirements + // - Range sync to complete import requirements? Can sampling for syncing lag behind and + // accumulate in fork-choice? + + match requester { + SamplingRequester::ImportedBlock(block_root) => { + debug!(self.log, "Sampling result"; "block_root" => %block_root, "result" => ?result); + + // TODO(das): Consider moving SamplingResult to the beacon_chain crate and import + // here. No need to add too much enum variants, just whatever the beacon_chain or + // fork-choice needs to make a decision. Currently the fork-choice only needs to + // be notified of successful samplings, i.e. sampling failures don't trigger pruning + match result { + Ok(_) => { + if let Err(e) = self + .network + .beacon_processor() + .send_sampling_completed(block_root) + { + warn!(self.log, "Error sending sampling result"; "block_root" => ?block_root, "reason" => ?e); + } + } + Err(e) => { + warn!(self.log, "Sampling failed"; "block_root" => %block_root, "reason" => ?e); + } + } + } + } + } + /// Handles receiving a response for a range sync request that should have both blocks and /// blobs. fn range_block_and_blobs_response( diff --git a/beacon_node/network/src/sync/mod.rs b/beacon_node/network/src/sync/mod.rs index 7b244bceceb..0fb01a73e07 100644 --- a/beacon_node/network/src/sync/mod.rs +++ b/beacon_node/network/src/sync/mod.rs @@ -8,6 +8,8 @@ pub mod manager; mod network_context; mod peer_sync_info; mod range_sync; +mod sampling; pub use manager::{BatchProcessResult, SyncMessage}; pub use range_sync::{BatchOperationOutcome, ChainId}; +pub use sampling::SamplingId; diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index fc91270c1dc..a8bb9f2afec 100644 --- a/beacon_node/network/src/sync/network_context.rs +++ b/beacon_node/network/src/sync/network_context.rs @@ -1,10 +1,14 @@ //! Provides network functionality for the Syncing thread. This fundamentally wraps a network //! channel and stores a global RPC ID to perform requests. -use self::requests::{ActiveBlobsByRootRequest, ActiveBlocksByRootRequest}; -pub use self::requests::{BlobsByRootSingleBlockRequest, BlocksByRootSingleRequest}; +use self::requests::{ + ActiveBlobsByRootRequest, ActiveBlocksByRootRequest, ActiveDataColumnsByRootRequest, +}; +pub use self::requests::{ + BlobsByRootSingleBlockRequest, BlocksByRootSingleRequest, DataColumnsByRootSingleBlockRequest, +}; use super::block_sidecar_coupling::BlocksAndBlobsRequestInfo; -use super::manager::{Id, RequestId as SyncRequestId}; +use super::manager::{DataColumnsByRootRequester, Id, RequestId as SyncRequestId}; use super::range_sync::{BatchId, ByRangeRequestType, ChainId}; use crate::network_beacon_processor::NetworkBeaconProcessor; use crate::service::{NetworkMessage, RequestId}; @@ -16,7 +20,9 @@ use beacon_chain::{BeaconChain, BeaconChainTypes, EngineState}; use fnv::FnvHashMap; use lighthouse_network::rpc::methods::BlobsByRangeRequest; use lighthouse_network::rpc::{BlocksByRangeRequest, GoodbyeReason, RPCError}; -use lighthouse_network::{Client, NetworkGlobals, PeerAction, PeerId, ReportSource, Request}; +use lighthouse_network::{ + Client, Eth2Enr, NetworkGlobals, PeerAction, PeerId, ReportSource, Request, +}; pub use requests::LookupVerifyError; use slog::{debug, trace, warn}; use std::collections::hash_map::Entry; @@ -24,7 +30,10 @@ use std::sync::Arc; use std::time::Duration; use tokio::sync::mpsc; use types::blob_sidecar::FixedBlobSidecarList; -use types::{BlobSidecar, EthSpec, SignedBeaconBlock}; +use types::data_column_sidecar::ColumnIndex; +use types::{ + BlobSidecar, DataColumnSidecar, DataColumnSubnetId, Epoch, EthSpec, SignedBeaconBlock, +}; mod requests; @@ -52,7 +61,7 @@ pub enum RpcEvent { RPCError(RPCError), } -pub type RpcProcessingResult = Option>; +pub type RpcProcessingResult = Option<(ID, Result<(T, Duration), LookupFailure>)>; pub enum LookupFailure { RpcError(RPCError), @@ -93,6 +102,8 @@ pub struct SyncNetworkContext { /// A mapping of active BlobsByRoot requests, including both current slot and parent lookups. blobs_by_root_requests: FnvHashMap>, + data_columns_by_root_requests: + FnvHashMap>, /// BlocksByRange requests paired with BlobsByRange range_blocks_and_blobs_requests: @@ -142,6 +153,7 @@ impl SyncNetworkContext { request_id: 1, blocks_by_root_requests: <_>::default(), blobs_by_root_requests: <_>::default(), + data_columns_by_root_requests: <_>::default(), range_blocks_and_blobs_requests: FnvHashMap::default(), network_beacon_processor, chain, @@ -149,6 +161,35 @@ impl SyncNetworkContext { } } + // TODO(das): epoch argument left here in case custody rotation is implemented + pub fn get_custodial_peers(&self, _epoch: Epoch, column_index: ColumnIndex) -> Vec { + let mut peer_ids = vec![]; + + for (peer_id, peer_info) in self.network_globals().peers.read().connected_peers() { + if let Some(enr) = peer_info.enr() { + // TODO(das): ignores decode errors + let custody_subnet_count = enr + .custody_subnet_count::() + .unwrap_or(T::EthSpec::min_custody_requirement() as u64); + // TODO(das): consider caching a map of subnet -> Vec and invalidating + // whenever a peer connected or disconnect event in received + let mut subnets = DataColumnSubnetId::compute_custody_subnets::( + enr.node_id().raw().into(), + custody_subnet_count, + ); + if subnets.any(|subnet| { + subnet + .columns::() + .any(|index| index == column_index) + }) { + peer_ids.push(*peer_id) + } + } + } + + peer_ids + } + pub fn network_globals(&self) -> &NetworkGlobals { &self.network_beacon_processor.network_globals } @@ -350,6 +391,37 @@ impl SyncNetworkContext { Ok(()) } + pub fn data_column_lookup_request( + &mut self, + requester: DataColumnsByRootRequester, + peer_id: PeerId, + request: DataColumnsByRootSingleBlockRequest, + ) -> Result<(), &'static str> { + let id = self.next_id(); + + debug!( + self.log, + "Sending DataColumnsByRoot Request"; + "method" => "DataColumnsByRoot", + "block_root" => ?request.block_root, + "indices" => ?request.indices, + "peer" => %peer_id, + "requester" => ?requester, + "id" => id, + ); + + self.send_network_msg(NetworkMessage::SendRequest { + peer_id, + request: Request::DataColumnsByRoot(request.clone().into_request(&self.chain.spec)), + request_id: RequestId::Sync(SyncRequestId::DataColumnsByRoot(id)), + })?; + + self.data_columns_by_root_requests + .insert(id, ActiveDataColumnsByRootRequest::new(request, requester)); + + Ok(()) + } + pub fn is_execution_engine_online(&self) -> bool { self.execution_engine_state == EngineState::Online } @@ -388,6 +460,31 @@ impl SyncNetworkContext { }); } + pub fn report_peer_on_rpc_error(&self, peer_id: &PeerId, error: &RPCError) { + // Note: logging the report event here with the full error display. The log inside + // `report_peer` only includes a smaller string, like "invalid_data" + debug!(self.log, "reporting peer for sync lookup error"; "error" => %error); + if let Some(action) = match error { + // Protocol errors are heavily penalized + RPCError::SSZDecodeError(..) + | RPCError::IoError(..) + | RPCError::ErrorResponse(..) + | RPCError::InvalidData(..) + | RPCError::HandlerRejected => Some(PeerAction::LowToleranceError), + // Timing / network errors are less penalized + // TODO: Is IoError a protocol error or network error? + RPCError::StreamTimeout | RPCError::IncompleteStream | RPCError::NegotiationTimeout => { + Some(PeerAction::MidToleranceError) + } + // Not supporting a specific protocol is tolerated. TODO: Are you sure? + RPCError::UnsupportedProtocol => None, + // Our fault, don't penalize peer + RPCError::InternalError(..) | RPCError::Disconnected => None, + } { + self.report_peer(*peer_id, action, error.into()); + } + } + /// Subscribes to core topics. pub fn subscribe_core_topics(&self) { self.network_send @@ -458,12 +555,12 @@ impl SyncNetworkContext { &mut self, request_id: SingleLookupReqId, block: RpcEvent>>, - ) -> RpcProcessingResult>> { + ) -> RpcProcessingResult<(), Arc>> { let Entry::Occupied(mut request) = self.blocks_by_root_requests.entry(request_id) else { return None; }; - Some(match block { + let resp = match block { RpcEvent::Response(block, seen_timestamp) => { match request.get_mut().add_response(block) { Ok(block) => Ok((block, seen_timestamp)), @@ -482,19 +579,20 @@ impl SyncNetworkContext { request.remove(); Err(e.into()) } - }) + }; + Some(((), resp)) } pub fn on_single_blob_response( &mut self, request_id: SingleLookupReqId, blob: RpcEvent>>, - ) -> RpcProcessingResult> { + ) -> RpcProcessingResult<(), FixedBlobSidecarList> { let Entry::Occupied(mut request) = self.blobs_by_root_requests.entry(request_id) else { return None; }; - Some(match blob { + let resp = match blob { RpcEvent::Response(blob, _) => match request.get_mut().add_response(blob) { Ok(Some(blobs)) => to_fixed_blob_sidecar_list(blobs) .map(|blobs| (blobs, timestamp_now())) @@ -518,7 +616,45 @@ impl SyncNetworkContext { request.remove(); Err(e.into()) } - }) + }; + Some(((), resp)) + } + + pub fn on_data_columns_by_root_response( + &mut self, + id: Id, + item: RpcEvent>>, + ) -> RpcProcessingResult>>> + { + let Entry::Occupied(mut request) = self.data_columns_by_root_requests.entry(id) else { + return None; + }; + + let requester = request.get().requester; + + let resp = match item { + RpcEvent::Response(item, _) => match request.get_mut().add_response(item) { + // TODO: Track last chunk timestamp + Ok(Some(items)) => Ok((items, timestamp_now())), + Ok(None) => return None, + Err(e) => { + request.remove(); + Err(e.into()) + } + }, + RpcEvent::StreamTermination => { + // Stream terminator + match request.remove().terminate() { + Some(items) => Ok((items, timestamp_now())), + None => return None, + } + } + RpcEvent::RPCError(e) => { + request.remove(); + Err(e.into()) + } + }; + Some((requester, resp)) } } diff --git a/beacon_node/network/src/sync/network_context/requests.rs b/beacon_node/network/src/sync/network_context/requests.rs index 0522b7fa384..2fc239b49b2 100644 --- a/beacon_node/network/src/sync/network_context/requests.rs +++ b/beacon_node/network/src/sync/network_context/requests.rs @@ -1,9 +1,13 @@ use beacon_chain::get_block_root; -use lighthouse_network::rpc::{methods::BlobsByRootRequest, BlocksByRootRequest}; +use lighthouse_network::rpc::{ + methods::{BlobsByRootRequest, DataColumnsByRootRequest}, + BlocksByRootRequest, RPCError, +}; use std::sync::Arc; use strum::IntoStaticStr; use types::{ - blob_sidecar::BlobIdentifier, BlobSidecar, ChainSpec, EthSpec, Hash256, SignedBeaconBlock, + blob_sidecar::BlobIdentifier, data_column_sidecar::DataColumnIdentifier, BlobSidecar, + ChainSpec, DataColumnSidecar, EthSpec, Hash256, SignedBeaconBlock, }; #[derive(Debug, PartialEq, Eq, IntoStaticStr)] @@ -147,3 +151,90 @@ impl ActiveBlobsByRootRequest { } } } + +#[derive(Debug, Clone)] +pub struct DataColumnsByRootSingleBlockRequest { + pub block_root: Hash256, + pub indices: Vec, +} + +impl DataColumnsByRootSingleBlockRequest { + pub fn into_request(self, spec: &ChainSpec) -> DataColumnsByRootRequest { + DataColumnsByRootRequest::new( + self.indices + .into_iter() + .map(|index| DataColumnIdentifier { + block_root: self.block_root, + index, + }) + .collect(), + spec, + ) + } +} + +pub struct ActiveDataColumnsByRootRequest { + pub requester: T, + request: DataColumnsByRootSingleBlockRequest, + items: Vec>>, + resolved: bool, +} + +impl ActiveDataColumnsByRootRequest { + pub fn new(request: DataColumnsByRootSingleBlockRequest, requester: T) -> Self { + Self { + requester, + request, + items: vec![], + resolved: false, + } + } + + /// Appends a chunk to this multi-item request. If all expected chunks are received, this + /// method returns `Some`, resolving the request before the stream terminator. + /// The active request SHOULD be dropped after `add_response` returns an error + pub fn add_response( + &mut self, + data_column: Arc>, + ) -> Result>>>, RPCError> { + if self.resolved { + return Err(RPCError::InvalidData("too many responses".to_string())); + } + + let block_root = data_column.block_root(); + if self.request.block_root != block_root { + return Err(RPCError::InvalidData(format!( + "un-requested block root {block_root:?}" + ))); + } + if !data_column.verify_inclusion_proof().unwrap_or(false) { + return Err(RPCError::InvalidData("invalid inclusion proof".to_string())); + } + if !self.request.indices.contains(&data_column.index) { + return Err(RPCError::InvalidData(format!( + "un-requested index {}", + data_column.index + ))); + } + if self.items.iter().any(|b| b.index == data_column.index) { + return Err(RPCError::InvalidData("duplicated data".to_string())); + } + + self.items.push(data_column); + if self.items.len() >= self.request.indices.len() { + // All expected chunks received, return result early + self.resolved = true; + Ok(Some(std::mem::take(&mut self.items))) + } else { + Ok(None) + } + } + + pub fn terminate(self) -> Option>>> { + if self.resolved { + None + } else { + Some(self.items) + } + } +} diff --git a/beacon_node/network/src/sync/sampling.rs b/beacon_node/network/src/sync/sampling.rs new file mode 100644 index 00000000000..50fb73f9bc6 --- /dev/null +++ b/beacon_node/network/src/sync/sampling.rs @@ -0,0 +1,543 @@ +use self::request::ActiveColumnSampleRequest; +use super::network_context::{LookupFailure, SyncNetworkContext}; +use crate::metrics; +use beacon_chain::BeaconChainTypes; +use fnv::FnvHashMap; +use lighthouse_network::{PeerAction, PeerId}; +use rand::{seq::SliceRandom, thread_rng}; +use slog::{debug, error, warn}; +use std::{ + collections::hash_map::Entry, collections::HashMap, marker::PhantomData, sync::Arc, + time::Duration, +}; +use types::{data_column_sidecar::ColumnIndex, DataColumnSidecar, EthSpec, Hash256, Slot}; + +pub type SamplingResult = Result<(), SamplingError>; + +#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] +pub struct SamplingId { + pub id: SamplingRequester, + pub column_index: ColumnIndex, +} + +#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] +pub enum SamplingRequester { + ImportedBlock(Hash256), +} + +type DataColumnSidecarList = Vec>>; + +pub struct Sampling { + // TODO(das): stalled sampling request are never cleaned up + requests: HashMap>, + sampling_config: SamplingConfig, + log: slog::Logger, +} + +impl Sampling { + pub fn new(sampling_config: SamplingConfig, log: slog::Logger) -> Self { + Self { + requests: <_>::default(), + sampling_config, + log, + } + } + + #[cfg(test)] + pub fn active_sampling_requests(&self) -> Vec { + self.requests.values().map(|r| r.block_root).collect() + } + + /// Create a new sampling request for a known block + /// + /// ### Returns + /// + /// - `Some`: Request completed, won't make more progress. Expect requester to act on the result. + /// - `None`: Request still active, requester should do no action + pub fn on_new_sample_request( + &mut self, + block_root: Hash256, + block_slot: Slot, + cx: &mut SyncNetworkContext, + ) -> Option<(SamplingRequester, SamplingResult)> { + let id = SamplingRequester::ImportedBlock(block_root); + + let request = match self.requests.entry(id) { + Entry::Vacant(e) => e.insert(ActiveSamplingRequest::new( + block_root, + block_slot, + id, + &self.sampling_config, + self.log.clone(), + )), + Entry::Occupied(_) => { + // Sampling is triggered from multiple sources, duplicate sampling requests are + // likely (gossip block + gossip data column) + // TODO(das): Should track failed sampling request for some time? Otherwise there's + // a risk of a loop with multiple triggers creating the request, then failing, + // and repeat. + debug!(self.log, "Ignoring duplicate sampling request"; "id" => ?id); + return None; + } + }; + + debug!(self.log, "Created new sample request"; "id" => ?id); + + // TOOD(das): If a node has very little peers, continue_sampling() will attempt to find enough + // to sample here, immediately failing the sampling request. There should be some grace + // period to allow the peer manager to find custody peers. + let result = request.continue_sampling(cx); + self.handle_sampling_result(result, &id) + } + + /// Insert a downloaded column into an active sampling request. Then make progress on the + /// entire request. + /// + /// ### Returns + /// + /// - `Some`: Request completed, won't make more progress. Expect requester to act on the result. + /// - `None`: Request still active, requester should do no action + pub fn on_sample_downloaded( + &mut self, + id: SamplingId, + peer_id: PeerId, + resp: Result<(DataColumnSidecarList, Duration), LookupFailure>, + cx: &mut SyncNetworkContext, + ) -> Option<(SamplingRequester, SamplingResult)> { + let Some(request) = self.requests.get_mut(&id.id) else { + // TOOD(das): This log can happen if the request is error'ed early and dropped + debug!(self.log, "Sample downloaded event for unknown request"; "id" => ?id); + return None; + }; + + let result = request.on_sample_downloaded(peer_id, id.column_index, resp, cx); + self.handle_sampling_result(result, &id.id) + } + + /// Insert a downloaded column into an active sampling request. Then make progress on the + /// entire request. + /// + /// ### Returns + /// + /// - `Some`: Request completed, won't make more progress. Expect requester to act on the result. + /// - `None`: Request still active, requester should do no action + pub fn on_sample_verified( + &mut self, + id: SamplingId, + result: Result<(), String>, + cx: &mut SyncNetworkContext, + ) -> Option<(SamplingRequester, SamplingResult)> { + let Some(request) = self.requests.get_mut(&id.id) else { + // TOOD(das): This log can happen if the request is error'ed early and dropped + debug!(self.log, "Sample verified event for unknown request"; "id" => ?id); + return None; + }; + + let result = request.on_sample_verified(id.column_index, result, cx); + self.handle_sampling_result(result, &id.id) + } + + /// Converts a result from the internal format of `ActiveSamplingRequest` (error first to use ? + /// conveniently), to an Option first format to use an `if let Some() { act on result }` pattern + /// in the sync manager. + fn handle_sampling_result( + &mut self, + result: Result, SamplingError>, + id: &SamplingRequester, + ) -> Option<(SamplingRequester, SamplingResult)> { + let result = result.transpose(); + if let Some(result) = result { + debug!(self.log, "Sampling request completed, removing"; "id" => ?id, "result" => ?result); + metrics::inc_counter_vec( + &metrics::SAMPLING_REQUEST_RESULT, + &[metrics::from_result(&result)], + ); + self.requests.remove(id); + Some((*id, result)) + } else { + None + } + } +} + +pub struct ActiveSamplingRequest { + block_root: Hash256, + block_slot: Slot, + requester_id: SamplingRequester, + column_requests: FnvHashMap, + column_shuffle: Vec, + required_successes: Vec, + /// Logger for the `SyncNetworkContext`. + pub log: slog::Logger, + _phantom: PhantomData, +} + +#[derive(Debug)] +pub enum SamplingError { + SendFailed(&'static str), + ProcessorUnavailable, + TooManyFailures, + BadState(String), + ColumnIndexOutOfBounds, +} + +/// Required success index by current failures, with p_target=5.00E-06 +/// Ref: https://colab.research.google.com/drive/18uUgT2i-m3CbzQ5TyP9XFKqTn1DImUJD#scrollTo=E82ITcgB5ATh +const REQUIRED_SUCCESSES: [usize; 11] = [16, 20, 23, 26, 29, 32, 34, 37, 39, 42, 44]; + +#[derive(Debug, Clone)] +pub enum SamplingConfig { + Default, + #[allow(dead_code)] + Custom { + required_successes: Vec, + }, +} + +impl ActiveSamplingRequest { + fn new( + block_root: Hash256, + block_slot: Slot, + requester_id: SamplingRequester, + sampling_config: &SamplingConfig, + log: slog::Logger, + ) -> Self { + // Select ahead of time the full list of to-sample columns + let mut column_shuffle = (0..::number_of_columns() as ColumnIndex) + .collect::>(); + let mut rng = thread_rng(); + column_shuffle.shuffle(&mut rng); + + Self { + block_root, + block_slot, + requester_id, + column_requests: <_>::default(), + column_shuffle, + required_successes: match sampling_config { + SamplingConfig::Default => REQUIRED_SUCCESSES.to_vec(), + SamplingConfig::Custom { required_successes } => required_successes.clone(), + }, + log, + _phantom: PhantomData, + } + } + + /// Insert a downloaded column into an active sampling request. Then make progress on the + /// entire request. + /// + /// ### Returns + /// + /// - `Err`: Sampling request has failed and will be dropped + /// - `Ok(Some)`: Sampling request has successfully completed and will be dropped + /// - `Ok(None)`: Sampling request still active + pub(crate) fn on_sample_downloaded( + &mut self, + _peer_id: PeerId, + column_index: ColumnIndex, + resp: Result<(DataColumnSidecarList, Duration), LookupFailure>, + cx: &mut SyncNetworkContext, + ) -> Result, SamplingError> { + // Select columns to sample + // Create individual request per column + // Progress requests + // If request fails retry or expand search + // If all good return + let Some(request) = self.column_requests.get_mut(&column_index) else { + warn!( + self.log, + "Received sampling response for unrequested column index" + ); + return Ok(None); + }; + + match resp { + Ok((mut data_columns, seen_timestamp)) => { + debug!(self.log, "Sample download success"; "block_root" => %self.block_root, "column_index" => column_index, "count" => data_columns.len()); + metrics::inc_counter_vec(&metrics::SAMPLE_DOWNLOAD_RESULT, &[metrics::SUCCESS]); + + // No need to check data_columns has len > 1, as the SyncNetworkContext ensure that + // only requested is returned (or none); + if let Some(data_column) = data_columns.pop() { + // Peer has data column, send to verify + let Some(beacon_processor) = cx.beacon_processor_if_enabled() else { + // If processor is not available, error the entire sampling + debug!(self.log, "Dropping sampling"; "block" => %self.block_root, "reason" => "beacon processor unavailable"); + return Err(SamplingError::ProcessorUnavailable); + }; + + debug!(self.log, "Sending data_column for verification"; "block" => ?self.block_root, "column_index" => column_index); + if let Err(e) = beacon_processor.send_rpc_data_columns( + self.block_root, + vec![data_column], + seen_timestamp, + SamplingId { + id: self.requester_id, + column_index, + }, + ) { + // TODO(das): Beacon processor is overloaded, what should we do? + error!(self.log, "Dropping sampling"; "block" => %self.block_root, "reason" => e.to_string()); + return Err(SamplingError::SendFailed("beacon processor send failure")); + } + } else { + // Peer does not have the requested data. + // TODO(das) what to do? + debug!(self.log, "Sampling peer claims to not have the data"; "block_root" => %self.block_root, "column_index" => column_index); + request.on_sampling_error()?; + } + } + Err(err) => { + debug!(self.log, "Sample download error"; "block_root" => %self.block_root, "column_index" => column_index, "error" => %err); + metrics::inc_counter_vec(&metrics::SAMPLE_DOWNLOAD_RESULT, &[metrics::FAILURE]); + + // Error downloading, maybe penalize peer and retry again. + // TODO(das) with different peer or different peer? + request.on_sampling_error()?; + } + }; + + self.continue_sampling(cx) + } + + /// Insert a column verification result into an active sampling request. Then make progress + /// on the entire request. + /// + /// ### Returns + /// + /// - `Err`: Sampling request has failed and will be dropped + /// - `Ok(Some)`: Sampling request has successfully completed and will be dropped + /// - `Ok(None)`: Sampling request still active + pub(crate) fn on_sample_verified( + &mut self, + column_index: ColumnIndex, + result: Result<(), String>, + cx: &mut SyncNetworkContext, + ) -> Result, SamplingError> { + // Select columns to sample + // Create individual request per column + // Progress requests + // If request fails retry or expand search + // If all good return + let Some(request) = self.column_requests.get_mut(&column_index) else { + warn!( + self.log, + "Received sampling response for unrequested column index" + ); + return Ok(None); + }; + + match result { + Ok(_) => { + debug!(self.log, "Sample verification success"; "block_root" => %self.block_root, "column_index" => column_index); + metrics::inc_counter_vec(&metrics::SAMPLE_VERIFY_RESULT, &[metrics::SUCCESS]); + + // Valid, continue_sampling will maybe consider sampling succees + request.on_sampling_success()?; + } + Err(err) => { + debug!(self.log, "Sample verification failure"; "block_root" => %self.block_root, "column_index" => column_index, "reason" => ?err); + metrics::inc_counter_vec(&metrics::SAMPLE_VERIFY_RESULT, &[metrics::FAILURE]); + + // TODO(das): Peer sent invalid data, penalize and try again from different peer + // TODO(das): Count individual failures + let peer_id = request.on_sampling_error()?; + cx.report_peer( + peer_id, + PeerAction::LowToleranceError, + "invalid data column", + ); + } + } + + self.continue_sampling(cx) + } + + pub(crate) fn continue_sampling( + &mut self, + cx: &mut SyncNetworkContext, + ) -> Result, SamplingError> { + // First check if sampling is completed, by computing `required_successes` + let mut successes = 0; + let mut failures = 0; + let mut ongoings = 0; + + for request in self.column_requests.values() { + if request.is_completed() { + successes += 1; + } + if request.is_failed() { + failures += 1; + } + if request.is_ongoing() { + ongoings += 1; + } + } + + // If there are too many failures, consider the sampling failed + let Some(required_successes) = self.required_successes.get(failures) else { + return Err(SamplingError::TooManyFailures); + }; + + // If there are enough successes, consider the sampling complete + if successes >= *required_successes { + return Ok(Some(())); + } + + let mut sent_requests = 0; + + // First, attempt to progress sampling by requesting more columns, so that request failures + // are accounted for below. + for idx in 0..*required_successes { + // Re-request columns. Note: out of bounds error should never happen, inputs are hardcoded + let column_index = *self + .column_shuffle + .get(idx) + .ok_or(SamplingError::ColumnIndexOutOfBounds)?; + let request = self + .column_requests + .entry(column_index) + .or_insert(ActiveColumnSampleRequest::new(column_index)); + + if request.request(self.block_root, self.block_slot, self.requester_id, cx)? { + sent_requests += 1 + } + } + + // Make sure that sampling doesn't stall, by ensuring that this sampling request will + // receive a new event of some type. If there are no ongoing requests, and no new + // request was sent, loop to increase the required_successes until the sampling fails if + // there are no peers. + if ongoings == 0 && sent_requests == 0 { + debug!(self.log, "Sampling request stalled"; "block_root" => %self.block_root); + } + + Ok(None) + } +} + +mod request { + use super::{SamplingError, SamplingId, SamplingRequester}; + use crate::sync::{ + manager::DataColumnsByRootRequester, + network_context::{DataColumnsByRootSingleBlockRequest, SyncNetworkContext}, + }; + use beacon_chain::BeaconChainTypes; + use lighthouse_network::PeerId; + use std::collections::HashSet; + use types::{data_column_sidecar::ColumnIndex, EthSpec, Hash256, Slot}; + + pub(crate) struct ActiveColumnSampleRequest { + column_index: ColumnIndex, + status: Status, + // TODO(das): Should downscore peers that claim to not have the sample? + #[allow(dead_code)] + peers_dont_have: HashSet, + } + + #[derive(Debug, Clone)] + enum Status { + NoPeers, + NotStarted, + Sampling(PeerId), + Verified, + } + + impl ActiveColumnSampleRequest { + pub(crate) fn new(column_index: ColumnIndex) -> Self { + Self { + column_index, + status: Status::NotStarted, + peers_dont_have: <_>::default(), + } + } + + pub(crate) fn is_completed(&self) -> bool { + match self.status { + Status::NoPeers | Status::NotStarted | Status::Sampling(_) => false, + Status::Verified => true, + } + } + + pub(crate) fn is_failed(&self) -> bool { + match self.status { + Status::NotStarted | Status::Sampling(_) | Status::Verified => false, + Status::NoPeers => true, + } + } + + pub(crate) fn is_ongoing(&self) -> bool { + match self.status { + Status::NotStarted | Status::NoPeers | Status::Verified => false, + Status::Sampling(_) => true, + } + } + + pub(crate) fn request( + &mut self, + block_root: Hash256, + block_slot: Slot, + requester: SamplingRequester, + cx: &mut SyncNetworkContext, + ) -> Result { + match &self.status { + Status::NoPeers | Status::NotStarted => {} // Ok to continue + Status::Sampling(_) => return Ok(false), // Already downloading + Status::Verified => return Ok(false), // Already completed + } + + // TODO: When is a fork and only a subset of your peers know about a block, sampling should only + // be queried on the peers on that fork. Should this case be handled? How to handle it? + let peer_ids = cx.get_custodial_peers( + block_slot.epoch(::slots_per_epoch()), + self.column_index, + ); + + // TODO(das) randomize custodial peer and avoid failing peers + if let Some(peer_id) = peer_ids.first().cloned() { + cx.data_column_lookup_request( + DataColumnsByRootRequester::Sampling(SamplingId { + id: requester, + column_index: self.column_index, + }), + peer_id, + DataColumnsByRootSingleBlockRequest { + block_root, + indices: vec![self.column_index], + }, + ) + .map_err(SamplingError::SendFailed)?; + + self.status = Status::Sampling(peer_id); + Ok(true) + } else { + self.status = Status::NoPeers; + Ok(false) + } + } + + pub(crate) fn on_sampling_error(&mut self) -> Result { + match self.status.clone() { + Status::Sampling(peer_id) => { + self.status = Status::NotStarted; + Ok(peer_id) + } + other => Err(SamplingError::BadState(format!( + "bad state on_sampling_error expected Sampling got {other:?}" + ))), + } + } + + pub(crate) fn on_sampling_success(&mut self) -> Result<(), SamplingError> { + match &self.status { + Status::Sampling(_) => { + self.status = Status::Verified; + Ok(()) + } + other => Err(SamplingError::BadState(format!( + "bad state on_sampling_success expected Sampling got {other:?}" + ))), + } + } + } +} diff --git a/consensus/types/src/data_column_sidecar.rs b/consensus/types/src/data_column_sidecar.rs index 6b1925f3c4c..564fd85bf72 100644 --- a/consensus/types/src/data_column_sidecar.rs +++ b/consensus/types/src/data_column_sidecar.rs @@ -11,6 +11,7 @@ use derivative::Derivative; use kzg::Kzg; use kzg::{Blob as KzgBlob, Error as KzgError}; use kzg::{KzgCommitment, KzgProof}; +use merkle_proof::MerkleTreeError; #[cfg(test)] use mockall_double::double; use safe_arith::ArithError; @@ -75,6 +76,12 @@ impl DataColumnSidecar { self.signed_block_header.message.tree_hash_root() } + /// Verifies the kzg commitment inclusion merkle proof. + pub fn verify_inclusion_proof(&self) -> Result { + // TODO(das): implement + Ok(true) + } + pub fn build_sidecars( blobs: &BlobSidecarList, block: &SignedBeaconBlock, From c85c205e00e8c84fb0080699bc33101de0349b18 Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Tue, 30 Apr 2024 16:18:07 +0900 Subject: [PATCH 09/76] Re-process early sampling requests (#5569) * Re-process early sampling requests # Conflicts: # beacon_node/beacon_processor/src/work_reprocessing_queue.rs # beacon_node/lighthouse_network/src/rpc/methods.rs # beacon_node/network/src/network_beacon_processor/rpc_methods.rs * Update beacon_node/beacon_processor/src/work_reprocessing_queue.rs Co-authored-by: Jimmy Chen * Add missing var * Beta compiler fixes and small typo fixes. * Remove duplicate method. --------- Co-authored-by: Jimmy Chen --- beacon_node/beacon_chain/src/beacon_chain.rs | 68 +++++++- .../beacon_chain/src/block_verification.rs | 6 +- beacon_node/beacon_chain/src/lib.rs | 1 + beacon_node/beacon_processor/src/lib.rs | 48 ++++-- beacon_node/beacon_processor/src/metrics.rs | 7 +- .../src/work_reprocessing_queue.rs | 102 ++++++++++++ .../lighthouse_network/src/rpc/methods.rs | 14 +- .../src/network_beacon_processor/mod.rs | 11 +- .../network_beacon_processor/rpc_methods.rs | 154 +++++++++++------- beacon_node/network/src/sync/sampling.rs | 4 +- 10 files changed, 332 insertions(+), 83 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 40bb103094f..093822dee69 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -122,7 +122,7 @@ use task_executor::{ShutdownReason, TaskExecutor}; use tokio_stream::Stream; use tree_hash::TreeHash; use types::blob_sidecar::FixedBlobSidecarList; -use types::data_column_sidecar::DataColumnSidecarList; +use types::data_column_sidecar::{ColumnIndex, DataColumnIdentifier, DataColumnSidecarList}; use types::payload::BlockProductionVersion; use types::*; @@ -541,6 +541,12 @@ pub struct BeaconBlockResponse> { pub consensus_block_value: u64, } +pub enum BlockImportStatus { + PendingImport(Arc>), + Imported, + Unknown, +} + impl FinalizationAndCanonicity { pub fn is_finalized(self) -> bool { self.slot_is_finalized && self.canonical @@ -1166,6 +1172,66 @@ impl BeaconChain { .map_or_else(|| self.get_data_columns(block_root), Ok) } + pub fn get_selected_data_columns_checking_all_caches( + &self, + block_root: Hash256, + indices: &[ColumnIndex], + ) -> Result>>, Error> { + let columns_from_availability_cache = indices + .iter() + .copied() + .filter_map(|index| { + self.data_availability_checker + .get_data_column(&DataColumnIdentifier { block_root, index }) + .transpose() + }) + .collect::, _>>()?; + // Existence of a column in the data availability cache and downstream caches is exclusive. + // If there's a single match in the availability cache we can safely skip other sources. + if !columns_from_availability_cache.is_empty() { + return Ok(columns_from_availability_cache); + } + + Ok(self + .early_attester_cache + .get_data_columns(block_root) + .map_or_else(|| self.get_data_columns(&block_root), Ok)? + .into_iter() + .filter(|dc| indices.contains(&dc.index)) + .collect()) + } + + /// Returns the import status of block checking (in order) pre-import caches, fork-choice, db store + pub fn get_block_import_status(&self, block_root: &Hash256) -> BlockImportStatus { + if let Some(block) = self + .reqresp_pre_import_cache + .read() + .get(block_root) + .map(|block| { + metrics::inc_counter(&metrics::BEACON_REQRESP_PRE_IMPORT_CACHE_HITS); + block.clone() + }) + { + return BlockImportStatus::PendingImport(block); + } + // Check fork-choice before early_attester_cache as the latter is pruned lazily + if self + .canonical_head + .fork_choice_read_lock() + .contains_block(block_root) + { + return BlockImportStatus::Imported; + } + if let Some(block) = self.early_attester_cache.get_block(*block_root) { + return BlockImportStatus::PendingImport(block); + } + if let Ok(true) = self.store.block_exists(block_root) { + BlockImportStatus::Imported + } else { + BlockImportStatus::Unknown + } + } + /// Returns the block at the given root, if any. /// /// ## Errors diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 8afaf1b1ecb..3a0b4daf8ea 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -98,7 +98,11 @@ use store::{Error as DBError, HotStateSummary, KeyValueStore, StoreOp}; use task_executor::JoinHandle; use tree_hash::TreeHash; use types::data_column_sidecar::DataColumnSidecarError; -use types::{BeaconBlockRef, BeaconState, BeaconStateError, BlobSidecarList, ChainSpec, DataColumnSidecar, DataColumnSubnetId, Epoch, EthSpec, ExecutionBlockHash, FullPayload, Hash256, InconsistentFork, PublicKey, PublicKeyBytes, RelativeEpoch, SignedBeaconBlock, SignedBeaconBlockHeader, Slot}; +use types::{ + BeaconBlockRef, BeaconState, BeaconStateError, BlobSidecarList, ChainSpec, DataColumnSidecar, + DataColumnSubnetId, Epoch, EthSpec, ExecutionBlockHash, FullPayload, Hash256, InconsistentFork, + PublicKey, PublicKeyBytes, RelativeEpoch, SignedBeaconBlock, SignedBeaconBlockHeader, Slot, +}; use types::{BlobSidecar, ExecPayload}; pub const POS_PANDA_BANNER: &str = r#" diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index e4c96e5e8b5..2fcde392fdc 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -73,6 +73,7 @@ pub use self::chain_config::ChainConfig; pub use self::errors::{BeaconChainError, BlockProductionError}; pub use self::historical_blocks::HistoricalBlockError; pub use attestation_verification::Error as AttestationError; +pub use beacon_chain::BlockImportStatus; pub use beacon_fork_choice_store::{BeaconForkChoiceStore, Error as ForkChoiceStoreError}; pub use block_verification::{ get_block_root, BlockError, ExecutionPayloadError, ExecutionPendingBlock, GossipVerifiedBlock, diff --git a/beacon_node/beacon_processor/src/lib.rs b/beacon_node/beacon_processor/src/lib.rs index 118a043addc..3eebf5718a1 100644 --- a/beacon_node/beacon_processor/src/lib.rs +++ b/beacon_node/beacon_processor/src/lib.rs @@ -62,11 +62,11 @@ use tokio::sync::mpsc; use tokio::sync::mpsc::error::TrySendError; use types::{Attestation, Hash256, SignedAggregateAndProof, SubnetId}; use types::{EthSpec, Slot}; -use work_reprocessing_queue::IgnoredRpcBlock; use work_reprocessing_queue::{ spawn_reprocess_scheduler, QueuedAggregate, QueuedLightClientUpdate, QueuedRpcBlock, QueuedUnaggregate, ReadyWork, }; +use work_reprocessing_queue::{IgnoredRpcBlock, QueuedSamplingRequest}; mod metrics; pub mod work_reprocessing_queue; @@ -141,6 +141,10 @@ const MAX_GOSSIP_OPTIMISTIC_UPDATE_QUEUE_LEN: usize = 1_024; /// for reprocessing before we start dropping them. const MAX_GOSSIP_OPTIMISTIC_UPDATE_REPROCESS_QUEUE_LEN: usize = 128; +/// The maximum number of queued retries of ReqResp sampling requests from the reprocess queue that +/// will be stored before dropping them. +const MAX_UNKNOWN_BLOCK_SAMPLING_REQUEST_QUEUE_LEN: usize = 16_384; + /// The maximum number of queued `SyncCommitteeMessage` objects that will be stored before we start dropping /// them. const MAX_SYNC_MESSAGE_QUEUE_LEN: usize = 2048; @@ -272,6 +276,7 @@ pub const LIGHT_CLIENT_OPTIMISTIC_UPDATE_REQUEST: &str = "light_client_optimisti pub const UNKNOWN_BLOCK_ATTESTATION: &str = "unknown_block_attestation"; pub const UNKNOWN_BLOCK_AGGREGATE: &str = "unknown_block_aggregate"; pub const UNKNOWN_LIGHT_CLIENT_UPDATE: &str = "unknown_light_client_update"; +pub const UNKNOWN_BLOCK_SAMPLING_REQUEST: &str = "unknown_block_sampling_request"; pub const GOSSIP_BLS_TO_EXECUTION_CHANGE: &str = "gossip_bls_to_execution_change"; pub const API_REQUEST_P0: &str = "api_request_p0"; pub const API_REQUEST_P1: &str = "api_request_p1"; @@ -527,6 +532,10 @@ impl From for WorkEvent { process_fn, }, }, + ReadyWork::SamplingRequest(QueuedSamplingRequest { process_fn, .. }) => Self { + drop_during_sync: true, + work: Work::UnknownBlockSamplingRequest { process_fn }, + }, ReadyWork::BackfillSync(QueuedBackfillBatch(process_fn)) => Self { drop_during_sync: false, work: Work::ChainSegmentBackfill(process_fn), @@ -610,6 +619,9 @@ pub enum Work { parent_root: Hash256, process_fn: BlockingFn, }, + UnknownBlockSamplingRequest { + process_fn: BlockingFn, + }, GossipAggregateBatch { aggregates: Vec>, process_batch: Box>) + Send + Sync>, @@ -699,8 +711,9 @@ impl Work { Work::LightClientFinalityUpdateRequest(_) => LIGHT_CLIENT_FINALITY_UPDATE_REQUEST, Work::UnknownBlockAttestation { .. } => UNKNOWN_BLOCK_ATTESTATION, Work::UnknownBlockAggregate { .. } => UNKNOWN_BLOCK_AGGREGATE, - Work::GossipBlsToExecutionChange(_) => GOSSIP_BLS_TO_EXECUTION_CHANGE, Work::UnknownLightClientOptimisticUpdate { .. } => UNKNOWN_LIGHT_CLIENT_UPDATE, + Work::UnknownBlockSamplingRequest { .. } => UNKNOWN_BLOCK_SAMPLING_REQUEST, + Work::GossipBlsToExecutionChange(_) => GOSSIP_BLS_TO_EXECUTION_CHANGE, Work::ApiRequestP0 { .. } => API_REQUEST_P0, Work::ApiRequestP1 { .. } => API_REQUEST_P1, } @@ -839,6 +852,8 @@ impl BeaconProcessor { let mut optimistic_update_queue = FifoQueue::new(MAX_GOSSIP_OPTIMISTIC_UPDATE_QUEUE_LEN); let mut unknown_light_client_update_queue = FifoQueue::new(MAX_GOSSIP_OPTIMISTIC_UPDATE_REPROCESS_QUEUE_LEN); + let mut unknown_block_sampling_request_queue = + FifoQueue::new(MAX_UNKNOWN_BLOCK_SAMPLING_REQUEST_QUEUE_LEN); // Using a FIFO queue since blocks need to be imported sequentially. let mut rpc_block_queue = FifoQueue::new(MAX_RPC_BLOCK_QUEUE_LEN); @@ -1158,16 +1173,22 @@ impl BeaconProcessor { // and BlocksByRoot) } else if let Some(item) = status_queue.pop() { self.spawn_worker(item, idle_tx); - } else if let Some(item) = bbrange_queue.pop() { - self.spawn_worker(item, idle_tx); + // Prioritize by_root requests over by_range as the former are time + // sensitive for recovery } else if let Some(item) = bbroots_queue.pop() { self.spawn_worker(item, idle_tx); - } else if let Some(item) = blbrange_queue.pop() { - self.spawn_worker(item, idle_tx); } else if let Some(item) = blbroots_queue.pop() { self.spawn_worker(item, idle_tx); } else if let Some(item) = dcbroots_queue.pop() { self.spawn_worker(item, idle_tx); + // Prioritize sampling requests after block syncing requests + } else if let Some(item) = unknown_block_sampling_request_queue.pop() { + self.spawn_worker(item, idle_tx); + // by_range sync after sampling + } else if let Some(item) = bbrange_queue.pop() { + self.spawn_worker(item, idle_tx); + } else if let Some(item) = blbrange_queue.pop() { + self.spawn_worker(item, idle_tx); // Check slashings after all other consensus messages so we prioritize // following head. // @@ -1344,6 +1365,9 @@ impl BeaconProcessor { Work::UnknownLightClientOptimisticUpdate { .. } => { unknown_light_client_update_queue.push(work, work_id, &self.log) } + Work::UnknownBlockSamplingRequest { .. } => { + unknown_block_sampling_request_queue.push(work, work_id, &self.log) + } Work::ApiRequestP0 { .. } => { api_request_p0_queue.push(work, work_id, &self.log) } @@ -1530,12 +1554,12 @@ impl BeaconProcessor { Work::ChainSegment(process_fn) => task_spawner.spawn_async(async move { process_fn.await; }), - Work::UnknownBlockAttestation { process_fn } => task_spawner.spawn_blocking(process_fn), - Work::UnknownBlockAggregate { process_fn } => task_spawner.spawn_blocking(process_fn), - Work::UnknownLightClientOptimisticUpdate { - parent_root: _, - process_fn, - } => task_spawner.spawn_blocking(process_fn), + Work::UnknownBlockAttestation { process_fn } + | Work::UnknownBlockAggregate { process_fn } + | Work::UnknownLightClientOptimisticUpdate { process_fn, .. } + | Work::UnknownBlockSamplingRequest { process_fn } => { + task_spawner.spawn_blocking(process_fn) + } Work::DelayedImportBlock { beacon_block_slot: _, beacon_block_root: _, diff --git a/beacon_node/beacon_processor/src/metrics.rs b/beacon_node/beacon_processor/src/metrics.rs index 503d29dd699..f4c9d61e7d9 100644 --- a/beacon_node/beacon_processor/src/metrics.rs +++ b/beacon_node/beacon_processor/src/metrics.rs @@ -162,7 +162,12 @@ lazy_static::lazy_static! { ); pub static ref BEACON_PROCESSOR_REPROCESSING_QUEUE_MATCHED_OPTIMISTIC_UPDATES: Result = try_create_int_counter( "beacon_processor_reprocessing_queue_matched_optimistic_updates", - "Number of queued light client optimistic updates where as matching block has been imported." + "Number of queued light client optimistic updates where a matching block has been imported." + ); + // TODO: This should be labeled instead of N single metrics + pub static ref BEACON_PROCESSOR_REPROCESSING_QUEUE_MATCHED_SAMPLING_REQUESTS: Result = try_create_int_counter( + "beacon_processor_reprocessing_queue_matches_sampling_requests", + "Number of queued sampling requests where a matching block has been imported." ); /// Errors and Debugging Stats diff --git a/beacon_node/beacon_processor/src/work_reprocessing_queue.rs b/beacon_node/beacon_processor/src/work_reprocessing_queue.rs index 496fa683d2c..b7f6249714d 100644 --- a/beacon_node/beacon_processor/src/work_reprocessing_queue.rs +++ b/beacon_node/beacon_processor/src/work_reprocessing_queue.rs @@ -51,6 +51,9 @@ pub const QUEUED_LIGHT_CLIENT_UPDATE_DELAY: Duration = Duration::from_secs(12); /// For how long to queue rpc blocks before sending them back for reprocessing. pub const QUEUED_RPC_BLOCK_DELAY: Duration = Duration::from_secs(4); +/// For how long to queue sampling requests for reprocessing. +pub const QUEUED_SAMPLING_REQUESTS_DELAY: Duration = Duration::from_secs(12); + /// Set an arbitrary upper-bound on the number of queued blocks to avoid DoS attacks. The fact that /// we signature-verify blocks before putting them in the queue *should* protect against this, but /// it's nice to have extra protection. @@ -62,6 +65,10 @@ const MAXIMUM_QUEUED_ATTESTATIONS: usize = 16_384; /// How many light client updates we keep before new ones get dropped. const MAXIMUM_QUEUED_LIGHT_CLIENT_UPDATES: usize = 128; +/// How many sampling requests we queue before new ones get dropped. +/// TODO(das): choose a sensible value +const MAXIMUM_QUEUED_SAMPLING_REQUESTS: usize = 16_384; + // Process backfill batch 50%, 60%, 80% through each slot. // // Note: use caution to set these fractions in a way that won't cause panic-y @@ -98,6 +105,8 @@ pub enum ReprocessQueueMessage { UnknownBlockAggregate(QueuedAggregate), /// A light client optimistic update that references a parent root that has not been seen as a parent. UnknownLightClientOptimisticUpdate(QueuedLightClientUpdate), + /// A sampling request that references an unknown block. + UnknownBlockSamplingRequest(QueuedSamplingRequest), /// A new backfill batch that needs to be scheduled for processing. BackfillSync(QueuedBackfillBatch), } @@ -110,6 +119,7 @@ pub enum ReadyWork { Unaggregate(QueuedUnaggregate), Aggregate(QueuedAggregate), LightClientUpdate(QueuedLightClientUpdate), + SamplingRequest(QueuedSamplingRequest), BackfillSync(QueuedBackfillBatch), } @@ -134,6 +144,12 @@ pub struct QueuedLightClientUpdate { pub process_fn: BlockingFn, } +/// A sampling request for which the corresponding block is not known while processing. +pub struct QueuedSamplingRequest { + pub beacon_block_root: Hash256, + pub process_fn: BlockingFn, +} + /// A block that arrived early and has been queued for later import. pub struct QueuedGossipBlock { pub beacon_block_slot: Slot, @@ -218,6 +234,8 @@ struct ReprocessQueue { attestations_delay_queue: DelayQueue, /// Queue to manage scheduled light client updates. lc_updates_delay_queue: DelayQueue, + /// Queue to manage scheduled sampling requests + sampling_requests_delay_queue: DelayQueue, /* Queued items */ /// Queued blocks. @@ -232,6 +250,10 @@ struct ReprocessQueue { queued_lc_updates: FnvHashMap, /// Light Client Updates per parent_root. awaiting_lc_updates_per_parent_root: HashMap>, + /// Queued sampling requests. + queued_sampling_requests: FnvHashMap, + /// Sampling requests per block root. + awaiting_sampling_requests_per_block_root: HashMap>, /// Queued backfill batches queued_backfill_batches: Vec, @@ -239,15 +261,18 @@ struct ReprocessQueue { /// Next attestation id, used for both aggregated and unaggregated attestations next_attestation: usize, next_lc_update: usize, + next_sampling_request_update: usize, early_block_debounce: TimeLatch, rpc_block_debounce: TimeLatch, attestation_delay_debounce: TimeLatch, lc_update_delay_debounce: TimeLatch, + sampling_request_delay_debounce: TimeLatch, next_backfill_batch_event: Option>>, slot_clock: Arc, } pub type QueuedLightClientUpdateId = usize; +pub type QueuedSamplingRequestId = usize; #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum QueuedAttestationId { @@ -403,19 +428,24 @@ impl ReprocessQueue { rpc_block_delay_queue: DelayQueue::new(), attestations_delay_queue: DelayQueue::new(), lc_updates_delay_queue: DelayQueue::new(), + sampling_requests_delay_queue: <_>::default(), queued_gossip_block_roots: HashSet::new(), queued_lc_updates: FnvHashMap::default(), queued_aggregates: FnvHashMap::default(), queued_unaggregates: FnvHashMap::default(), + queued_sampling_requests: <_>::default(), awaiting_attestations_per_root: HashMap::new(), awaiting_lc_updates_per_parent_root: HashMap::new(), + awaiting_sampling_requests_per_block_root: <_>::default(), queued_backfill_batches: Vec::new(), next_attestation: 0, next_lc_update: 0, + next_sampling_request_update: 0, early_block_debounce: TimeLatch::default(), rpc_block_debounce: TimeLatch::default(), attestation_delay_debounce: TimeLatch::default(), lc_update_delay_debounce: TimeLatch::default(), + sampling_request_delay_debounce: <_>::default(), next_backfill_batch_event: None, slot_clock, } @@ -639,6 +669,35 @@ impl ReprocessQueue { self.next_lc_update += 1; } + InboundEvent::Msg(UnknownBlockSamplingRequest(queued_sampling_request)) => { + if self.sampling_requests_delay_queue.len() >= MAXIMUM_QUEUED_SAMPLING_REQUESTS { + if self.sampling_request_delay_debounce.elapsed() { + error!( + log, + "Sampling requests delay queue is full"; + "queue_size" => MAXIMUM_QUEUED_SAMPLING_REQUESTS, + ); + } + // Drop the inbound message. + return; + } + + let id: QueuedSamplingRequestId = self.next_sampling_request_update; + self.next_sampling_request_update += 1; + + // Register the delay. + let delay_key = self + .sampling_requests_delay_queue + .insert(id, QUEUED_SAMPLING_REQUESTS_DELAY); + + self.awaiting_sampling_requests_per_block_root + .entry(queued_sampling_request.beacon_block_root) + .or_default() + .push(id); + + self.queued_sampling_requests + .insert(id, (queued_sampling_request, delay_key)); + } InboundEvent::Msg(BlockImported { block_root, parent_root, @@ -700,6 +759,49 @@ impl ReprocessQueue { ); } } + // Unqueue the sampling requests we have for this root, if any. + if let Some(queued_ids) = self + .awaiting_sampling_requests_per_block_root + .remove(&block_root) + { + let mut sent_count = 0; + let mut failed_to_send_count = 0; + + for id in queued_ids { + metrics::inc_counter( + &metrics::BEACON_PROCESSOR_REPROCESSING_QUEUE_MATCHED_SAMPLING_REQUESTS, + ); + + if let Some((queued, delay_key)) = self.queued_sampling_requests.remove(&id) + { + // Remove the delay. + self.sampling_requests_delay_queue.remove(&delay_key); + + // Send the work. + let work = ReadyWork::SamplingRequest(queued); + + if self.ready_work_tx.try_send(work).is_err() { + failed_to_send_count += 1; + } else { + sent_count += 1; + } + } else { + // This should never happen. + error!(log, "Unknown sampling request for block root"; "block_root" => ?block_root, "id" => ?id); + } + } + + if failed_to_send_count > 0 { + error!( + log, + "Ignored scheduled sampling requests for block"; + "hint" => "system may be overloaded", + "block_root" => ?block_root, + "failed_count" => failed_to_send_count, + "sent_count" => sent_count, + ); + } + } } InboundEvent::Msg(NewLightClientOptimisticUpdate { parent_root }) => { // Unqueue the light client optimistic updates we have for this root, if any. diff --git a/beacon_node/lighthouse_network/src/rpc/methods.rs b/beacon_node/lighthouse_network/src/rpc/methods.rs index 831e3efc117..18bf4e7dcfc 100644 --- a/beacon_node/lighthouse_network/src/rpc/methods.rs +++ b/beacon_node/lighthouse_network/src/rpc/methods.rs @@ -6,6 +6,7 @@ use serde::Serialize; use ssz::Encode; use ssz_derive::{Decode, Encode}; use ssz_types::{typenum::U256, VariableList}; +use std::collections::BTreeMap; use std::fmt::Display; use std::marker::PhantomData; use std::ops::Deref; @@ -13,7 +14,7 @@ use std::sync::Arc; use strum::IntoStaticStr; use superstruct::superstruct; use types::blob_sidecar::BlobIdentifier; -use types::data_column_sidecar::DataColumnIdentifier; +use types::data_column_sidecar::{ColumnIndex, DataColumnIdentifier}; use types::{ blob_sidecar::BlobSidecar, ChainSpec, DataColumnSidecar, Epoch, EthSpec, Hash256, LightClientBootstrap, LightClientFinalityUpdate, LightClientOptimisticUpdate, @@ -386,6 +387,17 @@ impl DataColumnsByRootRequest { ); Self { data_column_ids } } + + pub fn group_by_ordered_block_root(&self) -> Vec<(Hash256, Vec)> { + let mut column_indexes_by_block = BTreeMap::>::new(); + for request_id in self.data_column_ids.as_slice() { + column_indexes_by_block + .entry(request_id.block_root) + .or_default() + .push(request_id.index); + } + column_indexes_by_block.into_iter().collect() + } } /* RPC Handling and Grouping */ diff --git a/beacon_node/network/src/network_beacon_processor/mod.rs b/beacon_node/network/src/network_beacon_processor/mod.rs index e9ee06f045f..18965fcd6ef 100644 --- a/beacon_node/network/src/network_beacon_processor/mod.rs +++ b/beacon_node/network/src/network_beacon_processor/mod.rs @@ -650,8 +650,15 @@ impl NetworkBeaconProcessor { request: DataColumnsByRootRequest, ) -> Result<(), Error> { let processor = self.clone(); - let process_fn = - move || processor.handle_data_columns_by_root_request(peer_id, request_id, request); + let reprocess_tx = processor.reprocess_tx.clone(); + let process_fn = move || { + processor.handle_data_columns_by_root_request( + peer_id, + request_id, + request, + Some(reprocess_tx), + ) + }; self.try_send(BeaconWorkEvent { drop_during_sync: false, 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 51baa541465..065a6797e29 100644 --- a/beacon_node/network/src/network_beacon_processor/rpc_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/rpc_methods.rs @@ -2,7 +2,10 @@ use crate::network_beacon_processor::{NetworkBeaconProcessor, FUTURE_SLOT_TOLERA use crate::service::NetworkMessage; use crate::status::ToStatusMessage; use crate::sync::SyncMessage; -use beacon_chain::{BeaconChainError, BeaconChainTypes, HistoricalBlockError, WhenSlotSkipped}; +use beacon_chain::{ + BeaconChainError, BeaconChainTypes, BlockImportStatus, HistoricalBlockError, WhenSlotSkipped, +}; +use beacon_processor::work_reprocessing_queue::{QueuedSamplingRequest, ReprocessQueueMessage}; use itertools::process_results; use lighthouse_network::rpc::methods::{ BlobsByRangeRequest, BlobsByRootRequest, DataColumnsByRootRequest, @@ -13,9 +16,9 @@ use slog::{debug, error, warn}; use slot_clock::SlotClock; use std::collections::{hash_map::Entry, HashMap}; use std::sync::Arc; +use tokio::sync::mpsc; use tokio_stream::StreamExt; use types::blob_sidecar::BlobIdentifier; -use types::data_column_sidecar::DataColumnIdentifier; use types::{Epoch, EthSpec, ForkName, Hash256, Slot}; impl NetworkBeaconProcessor { @@ -323,81 +326,106 @@ impl NetworkBeaconProcessor { peer_id: PeerId, request_id: PeerRequestId, request: DataColumnsByRootRequest, + reprocess_tx: Option>, ) { - let Some(requested_root) = request - .data_column_ids - .as_slice() - .first() - .map(|id| id.block_root) - else { - // No data column ids requested. - return; - }; - let requested_indices = request - .data_column_ids - .as_slice() - .iter() - .map(|id| id.index) - .collect::>(); + let column_indexes_by_block = request.group_by_ordered_block_root(); let mut send_data_column_count = 0; - let mut data_column_list_results = HashMap::new(); - for id in request.data_column_ids.as_slice() { - // Attempt to get the data columns from the RPC cache. - if let Ok(Some(data_column)) = self.chain.data_availability_checker.get_data_column(id) + for (block_root, column_ids) in column_indexes_by_block.iter() { + match self + .chain + .get_selected_data_columns_checking_all_caches(*block_root, column_ids) { - self.send_response( - peer_id, - Response::DataColumnsByRoot(Some(data_column)), - request_id, - ); - send_data_column_count += 1; - } else { - let DataColumnIdentifier { - block_root: root, - index, - } = id; - - let data_column_list_result = match data_column_list_results.entry(root) { - Entry::Vacant(entry) => entry.insert( - self.chain - .get_data_columns_checking_early_attester_cache(root), - ), - Entry::Occupied(entry) => entry.into_mut(), - }; + Ok(data_columns) => { + for data_column in data_columns { + send_data_column_count += 1; + self.send_response( + peer_id, + Response::DataColumnsByRoot(Some(data_column)), + request_id, + ); + } + } + Err(e) => { + self.send_error_response( + peer_id, + RPCResponseErrorCode::ServerError, + // TODO(das): leak error details to ease debugging + format!("{:?}", e).to_string(), + request_id, + ); + error!(self.log, "Error getting data column"; + "block_root" => ?block_root, + "peer" => %peer_id, + "error" => ?e + ); + return; + } + } + } - match data_column_list_result.as_ref() { - Ok(data_columns_sidecar_list) => { - 'inner: for data_column_sidecar in data_columns_sidecar_list.iter() { - if data_column_sidecar.index == *index { - self.send_response( - peer_id, - Response::DataColumnsByRoot(Some(data_column_sidecar.clone())), - request_id, - ); - send_data_column_count += 1; - break 'inner; - } + let should_reprocess = column_indexes_by_block + .first() + .filter(|_| column_indexes_by_block.len() == 1 && send_data_column_count == 0) + .and_then( + |(block_root, _)| match self.chain.get_block_import_status(block_root) { + BlockImportStatus::PendingImport(block) => { + if block.num_expected_blobs() > 0 { + // Known block not yet imported (still have not received columns) but we + // are certain the block has data. Schedule this request for retry once + // the block is imported + // TODO(das): consider only re-processing for some range of slots + Some(*block_root) + } else { + // We know the block has no data, do not retry + None } } - Err(e) => { - debug!( - self.log, - "Error fetching data column for peer"; - "peer" => %peer_id, - "request_root" => ?root, - "error" => ?e, - ); + BlockImportStatus::Imported => { + // If the block is imported, custody columns should also be imported and be + // returned in the attempts above to retrieve columns. Do not retry + None } - } + BlockImportStatus::Unknown => { + // Two options: + // (a) race condition where peer receives a block before than us and has + // started a sampling request + // (b) peer sent a sampling request for a random root we will never receive + // to fill our reprocessing queue + // TODO(das): high tolerance penalty for requests that never resolve + Some(*block_root) + } + }, + ); + + if let (Some(beacon_block_root), Some(reprocess_tx)) = (should_reprocess, reprocess_tx) { + let processor = self.clone(); + if reprocess_tx + .try_send(ReprocessQueueMessage::UnknownBlockSamplingRequest( + QueuedSamplingRequest { + beacon_block_root, + process_fn: Box::new(move || { + processor.handle_data_columns_by_root_request( + peer_id, request_id, request, + None, // Only allow a single retry + ) + }), + }, + )) + .is_err() + { + error!( + self.log, + "Failed to send UnknownBlockSamplingRequest for re-processing" + ) } } + debug!( self.log, "Received DataColumnsByRoot Request"; "peer" => %peer_id, - "request_root" => %requested_root, - "request_indices" => ?requested_indices, + "request" => ?column_indexes_by_block, "returned" => send_data_column_count ); diff --git a/beacon_node/network/src/sync/sampling.rs b/beacon_node/network/src/sync/sampling.rs index 50fb73f9bc6..9ee8616daac 100644 --- a/beacon_node/network/src/sync/sampling.rs +++ b/beacon_node/network/src/sync/sampling.rs @@ -174,10 +174,10 @@ pub struct ActiveSamplingRequest { #[derive(Debug)] pub enum SamplingError { - SendFailed(&'static str), + SendFailed(#[allow(dead_code)] &'static str), ProcessorUnavailable, TooManyFailures, - BadState(String), + BadState(#[allow(dead_code)] String), ColumnIndexOutOfBounds, } From d5f3562fccb71b73ef58fa38c2f3665df232827b Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Thu, 2 May 2024 15:07:17 +1000 Subject: [PATCH 10/76] Fix merge conflict. --- .github/workflows/test-suite.yml | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index d48fd7b1f02..8741df4ba12 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -273,27 +273,17 @@ jobs: - name: Install lighthouse run: | make -<<<<<<< HEAD - name: Install lcli - # TODO: uncomment after the version of lcli in https://github.com/sigp/lighthouse/pull/5137 - # is installed on the runners - # if: env.SELF_HOSTED_RUNNERS == 'false' + if: env.SELF_HOSTED_RUNNERS == 'false' run: make install-lcli - name: Run the doppelganger protection failure test script run: | -======= - - name: Install lcli - if: env.SELF_HOSTED_RUNNERS == 'false' - run: make install-lcli - - name: Run the doppelganger protection failure test script - run: | ->>>>>>> sigp/unstable cd scripts/tests ./doppelganger_protection.sh failure genesis.json - - name: Run the doppelganger protection success test script - run: | - cd scripts/tests - ./doppelganger_protection.sh success genesis.json + - name: Run the doppelganger protection success test script + run: | + cd scripts/tests + ./doppelganger_protection.sh success genesis.json execution-engine-integration-ubuntu: name: execution-engine-integration-ubuntu runs-on: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "linux", "CI", "small"]') || 'ubuntu-latest' }} From 0644709830fc252f65c31c94856ad70ccebf9488 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Thu, 2 May 2024 17:32:40 +1000 Subject: [PATCH 11/76] Add data columns by root to currently supported protocol list (#5678) * Add data columns by root to currently supported protocol list. * Add missing data column by roots handling. --- .../lighthouse_network/src/rpc/protocol.rs | 2 ++ beacon_node/network/src/router.rs | 32 ++++++++++++++++--- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/beacon_node/lighthouse_network/src/rpc/protocol.rs b/beacon_node/lighthouse_network/src/rpc/protocol.rs index 6df0b4702f9..946db6459e7 100644 --- a/beacon_node/lighthouse_network/src/rpc/protocol.rs +++ b/beacon_node/lighthouse_network/src/rpc/protocol.rs @@ -373,6 +373,8 @@ impl SupportedProtocol { supported.extend_from_slice(&[ ProtocolId::new(SupportedProtocol::BlobsByRootV1, Encoding::SSZSnappy), ProtocolId::new(SupportedProtocol::BlobsByRangeV1, Encoding::SSZSnappy), + // TODO(das): add to PeerDAS fork + ProtocolId::new(SupportedProtocol::DataColumnsByRootV1, Encoding::SSZSnappy), ]); } supported diff --git a/beacon_node/network/src/router.rs b/beacon_node/network/src/router.rs index caa9c38af3a..170ea595949 100644 --- a/beacon_node/network/src/router.rs +++ b/beacon_node/network/src/router.rs @@ -638,11 +638,35 @@ impl Router { /// Handle a `DataColumnsByRoot` response from the peer. pub fn on_data_columns_by_root_response( &mut self, - _peer_id: PeerId, - _request_id: RequestId, - _data_column_sidecar: Option>>, + peer_id: PeerId, + request_id: RequestId, + data_column: Option>>, ) { - // TODO(das) implement `DataColumnsByRoot` response handling + let request_id = match request_id { + RequestId::Sync(sync_id) => match sync_id { + id @ SyncId::DataColumnsByRoot { .. } => id, + other => { + crit!(self.log, "DataColumnsByRoot response on incorrect request"; "request" => ?other); + return; + } + }, + RequestId::Router => { + crit!(self.log, "All DataColumnsByRoot requests belong to sync"; "peer_id" => %peer_id); + return; + } + }; + + trace!( + self.log, + "Received DataColumnsByRoot Response"; + "peer" => %peer_id, + ); + self.send_to_sync(SyncMessage::RpcDataColumn { + request_id, + peer_id, + data_column, + seen_timestamp: timestamp_now(), + }); } fn handle_beacon_processor_send_result( From bc190e79c1cd39b40805a05983c984225d3f807b Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Wed, 8 May 2024 11:17:21 +1000 Subject: [PATCH 12/76] Fix simulator tests on `das` branch (#5731) * Bump genesis delay in sim tests as KZG setup takes longer for DAS. * Fix incorrect YAML spacing. --- .github/workflows/test-suite.yml | 368 +++++++++++++------------- testing/simulator/src/basic_sim.rs | 2 +- testing/simulator/src/fallback_sim.rs | 2 +- 3 files changed, 186 insertions(+), 186 deletions(-) diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index 8741df4ba12..a5e9263d60f 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -34,33 +34,33 @@ jobs: runs-on: ubuntu-latest if: github.event_name == 'pull_request' || github.event_name == 'merge_group' steps: - - name: Check that the pull request is not targeting the stable branch - run: test ${{ github.base_ref }} != "stable" + - name: Check that the pull request is not targeting the stable branch + run: test ${{ github.base_ref }} != "stable" release-tests-ubuntu: name: release-tests-ubuntu # Use self-hosted runners only on the sigp repo. runs-on: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "linux", "CI", "large"]') || 'ubuntu-latest' }} steps: - - uses: actions/checkout@v4 - - name: Get latest version of stable Rust - if: env.SELF_HOSTED_RUNNERS == 'false' - uses: moonrepo/setup-rust@v1 - with: + - uses: actions/checkout@v4 + - name: Get latest version of stable Rust + if: env.SELF_HOSTED_RUNNERS == 'false' + uses: moonrepo/setup-rust@v1 + with: channel: stable cache-target: release bins: cargo-nextest - env: + env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Install Foundry (anvil) - if: env.SELF_HOSTED_RUNNERS == 'false' - uses: foundry-rs/foundry-toolchain@v1 - with: - version: nightly-ca67d15f4abd46394b324c50e21e66f306a1162d - - name: Run tests in release - run: make nextest-release - - name: Show cache stats - if: env.SELF_HOSTED_RUNNERS == 'true' - run: sccache --show-stats + - name: Install Foundry (anvil) + if: env.SELF_HOSTED_RUNNERS == 'false' + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly-ca67d15f4abd46394b324c50e21e66f306a1162d + - name: Run tests in release + run: make nextest-release + - name: Show cache stats + if: env.SELF_HOSTED_RUNNERS == 'true' + run: sccache --show-stats # FIXME(das): disabled for now as the c-kzg-4844 `das` branch doesn't build on windows. # release-tests-windows: # name: release-tests-windows @@ -101,155 +101,155 @@ jobs: # Use self-hosted runners only on the sigp repo. runs-on: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "linux", "CI", "large"]') || 'ubuntu-latest' }} env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@v4 - - name: Get latest version of stable Rust - if: env.SELF_HOSTED_RUNNERS == 'false' - uses: moonrepo/setup-rust@v1 - with: + - uses: actions/checkout@v4 + - name: Get latest version of stable Rust + if: env.SELF_HOSTED_RUNNERS == 'false' + uses: moonrepo/setup-rust@v1 + with: channel: stable cache-target: release bins: cargo-nextest - - name: Run beacon_chain tests for all known forks - run: make test-beacon-chain - - name: Show cache stats - if: env.SELF_HOSTED_RUNNERS == 'true' - run: sccache --show-stats + - name: Run beacon_chain tests for all known forks + run: make test-beacon-chain + - name: Show cache stats + if: env.SELF_HOSTED_RUNNERS == 'true' + run: sccache --show-stats op-pool-tests: name: op-pool-tests runs-on: ubuntu-latest env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@v4 - - name: Get latest version of stable Rust - uses: moonrepo/setup-rust@v1 - with: + - uses: actions/checkout@v4 + - name: Get latest version of stable Rust + uses: moonrepo/setup-rust@v1 + with: channel: stable cache-target: release bins: cargo-nextest - - name: Run operation_pool tests for all known forks - run: make test-op-pool + - name: Run operation_pool tests for all known forks + run: make test-op-pool network-tests: name: network-tests runs-on: ubuntu-latest env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@v4 - - name: Get latest version of stable Rust - uses: moonrepo/setup-rust@v1 - with: - channel: stable - cache-target: release - bins: cargo-nextest - - name: Run network tests for all known forks - run: make test-network + - uses: actions/checkout@v4 + - name: Get latest version of stable Rust + uses: moonrepo/setup-rust@v1 + with: + channel: stable + cache-target: release + bins: cargo-nextest + - name: Run network tests for all known forks + run: make test-network slasher-tests: name: slasher-tests runs-on: ubuntu-latest env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@v4 - - name: Get latest version of stable Rust - uses: moonrepo/setup-rust@v1 - with: + - uses: actions/checkout@v4 + - name: Get latest version of stable Rust + uses: moonrepo/setup-rust@v1 + with: channel: stable cache-target: release bins: cargo-nextest - - name: Run slasher tests for all supported backends - run: make test-slasher + - name: Run slasher tests for all supported backends + run: make test-slasher debug-tests-ubuntu: name: debug-tests-ubuntu # Use self-hosted runners only on the sigp repo. runs-on: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "linux", "CI", "large"]') || 'ubuntu-latest' }} env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@v4 - - name: Get latest version of stable Rust - if: env.SELF_HOSTED_RUNNERS == 'false' - uses: moonrepo/setup-rust@v1 - with: + - uses: actions/checkout@v4 + - name: Get latest version of stable Rust + if: env.SELF_HOSTED_RUNNERS == 'false' + uses: moonrepo/setup-rust@v1 + with: channel: stable bins: cargo-nextest - - name: Install Foundry (anvil) - if: env.SELF_HOSTED_RUNNERS == 'false' - uses: foundry-rs/foundry-toolchain@v1 - with: - version: nightly-ca67d15f4abd46394b324c50e21e66f306a1162d - - name: Run tests in debug - run: make nextest-debug - - name: Show cache stats - if: env.SELF_HOSTED_RUNNERS == 'true' - run: sccache --show-stats + - name: Install Foundry (anvil) + if: env.SELF_HOSTED_RUNNERS == 'false' + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly-ca67d15f4abd46394b324c50e21e66f306a1162d + - name: Run tests in debug + run: make nextest-debug + - name: Show cache stats + if: env.SELF_HOSTED_RUNNERS == 'true' + run: sccache --show-stats state-transition-vectors-ubuntu: name: state-transition-vectors-ubuntu runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - name: Get latest version of stable Rust - uses: moonrepo/setup-rust@v1 - with: + - uses: actions/checkout@v4 + - name: Get latest version of stable Rust + uses: moonrepo/setup-rust@v1 + with: channel: stable cache-target: release - - name: Run state_transition_vectors in release. - run: make run-state-transition-tests + - name: Run state_transition_vectors in release. + run: make run-state-transition-tests ef-tests-ubuntu: name: ef-tests-ubuntu # Use self-hosted runners only on the sigp repo. runs-on: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "linux", "CI", "small"]') || 'ubuntu-latest' }} env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@v4 - - name: Get latest version of stable Rust - if: env.SELF_HOSTED_RUNNERS == 'false' - uses: moonrepo/setup-rust@v1 - with: + - uses: actions/checkout@v4 + - name: Get latest version of stable Rust + if: env.SELF_HOSTED_RUNNERS == 'false' + uses: moonrepo/setup-rust@v1 + with: channel: stable cache-target: release bins: cargo-nextest - - name: Run consensus-spec-tests with blst and fake_crypto - run: make nextest-ef - - name: Show cache stats - if: env.SELF_HOSTED_RUNNERS == 'true' - run: sccache --show-stats + - name: Run consensus-spec-tests with blst and fake_crypto + run: make nextest-ef + - name: Show cache stats + if: env.SELF_HOSTED_RUNNERS == 'true' + run: sccache --show-stats dockerfile-ubuntu: name: dockerfile-ubuntu runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - name: Build the root Dockerfile - run: docker build --build-arg FEATURES=portable -t lighthouse:local . - - name: Test the built image - run: docker run -t lighthouse:local lighthouse --version + - uses: actions/checkout@v4 + - name: Build the root Dockerfile + run: docker build --build-arg FEATURES=portable -t lighthouse:local . + - name: Test the built image + run: docker run -t lighthouse:local lighthouse --version basic-simulator-ubuntu: name: basic-simulator-ubuntu runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - name: Get latest version of stable Rust - uses: moonrepo/setup-rust@v1 - with: + - uses: actions/checkout@v4 + - name: Get latest version of stable Rust + uses: moonrepo/setup-rust@v1 + with: channel: stable cache-target: release - - name: Run a basic beacon chain sim that starts from Bellatrix - run: cargo run --release --bin simulator basic-sim + - name: Run a basic beacon chain sim that starts from Bellatrix + run: cargo run --release --bin simulator basic-sim fallback-simulator-ubuntu: name: fallback-simulator-ubuntu runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - name: Get latest version of stable Rust - uses: moonrepo/setup-rust@v1 - with: + - uses: actions/checkout@v4 + - name: Get latest version of stable Rust + uses: moonrepo/setup-rust@v1 + with: channel: stable cache-target: release - - name: Run a beacon chain sim which tests VC fallback behaviour - run: cargo run --release --bin simulator fallback-sim + - name: Run a beacon chain sim which tests VC fallback behaviour + run: cargo run --release --bin simulator fallback-sim doppelganger-protection-test: name: doppelganger-protection-test runs-on: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "linux", "CI", "small"]') || 'ubuntu-latest' }} @@ -257,111 +257,111 @@ jobs: # Enable portable to prevent issues with caching `blst` for the wrong CPU type FEATURES: jemalloc,portable steps: - - uses: actions/checkout@v4 - - name: Get latest version of stable Rust - if: env.SELF_HOSTED_RUNNERS == 'false' - uses: moonrepo/setup-rust@v1 - with: + - uses: actions/checkout@v4 + - name: Get latest version of stable Rust + if: env.SELF_HOSTED_RUNNERS == 'false' + uses: moonrepo/setup-rust@v1 + with: channel: stable cache-target: release - - name: Install geth - if: env.SELF_HOSTED_RUNNERS == 'false' - run: | + - name: Install geth + if: env.SELF_HOSTED_RUNNERS == 'false' + run: | sudo add-apt-repository -y ppa:ethereum/ethereum sudo apt-get update sudo apt-get install ethereum - - name: Install lighthouse - run: | + - name: Install lighthouse + run: | make - - name: Install lcli - if: env.SELF_HOSTED_RUNNERS == 'false' - run: make install-lcli - - name: Run the doppelganger protection failure test script - run: | + - name: Install lcli + if: env.SELF_HOSTED_RUNNERS == 'false' + run: make install-lcli + - name: Run the doppelganger protection failure test script + run: | cd scripts/tests ./doppelganger_protection.sh failure genesis.json - - name: Run the doppelganger protection success test script - run: | - cd scripts/tests - ./doppelganger_protection.sh success genesis.json + - name: Run the doppelganger protection success test script + run: | + cd scripts/tests + ./doppelganger_protection.sh success genesis.json execution-engine-integration-ubuntu: name: execution-engine-integration-ubuntu runs-on: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "linux", "CI", "small"]') || 'ubuntu-latest' }} steps: - - uses: actions/checkout@v4 - - name: Get latest version of stable Rust - if: env.SELF_HOSTED_RUNNERS == 'false' - uses: moonrepo/setup-rust@v1 - with: + - uses: actions/checkout@v4 + - name: Get latest version of stable Rust + if: env.SELF_HOSTED_RUNNERS == 'false' + uses: moonrepo/setup-rust@v1 + with: channel: stable cache-target: release cache: false - env: + env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Add go compiler to $PATH - if: env.SELF_HOSTED_RUNNERS == 'true' - run: echo "/usr/local/go/bin" >> $GITHUB_PATH - - name: Run exec engine integration tests in release - run: make test-exec-engine + - name: Add go compiler to $PATH + if: env.SELF_HOSTED_RUNNERS == 'true' + run: echo "/usr/local/go/bin" >> $GITHUB_PATH + - name: Run exec engine integration tests in release + run: make test-exec-engine check-code: name: check-code runs-on: ubuntu-latest env: CARGO_INCREMENTAL: 1 steps: - - uses: actions/checkout@v4 - - name: Get latest version of stable Rust - uses: moonrepo/setup-rust@v1 - with: + - uses: actions/checkout@v4 + - name: Get latest version of stable Rust + uses: moonrepo/setup-rust@v1 + with: channel: stable cache-target: release components: rustfmt,clippy bins: cargo-audit - - name: Check formatting with cargo fmt - run: make cargo-fmt - - name: Lint code for quality and style with Clippy - run: make lint - - name: Certify Cargo.lock freshness - run: git diff --exit-code Cargo.lock - - name: Typecheck benchmark code without running it - run: make check-benches - - name: Validate state_processing feature arbitrary-fuzz - run: make arbitrary-fuzz - - name: Run cargo audit - run: make audit-CI - - name: Run cargo vendor to make sure dependencies can be vendored for packaging, reproducibility and archival purpose - run: CARGO_HOME=$(readlink -f $HOME) make vendor + - name: Check formatting with cargo fmt + run: make cargo-fmt + - name: Lint code for quality and style with Clippy + run: make lint + - name: Certify Cargo.lock freshness + run: git diff --exit-code Cargo.lock + - name: Typecheck benchmark code without running it + run: make check-benches + - name: Validate state_processing feature arbitrary-fuzz + run: make arbitrary-fuzz + - name: Run cargo audit + run: make audit-CI + - name: Run cargo vendor to make sure dependencies can be vendored for packaging, reproducibility and archival purpose + run: CARGO_HOME=$(readlink -f $HOME) make vendor check-msrv: name: check-msrv runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - name: Install Rust at Minimum Supported Rust Version (MSRV) - run: | - metadata=$(cargo metadata --no-deps --format-version 1) - msrv=$(echo $metadata | jq -r '.packages | map(select(.name == "lighthouse")) | .[0].rust_version') - rustup override set $msrv - - name: Run cargo check - run: cargo check --workspace + - uses: actions/checkout@v4 + - name: Install Rust at Minimum Supported Rust Version (MSRV) + run: | + metadata=$(cargo metadata --no-deps --format-version 1) + msrv=$(echo $metadata | jq -r '.packages | map(select(.name == "lighthouse")) | .[0].rust_version') + rustup override set $msrv + - name: Run cargo check + run: cargo check --workspace cargo-udeps: name: cargo-udeps runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - name: Get latest version of nightly Rust - uses: moonrepo/setup-rust@v1 - with: + - uses: actions/checkout@v4 + - name: Get latest version of nightly Rust + uses: moonrepo/setup-rust@v1 + with: channel: nightly bins: cargo-udeps cache: false - env: + env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Create Cargo config dir - run: mkdir -p .cargo - - name: Install custom Cargo config - run: cp -f .github/custom/config.toml .cargo/config.toml - - name: Run cargo udeps to identify unused crates in the dependency graph - run: make udeps + - name: Create Cargo config dir + run: mkdir -p .cargo + - name: Install custom Cargo config + run: cp -f .github/custom/config.toml .cargo/config.toml + - name: Run cargo udeps to identify unused crates in the dependency graph + run: make udeps env: # Allow warnings on Nightly RUSTFLAGS: "" @@ -369,25 +369,25 @@ jobs: name: compile-with-beta-compiler runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - name: Install dependencies - run: sudo apt update && sudo apt install -y git gcc g++ make cmake pkg-config llvm-dev libclang-dev clang - - name: Use Rust beta - run: rustup override set beta - - name: Run make - run: make + - uses: actions/checkout@v4 + - name: Install dependencies + run: sudo apt update && sudo apt install -y git gcc g++ make cmake pkg-config llvm-dev libclang-dev clang + - name: Use Rust beta + run: rustup override set beta + - name: Run make + run: make cli-check: name: cli-check runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - name: Get latest version of stable Rust - uses: moonrepo/setup-rust@v1 - with: - channel: stable - cache-target: release - - name: Run Makefile to trigger the bash script - run: make cli + - uses: actions/checkout@v4 + - name: Get latest version of stable Rust + uses: moonrepo/setup-rust@v1 + with: + channel: stable + cache-target: release + - name: Run Makefile to trigger the bash script + run: make cli # This job succeeds ONLY IF all others succeed. It is used by the merge queue to determine whether # a PR is safe to merge. New jobs should be added here. test-suite-success: diff --git a/testing/simulator/src/basic_sim.rs b/testing/simulator/src/basic_sim.rs index 755bb71b430..a3a9eda9d55 100644 --- a/testing/simulator/src/basic_sim.rs +++ b/testing/simulator/src/basic_sim.rs @@ -16,7 +16,7 @@ use tokio::time::sleep; use types::{Epoch, EthSpec, MinimalEthSpec}; const END_EPOCH: u64 = 16; -const GENESIS_DELAY: u64 = 32; +const GENESIS_DELAY: u64 = 60; const ALTAIR_FORK_EPOCH: u64 = 0; const BELLATRIX_FORK_EPOCH: u64 = 0; const CAPELLA_FORK_EPOCH: u64 = 1; diff --git a/testing/simulator/src/fallback_sim.rs b/testing/simulator/src/fallback_sim.rs index c9deeba04d9..9e761b60563 100644 --- a/testing/simulator/src/fallback_sim.rs +++ b/testing/simulator/src/fallback_sim.rs @@ -15,7 +15,7 @@ use tokio::time::sleep; use types::{Epoch, EthSpec, MinimalEthSpec}; const END_EPOCH: u64 = 16; -const GENESIS_DELAY: u64 = 32; +const GENESIS_DELAY: u64 = 60; const ALTAIR_FORK_EPOCH: u64 = 0; const BELLATRIX_FORK_EPOCH: u64 = 0; const CAPELLA_FORK_EPOCH: u64 = 1; From bc51e70d5ac110e8f8f04b7ae3a2696a462520ab Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Thu, 9 May 2024 02:14:32 +0200 Subject: [PATCH 13/76] DataColumnByRange boilerplate (#5353) * add boilerplate * fmt --- beacon_node/beacon_processor/src/lib.rs | 16 +- beacon_node/beacon_processor/src/metrics.rs | 5 + .../src/peer_manager/mod.rs | 3 + .../src/rpc/codec/ssz_snappy.rs | 28 ++- .../lighthouse_network/src/rpc/config.rs | 9 + .../lighthouse_network/src/rpc/methods.rs | 33 +++ beacon_node/lighthouse_network/src/rpc/mod.rs | 1 + .../lighthouse_network/src/rpc/outbound.rs | 12 + .../lighthouse_network/src/rpc/protocol.rs | 32 ++- .../src/rpc/rate_limiter.rs | 14 ++ .../src/service/api_types.rs | 13 ++ .../lighthouse_network/src/service/mod.rs | 15 ++ .../src/network_beacon_processor/mod.rs | 19 +- .../network_beacon_processor/rpc_methods.rs | 217 +++++++++++++++++- beacon_node/network/src/router.rs | 34 +++ beacon_node/network/src/sync/manager.rs | 28 +++ consensus/types/src/data_column_sidecar.rs | 21 ++ 17 files changed, 492 insertions(+), 8 deletions(-) diff --git a/beacon_node/beacon_processor/src/lib.rs b/beacon_node/beacon_processor/src/lib.rs index 3eebf5718a1..c8f95046427 100644 --- a/beacon_node/beacon_processor/src/lib.rs +++ b/beacon_node/beacon_processor/src/lib.rs @@ -193,6 +193,10 @@ const MAX_BLOBS_BY_ROOTS_QUEUE_LEN: usize = 1_024; /// will be stored before we start dropping them. const MAX_DATA_COLUMNS_BY_ROOTS_QUEUE_LEN: usize = 2_048; +/// The maximum number of queued `DataColumnsByRangeRequest` objects received from the network RPC that +/// will be stored before we start dropping them. +const MAX_DATA_COLUMNS_BY_RANGE_QUEUE_LEN: usize = 2_048; + /// Maximum number of `SignedBlsToExecutionChange` messages to queue before dropping them. /// /// This value is set high to accommodate the large spike that is expected immediately after Capella @@ -270,6 +274,7 @@ pub const BLOCKS_BY_ROOTS_REQUEST: &str = "blocks_by_roots_request"; pub const BLOBS_BY_RANGE_REQUEST: &str = "blobs_by_range_request"; pub const BLOBS_BY_ROOTS_REQUEST: &str = "blobs_by_roots_request"; pub const DATA_COLUMNS_BY_ROOTS_REQUEST: &str = "data_columns_by_roots_request"; +pub const DATA_COLUMNS_BY_RANGE_REQUEST: &str = "data_columns_by_range_request"; pub const LIGHT_CLIENT_BOOTSTRAP_REQUEST: &str = "light_client_bootstrap"; pub const LIGHT_CLIENT_FINALITY_UPDATE_REQUEST: &str = "light_client_finality_update_request"; pub const LIGHT_CLIENT_OPTIMISTIC_UPDATE_REQUEST: &str = "light_client_optimistic_update_request"; @@ -660,6 +665,7 @@ pub enum Work { BlobsByRangeRequest(BlockingFn), BlobsByRootsRequest(BlockingFn), DataColumnsByRootsRequest(BlockingFn), + DataColumnsByRangeRequest(BlockingFn), GossipBlsToExecutionChange(BlockingFn), LightClientBootstrapRequest(BlockingFn), LightClientOptimisticUpdateRequest(BlockingFn), @@ -706,6 +712,7 @@ impl Work { Work::BlobsByRangeRequest(_) => BLOBS_BY_RANGE_REQUEST, Work::BlobsByRootsRequest(_) => BLOBS_BY_ROOTS_REQUEST, Work::DataColumnsByRootsRequest(_) => DATA_COLUMNS_BY_ROOTS_REQUEST, + Work::DataColumnsByRangeRequest(_) => DATA_COLUMNS_BY_RANGE_REQUEST, Work::LightClientBootstrapRequest(_) => LIGHT_CLIENT_BOOTSTRAP_REQUEST, Work::LightClientOptimisticUpdateRequest(_) => LIGHT_CLIENT_OPTIMISTIC_UPDATE_REQUEST, Work::LightClientFinalityUpdateRequest(_) => LIGHT_CLIENT_FINALITY_UPDATE_REQUEST, @@ -873,6 +880,7 @@ impl BeaconProcessor { let mut blbroots_queue = FifoQueue::new(MAX_BLOBS_BY_ROOTS_QUEUE_LEN); let mut blbrange_queue = FifoQueue::new(MAX_BLOBS_BY_RANGE_QUEUE_LEN); let mut dcbroots_queue = FifoQueue::new(MAX_DATA_COLUMNS_BY_ROOTS_QUEUE_LEN); + let mut dcbrange_queue = FifoQueue::new(MAX_DATA_COLUMNS_BY_RANGE_QUEUE_LEN); let mut gossip_bls_to_execution_change_queue = FifoQueue::new(MAX_BLS_TO_EXECUTION_CHANGE_QUEUE_LEN); @@ -1181,6 +1189,8 @@ impl BeaconProcessor { self.spawn_worker(item, idle_tx); } else if let Some(item) = dcbroots_queue.pop() { self.spawn_worker(item, idle_tx); + } else if let Some(item) = dcbrange_queue.pop() { + self.spawn_worker(item, idle_tx); // Prioritize sampling requests after block syncing requests } else if let Some(item) = unknown_block_sampling_request_queue.pop() { self.spawn_worker(item, idle_tx); @@ -1362,6 +1372,9 @@ impl BeaconProcessor { Work::DataColumnsByRootsRequest { .. } => { dcbroots_queue.push(work, work_id, &self.log) } + Work::DataColumnsByRangeRequest { .. } => { + dcbrange_queue.push(work, work_id, &self.log) + } Work::UnknownLightClientOptimisticUpdate { .. } => { unknown_light_client_update_queue.push(work, work_id, &self.log) } @@ -1577,7 +1590,8 @@ impl BeaconProcessor { }), Work::BlobsByRangeRequest(process_fn) | Work::BlobsByRootsRequest(process_fn) - | Work::DataColumnsByRootsRequest(process_fn) => { + | Work::DataColumnsByRootsRequest(process_fn) + | Work::DataColumnsByRangeRequest(process_fn) => { task_spawner.spawn_blocking(process_fn) } Work::BlocksByRangeRequest(work) | Work::BlocksByRootsRequest(work) => { diff --git a/beacon_node/beacon_processor/src/metrics.rs b/beacon_node/beacon_processor/src/metrics.rs index f4c9d61e7d9..997d6c9039e 100644 --- a/beacon_node/beacon_processor/src/metrics.rs +++ b/beacon_node/beacon_processor/src/metrics.rs @@ -86,6 +86,11 @@ lazy_static::lazy_static! { "beacon_processor_rpc_blob_queue_total", "Count of blobs from the rpc waiting to be verified." ); + // Rpc data columns. + pub static ref BEACON_PROCESSOR_RPC_DATA_COLUMNS_QUEUE_TOTAL: Result = try_create_int_gauge( + "beacon_processor_rpc_data_columns_total", + "Count of data columns from the rpc waiting to be verified." + ); // Rpc verify data columns pub static ref BEACON_PROCESSOR_RPC_VERIFY_DATA_COLUMN_QUEUE_TOTAL: Result = try_create_int_gauge( "beacon_processor_rpc_verify_data_column_queue_total", diff --git a/beacon_node/lighthouse_network/src/peer_manager/mod.rs b/beacon_node/lighthouse_network/src/peer_manager/mod.rs index 1d768f17453..d3d7b34eba8 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/mod.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/mod.rs @@ -563,6 +563,7 @@ impl PeerManager { Protocol::LightClientFinalityUpdate => return, Protocol::BlobsByRoot => PeerAction::MidToleranceError, Protocol::DataColumnsByRoot => PeerAction::MidToleranceError, + Protocol::DataColumnsByRange => PeerAction::MidToleranceError, Protocol::Goodbye => PeerAction::LowToleranceError, Protocol::MetaData => PeerAction::LowToleranceError, Protocol::Status => PeerAction::LowToleranceError, @@ -582,6 +583,7 @@ impl PeerManager { Protocol::BlobsByRange => return, Protocol::BlobsByRoot => return, Protocol::DataColumnsByRoot => return, + Protocol::DataColumnsByRange => return, Protocol::Goodbye => return, Protocol::LightClientBootstrap => return, Protocol::LightClientOptimisticUpdate => return, @@ -603,6 +605,7 @@ impl PeerManager { Protocol::BlobsByRange => PeerAction::MidToleranceError, Protocol::BlobsByRoot => PeerAction::MidToleranceError, Protocol::DataColumnsByRoot => PeerAction::MidToleranceError, + Protocol::DataColumnsByRange => PeerAction::MidToleranceError, Protocol::LightClientBootstrap => return, Protocol::LightClientOptimisticUpdate => return, Protocol::LightClientFinalityUpdate => return, diff --git a/beacon_node/lighthouse_network/src/rpc/codec/ssz_snappy.rs b/beacon_node/lighthouse_network/src/rpc/codec/ssz_snappy.rs index e0f14ee1401..90eca79d98a 100644 --- a/beacon_node/lighthouse_network/src/rpc/codec/ssz_snappy.rs +++ b/beacon_node/lighthouse_network/src/rpc/codec/ssz_snappy.rs @@ -72,6 +72,7 @@ impl Encoder> for SSZSnappyInboundCodec { RPCResponse::BlobsByRange(res) => res.as_ssz_bytes(), RPCResponse::BlobsByRoot(res) => res.as_ssz_bytes(), RPCResponse::DataColumnsByRoot(res) => res.as_ssz_bytes(), + RPCResponse::DataColumnsByRange(res) => res.as_ssz_bytes(), RPCResponse::LightClientBootstrap(res) => res.as_ssz_bytes(), RPCResponse::LightClientOptimisticUpdate(res) => res.as_ssz_bytes(), RPCResponse::LightClientFinalityUpdate(res) => res.as_ssz_bytes(), @@ -227,6 +228,7 @@ impl Encoder> for SSZSnappyOutboundCodec { OutboundRequest::BlobsByRange(req) => req.as_ssz_bytes(), OutboundRequest::BlobsByRoot(req) => req.blob_ids.as_ssz_bytes(), OutboundRequest::DataColumnsByRoot(req) => req.data_column_ids.as_ssz_bytes(), + OutboundRequest::DataColumnsByRange(req) => req.data_column_ids.as_ssz_bytes(), OutboundRequest::Ping(req) => req.as_ssz_bytes(), OutboundRequest::MetaData(_) => return Ok(()), // no metadata to encode }; @@ -419,7 +421,8 @@ fn context_bytes( } RPCResponse::BlobsByRange(_) | RPCResponse::BlobsByRoot(_) - | RPCResponse::DataColumnsByRoot(_) => { + | RPCResponse::DataColumnsByRoot(_) + | RPCResponse::DataColumnsByRange(_) => { // TODO(electra) return fork_context.to_context_bytes(ForkName::Deneb); } @@ -526,6 +529,9 @@ fn handle_rpc_request( )?, }, ))), + SupportedProtocol::DataColumnsByRangeV1 => Ok(Some(InboundRequest::DataColumnsByRange( + DataColumnsByRangeRequest::from_ssz_bytes(decoded_buffer)?, + ))), SupportedProtocol::PingV1 => Ok(Some(InboundRequest::Ping(Ping { data: u64::from_ssz_bytes(decoded_buffer)?, }))), @@ -635,6 +641,23 @@ fn handle_rpc_response( ), )), }, + SupportedProtocol::DataColumnsByRangeV1 => match fork_name { + // TODO(das): update fork name + Some(ForkName::Deneb) => Ok(Some(RPCResponse::DataColumnsByRange(Arc::new( + DataColumnSidecar::from_ssz_bytes(decoded_buffer)?, + )))), + Some(_) => Err(RPCError::ErrorResponse( + RPCResponseErrorCode::InvalidRequest, + "Invalid fork name for data columns by range".to_string(), + )), + None => Err(RPCError::ErrorResponse( + RPCResponseErrorCode::InvalidRequest, + format!( + "No context bytes provided for {:?} response", + versioned_protocol + ), + )), + }, SupportedProtocol::PingV1 => Ok(Some(RPCResponse::Pong(Ping { data: u64::from_ssz_bytes(decoded_buffer)?, }))), @@ -1046,6 +1069,9 @@ mod tests { OutboundRequest::DataColumnsByRoot(dcbroot) => { assert_eq!(decoded, InboundRequest::DataColumnsByRoot(dcbroot)) } + OutboundRequest::DataColumnsByRange(dcbrange) => { + assert_eq!(decoded, InboundRequest::DataColumnsByRange(dcbrange)) + } OutboundRequest::Ping(ping) => { assert_eq!(decoded, InboundRequest::Ping(ping)) } diff --git a/beacon_node/lighthouse_network/src/rpc/config.rs b/beacon_node/lighthouse_network/src/rpc/config.rs index 038b0fe12cb..a357495e7a8 100644 --- a/beacon_node/lighthouse_network/src/rpc/config.rs +++ b/beacon_node/lighthouse_network/src/rpc/config.rs @@ -92,6 +92,7 @@ pub struct RateLimiterConfig { pub(super) blobs_by_range_quota: Quota, pub(super) blobs_by_root_quota: Quota, pub(super) data_columns_by_root_quota: Quota, + pub(super) data_columns_by_range_quota: Quota, pub(super) light_client_bootstrap_quota: Quota, pub(super) light_client_optimistic_update_quota: Quota, pub(super) light_client_finality_update_quota: Quota, @@ -107,6 +108,7 @@ impl RateLimiterConfig { pub const DEFAULT_BLOBS_BY_RANGE_QUOTA: Quota = Quota::n_every(768, 10); pub const DEFAULT_BLOBS_BY_ROOT_QUOTA: Quota = Quota::n_every(128, 10); pub const DEFAULT_DATA_COLUMNS_BY_ROOT_QUOTA: Quota = Quota::n_every(128, 10); + pub const DEFAULT_DATA_COLUMNS_BY_RANGE_QUOTA: Quota = Quota::n_every(128, 10); pub const DEFAULT_LIGHT_CLIENT_BOOTSTRAP_QUOTA: Quota = Quota::one_every(10); pub const DEFAULT_LIGHT_CLIENT_OPTIMISTIC_UPDATE_QUOTA: Quota = Quota::one_every(10); pub const DEFAULT_LIGHT_CLIENT_FINALITY_UPDATE_QUOTA: Quota = Quota::one_every(10); @@ -124,6 +126,7 @@ impl Default for RateLimiterConfig { blobs_by_range_quota: Self::DEFAULT_BLOBS_BY_RANGE_QUOTA, blobs_by_root_quota: Self::DEFAULT_BLOBS_BY_ROOT_QUOTA, data_columns_by_root_quota: Self::DEFAULT_DATA_COLUMNS_BY_ROOT_QUOTA, + data_columns_by_range_quota: Self::DEFAULT_DATA_COLUMNS_BY_RANGE_QUOTA, light_client_bootstrap_quota: Self::DEFAULT_LIGHT_CLIENT_BOOTSTRAP_QUOTA, light_client_optimistic_update_quota: Self::DEFAULT_LIGHT_CLIENT_OPTIMISTIC_UPDATE_QUOTA, @@ -178,6 +181,7 @@ impl FromStr for RateLimiterConfig { let mut blobs_by_range_quota = None; let mut blobs_by_root_quota = None; let mut data_columns_by_root_quota = None; + let mut data_columns_by_range_quota = None; let mut light_client_bootstrap_quota = None; let mut light_client_optimistic_update_quota = None; let mut light_client_finality_update_quota = None; @@ -195,6 +199,9 @@ impl FromStr for RateLimiterConfig { Protocol::DataColumnsByRoot => { data_columns_by_root_quota = data_columns_by_root_quota.or(quota) } + Protocol::DataColumnsByRange => { + data_columns_by_range_quota = data_columns_by_range_quota.or(quota) + } Protocol::Ping => ping_quota = ping_quota.or(quota), Protocol::MetaData => meta_data_quota = meta_data_quota.or(quota), Protocol::LightClientBootstrap => { @@ -224,6 +231,8 @@ impl FromStr for RateLimiterConfig { blobs_by_root_quota: blobs_by_root_quota.unwrap_or(Self::DEFAULT_BLOBS_BY_ROOT_QUOTA), data_columns_by_root_quota: data_columns_by_root_quota .unwrap_or(Self::DEFAULT_DATA_COLUMNS_BY_ROOT_QUOTA), + data_columns_by_range_quota: data_columns_by_range_quota + .unwrap_or(Self::DEFAULT_DATA_COLUMNS_BY_RANGE_QUOTA), light_client_bootstrap_quota: light_client_bootstrap_quota .unwrap_or(Self::DEFAULT_LIGHT_CLIENT_BOOTSTRAP_QUOTA), light_client_optimistic_update_quota: light_client_optimistic_update_quota diff --git a/beacon_node/lighthouse_network/src/rpc/methods.rs b/beacon_node/lighthouse_network/src/rpc/methods.rs index 18bf4e7dcfc..c5ea34b30dd 100644 --- a/beacon_node/lighthouse_network/src/rpc/methods.rs +++ b/beacon_node/lighthouse_network/src/rpc/methods.rs @@ -295,6 +295,25 @@ impl BlobsByRangeRequest { } } +/// Request a number of beacon data columns from a peer. +#[derive(Encode, Decode, Clone, Debug, PartialEq)] +pub struct DataColumnsByRangeRequest { + /// The starting slot to request data columns. + pub start_slot: u64, + + /// The number of slots from the start slot. + pub count: u64, + + /// The list of beacon block roots and column indices being requested. + pub data_column_ids: Vec, +} + +impl DataColumnsByRangeRequest { + pub fn max_data_columns_requested(&self) -> u64 { + self.count.saturating_mul(E::max_blobs_per_block() as u64) + } +} + /// Request a number of beacon block roots from a peer. #[superstruct( variants(V1, V2), @@ -433,6 +452,9 @@ pub enum RPCResponse { /// A response to a get DATA_COLUMN_SIDECARS_BY_ROOT request. DataColumnsByRoot(Arc>), + /// A response to a get DATA_COLUMN_SIDECARS_BY_RANGE request. + DataColumnsByRange(Arc>), + /// A PONG response to a PING request. Pong(Ping), @@ -457,6 +479,9 @@ pub enum ResponseTermination { /// Data column sidecars by root stream termination. DataColumnsByRoot, + + /// Data column sidecars by range stream termination. + DataColumnsByRange, } /// The structured response containing a result/code indicating success or failure @@ -548,6 +573,7 @@ impl RPCResponse { RPCResponse::BlobsByRange(_) => Protocol::BlobsByRange, RPCResponse::BlobsByRoot(_) => Protocol::BlobsByRoot, RPCResponse::DataColumnsByRoot(_) => Protocol::DataColumnsByRoot, + RPCResponse::DataColumnsByRange(_) => Protocol::DataColumnsByRange, RPCResponse::Pong(_) => Protocol::Ping, RPCResponse::MetaData(_) => Protocol::MetaData, RPCResponse::LightClientBootstrap(_) => Protocol::LightClientBootstrap, @@ -596,6 +622,13 @@ impl std::fmt::Display for RPCResponse { RPCResponse::DataColumnsByRoot(sidecar) => { write!(f, "DataColumnsByRoot: Data column slot: {}", sidecar.slot()) } + RPCResponse::DataColumnsByRange(sidecar) => { + write!( + f, + "DataColumnsByRange: Data column slot: {}", + sidecar.slot() + ) + } RPCResponse::Pong(ping) => write!(f, "Pong: {}", ping.data), RPCResponse::MetaData(metadata) => write!(f, "Metadata: {}", metadata.seq_number()), RPCResponse::LightClientBootstrap(bootstrap) => { diff --git a/beacon_node/lighthouse_network/src/rpc/mod.rs b/beacon_node/lighthouse_network/src/rpc/mod.rs index 59b7c2b73a3..c7c04659888 100644 --- a/beacon_node/lighthouse_network/src/rpc/mod.rs +++ b/beacon_node/lighthouse_network/src/rpc/mod.rs @@ -420,6 +420,7 @@ where ResponseTermination::BlobsByRange => Protocol::BlobsByRange, ResponseTermination::BlobsByRoot => Protocol::BlobsByRoot, ResponseTermination::DataColumnsByRoot => Protocol::DataColumnsByRoot, + ResponseTermination::DataColumnsByRange => Protocol::DataColumnsByRange, }, ), }; diff --git a/beacon_node/lighthouse_network/src/rpc/outbound.rs b/beacon_node/lighthouse_network/src/rpc/outbound.rs index 2302ac494f5..bcb76c00081 100644 --- a/beacon_node/lighthouse_network/src/rpc/outbound.rs +++ b/beacon_node/lighthouse_network/src/rpc/outbound.rs @@ -37,6 +37,7 @@ pub enum OutboundRequest { BlobsByRange(BlobsByRangeRequest), BlobsByRoot(BlobsByRootRequest), DataColumnsByRoot(DataColumnsByRootRequest), + DataColumnsByRange(DataColumnsByRangeRequest), Ping(Ping), MetaData(MetadataRequest), } @@ -84,6 +85,10 @@ impl OutboundRequest { SupportedProtocol::DataColumnsByRootV1, Encoding::SSZSnappy, )], + OutboundRequest::DataColumnsByRange(_) => vec![ProtocolId::new( + SupportedProtocol::DataColumnsByRangeV1, + Encoding::SSZSnappy, + )], OutboundRequest::Ping(_) => vec![ProtocolId::new( SupportedProtocol::PingV1, Encoding::SSZSnappy, @@ -106,6 +111,7 @@ impl OutboundRequest { OutboundRequest::BlobsByRange(req) => req.max_blobs_requested::(), OutboundRequest::BlobsByRoot(req) => req.blob_ids.len() as u64, OutboundRequest::DataColumnsByRoot(req) => req.data_column_ids.len() as u64, + OutboundRequest::DataColumnsByRange(req) => req.data_column_ids.len() as u64, OutboundRequest::Ping(_) => 1, OutboundRequest::MetaData(_) => 1, } @@ -120,6 +126,7 @@ impl OutboundRequest { OutboundRequest::BlobsByRange(_) => false, OutboundRequest::BlobsByRoot(_) => false, OutboundRequest::DataColumnsByRoot(_) => false, + OutboundRequest::DataColumnsByRange(_) => false, OutboundRequest::Ping(_) => true, OutboundRequest::MetaData(_) => true, } @@ -141,6 +148,7 @@ impl OutboundRequest { OutboundRequest::BlobsByRange(_) => SupportedProtocol::BlobsByRangeV1, OutboundRequest::BlobsByRoot(_) => SupportedProtocol::BlobsByRootV1, OutboundRequest::DataColumnsByRoot(_) => SupportedProtocol::DataColumnsByRootV1, + OutboundRequest::DataColumnsByRange(_) => SupportedProtocol::DataColumnsByRangeV1, OutboundRequest::Ping(_) => SupportedProtocol::PingV1, OutboundRequest::MetaData(req) => match req { MetadataRequest::V1(_) => SupportedProtocol::MetaDataV1, @@ -160,6 +168,7 @@ impl OutboundRequest { OutboundRequest::BlobsByRange(_) => ResponseTermination::BlobsByRange, OutboundRequest::BlobsByRoot(_) => ResponseTermination::BlobsByRoot, OutboundRequest::DataColumnsByRoot(_) => ResponseTermination::DataColumnsByRoot, + OutboundRequest::DataColumnsByRange(_) => ResponseTermination::DataColumnsByRange, OutboundRequest::Status(_) => unreachable!(), OutboundRequest::Goodbye(_) => unreachable!(), OutboundRequest::Ping(_) => unreachable!(), @@ -218,6 +227,9 @@ impl std::fmt::Display for OutboundRequest { OutboundRequest::BlobsByRange(req) => write!(f, "Blobs by range: {:?}", req), OutboundRequest::BlobsByRoot(req) => write!(f, "Blobs by root: {:?}", req), OutboundRequest::DataColumnsByRoot(req) => write!(f, "Data columns by root: {:?}", req), + OutboundRequest::DataColumnsByRange(req) => { + write!(f, "Data columns by range: {:?}", req) + } OutboundRequest::Ping(ping) => write!(f, "Ping: {}", ping.data), OutboundRequest::MetaData(_) => write!(f, "MetaData request"), } diff --git a/beacon_node/lighthouse_network/src/rpc/protocol.rs b/beacon_node/lighthouse_network/src/rpc/protocol.rs index 946db6459e7..1447a57e706 100644 --- a/beacon_node/lighthouse_network/src/rpc/protocol.rs +++ b/beacon_node/lighthouse_network/src/rpc/protocol.rs @@ -252,6 +252,9 @@ pub enum Protocol { /// The `DataColumnSidecarsByRoot` protocol name. #[strum(serialize = "data_column_sidecars_by_root")] DataColumnsByRoot, + /// The `DataColumnSidecarsByRange` protocol name. + #[strum(serialize = "data_column_sidecars_by_range")] + DataColumnsByRange, /// The `Ping` protocol name. Ping, /// The `MetaData` protocol name. @@ -278,6 +281,7 @@ impl Protocol { Protocol::BlobsByRange => Some(ResponseTermination::BlobsByRange), Protocol::BlobsByRoot => Some(ResponseTermination::BlobsByRoot), Protocol::DataColumnsByRoot => Some(ResponseTermination::DataColumnsByRoot), + Protocol::DataColumnsByRange => Some(ResponseTermination::DataColumnsByRange), Protocol::Ping => None, Protocol::MetaData => None, Protocol::LightClientBootstrap => None, @@ -305,6 +309,7 @@ pub enum SupportedProtocol { BlobsByRangeV1, BlobsByRootV1, DataColumnsByRootV1, + DataColumnsByRangeV1, PingV1, MetaDataV1, MetaDataV2, @@ -325,6 +330,7 @@ impl SupportedProtocol { SupportedProtocol::BlobsByRangeV1 => "1", SupportedProtocol::BlobsByRootV1 => "1", SupportedProtocol::DataColumnsByRootV1 => "1", + SupportedProtocol::DataColumnsByRangeV1 => "1", SupportedProtocol::PingV1 => "1", SupportedProtocol::MetaDataV1 => "1", SupportedProtocol::MetaDataV2 => "2", @@ -345,6 +351,7 @@ impl SupportedProtocol { SupportedProtocol::BlobsByRangeV1 => Protocol::BlobsByRange, SupportedProtocol::BlobsByRootV1 => Protocol::BlobsByRoot, SupportedProtocol::DataColumnsByRootV1 => Protocol::DataColumnsByRoot, + SupportedProtocol::DataColumnsByRangeV1 => Protocol::DataColumnsByRange, SupportedProtocol::PingV1 => Protocol::Ping, SupportedProtocol::MetaDataV1 => Protocol::MetaData, SupportedProtocol::MetaDataV2 => Protocol::MetaData, @@ -486,6 +493,10 @@ impl ProtocolId { ), Protocol::BlobsByRoot => RpcLimits::new(0, spec.max_blobs_by_root_request), Protocol::DataColumnsByRoot => RpcLimits::new(0, spec.max_data_columns_by_root_request), + Protocol::DataColumnsByRange => RpcLimits::new( + ::ssz_fixed_len(), + ::ssz_fixed_len(), + ), Protocol::Ping => RpcLimits::new( ::ssz_fixed_len(), ::ssz_fixed_len(), @@ -512,10 +523,8 @@ impl ProtocolId { Protocol::BlocksByRoot => rpc_block_limits_by_fork(fork_context.current_fork()), Protocol::BlobsByRange => rpc_blob_limits::(), Protocol::BlobsByRoot => rpc_blob_limits::(), - Protocol::DataColumnsByRoot => RpcLimits::new( - DataColumnSidecar::::min_size(), - DataColumnSidecar::::max_size(), - ), + Protocol::DataColumnsByRoot => rpc_data_column_limits::(), + Protocol::DataColumnsByRange => rpc_data_column_limits::(), Protocol::Ping => RpcLimits::new( ::ssz_fixed_len(), ::ssz_fixed_len(), @@ -545,6 +554,7 @@ impl ProtocolId { | SupportedProtocol::BlobsByRangeV1 | SupportedProtocol::BlobsByRootV1 | SupportedProtocol::DataColumnsByRootV1 + | SupportedProtocol::DataColumnsByRangeV1 | SupportedProtocol::LightClientBootstrapV1 | SupportedProtocol::LightClientOptimisticUpdateV1 | SupportedProtocol::LightClientFinalityUpdateV1 => true, @@ -585,6 +595,13 @@ pub fn rpc_blob_limits() -> RpcLimits { ) } +pub fn rpc_data_column_limits() -> RpcLimits { + RpcLimits::new( + DataColumnSidecar::::empty().as_ssz_bytes().len(), + DataColumnSidecar::::max_size(), + ) +} + /* Inbound upgrade */ // The inbound protocol reads the request, decodes it and returns the stream to the protocol @@ -665,6 +682,7 @@ pub enum InboundRequest { BlobsByRange(BlobsByRangeRequest), BlobsByRoot(BlobsByRootRequest), DataColumnsByRoot(DataColumnsByRootRequest), + DataColumnsByRange(DataColumnsByRangeRequest), LightClientBootstrap(LightClientBootstrapRequest), LightClientOptimisticUpdate, LightClientFinalityUpdate, @@ -686,6 +704,7 @@ impl InboundRequest { InboundRequest::BlobsByRange(req) => req.max_blobs_requested::(), InboundRequest::BlobsByRoot(req) => req.blob_ids.len() as u64, InboundRequest::DataColumnsByRoot(req) => req.data_column_ids.len() as u64, + InboundRequest::DataColumnsByRange(req) => req.data_column_ids.len() as u64, InboundRequest::Ping(_) => 1, InboundRequest::MetaData(_) => 1, InboundRequest::LightClientBootstrap(_) => 1, @@ -710,6 +729,7 @@ impl InboundRequest { InboundRequest::BlobsByRange(_) => SupportedProtocol::BlobsByRangeV1, InboundRequest::BlobsByRoot(_) => SupportedProtocol::BlobsByRootV1, InboundRequest::DataColumnsByRoot(_) => SupportedProtocol::DataColumnsByRootV1, + InboundRequest::DataColumnsByRange(_) => SupportedProtocol::DataColumnsByRangeV1, InboundRequest::Ping(_) => SupportedProtocol::PingV1, InboundRequest::MetaData(req) => match req { MetadataRequest::V1(_) => SupportedProtocol::MetaDataV1, @@ -736,6 +756,7 @@ impl InboundRequest { InboundRequest::BlobsByRange(_) => ResponseTermination::BlobsByRange, InboundRequest::BlobsByRoot(_) => ResponseTermination::BlobsByRoot, InboundRequest::DataColumnsByRoot(_) => ResponseTermination::DataColumnsByRoot, + InboundRequest::DataColumnsByRange(_) => ResponseTermination::DataColumnsByRange, InboundRequest::Status(_) => unreachable!(), InboundRequest::Goodbye(_) => unreachable!(), InboundRequest::Ping(_) => unreachable!(), @@ -847,6 +868,9 @@ impl std::fmt::Display for InboundRequest { InboundRequest::BlobsByRange(req) => write!(f, "Blobs by range: {:?}", req), InboundRequest::BlobsByRoot(req) => write!(f, "Blobs by root: {:?}", req), InboundRequest::DataColumnsByRoot(req) => write!(f, "Data columns by root: {:?}", req), + InboundRequest::DataColumnsByRange(req) => { + write!(f, "Data columns by range: {:?}", req) + } InboundRequest::Ping(ping) => write!(f, "Ping: {}", ping.data), InboundRequest::MetaData(_) => write!(f, "MetaData request"), InboundRequest::LightClientBootstrap(bootstrap) => { diff --git a/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs b/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs index 411f82be5c4..9fb085efd86 100644 --- a/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs +++ b/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs @@ -99,6 +99,8 @@ pub struct RPCRateLimiter { blbroot_rl: Limiter, /// DataColumnssByRoot rate limiter. dcbroot_rl: Limiter, + /// DataColumnsByRange rate limiter. + dcbrange_rl: Limiter, /// LightClientBootstrap rate limiter. lc_bootstrap_rl: Limiter, /// LightClientOptimisticUpdate rate limiter. @@ -137,6 +139,8 @@ pub struct RPCRateLimiterBuilder { blbroot_quota: Option, /// Quota for the DataColumnsByRoot protocol. dcbroot_quota: Option, + /// Quota for the DataColumnsByRange protocol. + dcbrange_quota: Option, /// Quota for the LightClientBootstrap protocol. lcbootstrap_quota: Option, /// Quota for the LightClientOptimisticUpdate protocol. @@ -159,6 +163,7 @@ impl RPCRateLimiterBuilder { Protocol::BlobsByRange => self.blbrange_quota = q, Protocol::BlobsByRoot => self.blbroot_quota = q, Protocol::DataColumnsByRoot => self.dcbroot_quota = q, + Protocol::DataColumnsByRange => self.dcbrange_quota = q, Protocol::LightClientBootstrap => self.lcbootstrap_quota = q, Protocol::LightClientOptimisticUpdate => self.lc_optimistic_update_quota = q, Protocol::LightClientFinalityUpdate => self.lc_finality_update_quota = q, @@ -200,6 +205,10 @@ impl RPCRateLimiterBuilder { .dcbroot_quota .ok_or("DataColumnsByRoot quota not specified")?; + let dcbrange_quota = self + .dcbrange_quota + .ok_or("DataColumnsByRange quota not specified")?; + // create the rate limiters let ping_rl = Limiter::from_quota(ping_quota)?; let metadata_rl = Limiter::from_quota(metadata_quota)?; @@ -210,6 +219,7 @@ impl RPCRateLimiterBuilder { let blbrange_rl = Limiter::from_quota(blbrange_quota)?; let blbroot_rl = Limiter::from_quota(blbroots_quota)?; let dcbroot_rl = Limiter::from_quota(dcbroot_quota)?; + let dcbrange_rl = Limiter::from_quota(dcbrange_quota)?; let lc_bootstrap_rl = Limiter::from_quota(lc_bootstrap_quota)?; let lc_optimistic_update_rl = Limiter::from_quota(lc_optimistic_update_quota)?; let lc_finality_update_rl = Limiter::from_quota(lc_finality_update_quota)?; @@ -229,6 +239,7 @@ impl RPCRateLimiterBuilder { blbrange_rl, blbroot_rl, dcbroot_rl, + dcbrange_rl, lc_bootstrap_rl, lc_optimistic_update_rl, lc_finality_update_rl, @@ -274,6 +285,7 @@ impl RPCRateLimiter { blobs_by_range_quota, blobs_by_root_quota, data_columns_by_root_quota, + data_columns_by_range_quota, light_client_bootstrap_quota, light_client_optimistic_update_quota, light_client_finality_update_quota, @@ -289,6 +301,7 @@ impl RPCRateLimiter { .set_quota(Protocol::BlobsByRange, blobs_by_range_quota) .set_quota(Protocol::BlobsByRoot, blobs_by_root_quota) .set_quota(Protocol::DataColumnsByRoot, data_columns_by_root_quota) + .set_quota(Protocol::DataColumnsByRange, data_columns_by_range_quota) .set_quota(Protocol::LightClientBootstrap, light_client_bootstrap_quota) .set_quota( Protocol::LightClientOptimisticUpdate, @@ -326,6 +339,7 @@ impl RPCRateLimiter { Protocol::BlobsByRange => &mut self.blbrange_rl, Protocol::BlobsByRoot => &mut self.blbroot_rl, Protocol::DataColumnsByRoot => &mut self.dcbroot_rl, + Protocol::DataColumnsByRange => &mut self.dcbrange_rl, Protocol::LightClientBootstrap => &mut self.lc_bootstrap_rl, Protocol::LightClientOptimisticUpdate => &mut self.lc_optimistic_update_rl, Protocol::LightClientFinalityUpdate => &mut self.lc_finality_update_rl, diff --git a/beacon_node/lighthouse_network/src/service/api_types.rs b/beacon_node/lighthouse_network/src/service/api_types.rs index 37d650d1b97..0902b6d27ba 100644 --- a/beacon_node/lighthouse_network/src/service/api_types.rs +++ b/beacon_node/lighthouse_network/src/service/api_types.rs @@ -16,6 +16,8 @@ use crate::rpc::{ OutboundRequest, SubstreamId, }; +use super::methods::DataColumnsByRangeRequest; + /// Identifier of requests sent by a peer. pub type PeerRequestId = (ConnectionId, SubstreamId); @@ -51,6 +53,8 @@ pub enum Request { BlobsByRoot(BlobsByRootRequest), /// A request data columns root request. DataColumnsByRoot(DataColumnsByRootRequest), + /// A request data columns by range request. + DataColumnsByRange(DataColumnsByRangeRequest), } impl std::convert::From for OutboundRequest { @@ -81,6 +85,7 @@ impl std::convert::From for OutboundRequest { Request::BlobsByRange(r) => OutboundRequest::BlobsByRange(r), Request::BlobsByRoot(r) => OutboundRequest::BlobsByRoot(r), Request::DataColumnsByRoot(r) => OutboundRequest::DataColumnsByRoot(r), + Request::DataColumnsByRange(r) => OutboundRequest::DataColumnsByRange(r), Request::Status(s) => OutboundRequest::Status(s), } } @@ -100,6 +105,8 @@ pub enum Response { BlocksByRange(Option>>), /// A response to a get BLOBS_BY_RANGE request. A None response signals the end of the batch. BlobsByRange(Option>>), + /// A response to a get DATA_COLUMN_SIDECARS_BY_Range request. + DataColumnsByRange(Option>>), /// A response to a get BLOCKS_BY_ROOT request. BlocksByRoot(Option>>), /// A response to a get BLOBS_BY_ROOT request. @@ -137,6 +144,12 @@ impl std::convert::From> for RPCCodedResponse { Some(d) => RPCCodedResponse::Success(RPCResponse::DataColumnsByRoot(d)), None => RPCCodedResponse::StreamTermination(ResponseTermination::DataColumnsByRoot), }, + Response::DataColumnsByRange(r) => match r { + Some(d) => RPCCodedResponse::Success(RPCResponse::DataColumnsByRange(d)), + None => { + RPCCodedResponse::StreamTermination(ResponseTermination::DataColumnsByRange) + } + }, Response::Status(s) => RPCCodedResponse::Success(RPCResponse::Status(s)), Response::LightClientBootstrap(b) => { RPCCodedResponse::Success(RPCResponse::LightClientBootstrap(b)) diff --git a/beacon_node/lighthouse_network/src/service/mod.rs b/beacon_node/lighthouse_network/src/service/mod.rs index 1873c17aecb..4509027c7c9 100644 --- a/beacon_node/lighthouse_network/src/service/mod.rs +++ b/beacon_node/lighthouse_network/src/service/mod.rs @@ -1201,6 +1201,9 @@ impl Network { Request::DataColumnsByRoot { .. } => { metrics::inc_counter_vec(&metrics::TOTAL_RPC_REQUESTS, &["data_columns_by_root"]) } + Request::DataColumnsByRange { .. } => { + metrics::inc_counter_vec(&metrics::TOTAL_RPC_REQUESTS, &["data_columns_by_range"]) + } } NetworkEvent::RequestReceived { peer_id, @@ -1533,6 +1536,14 @@ impl Network { ); Some(event) } + InboundRequest::DataColumnsByRange(req) => { + let event = self.build_request( + peer_request_id, + peer_id, + Request::DataColumnsByRange(req), + ); + Some(event) + } InboundRequest::LightClientBootstrap(req) => { let event = self.build_request( peer_request_id, @@ -1593,6 +1604,9 @@ impl Network { RPCResponse::DataColumnsByRoot(resp) => { self.build_response(id, peer_id, Response::DataColumnsByRoot(Some(resp))) } + RPCResponse::DataColumnsByRange(resp) => { + self.build_response(id, peer_id, Response::DataColumnsByRange(Some(resp))) + } // Should never be reached RPCResponse::LightClientBootstrap(bootstrap) => { self.build_response(id, peer_id, Response::LightClientBootstrap(bootstrap)) @@ -1616,6 +1630,7 @@ impl Network { ResponseTermination::BlobsByRange => Response::BlobsByRange(None), ResponseTermination::BlobsByRoot => Response::BlobsByRoot(None), ResponseTermination::DataColumnsByRoot => Response::DataColumnsByRoot(None), + ResponseTermination::DataColumnsByRange => Response::DataColumnsByRange(None), }; self.build_response(id, peer_id, response) } diff --git a/beacon_node/network/src/network_beacon_processor/mod.rs b/beacon_node/network/src/network_beacon_processor/mod.rs index 18965fcd6ef..5ce2f3feed2 100644 --- a/beacon_node/network/src/network_beacon_processor/mod.rs +++ b/beacon_node/network/src/network_beacon_processor/mod.rs @@ -11,7 +11,7 @@ use beacon_processor::{ WorkEvent as BeaconWorkEvent, }; use lighthouse_network::rpc::methods::{ - BlobsByRangeRequest, BlobsByRootRequest, DataColumnsByRootRequest, + BlobsByRangeRequest, BlobsByRootRequest, DataColumnsByRangeRequest, DataColumnsByRootRequest, }; use lighthouse_network::{ rpc::{BlocksByRangeRequest, BlocksByRootRequest, LightClientBootstrapRequest, StatusMessage}, @@ -666,6 +666,23 @@ impl NetworkBeaconProcessor { }) } + /// Create a new work event to process `DataColumnsByRange`s from the RPC network. + pub fn send_data_columns_by_range_request( + self: &Arc, + peer_id: PeerId, + request_id: PeerRequestId, + request: DataColumnsByRangeRequest, + ) -> Result<(), Error> { + let processor = self.clone(); + let process_fn = + move || processor.handle_data_columns_by_range_request(peer_id, request_id, request); + + self.try_send(BeaconWorkEvent { + drop_during_sync: false, + work: Work::DataColumnsByRangeRequest(Box::new(process_fn)), + }) + } + /// Create a new work event to process `LightClientBootstrap`s from the RPC network. pub fn send_light_client_bootstrap_request( self: &Arc, 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 065a6797e29..04329762629 100644 --- a/beacon_node/network/src/network_beacon_processor/rpc_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/rpc_methods.rs @@ -8,7 +8,7 @@ use beacon_chain::{ use beacon_processor::work_reprocessing_queue::{QueuedSamplingRequest, ReprocessQueueMessage}; use itertools::process_results; use lighthouse_network::rpc::methods::{ - BlobsByRangeRequest, BlobsByRootRequest, DataColumnsByRootRequest, + BlobsByRangeRequest, BlobsByRootRequest, DataColumnsByRangeRequest, DataColumnsByRootRequest, }; use lighthouse_network::rpc::*; use lighthouse_network::{PeerId, PeerRequestId, ReportSource, Response, SyncInfo}; @@ -934,6 +934,221 @@ impl NetworkBeaconProcessor { Ok(()) } + /// Handle a `DataColumnsByRange` request from the peer. + pub fn handle_data_columns_by_range_request( + self: Arc, + peer_id: PeerId, + request_id: PeerRequestId, + req: DataColumnsByRangeRequest, + ) { + debug!(self.log, "Received DataColumnsByRange Request"; + "peer_id" => %peer_id, + "count" => req.count, + "start_slot" => req.start_slot, + ); + + // Should not send more than max request data columns + if req.max_data_columns_requested::() + > self.chain.spec.max_request_data_column_sidecars + { + return self.send_error_response( + peer_id, + RPCResponseErrorCode::InvalidRequest, + "Request exceeded `MAX_REQUEST_DATA_COLUMN_SIDECARS`".into(), + request_id, + ); + } + + let request_start_slot = Slot::from(req.start_slot); + + let data_availability_boundary_slot = match self.chain.data_availability_boundary() { + Some(boundary) => boundary.start_slot(T::EthSpec::slots_per_epoch()), + None => { + debug!(self.log, "Deneb fork is disabled"); + self.send_error_response( + peer_id, + RPCResponseErrorCode::InvalidRequest, + "Deneb fork is disabled".into(), + request_id, + ); + return; + } + }; + + let oldest_data_column_slot = self + .chain + .store + .get_data_column_info() + .oldest_data_column_slot + .unwrap_or(data_availability_boundary_slot); + + if request_start_slot < oldest_data_column_slot { + debug!( + self.log, + "Range request start slot is older than data availability boundary."; + "requested_slot" => request_start_slot, + "oldest_data_column_slot" => oldest_data_column_slot, + "data_availability_boundary" => data_availability_boundary_slot + ); + + return if data_availability_boundary_slot < oldest_data_column_slot { + self.send_error_response( + peer_id, + RPCResponseErrorCode::ResourceUnavailable, + "data columns pruned within boundary".into(), + request_id, + ) + } else { + self.send_error_response( + peer_id, + RPCResponseErrorCode::InvalidRequest, + "Req outside availability period".into(), + request_id, + ) + }; + } + + let forwards_block_root_iter = + match self.chain.forwards_iter_block_roots(request_start_slot) { + Ok(iter) => iter, + Err(BeaconChainError::HistoricalBlockError( + HistoricalBlockError::BlockOutOfRange { + slot, + oldest_block_slot, + }, + )) => { + debug!(self.log, "Range request failed during backfill"; + "requested_slot" => slot, + "oldest_known_slot" => oldest_block_slot + ); + return self.send_error_response( + peer_id, + RPCResponseErrorCode::ResourceUnavailable, + "Backfilling".into(), + request_id, + ); + } + Err(e) => { + self.send_error_response( + peer_id, + RPCResponseErrorCode::ServerError, + "Database error".into(), + request_id, + ); + return error!(self.log, "Unable to obtain root iter"; + "request" => ?req, + "peer" => %peer_id, + "error" => ?e + ); + } + }; + + // Use `WhenSlotSkipped::Prev` to get the most recent block root prior to + // `request_start_slot` in order to check whether the `request_start_slot` is a skip. + let mut last_block_root = req.start_slot.checked_sub(1).and_then(|prev_slot| { + self.chain + .block_root_at_slot(Slot::new(prev_slot), WhenSlotSkipped::Prev) + .ok() + .flatten() + }); + + // Pick out the required blocks, ignoring skip-slots. + let maybe_block_roots = process_results(forwards_block_root_iter, |iter| { + iter.take_while(|(_, slot)| slot.as_u64() < req.start_slot.saturating_add(req.count)) + // map skip slots to None + .map(|(root, _)| { + let result = if Some(root) == last_block_root { + None + } else { + Some(root) + }; + last_block_root = Some(root); + result + }) + .collect::>>() + }); + + let block_roots = match maybe_block_roots { + Ok(block_roots) => block_roots, + Err(e) => { + return error!(self.log, "Error during iteration over blocks"; + "request" => ?req, + "peer" => %peer_id, + "error" => ?e + ) + } + }; + + // remove all skip slots + let block_roots = block_roots.into_iter().flatten(); + + let mut data_columns_sent = 0; + let mut send_response = true; + + for root in block_roots { + match self.chain.get_data_columns(&root) { + Ok(data_column_sidecar_list) => { + for data_column_sidecar in data_column_sidecar_list.iter() { + for &data_column_id in req.data_column_ids.iter() { + if data_column_sidecar.id() == data_column_id { + data_columns_sent += 1; + self.send_network_message(NetworkMessage::SendResponse { + peer_id, + response: Response::DataColumnsByRange(Some( + data_column_sidecar.clone(), + )), + id: request_id, + }); + } + } + } + } + Err(e) => { + error!( + self.log, + "Error fetching data columns block root"; + "request" => ?req, + "peer" => %peer_id, + "block_root" => ?root, + "error" => ?e + ); + self.send_error_response( + peer_id, + RPCResponseErrorCode::ServerError, + "No data columns and failed fetching corresponding block".into(), + request_id, + ); + send_response = false; + break; + } + } + } + + let current_slot = self + .chain + .slot() + .unwrap_or_else(|_| self.chain.slot_clock.genesis_slot()); + + debug!( + self.log, + "DataColumnsByRange Response processed"; + "peer" => %peer_id, + "start_slot" => req.start_slot, + "current_slot" => current_slot, + "requested" => req.count, + "returned" => data_columns_sent + ); + + if send_response { + // send the stream terminator + self.send_network_message(NetworkMessage::SendResponse { + peer_id, + response: Response::DataColumnsByRange(None), + id: request_id, + }); + } + } + /// Helper function to ensure single item protocol always end with either a single chunk or an /// error fn terminate_response_single_item Response>( diff --git a/beacon_node/network/src/router.rs b/beacon_node/network/src/router.rs index 170ea595949..944f21a4700 100644 --- a/beacon_node/network/src/router.rs +++ b/beacon_node/network/src/router.rs @@ -220,6 +220,10 @@ impl Router { self.network_beacon_processor .send_data_columns_by_roots_request(peer_id, request_id, request), ), + Request::DataColumnsByRange(request) => self.handle_beacon_processor_send_result( + self.network_beacon_processor + .send_data_columns_by_range_request(peer_id, request_id, request), + ), Request::LightClientBootstrap(request) => self.handle_beacon_processor_send_result( self.network_beacon_processor .send_light_client_bootstrap_request(peer_id, request_id, request), @@ -265,6 +269,9 @@ impl Router { Response::DataColumnsByRoot(data_column) => { self.on_data_columns_by_root_response(peer_id, request_id, data_column); } + Response::DataColumnsByRange(data_column) => { + self.on_data_columns_by_range_response(peer_id, request_id, data_column); + } // Light client responses should not be received Response::LightClientBootstrap(_) | Response::LightClientOptimisticUpdate(_) @@ -669,6 +676,33 @@ impl Router { }); } + pub fn on_data_columns_by_range_response( + &mut self, + peer_id: PeerId, + request_id: RequestId, + data_column: Option>>, + ) { + trace!( + self.log, + "Received DataColumnsByRange Response"; + "peer" => %peer_id, + ); + + if let RequestId::Sync(id) = request_id { + self.send_to_sync(SyncMessage::RpcDataColumn { + peer_id, + request_id: id, + data_column, + seen_timestamp: timestamp_now(), + }); + } else { + crit!( + self.log, + "All data columns 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 35b5e73e4dc..d4143774a92 100644 --- a/beacon_node/network/src/sync/manager.rs +++ b/beacon_node/network/src/sync/manager.rs @@ -92,6 +92,8 @@ pub enum RequestId { DataColumnsByRoot(Id), /// Range request that is composed by both a block range request and a blob range request. RangeBlockAndBlobs { id: Id }, + /// Range request that is composed by both a block range request and a blob range request. + RangeBlockAndDataColumns { id: Id }, } #[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] @@ -406,6 +408,29 @@ impl SyncManager { } } } + RequestId::RangeBlockAndDataColumns { id } => { + if let Some(sender_id) = self.network.range_request_failed(id) { + match sender_id { + RangeRequestId::RangeSync { chain_id, batch_id } => { + self.range_sync.inject_error( + &mut self.network, + peer_id, + batch_id, + chain_id, + id, + ); + self.update_sync_state(); + } + RangeRequestId::BackfillSync { batch_id } => match self + .backfill_sync + .inject_error(&mut self.network, batch_id, &peer_id, id) + { + Ok(_) => {} + Err(_) => self.update_sync_state(), + }, + } + } + } } } @@ -936,6 +961,9 @@ impl SyncManager { RequestId::RangeBlockAndBlobs { id } => { todo!("TODO(das): handle sampling for range sync based on {id}"); } + RequestId::RangeBlockAndDataColumns { id } => { + todo!("TODO(das): handle sampling for range sync based on {id}"); + } } } diff --git a/consensus/types/src/data_column_sidecar.rs b/consensus/types/src/data_column_sidecar.rs index 564fd85bf72..7e038d81f5c 100644 --- a/consensus/types/src/data_column_sidecar.rs +++ b/consensus/types/src/data_column_sidecar.rs @@ -207,6 +207,27 @@ impl DataColumnSidecar { .as_ssz_bytes() .len() } + + pub fn empty() -> Self { + Self { + index: 0, + column: DataColumn::::default(), + kzg_commitments: VariableList::default(), + kzg_proofs: VariableList::default(), + signed_block_header: SignedBeaconBlockHeader { + message: BeaconBlockHeader::empty(), + signature: Signature::empty(), + }, + kzg_commitments_inclusion_proof: Default::default(), + } + } + + pub fn id(&self) -> DataColumnIdentifier { + DataColumnIdentifier { + block_root: self.block_root(), + index: self.index, + } + } } #[derive(Debug)] From a970f647a1d26ed94873496db985eb7f1d0f6815 Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Thu, 9 May 2024 13:27:14 +0900 Subject: [PATCH 14/76] PeerDAS custody lookup sync (#5684) * Implement custody sync * Lint * Fix tests * Fix rebase issue --- beacon_node/beacon_chain/src/beacon_chain.rs | 69 ++++- .../src/block_verification_types.rs | 73 ++++- .../src/data_availability_checker.rs | 127 +++++--- .../src/data_availability_checker/error.rs | 2 + .../overflow_lru_cache.rs | 77 +++-- .../src/data_column_verification.rs | 93 +++++- beacon_node/beacon_processor/src/lib.rs | 15 + beacon_node/beacon_processor/src/metrics.rs | 8 +- .../lighthouse_network/src/rpc/methods.rs | 4 + .../lighthouse_network/src/types/globals.rs | 1 + .../src/network_beacon_processor/mod.rs | 30 +- .../network_beacon_processor/sync_methods.rs | 53 ++++ .../network/src/sync/block_lookups/common.rs | 57 +++- .../network/src/sync/block_lookups/mod.rs | 83 ++--- .../sync/block_lookups/single_block_lookup.rs | 174 +++++------ .../network/src/sync/block_lookups/tests.rs | 221 +++++++++++-- .../src/sync/block_sidecar_coupling.rs | 26 +- beacon_node/network/src/sync/manager.rs | 73 +++-- .../network/src/sync/network_context.rs | 291 ++++++++++++++++-- .../src/sync/network_context/custody.rs | 285 +++++++++++++++++ .../network/src/sync/range_sync/batch.rs | 1 + beacon_node/network/src/sync/sampling.rs | 4 +- 22 files changed, 1424 insertions(+), 343 deletions(-) create mode 100644 beacon_node/network/src/sync/network_context/custody.rs diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index e6028b80e67..b9c1156c478 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -23,7 +23,9 @@ use crate::chain_config::ChainConfig; use crate::data_availability_checker::{ Availability, AvailabilityCheckError, AvailableBlock, DataAvailabilityChecker, }; -use crate::data_column_verification::{GossipDataColumnError, GossipVerifiedDataColumn}; +use crate::data_column_verification::{ + CustodyDataColumn, GossipDataColumnError, GossipVerifiedDataColumn, +}; use crate::early_attester_cache::EarlyAttesterCache; use crate::errors::{BeaconChainError as Error, BlockProductionError}; use crate::eth1_chain::{Eth1Chain, Eth1ChainBackend}; @@ -3074,6 +3076,33 @@ impl BeaconChain { self.remove_notified(&block_root, r) } + /// Cache the blobs in the processing cache, process it, then evict it from the cache if it was + /// imported or errors. + pub async fn process_rpc_custody_columns( + self: &Arc, + block_root: Hash256, + custody_columns: Vec>, + ) -> 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 + .canonical_head + .fork_choice_read_lock() + .contains_block(&block_root) + { + return Err(BlockError::BlockIsAlreadyKnown(block_root)); + } + + // TODO(das): custody column SSE event + // TODO(das): Why is the slot necessary here? + let slot = Slot::new(0); + + let r = self + .check_rpc_custody_columns_availability_and_import(slot, block_root, custody_columns) + .await; + self.remove_notified(&block_root, r) + } + /// Remove any block components from the *processing cache* if we no longer require them. If the /// block was imported full or erred, we no longer require them. fn remove_notified( @@ -3374,6 +3403,44 @@ impl BeaconChain { self.process_availability(slot, availability).await } + /// Checks if the provided columns can make any cached blocks available, and imports immediately + /// if so, otherwise caches the columns in the data availability checker. + async fn check_rpc_custody_columns_availability_and_import( + self: &Arc, + slot: Slot, + block_root: Hash256, + custody_columns: Vec>, + ) -> Result> { + // Need to scope this to ensure the lock is dropped before calling `process_availability` + // Even an explicit drop is not enough to convince the borrow checker. + { + let mut slashable_cache = self.observed_slashable.write(); + for header in custody_columns + .iter() + .map(|c| c.as_data_column().signed_block_header.clone()) + .unique() + { + if verify_header_signature::>(self, &header).is_ok() { + slashable_cache + .observe_slashable( + header.message.slot, + header.message.proposer_index, + block_root, + ) + .map_err(|e| BlockError::BeaconChainError(e.into()))?; + if let Some(slasher) = self.slasher.as_ref() { + slasher.accept_block_header(header); + } + } + } + } + let availability = self + .data_availability_checker + .put_rpc_custody_columns(block_root, custody_columns)?; + + self.process_availability(slot, availability).await + } + /// Imports a fully available block. Otherwise, returns `AvailabilityProcessingStatus::MissingComponents` /// /// An error is returned if the block was unable to be imported. It may be partially imported diff --git a/beacon_node/beacon_chain/src/block_verification_types.rs b/beacon_node/beacon_chain/src/block_verification_types.rs index 68019712395..1a0ff1d6bbc 100644 --- a/beacon_node/beacon_chain/src/block_verification_types.rs +++ b/beacon_node/beacon_chain/src/block_verification_types.rs @@ -2,7 +2,9 @@ use crate::blob_verification::{GossipBlobError, GossipVerifiedBlobList}; use crate::block_verification::BlockError; use crate::data_availability_checker::AvailabilityCheckError; pub use crate::data_availability_checker::{AvailableBlock, MaybeAvailableBlock}; -use crate::data_column_verification::{GossipDataColumnError, GossipVerifiedDataColumnList}; +use crate::data_column_verification::{ + CustodyDataColumn, CustodyDataColumnList, GossipDataColumnError, GossipVerifiedDataColumnList, +}; use crate::eth1_finalization_cache::Eth1FinalizationData; use crate::{get_block_root, GossipVerifiedBlock, PayloadVerificationOutcome}; use derivative::Derivative; @@ -11,7 +13,7 @@ use state_processing::ConsensusContext; use std::fmt::{Debug, Formatter}; use std::sync::Arc; use types::blob_sidecar::{self, BlobIdentifier, FixedBlobSidecarList}; -use types::data_column_sidecar::{self, DataColumnSidecarList}; +use types::data_column_sidecar::{self}; use types::{ BeaconBlockRef, BeaconState, BlindedPayload, BlobSidecarList, Epoch, EthSpec, Hash256, SignedBeaconBlock, SignedBeaconBlockHeader, Slot, @@ -52,7 +54,7 @@ impl RpcBlock { match &self.block { RpcBlockInner::Block(block) => block, RpcBlockInner::BlockAndBlobs(block, _) => block, - RpcBlockInner::BlockAndDataColumns(block, _) => block, + RpcBlockInner::BlockAndCustodyColumns(block, _) => block, } } @@ -60,7 +62,7 @@ impl RpcBlock { match &self.block { RpcBlockInner::Block(block) => block.clone(), RpcBlockInner::BlockAndBlobs(block, _) => block.clone(), - RpcBlockInner::BlockAndDataColumns(block, _) => block.clone(), + RpcBlockInner::BlockAndCustodyColumns(block, _) => block.clone(), } } @@ -68,7 +70,7 @@ impl RpcBlock { match &self.block { RpcBlockInner::Block(_) => None, RpcBlockInner::BlockAndBlobs(_, blobs) => Some(blobs), - RpcBlockInner::BlockAndDataColumns(_, _) => None, + RpcBlockInner::BlockAndCustodyColumns(_, _) => None, } } } @@ -86,7 +88,10 @@ enum RpcBlockInner { BlockAndBlobs(Arc>, BlobSidecarList), /// This variant is used with parent lookups and by-range responses. It should have all /// requested data columns, all block roots matching for this block. - BlockAndDataColumns(Arc>, DataColumnSidecarList), + BlockAndCustodyColumns( + Arc>, + VariableList, ::DataColumnCount>, + ), } impl RpcBlock { @@ -144,6 +149,35 @@ impl RpcBlock { }) } + pub fn new_with_custody_columns( + block_root: Option, + block: Arc>, + custody_columns: Vec>, + ) -> Result { + let block_root = block_root.unwrap_or_else(|| get_block_root(&block)); + + if let Ok(block_commitments) = block.message().body().blob_kzg_commitments() { + // The number of required custody columns is out of scope here. + if !block_commitments.is_empty() && custody_columns.is_empty() { + return Err(AvailabilityCheckError::MissingCustodyColumns); + } + } + // Treat empty blob lists as if they are missing. + let inner = if custody_columns.is_empty() { + RpcBlockInner::BlockAndCustodyColumns( + block, + VariableList::new(custody_columns) + .expect("TODO(das): custody vec should never exceed len"), + ) + } else { + RpcBlockInner::Block(block) + }; + Ok(Self { + block_root, + block: inner, + }) + } + pub fn new_from_fixed( block_root: Hash256, block: Arc>, @@ -168,27 +202,27 @@ impl RpcBlock { Hash256, Arc>, Option>, - Option>, + Option>, ) { let block_root = self.block_root(); match self.block { RpcBlockInner::Block(block) => (block_root, block, None, None), RpcBlockInner::BlockAndBlobs(block, blobs) => (block_root, block, Some(blobs), None), - RpcBlockInner::BlockAndDataColumns(block, data_columns) => { + RpcBlockInner::BlockAndCustodyColumns(block, data_columns) => { (block_root, block, None, Some(data_columns)) } } } pub fn n_blobs(&self) -> usize { match &self.block { - RpcBlockInner::Block(_) | RpcBlockInner::BlockAndDataColumns(_, _) => 0, + RpcBlockInner::Block(_) | RpcBlockInner::BlockAndCustodyColumns(_, _) => 0, RpcBlockInner::BlockAndBlobs(_, blobs) => blobs.len(), } } pub fn n_data_columns(&self) -> usize { match &self.block { RpcBlockInner::Block(_) | RpcBlockInner::BlockAndBlobs(_, _) => 0, - RpcBlockInner::BlockAndDataColumns(_, data_columns) => data_columns.len(), + RpcBlockInner::BlockAndCustodyColumns(_, data_columns) => data_columns.len(), } } } @@ -545,7 +579,20 @@ impl AsBlock for AvailableBlock { let inner = match (blobs_opt, data_columns_opt) { (None, None) => RpcBlockInner::Block(block), (Some(blobs), _) => RpcBlockInner::BlockAndBlobs(block, blobs), - (_, Some(data_columns)) => RpcBlockInner::BlockAndDataColumns(block, data_columns), + (_, Some(data_columns)) => RpcBlockInner::BlockAndCustodyColumns( + block, + VariableList::new( + data_columns + .into_iter() + // TODO(das): This is an ugly hack that should be removed. After updating + // store types to handle custody data columns this should not be required. + // It's okay-ish because available blocks must have all the required custody + // columns. + .map(|d| CustodyDataColumn::from_asserted_custody(d)) + .collect(), + ) + .expect("data column list is within bounds"), + ), }; RpcBlock { block_root, @@ -577,14 +624,14 @@ impl AsBlock for RpcBlock { match &self.block { RpcBlockInner::Block(block) => block, RpcBlockInner::BlockAndBlobs(block, _) => block, - RpcBlockInner::BlockAndDataColumns(block, _) => block, + RpcBlockInner::BlockAndCustodyColumns(block, _) => block, } } fn block_cloned(&self) -> Arc> { match &self.block { RpcBlockInner::Block(block) => block.clone(), RpcBlockInner::BlockAndBlobs(block, _) => block.clone(), - RpcBlockInner::BlockAndDataColumns(block, _) => block.clone(), + RpcBlockInner::BlockAndCustodyColumns(block, _) => block.clone(), } } fn canonical_root(&self) -> Hash256 { diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index 6c8afaa01a6..202e733cc13 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::{BeaconChain, BeaconChainTypes, BeaconStore}; use kzg::Kzg; use slog::{debug, error, o, Logger}; use slot_clock::SlotClock; +use ssz_types::VariableList; use std::fmt; use std::fmt::Debug; use std::num::NonZeroUsize; @@ -22,7 +23,10 @@ mod error; mod overflow_lru_cache; mod state_lru_cache; -use crate::data_column_verification::{verify_kzg_for_data_column_list, GossipVerifiedDataColumn}; +use crate::data_column_verification::{ + verify_kzg_for_data_column_list, CustodyDataColumn, GossipVerifiedDataColumn, + KzgVerifiedCustodyDataColumn, +}; pub use error::{Error as AvailabilityCheckError, ErrorCategory as AvailabilityCheckErrorCategory}; use types::data_column_sidecar::{DataColumnIdentifier, DataColumnSidecarList}; use types::non_zero_usize::new_non_zero_usize; @@ -45,7 +49,6 @@ pub struct DataAvailabilityChecker { availability_cache: Arc>, slot_clock: T::SlotClock, kzg: Option>, - log: Logger, spec: ChainSpec, } @@ -98,7 +101,6 @@ impl DataAvailabilityChecker { Ok(Self { availability_cache: Arc::new(overflow_cache), slot_clock, - log: log.clone(), kzg, spec, }) @@ -133,6 +135,15 @@ impl DataAvailabilityChecker { }) } + /// Return the set of imported custody column indexes for `block_root`. Returns None if there is + /// no block component for `block_root`. + pub fn imported_custody_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()) + }) + } + /// Get a blob from the availability cache. pub fn get_blob( &self, @@ -173,6 +184,27 @@ 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. + pub fn put_rpc_custody_columns( + &self, + block_root: Hash256, + custody_columns: Vec>, + ) -> Result, AvailabilityCheckError> { + let Some(kzg) = self.kzg.as_ref() else { + return Err(AvailabilityCheckError::KzgNotInitialized); + }; + + // TODO(das): report which column is invalid for proper peer scoring + let verified_custody_columns = custody_columns + .into_iter() + .map(|c| KzgVerifiedCustodyDataColumn::new(c, kzg).map_err(AvailabilityCheckError::Kzg)) + .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. @@ -195,10 +227,14 @@ impl DataAvailabilityChecker { &self, gossip_data_column: GossipVerifiedDataColumn, ) -> Result, AvailabilityCheckError> { - self.availability_cache.put_kzg_verified_data_columns( - gossip_data_column.block_root(), - vec![gossip_data_column.into_inner()], - ) + let block_root = gossip_data_column.block_root(); + + // TODO(das): ensure that our custody requirements include this column + let custody_column = + KzgVerifiedCustodyDataColumn::from_asserted_custody(gossip_data_column.into_inner()); + + self.availability_cache + .put_kzg_verified_data_columns(block_root, vec![custody_column]) } /// Check if we have all the blobs for a block. Returns `Availability` which has information @@ -253,7 +289,16 @@ impl DataAvailabilityChecker { block, blobs: None, blobs_available_timestamp: None, - data_columns, + // TODO(das): update store type to prevent this conversion + data_columns: Some( + VariableList::new( + data_column_list + .into_iter() + .map(|d| d.clone_arc()) + .collect(), + ) + .expect("data column list is within bounds"), + ), })) } else { Ok(MaybeAvailableBlock::AvailabilityPending { block_root, block }) @@ -322,7 +367,13 @@ impl DataAvailabilityChecker { block, blobs: None, blobs_available_timestamp: None, - data_columns, + // TODO(das): update store type to prevent this conversion + data_columns: data_columns.map(|data_columns| { + VariableList::new( + data_columns.into_iter().map(|d| d.into_inner()).collect(), + ) + .expect("data column list is within bounds") + }), }) } else { MaybeAvailableBlock::AvailabilityPending { block_root, block } @@ -344,20 +395,26 @@ impl DataAvailabilityChecker { } /// Determines the blob requirements for a block. If the block is pre-deneb, no blobs are required. - /// If the block's epoch is from prior to the data availability boundary, no blobs are required. + /// If the epoch is from prior to the data availability boundary, no blobs are required. + pub fn blobs_required_for_epoch(&self, epoch: Epoch) -> bool { + self.da_check_required_for_epoch(epoch) && !self.is_peer_das_enabled_for_epoch(epoch) + } + + /// Determines the data column requirements for an epoch. + /// - If the epoch is pre-peerdas, no data columns are required. + /// - If the epoch is from prior to the data availability boundary, no data columns are required. + pub fn data_columns_required_for_epoch(&self, epoch: Epoch) -> bool { + self.da_check_required_for_epoch(epoch) && self.is_peer_das_enabled_for_epoch(epoch) + } + + /// See `Self::blobs_required_for_epoch` fn blobs_required_for_block(&self, block: &SignedBeaconBlock) -> bool { - block.num_expected_blobs() > 0 - && self.da_check_required_for_epoch(block.epoch()) - && !self.is_peer_das_enabled_for_epoch(block.epoch()) + block.num_expected_blobs() > 0 && self.blobs_required_for_epoch(block.epoch()) } - /// Determines the data column requirements for a block. - /// - If the block is pre-peerdas, no data columns are required. - /// - If the block's epoch is from prior to the data availability boundary, no data columns are required. + /// See `Self::data_columns_required_for_epoch` fn data_columns_required_for_block(&self, block: &SignedBeaconBlock) -> bool { - block.num_expected_blobs() > 0 - && self.da_check_required_for_epoch(block.epoch()) - && self.is_peer_das_enabled_for_epoch(block.epoch()) + block.num_expected_blobs() > 0 && self.data_columns_required_for_epoch(block.epoch()) } /// Returns true if the given epoch is greater than or equal to the `PEER_DAS_EPOCH`. @@ -370,18 +427,14 @@ impl DataAvailabilityChecker { /// 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.spec.deneb_fork_epoch.and_then(|fork_epoch| { - self.slot_clock - .now() - .map(|slot| slot.epoch(T::EthSpec::slots_per_epoch())) - .map(|current_epoch| { - std::cmp::max( - fork_epoch, - current_epoch - .saturating_sub(self.spec.min_epochs_for_blob_sidecars_requests), - ) - }) - }) + let fork_epoch = self.spec.deneb_fork_epoch?; + let current_slot = self.slot_clock.now()?; + Some(std::cmp::max( + fork_epoch, + current_slot + .epoch(T::EthSpec::slots_per_epoch()) + .saturating_sub(self.spec.min_epochs_for_blob_sidecars_requests), + )) } /// Returns true if the given epoch lies within the da boundary and false otherwise. @@ -390,18 +443,6 @@ impl DataAvailabilityChecker { .map_or(false, |da_epoch| block_epoch >= da_epoch) } - pub fn da_check_required_for_current_epoch(&self) -> bool { - let Some(current_slot) = self.slot_clock.now_or_genesis() else { - error!( - self.log, - "Failed to read slot clock when checking for missing blob ids" - ); - return false; - }; - - self.da_check_required_for_epoch(current_slot.epoch(T::EthSpec::slots_per_epoch())) - } - /// Returns `true` if the current epoch is greater than or equal to the `Deneb` epoch. pub fn is_deneb(&self) -> bool { self.slot_clock.now().map_or(false, |slot| { 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 1a61d28608c..79793d6dc29 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker/error.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker/error.rs @@ -14,6 +14,7 @@ pub enum Error { Unexpected, SszTypes(ssz_types::Error), MissingBlobs, + MissingCustodyColumns, BlobIndexInvalid(u64), DataColumnIndexInvalid(u64), StoreError(store::Error), @@ -38,6 +39,7 @@ impl Error { Error::KzgNotInitialized | Error::SszTypes(_) | Error::MissingBlobs + | Error::MissingCustodyColumns | Error::StoreError(_) | Error::DecodeError(_) | Error::Unexpected diff --git a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs index c511d2c1cc2..9d62499711c 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs @@ -34,7 +34,7 @@ use crate::block_verification_types::{ AvailabilityPendingExecutedBlock, AvailableBlock, AvailableExecutedBlock, }; use crate::data_availability_checker::{Availability, AvailabilityCheckError}; -use crate::data_column_verification::KzgVerifiedDataColumn; +use crate::data_column_verification::{KzgVerifiedCustodyDataColumn, KzgVerifiedDataColumn}; use crate::store::{DBColumn, KeyValueStore}; use crate::BeaconChainTypes; use lru::LruCache; @@ -47,7 +47,7 @@ use std::num::NonZeroUsize; use std::{collections::HashSet, sync::Arc}; use types::blob_sidecar::BlobIdentifier; use types::data_column_sidecar::DataColumnIdentifier; -use types::{BlobSidecar, ChainSpec, DataColumnSidecar, Epoch, EthSpec, Hash256}; +use types::{BlobSidecar, ChainSpec, ColumnIndex, DataColumnSidecar, Epoch, EthSpec, Hash256}; /// This represents the components of a partially available block /// @@ -59,7 +59,7 @@ use types::{BlobSidecar, ChainSpec, DataColumnSidecar, Epoch, EthSpec, Hash256}; pub struct PendingComponents { pub block_root: Hash256, pub verified_blobs: FixedVector>, E::MaxBlobsPerBlock>, - pub verified_data_columns: VariableList, E::DataColumnCount>, + pub verified_data_columns: VariableList, E::DataColumnCount>, pub executed_block: Option>, } @@ -85,17 +85,11 @@ impl PendingComponents { pub fn get_cached_data_column( &self, data_column_index: u64, - ) -> Option<&KzgVerifiedDataColumn> { + ) -> Option>> { self.verified_data_columns .iter() - .find(|d| d.data_column_index() == data_column_index) - } - - /// Returns an immutable reference to the list of cached data columns. - pub fn get_cached_data_columns( - &self, - ) -> &VariableList, E::DataColumnCount> { - &self.verified_data_columns + .find(|d| d.index() == data_column_index) + .map(|d| d.clone_arc()) } /// Returns a mutable reference to the cached block. @@ -110,13 +104,6 @@ impl PendingComponents { &mut self.verified_blobs } - /// Returns a mutable reference to the fixed vector of cached data columns. - pub fn get_cached_data_columns_mut( - &mut self, - ) -> &mut VariableList, E::DataColumnCount> { - &mut self.verified_data_columns - } - /// Checks if a blob exists at the given index in the cache. /// /// Returns: @@ -134,10 +121,8 @@ impl PendingComponents { /// Returns: /// - `true` if a data column for the given index exists. /// - `false` otherwise. - fn data_column_exists(&self, data_colum_index: u64) -> bool { - self.get_cached_data_columns() - .iter() - .any(|d| d.data_column_index() == data_colum_index) + fn data_column_exists(&self, data_column_index: u64) -> bool { + self.get_cached_data_column(data_column_index).is_some() } /// Returns the number of blobs that are expected to be present. Returns `None` if we don't have a @@ -157,7 +142,15 @@ impl PendingComponents { /// Returns the number of data columns that have been received and are stored in the cache. pub fn num_received_data_columns(&self) -> usize { - self.get_cached_data_columns().iter().count() + self.verified_data_columns.iter().count() + } + + /// 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 a block into the cache. @@ -208,13 +201,13 @@ impl PendingComponents { } /// Merges a given set of data columns into the cache. - fn merge_data_columns>>( + fn merge_data_columns>>( &mut self, kzg_verified_data_columns: I, ) -> Result<(), AvailabilityCheckError> { for data_column in kzg_verified_data_columns { // TODO(das): Add equivalent checks for data columns if necessary - if !self.data_column_exists(data_column.data_column_index()) { + if !self.data_column_exists(data_column.index()) { self.verified_data_columns.push(data_column)?; } } @@ -325,13 +318,6 @@ impl PendingComponents { }; let verified_blobs = VariableList::new(verified_blobs)?; - // TODO(das) Do we need a check here for number of expected custody columns? - let verified_data_columns = verified_data_columns - .into_iter() - .map(|d| d.to_data_column()) - .collect::>() - .into(); - let executed_block = recover(diet_executed_block)?; let AvailabilityPendingExecutedBlock { @@ -345,7 +331,17 @@ impl PendingComponents { block, blobs: Some(verified_blobs), blobs_available_timestamp, - data_columns: Some(verified_data_columns), + // TODO(das) Do we need a check here for number of expected custody columns? + // TODO(das): Update store types to prevent this conversion + data_columns: Some( + VariableList::new( + verified_data_columns + .into_iter() + .map(|d| d.into_inner()) + .collect(), + ) + .expect("data column list is within bounds"), + ), }; Ok(Availability::Available(Box::new( AvailableExecutedBlock::new(available_block, import_data, payload_verification_outcome), @@ -468,7 +464,7 @@ impl OverflowStore { for data_column in pending_components.verified_data_columns.into_iter() { let key = OverflowKey::from_data_column_id::(DataColumnIdentifier { block_root, - index: data_column.data_column_index(), + index: data_column.index(), })?; self.0.hot_db.put_bytes( @@ -513,11 +509,10 @@ impl OverflowStore { } OverflowKey::DataColumn(_, _index) => { let data_column = - KzgVerifiedDataColumn::from_ssz_bytes(value_bytes.as_slice())?; + KzgVerifiedCustodyDataColumn::from_ssz_bytes(value_bytes.as_slice())?; maybe_pending_components .get_or_insert_with(|| PendingComponents::empty(block_root)) - .get_cached_data_columns_mut() - .push(data_column)?; + .merge_data_columns(vec![data_column])?; } } } @@ -633,9 +628,7 @@ impl Critical { data_column_id: &DataColumnIdentifier, ) -> Result>>, AvailabilityCheckError> { if let Some(pending_components) = self.in_memory.peek(&data_column_id.block_root) { - Ok(pending_components - .get_cached_data_column(data_column_id.index) - .map(|data_column| data_column.clone_data_column())) + Ok(pending_components.get_cached_data_column(data_column_id.index)) } else { Ok(None) } @@ -811,7 +804,7 @@ impl OverflowLRUCache { } pub fn put_kzg_verified_data_columns< - I: IntoIterator>, + I: IntoIterator>, >( &self, block_root: Hash256, diff --git a/beacon_node/beacon_chain/src/data_column_verification.rs b/beacon_node/beacon_chain/src/data_column_verification.rs index 7f1338bdb58..136ca037d0b 100644 --- a/beacon_node/beacon_chain/src/data_column_verification.rs +++ b/beacon_node/beacon_chain/src/data_column_verification.rs @@ -131,15 +131,15 @@ impl GossipVerifiedDataColumn { } pub fn slot(&self) -> Slot { - self.data_column.data_column.slot() + self.data_column.data.slot() } pub fn index(&self) -> ColumnIndex { - self.data_column.data_column.index + self.data_column.data.index } pub fn signed_block_header(&self) -> SignedBeaconBlockHeader { - self.data_column.data_column.signed_block_header.clone() + self.data_column.data.signed_block_header.clone() } pub fn into_inner(self) -> KzgVerifiedDataColumn { @@ -152,7 +152,7 @@ impl GossipVerifiedDataColumn { #[derivative(PartialEq, Eq)] #[ssz(struct_behaviour = "transparent")] pub struct KzgVerifiedDataColumn { - data_column: Arc>, + data: Arc>, } impl KzgVerifiedDataColumn { @@ -160,18 +160,91 @@ impl KzgVerifiedDataColumn { verify_kzg_for_data_column(data_column, kzg) } pub fn to_data_column(self) -> Arc> { - self.data_column + self.data } pub fn as_data_column(&self) -> &DataColumnSidecar { - &self.data_column + &self.data } /// This is cheap as we're calling clone on an Arc pub fn clone_data_column(&self) -> Arc> { - self.data_column.clone() + self.data.clone() } pub fn data_column_index(&self) -> u64 { - self.data_column.index + self.data.index + } +} + +pub type CustodyDataColumnList = + VariableList, ::DataColumnCount>; + +/// Data column that we must custody +#[derive(Debug, Derivative, Clone, Encode, Decode)] +#[derivative(PartialEq, Eq, Hash(bound = "E: EthSpec"))] +#[ssz(struct_behaviour = "transparent")] +pub struct CustodyDataColumn { + data: Arc>, +} + +impl CustodyDataColumn { + /// Mark a column as custody column. Caller must ensure that our current custody requirements + /// include this column + pub fn from_asserted_custody(data: Arc>) -> Self { + Self { data } + } + + pub fn into_inner(self) -> Arc> { + self.data + } + pub fn as_data_column(&self) -> &DataColumnSidecar { + &self.data + } + /// This is cheap as we're calling clone on an Arc + pub fn clone_arc(&self) -> Arc> { + self.data.clone() + } + pub fn index(&self) -> u64 { + self.data.index + } +} + +/// Data column that we must custody and has completed kzg verification +#[derive(Debug, Derivative, Clone, Encode, Decode)] +#[derivative(PartialEq, Eq)] +#[ssz(struct_behaviour = "transparent")] +pub struct KzgVerifiedCustodyDataColumn { + data: Arc>, +} + +impl KzgVerifiedCustodyDataColumn { + /// Mark a column as custody column. Caller must ensure that our current custody requirements + /// include this column + pub fn from_asserted_custody(kzg_verified: KzgVerifiedDataColumn) -> Self { + Self { + data: kzg_verified.to_data_column(), + } + } + + /// Verify a column already marked as custody column + pub fn new(data_column: CustodyDataColumn, _kzg: &Kzg) -> Result { + // TODO(das): verify kzg proof + Ok(Self { + data: data_column.data, + }) + } + + pub fn into_inner(self) -> Arc> { + self.data + } + pub fn as_data_column(&self) -> &DataColumnSidecar { + &self.data + } + /// This is cheap as we're calling clone on an Arc + pub fn clone_arc(&self) -> Arc> { + self.data.clone() + } + pub fn index(&self) -> u64 { + self.data.index } } @@ -189,7 +262,7 @@ pub fn verify_kzg_for_data_column( // data_column.kzg_commitment, // data_column.kzg_proof, // )?; - Ok(KzgVerifiedDataColumn { data_column }) + Ok(KzgVerifiedDataColumn { data: data_column }) } /// Complete kzg verification for a list of `DataColumnSidecar`s. @@ -202,7 +275,7 @@ pub fn verify_kzg_for_data_column_list<'a, E: EthSpec, I>( _kzg: &'a Kzg, ) -> Result<(), KzgError> where - I: Iterator>>, + I: Iterator>, { // TODO(das): implement kzg verification Ok(()) diff --git a/beacon_node/beacon_processor/src/lib.rs b/beacon_node/beacon_processor/src/lib.rs index c8f95046427..cc6ed245d68 100644 --- a/beacon_node/beacon_processor/src/lib.rs +++ b/beacon_node/beacon_processor/src/lib.rs @@ -162,6 +162,7 @@ const MAX_RPC_BLOCK_QUEUE_LEN: usize = 1_024; const MAX_RPC_BLOB_QUEUE_LEN: usize = 1_024; /// TODO(das): Placeholder number +const MAX_RPC_CUSTODY_COLUMN_QUEUE_LEN: usize = 1000; const MAX_RPC_VERIFY_DATA_COLUMN_QUEUE_LEN: usize = 1000; const MAX_SAMPLING_RESULT_QUEUE_LEN: usize = 1000; @@ -264,6 +265,7 @@ pub const GOSSIP_LIGHT_CLIENT_OPTIMISTIC_UPDATE: &str = "light_client_optimistic pub const RPC_BLOCK: &str = "rpc_block"; pub const IGNORED_RPC_BLOCK: &str = "ignored_rpc_block"; pub const RPC_BLOBS: &str = "rpc_blob"; +pub const RPC_CUSTODY_COLUMN: &str = "rpc_custody_column"; pub const RPC_VERIFY_DATA_COLUMNS: &str = "rpc_verify_data_columns"; pub const SAMPLING_RESULT: &str = "sampling_result"; pub const CHAIN_SEGMENT: &str = "chain_segment"; @@ -652,6 +654,7 @@ pub enum Work { RpcBlobs { process_fn: AsyncFn, }, + RpcCustodyColumn(AsyncFn), RpcVerifyDataColumn(AsyncFn), SamplingResult(AsyncFn), IgnoredRpcBlock { @@ -701,6 +704,7 @@ impl Work { Work::GossipLightClientOptimisticUpdate(_) => GOSSIP_LIGHT_CLIENT_OPTIMISTIC_UPDATE, Work::RpcBlock { .. } => RPC_BLOCK, Work::RpcBlobs { .. } => RPC_BLOBS, + Work::RpcCustodyColumn { .. } => RPC_CUSTODY_COLUMN, Work::RpcVerifyDataColumn(_) => RPC_VERIFY_DATA_COLUMNS, Work::SamplingResult(_) => SAMPLING_RESULT, Work::IgnoredRpcBlock { .. } => IGNORED_RPC_BLOCK, @@ -865,6 +869,7 @@ impl BeaconProcessor { // Using a FIFO queue since blocks need to be imported sequentially. let mut rpc_block_queue = FifoQueue::new(MAX_RPC_BLOCK_QUEUE_LEN); let mut rpc_blob_queue = FifoQueue::new(MAX_RPC_BLOB_QUEUE_LEN); + let mut rpc_custody_column_queue = FifoQueue::new(MAX_RPC_CUSTODY_COLUMN_QUEUE_LEN); let mut rpc_verify_data_column_queue = FifoQueue::new(MAX_RPC_VERIFY_DATA_COLUMN_QUEUE_LEN); let mut sampling_result_queue = FifoQueue::new(MAX_SAMPLING_RESULT_QUEUE_LEN); let mut chain_segment_queue = FifoQueue::new(MAX_CHAIN_SEGMENT_QUEUE_LEN); @@ -1024,6 +1029,8 @@ impl BeaconProcessor { } else if let Some(item) = rpc_blob_queue.pop() { self.spawn_worker(item, idle_tx); // TODO(das): decide proper priorization for sampling columns + } else if let Some(item) = rpc_custody_column_queue.pop() { + self.spawn_worker(item, idle_tx); } else if let Some(item) = rpc_verify_data_column_queue.pop() { self.spawn_worker(item, idle_tx); } else if let Some(item) = sampling_result_queue.pop() { @@ -1326,6 +1333,9 @@ impl BeaconProcessor { rpc_block_queue.push(work, work_id, &self.log) } Work::RpcBlobs { .. } => rpc_blob_queue.push(work, work_id, &self.log), + Work::RpcCustodyColumn { .. } => { + rpc_custody_column_queue.push(work, work_id, &self.log) + } Work::RpcVerifyDataColumn(_) => { rpc_verify_data_column_queue.push(work, work_id, &self.log) } @@ -1431,6 +1441,10 @@ impl BeaconProcessor { &metrics::BEACON_PROCESSOR_RPC_BLOB_QUEUE_TOTAL, rpc_blob_queue.len() as i64, ); + metrics::set_gauge( + &metrics::BEACON_PROCESSOR_RPC_CUSTODY_COLUMN_QUEUE_TOTAL, + rpc_custody_column_queue.len() as i64, + ); metrics::set_gauge( &metrics::BEACON_PROCESSOR_RPC_VERIFY_DATA_COLUMN_QUEUE_TOTAL, rpc_verify_data_column_queue.len() as i64, @@ -1580,6 +1594,7 @@ impl BeaconProcessor { } => task_spawner.spawn_async(process_fn), Work::RpcBlock { process_fn } | Work::RpcBlobs { process_fn } + | Work::RpcCustodyColumn(process_fn) | Work::RpcVerifyDataColumn(process_fn) | Work::SamplingResult(process_fn) => task_spawner.spawn_async(process_fn), Work::IgnoredRpcBlock { process_fn } => task_spawner.spawn_blocking(process_fn), diff --git a/beacon_node/beacon_processor/src/metrics.rs b/beacon_node/beacon_processor/src/metrics.rs index 997d6c9039e..8b554ad8e27 100644 --- a/beacon_node/beacon_processor/src/metrics.rs +++ b/beacon_node/beacon_processor/src/metrics.rs @@ -86,10 +86,10 @@ lazy_static::lazy_static! { "beacon_processor_rpc_blob_queue_total", "Count of blobs from the rpc waiting to be verified." ); - // Rpc data columns. - pub static ref BEACON_PROCESSOR_RPC_DATA_COLUMNS_QUEUE_TOTAL: Result = try_create_int_gauge( - "beacon_processor_rpc_data_columns_total", - "Count of data columns from the rpc waiting to be verified." + // Rpc custody data columns. + pub static ref BEACON_PROCESSOR_RPC_CUSTODY_COLUMN_QUEUE_TOTAL: Result = try_create_int_gauge( + "beacon_processor_rpc_custody_column_queue_total", + "Count of custody columns from the rpc waiting to be imported." ); // Rpc verify data columns pub static ref BEACON_PROCESSOR_RPC_VERIFY_DATA_COLUMN_QUEUE_TOTAL: Result = try_create_int_gauge( diff --git a/beacon_node/lighthouse_network/src/rpc/methods.rs b/beacon_node/lighthouse_network/src/rpc/methods.rs index c5ea34b30dd..84add18ee05 100644 --- a/beacon_node/lighthouse_network/src/rpc/methods.rs +++ b/beacon_node/lighthouse_network/src/rpc/methods.rs @@ -407,6 +407,10 @@ impl DataColumnsByRootRequest { Self { data_column_ids } } + pub fn new_single(block_root: Hash256, index: ColumnIndex, spec: &ChainSpec) -> Self { + Self::new(vec![DataColumnIdentifier { block_root, index }], spec) + } + pub fn group_by_ordered_block_root(&self) -> Vec<(Hash256, Vec)> { let mut column_indexes_by_block = BTreeMap::>::new(); for request_id in self.data_column_ids.as_slice() { diff --git a/beacon_node/lighthouse_network/src/types/globals.rs b/beacon_node/lighthouse_network/src/types/globals.rs index ff2cb97d057..5dd74f25726 100644 --- a/beacon_node/lighthouse_network/src/types/globals.rs +++ b/beacon_node/lighthouse_network/src/types/globals.rs @@ -115,6 +115,7 @@ impl NetworkGlobals { pub fn custody_columns(&self, _epoch: Epoch) -> Result, &'static str> { let enr = self.local_enr(); let node_id = enr.node_id().raw().into(); + // TODO(das): cache this number at start-up to not make this fallible let custody_subnet_count = enr.custody_subnet_count::()?; Ok( DataColumnSubnetId::compute_custody_columns::(node_id, custody_subnet_count) diff --git a/beacon_node/network/src/network_beacon_processor/mod.rs b/beacon_node/network/src/network_beacon_processor/mod.rs index 5ce2f3feed2..55a07d78e5b 100644 --- a/beacon_node/network/src/network_beacon_processor/mod.rs +++ b/beacon_node/network/src/network_beacon_processor/mod.rs @@ -2,7 +2,9 @@ use crate::{ service::NetworkMessage, sync::{manager::BlockProcessType, SamplingId, SyncMessage}, }; -use beacon_chain::block_verification_types::RpcBlock; +use beacon_chain::{ + block_verification_types::RpcBlock, data_column_verification::CustodyDataColumn, +}; use beacon_chain::{builder::Witness, eth1_chain::CachingEth1Backend, BeaconChain}; use beacon_chain::{BeaconChainTypes, NotifyExecutionLayer}; use beacon_processor::{ @@ -478,8 +480,32 @@ impl NetworkBeaconProcessor { }) } + /// Create a new `Work` event for some custody columns. `process_rpc_custody_columns` reports + /// the result back to sync. + pub fn send_rpc_custody_columns( + self: &Arc, + block_root: Hash256, + custody_columns: Vec>, + seen_timestamp: Duration, + process_type: BlockProcessType, + ) -> Result<(), Error> { + let s = self.clone(); + self.try_send(BeaconWorkEvent { + drop_during_sync: false, + work: Work::RpcCustodyColumn(Box::pin(async move { + s.process_rpc_custody_columns( + block_root, + custody_columns, + seen_timestamp, + process_type, + ) + .await; + })), + }) + } + /// Create a new `Work` event for some data_columns from ReqResp - pub fn send_rpc_data_columns( + pub fn send_rpc_validate_data_columns( self: &Arc, block_root: Hash256, data_columns: Vec>>, 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 03fd30abb79..7c432a4866b 100644 --- a/beacon_node/network/src/network_beacon_processor/sync_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/sync_methods.rs @@ -8,6 +8,7 @@ use crate::sync::{ use beacon_chain::block_verification_types::{AsBlock, RpcBlock}; use beacon_chain::data_availability_checker::AvailabilityCheckError; use beacon_chain::data_availability_checker::MaybeAvailableBlock; +use beacon_chain::data_column_verification::CustodyDataColumn; use beacon_chain::{ validator_monitor::get_slot_delay_ms, AvailabilityProcessingStatus, BeaconChainError, BeaconChainTypes, BlockError, ChainSegmentResult, HistoricalBlockError, NotifyExecutionLayer, @@ -303,6 +304,58 @@ impl NetworkBeaconProcessor { }); } + pub async fn process_rpc_custody_columns( + self: Arc>, + block_root: Hash256, + custody_columns: Vec>, + _seen_timestamp: Duration, + process_type: BlockProcessType, + ) { + let result = self + .chain + .process_rpc_custody_columns(block_root, custody_columns) + .await; + + match &result { + Ok(AvailabilityProcessingStatus::Imported(hash)) => { + debug!( + self.log, + "Block components retrieved"; + "result" => "imported block and custody columns", + "block_hash" => %hash, + ); + self.chain.recompute_head_at_current_slot().await; + } + Ok(AvailabilityProcessingStatus::MissingComponents(_, _)) => { + debug!( + self.log, + "Missing components over rpc"; + "block_hash" => %block_root, + ); + } + Err(BlockError::BlockIsAlreadyKnown(_)) => { + debug!( + self.log, + "Custody columns have already been imported"; + "block_hash" => %block_root, + ); + } + Err(e) => { + warn!( + self.log, + "Error when importing rpc custody columns"; + "error" => ?e, + "block_hash" => %block_root, + ); + } + } + + self.send_sync_message(SyncMessage::BlockComponentProcessed { + process_type, + result: result.into(), + }); + } + /// Validate a list of data columns received from RPC requests pub async fn validate_rpc_data_columns( self: Arc>, diff --git a/beacon_node/network/src/sync/block_lookups/common.rs b/beacon_node/network/src/sync/block_lookups/common.rs index fa63e37c1b3..5491bd7120f 100644 --- a/beacon_node/network/src/sync/block_lookups/common.rs +++ b/beacon_node/network/src/sync/block_lookups/common.rs @@ -5,6 +5,7 @@ use crate::sync::block_lookups::{BlobRequestState, BlockRequestState, PeerId}; use crate::sync::manager::{BlockProcessType, Id, SLOT_IMPORT_TOLERANCE}; use crate::sync::network_context::SyncNetworkContext; use beacon_chain::block_verification_types::RpcBlock; +use beacon_chain::data_column_verification::CustodyDataColumn; use beacon_chain::BeaconChainTypes; use std::sync::Arc; use types::blob_sidecar::FixedBlobSidecarList; @@ -13,10 +14,13 @@ use types::SignedBeaconBlock; use super::single_block_lookup::DownloadResult; use super::SingleLookupId; +use super::single_block_lookup::CustodyRequestState; + #[derive(Debug, Copy, Clone)] pub enum ResponseType { Block, Blob, + CustodyColumn, } /// The maximum depth we will search for a parent block. In principle we should have sync'd any @@ -94,7 +98,7 @@ impl RequestState for BlockRequestState { value, block_root, seen_timestamp, - peer_id: _, + .. } = download_result; cx.send_block_for_processing( block_root, @@ -147,7 +151,7 @@ impl RequestState for BlobRequestState { value, block_root, seen_timestamp, - peer_id: _, + .. } = download_result; cx.send_blobs_for_processing( block_root, @@ -171,3 +175,52 @@ impl RequestState for BlobRequestState { &mut self.state } } + +impl RequestState for CustodyRequestState { + type VerifiedResponseType = Vec>; + + fn make_request( + &self, + id: Id, + // TODO(das): consider selecting peers that have custody but are in this set + _peer_id: PeerId, + downloaded_block_expected_blobs: Option, + cx: &mut SyncNetworkContext, + ) -> Result { + cx.custody_lookup_request(id, self.block_root, downloaded_block_expected_blobs) + .map_err(LookupRequestError::SendFailed) + } + + fn send_for_processing( + id: Id, + download_result: DownloadResult, + cx: &SyncNetworkContext, + ) -> Result<(), LookupRequestError> { + let DownloadResult { + value, + block_root, + seen_timestamp, + .. + } = download_result; + cx.send_custody_columns_for_processing( + block_root, + value, + seen_timestamp, + BlockProcessType::SingleCustodyColumn(id), + ) + .map_err(LookupRequestError::SendFailed) + } + + fn response_type() -> ResponseType { + ResponseType::CustodyColumn + } + fn request_state_mut(request: &mut SingleBlockLookup) -> &mut Self { + &mut request.custody_request_state + } + 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 3da2577114c..1b6f449ec37 100644 --- a/beacon_node/network/src/sync/block_lookups/mod.rs +++ b/beacon_node/network/src/sync/block_lookups/mod.rs @@ -2,12 +2,11 @@ use self::parent_chain::{compute_parent_chains, NodeChain}; pub use self::single_block_lookup::DownloadResult; use self::single_block_lookup::{LookupRequestError, LookupResult, SingleBlockLookup}; use super::manager::{BlockProcessType, BlockProcessingResult}; -use super::network_context::{RpcProcessingResult, SyncNetworkContext}; +use super::network_context::{LookupFailure, PeerGroup, SyncNetworkContext}; use crate::metrics; use crate::sync::block_lookups::common::{ResponseType, PARENT_DEPTH_TOLERANCE}; use crate::sync::block_lookups::parent_chain::find_oldest_fork_ancestor; use crate::sync::manager::Id; -use crate::sync::network_context::LookupFailure; use beacon_chain::block_verification_types::AsBlock; use beacon_chain::data_availability_checker::AvailabilityCheckErrorCategory; use beacon_chain::{AvailabilityProcessingStatus, BeaconChainTypes, BlockError}; @@ -15,7 +14,7 @@ pub use common::RequestState; use fnv::FnvHashMap; use lighthouse_network::{PeerAction, PeerId}; use lru_cache::LRUTimeCache; -pub use single_block_lookup::{BlobRequestState, BlockRequestState}; +pub use single_block_lookup::{BlobRequestState, BlockRequestState, CustodyRequestState}; use slog::{debug, error, warn, Logger}; use std::collections::hash_map::Entry; use std::sync::Arc; @@ -188,7 +187,7 @@ impl BlockLookups { .iter() .find(|(_, l)| l.block_root() == block_to_drop) { - for &peer_id in lookup.all_used_peers() { + for &peer_id in lookup.all_peers() { cx.report_peer( peer_id, PeerAction::LowToleranceError, @@ -306,11 +305,10 @@ impl BlockLookups { pub fn on_download_response>( &mut self, id: SingleLookupId, - peer_id: PeerId, - response: RpcProcessingResult, + response: Result<(R::VerifiedResponseType, PeerGroup, Duration), LookupFailure>, cx: &mut SyncNetworkContext, ) { - let result = self.on_download_response_inner::(id, peer_id, response, cx); + let result = self.on_download_response_inner::(id, response, cx); self.on_lookup_result(id, result, "download_response", cx); } @@ -318,16 +316,10 @@ impl BlockLookups { pub fn on_download_response_inner>( &mut self, id: SingleLookupId, - peer_id: PeerId, - response: RpcProcessingResult, + response: Result<(R::VerifiedResponseType, PeerGroup, Duration), LookupFailure>, cx: &mut SyncNetworkContext, ) -> Result { - // Downscore peer even if lookup is not known - // Only downscore lookup verify errors. RPC errors are downscored in the network handler. - if let Err(LookupFailure::LookupVerifyError(e)) = &response { - // Note: the error is displayed in full debug form on the match below - cx.report_peer(peer_id, PeerAction::LowToleranceError, e.into()); - } + // Note: do not downscore peers here for requests errors, SyncNetworkContext does it. let response_type = R::response_type(); let Some(lookup) = self.single_block_lookups.get_mut(&id) else { @@ -341,12 +333,12 @@ impl BlockLookups { let request_state = R::request_state_mut(lookup).get_state_mut(); match response { - Ok((response, seen_timestamp)) => { + Ok((response, peer_group, seen_timestamp)) => { debug!(self.log, "Received lookup download success"; "block_root" => ?block_root, "id" => id, - "peer_id" => %peer_id, + "peer_group" => ?peer_group, "response_type" => ?response_type, ); @@ -357,18 +349,19 @@ impl BlockLookups { value: response, block_root, seen_timestamp, - peer_id, + peer_group, })?; // continue_request will send for processing as the request state is AwaitingProcessing } Err(e) => { + // TODO(das): is it okay to not log the peer source of request failures? Then we + // should log individual requests failures in the SyncNetworkContext debug!(self.log, "Received lookup download failure"; "block_root" => ?block_root, "id" => id, - "peer_id" => %peer_id, "response_type" => ?response_type, - "error" => %e, + "error" => ?e, ); request_state.on_download_failure()?; @@ -407,6 +400,9 @@ impl BlockLookups { BlockProcessType::SingleBlob { id } => { self.on_processing_result_inner::>(id, result, cx) } + BlockProcessType::SingleCustodyColumn(id) => { + self.on_processing_result_inner::>(id, result, cx) + } }; self.on_lookup_result(process_type.id(), lookup_result, "processing_result", cx); } @@ -442,10 +438,9 @@ impl BlockLookups { Action::Continue } - BlockProcessingResult::Ok(AvailabilityProcessingStatus::MissingComponents( - _, - _block_root, - )) => { + BlockProcessingResult::Ok(AvailabilityProcessingStatus::MissingComponents { + .. + }) => { // `on_processing_success` is called here to ensure the request state is updated prior to checking // if both components have been processed. request_state.on_processing_success()?; @@ -454,16 +449,20 @@ impl BlockLookups { // wrong. If we already had both a block and blobs response processed, we should penalize the // blobs peer because they did not provide all blobs on the initial request. if lookup.both_components_processed() { + // TODO(das): extend to columns peers too if let Some(blob_peer) = lookup .blob_request_state .state .on_post_process_validation_failure()? { - cx.report_peer( - blob_peer, - PeerAction::MidToleranceError, - "sent_incomplete_blobs", - ); + // TODO(das): downscore only the peer that served the request for all blobs + for peer in blob_peer.all() { + cx.report_peer( + *peer, + PeerAction::MidToleranceError, + "sent_incomplete_blobs", + ); + } } } Action::Retry @@ -521,15 +520,21 @@ impl BlockLookups { } other => { debug!(self.log, "Invalid lookup component"; "block_root" => ?block_root, "component" => ?R::response_type(), "error" => ?other); - let peer_id = request_state.on_processing_failure()?; - cx.report_peer( - peer_id, - PeerAction::MidToleranceError, - match R::response_type() { - ResponseType::Block => "lookup_block_processing_failure", - ResponseType::Blob => "lookup_blobs_processing_failure", - }, - ); + let peer_group = request_state.on_processing_failure()?; + // TOOD(das): only downscore peer subgroup that provided the invalid proof + for peer in peer_group.all() { + cx.report_peer( + *peer, + PeerAction::MidToleranceError, + match R::response_type() { + ResponseType::Block => "lookup_block_processing_failure", + ResponseType::Blob => "lookup_blobs_processing_failure", + ResponseType::CustodyColumn => { + "lookup_custody_column_processing_failure" + } + }, + ); + } Action::Retry } @@ -544,7 +549,7 @@ impl BlockLookups { lookup.continue_requests(cx) } Action::ParentUnknown { parent_root } => { - let peers = lookup.all_available_peers().cloned().collect::>(); + let peers = lookup.all_peers().cloned().collect::>(); lookup.set_awaiting_parent(parent_root); debug!(self.log, "Marking lookup as awaiting parent"; "id" => lookup.id, "block_root" => ?block_root, "parent_root" => ?parent_root); self.search_parent_of_child(parent_root, block_root, &peers, cx); 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 a5729f39062..9020aea52e8 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 @@ -2,9 +2,9 @@ use super::common::ResponseType; use super::{BlockComponent, PeerId, SINGLE_BLOCK_LOOKUP_MAX_ATTEMPTS}; use crate::sync::block_lookups::common::RequestState; use crate::sync::block_lookups::Id; -use crate::sync::network_context::SyncNetworkContext; +use crate::sync::network_context::{PeerGroup, SyncNetworkContext}; +use beacon_chain::data_column_verification::CustodyDataColumn; use beacon_chain::BeaconChainTypes; -use itertools::Itertools; use rand::seq::IteratorRandom; use std::collections::HashSet; use std::fmt::Debug; @@ -47,8 +47,11 @@ pub struct SingleBlockLookup { pub id: Id, pub block_request_state: BlockRequestState, pub blob_request_state: BlobRequestState, + pub custody_request_state: CustodyRequestState, block_root: Hash256, awaiting_parent: Option, + /// Peers that claim to have imported this block + peers: HashSet, } impl SingleBlockLookup { @@ -60,10 +63,12 @@ impl SingleBlockLookup { ) -> Self { Self { id, - block_request_state: BlockRequestState::new(requested_block_root, peers), - blob_request_state: BlobRequestState::new(requested_block_root, peers), + block_request_state: BlockRequestState::new(requested_block_root), + blob_request_state: BlobRequestState::new(requested_block_root), + custody_request_state: CustodyRequestState::new(requested_block_root), block_root: requested_block_root, awaiting_parent, + peers: peers.iter().copied().collect(), } } @@ -110,22 +115,9 @@ impl SingleBlockLookup { self.block_root() == block_root } - /// Get all unique used peers across block and blob requests. - pub fn all_used_peers(&self) -> impl Iterator + '_ { - self.block_request_state - .state - .get_used_peers() - .chain(self.blob_request_state.state.get_used_peers()) - .unique() - } - - /// Get all unique available peers across block and blob requests. - pub fn all_available_peers(&self) -> impl Iterator + '_ { - self.block_request_state - .state - .get_available_peers() - .chain(self.blob_request_state.state.get_available_peers()) - .unique() + /// Get all unique peers across block and blob requests. + pub fn all_peers(&self) -> impl Iterator + '_ { + self.peers.iter() } /// Makes progress on all requests of this lookup. Any error is not recoverable and must result @@ -137,12 +129,14 @@ impl SingleBlockLookup { // TODO: Check what's necessary to download, specially for blobs self.continue_request::>(cx)?; self.continue_request::>(cx)?; + self.continue_request::>(cx)?; // If all components of this lookup are already processed, there will be no future events // that can make progress so it must be dropped. Consider the lookup completed. // This case can happen if we receive the components from gossip during a retry. if self.block_request_state.state.is_processed() && self.blob_request_state.state.is_processed() + && self.custody_request_state.state.is_processed() { Ok(LookupResult::Completed) } else { @@ -151,22 +145,27 @@ impl SingleBlockLookup { } /// Potentially makes progress on this request if it's in a progress-able state - pub fn continue_request>( + fn continue_request>( &mut self, cx: &mut SyncNetworkContext, ) -> Result<(), LookupRequestError> { let id = self.id; let awaiting_parent = self.awaiting_parent.is_some(); - let downloaded_block_expected_blobs = self - .block_request_state - .state - .peek_downloaded_data() - .map(|block| block.num_expected_blobs()); let block_is_processed = self.block_request_state.state.is_processed(); let request = R::request_state_mut(self); // Attempt to progress awaiting downloads if request.get_state().is_awaiting_download() { + let downloaded_block_expected_blobs = self + .block_request_state + .state + .peek_downloaded_data() + .map(|block| block.num_expected_blobs()); + let peer_id = self + .get_rand_available_peer() + .ok_or(LookupRequestError::NoPeers)?; + let request = R::request_state_mut(self); + // Verify the current request has not exceeded the maximum number of attempts. let request_state = request.get_state(); if request_state.failed_attempts() >= SINGLE_BLOCK_LOOKUP_MAX_ATTEMPTS { @@ -174,11 +173,6 @@ impl SingleBlockLookup { return Err(LookupRequestError::TooManyAttempts { cannot_process }); } - let peer_id = request - .get_state_mut() - .use_rand_available_peer() - .ok_or(LookupRequestError::NoPeers)?; - // make_request returns true only if a request needs to be made if request.make_request(id, peer_id, downloaded_block_expected_blobs, cx)? { request.get_state_mut().on_download_start()?; @@ -205,22 +199,30 @@ impl SingleBlockLookup { /// Add peer to all request states. The peer must be able to serve this request. /// Returns true if the peer was newly inserted into some request state. pub fn add_peer(&mut self, peer_id: PeerId) -> bool { - let inserted_block = self.block_request_state.state.add_peer(&peer_id); - let inserted_blob = self.blob_request_state.state.add_peer(&peer_id); - inserted_block || inserted_blob + self.peers.insert(peer_id) } /// Returns true if the block has already been downloaded. pub fn both_components_processed(&self) -> bool { self.block_request_state.state.is_processed() && self.blob_request_state.state.is_processed() + && self.custody_request_state.state.is_processed() } /// Remove peer from available peers. Return true if there are no more available peers and all /// requests are not expecting any future event (AwaitingDownload). pub fn remove_peer(&mut self, peer_id: &PeerId) -> bool { - self.block_request_state.state.remove_peer(peer_id) - && self.blob_request_state.state.remove_peer(peer_id) + self.peers.remove(peer_id); + + self.peers.is_empty() + && self.block_request_state.state.is_awaiting_download() + && self.blob_request_state.state.is_awaiting_download() + && self.custody_request_state.state.is_awaiting_download() + } + + /// Selects a random peer from available peers if any, inserts it in used peers and returns it. + pub fn get_rand_available_peer(&mut self) -> Option { + self.peers.iter().choose(&mut rand::thread_rng()).copied() } } @@ -231,10 +233,25 @@ pub struct BlobRequestState { } impl BlobRequestState { - pub fn new(block_root: Hash256, peer_source: &[PeerId]) -> Self { + pub fn new(block_root: Hash256) -> Self { Self { block_root, - state: SingleLookupRequestState::new(peer_source), + state: SingleLookupRequestState::new(), + } + } +} + +/// The state of the blob request component of a `SingleBlockLookup`. +pub struct CustodyRequestState { + pub block_root: Hash256, + pub state: SingleLookupRequestState>>, +} + +impl CustodyRequestState { + pub fn new(block_root: Hash256) -> Self { + Self { + block_root, + state: SingleLookupRequestState::new(), } } } @@ -246,40 +263,36 @@ pub struct BlockRequestState { } impl BlockRequestState { - pub fn new(block_root: Hash256, peers: &[PeerId]) -> Self { + pub fn new(block_root: Hash256) -> Self { Self { requested_block_root: block_root, - state: SingleLookupRequestState::new(peers), + state: SingleLookupRequestState::new(), } } } -#[derive(Debug, PartialEq, Eq, Clone)] +#[derive(Debug, Clone)] pub struct DownloadResult { pub value: T, pub block_root: Hash256, pub seen_timestamp: Duration, - pub peer_id: PeerId, + pub peer_group: PeerGroup, } -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug)] pub enum State { AwaitingDownload, Downloading, AwaitingProcess(DownloadResult), Processing(DownloadResult), - Processed(Option), + Processed(Option), } /// Object representing the state of a single block or blob lookup request. -#[derive(PartialEq, Eq, Debug)] +#[derive(Debug)] pub struct SingleLookupRequestState { /// State of this request. state: State, - /// Peers that should have this block or blob. - available_peers: HashSet, - /// Peers from which we have requested this block. - used_peers: HashSet, /// How many times have we attempted to process this block or blob. failed_processing: u8, /// How many times have we attempted to download this block or blob. @@ -287,16 +300,9 @@ pub struct SingleLookupRequestState { } impl SingleLookupRequestState { - pub fn new(peers: &[PeerId]) -> Self { - let mut available_peers = HashSet::default(); - for peer in peers.iter().copied() { - available_peers.insert(peer); - } - + pub fn new() -> Self { Self { state: State::AwaitingDownload, - available_peers, - used_peers: HashSet::default(), failed_processing: 0, failed_downloading: 0, } @@ -414,13 +420,13 @@ impl SingleLookupRequestState { } /// Registers a failure in processing a block. - pub fn on_processing_failure(&mut self) -> Result { + pub fn on_processing_failure(&mut self) -> Result { match &self.state { State::Processing(result) => { - let peer_id = result.peer_id; + let peers_source = result.peer_group.clone(); self.failed_processing = self.failed_processing.saturating_add(1); self.state = State::AwaitingDownload; - Ok(peer_id) + Ok(peers_source) } other => Err(LookupRequestError::BadState(format!( "Bad state on_processing_failure expected Processing got {other}" @@ -428,12 +434,12 @@ impl SingleLookupRequestState { } } - pub fn on_processing_success(&mut self) -> Result { + pub fn on_processing_success(&mut self) -> Result { match &self.state { State::Processing(result) => { - let peer_id = result.peer_id; - self.state = State::Processed(Some(peer_id)); - Ok(peer_id) + let peer_group = result.peer_group.clone(); + self.state = State::Processed(Some(peer_group.clone())); + Ok(peer_group) } other => Err(LookupRequestError::BadState(format!( "Bad state on_processing_success expected Processing got {other}" @@ -443,13 +449,13 @@ impl SingleLookupRequestState { pub fn on_post_process_validation_failure( &mut self, - ) -> Result, LookupRequestError> { + ) -> Result, LookupRequestError> { match &self.state { - State::Processed(peer_id) => { - let peer_id = *peer_id; + State::Processed(peer_group) => { + let peer_group = peer_group.clone(); self.failed_processing = self.failed_processing.saturating_add(1); self.state = State::AwaitingDownload; - Ok(peer_id) + Ok(peer_group) } other => Err(LookupRequestError::BadState(format!( "Bad state on_post_process_validation_failure expected Processed got {other}" @@ -478,38 +484,6 @@ impl SingleLookupRequestState { pub fn more_failed_processing_attempts(&self) -> bool { self.failed_processing >= self.failed_downloading } - - /// Add peer to this request states. The peer must be able to serve this request. - /// Returns true if the peer is newly inserted. - pub fn add_peer(&mut self, peer_id: &PeerId) -> bool { - self.available_peers.insert(*peer_id) - } - - /// Remove peer from available peers. Return true if there are no more available peers and the - /// request is not expecting any future event (AwaitingDownload). - pub fn remove_peer(&mut self, disconnected_peer_id: &PeerId) -> bool { - self.available_peers.remove(disconnected_peer_id); - self.available_peers.is_empty() && self.is_awaiting_download() - } - - pub fn get_used_peers(&self) -> impl Iterator { - self.used_peers.iter() - } - - pub fn get_available_peers(&self) -> impl Iterator { - self.available_peers.iter() - } - - /// Selects a random peer from available peers if any, inserts it in used peers and returns it. - pub fn use_rand_available_peer(&mut self) -> Option { - let peer_id = self - .available_peers - .iter() - .choose(&mut rand::thread_rng()) - .copied()?; - self.used_peers.insert(peer_id); - Some(peer_id) - } } impl std::fmt::Display for State { diff --git a/beacon_node/network/src/sync/block_lookups/tests.rs b/beacon_node/network/src/sync/block_lookups/tests.rs index 601cfadf3a6..f2fa7bd5be6 100644 --- a/beacon_node/network/src/sync/block_lookups/tests.rs +++ b/beacon_node/network/src/sync/block_lookups/tests.rs @@ -1,7 +1,11 @@ use crate::network_beacon_processor::NetworkBeaconProcessor; use crate::service::RequestId; -use crate::sync::manager::{RequestId as SyncRequestId, SingleLookupReqId, SyncManager}; +use crate::sync::manager::{ + DataColumnsByRootRequestId, DataColumnsByRootRequester, RequestId as SyncRequestId, + SingleLookupReqId, SyncManager, +}; +use crate::sync::network_context::custody::CustodyRequester; use crate::sync::sampling::{SamplingConfig, SamplingRequester}; use crate::sync::{SamplingId, SyncMessage}; use crate::NetworkMessage; @@ -14,7 +18,7 @@ use beacon_chain::block_verification_types::RpcBlock; use beacon_chain::builder::Witness; use beacon_chain::eth1_chain::CachingEth1Backend; use beacon_chain::test_utils::{ - build_log, generate_rand_block_and_blobs, generate_rand_block_and_data_columns, + build_log, generate_rand_block_and_blobs, generate_rand_block_and_data_columns, test_spec, BeaconChainHarness, EphemeralHarnessType, NumBlobs, }; use beacon_chain::validator_monitor::timestamp_now; @@ -27,11 +31,11 @@ use slot_clock::{ManualSlotClock, SlotClock, TestingSlotClock}; use store::MemoryStore; use tokio::sync::mpsc; use types::data_column_sidecar::ColumnIndex; -use types::DataColumnSidecar; use types::{ test_utils::{SeedableRng, XorShiftRng}, BlobSidecar, ForkName, MinimalEthSpec as E, SignedBeaconBlock, Slot, }; +use types::{DataColumnSidecar, Epoch}; type T = Witness, E, MemoryStore, MemoryStore>; @@ -82,16 +86,29 @@ const D: Duration = Duration::new(0, 0); const PARENT_FAIL_TOLERANCE: u8 = SINGLE_BLOCK_LOOKUP_MAX_ATTEMPTS; const SAMPLING_REQUIRED_SUCCESSES: usize = 2; -type SamplingIds = Vec<(Id, ColumnIndex)>; +type SamplingIds = Vec<(DataColumnsByRootRequestId, ColumnIndex)>; + +struct TestRigConfig { + peer_das_enabled: bool, +} impl TestRig { - fn test_setup() -> Self { + fn test_setup_with_config(config: Option) -> Self { let enable_log = cfg!(feature = "test_logger"); let log = build_log(slog::Level::Trace, enable_log); + // Use `fork_from_env` logic to set correct fork epochs + let mut spec = test_spec::(); + + if let Some(config) = config { + if config.peer_das_enabled { + spec.peer_das_epoch = Some(Epoch::new(0)); + } + } + // Initialise a new beacon chain let harness = BeaconChainHarness::>::builder(E) - .default_spec() + .spec(spec) .logger(log.clone()) .deterministic_keypairs(1) .fresh_ephemeral_store() @@ -105,6 +122,8 @@ impl TestRig { let chain = harness.chain.clone(); let (network_tx, network_rx) = mpsc::unbounded_channel(); + // TODO(das): make the generation of the ENR use the deterministic rng to have consistent + // column assignments let globals = Arc::new(NetworkGlobals::new_test_globals(Vec::new(), &log)); let (beacon_processor, beacon_processor_rx) = NetworkBeaconProcessor::null_for_testing( globals, @@ -145,6 +164,10 @@ impl TestRig { } } + fn test_setup() -> Self { + Self::test_setup_with_config(None) + } + fn test_setup_after_deneb() -> Option { let r = Self::test_setup(); if r.after_deneb() { @@ -154,6 +177,17 @@ impl TestRig { } } + fn test_setup_after_peerdas() -> Option { + let r = Self::test_setup_with_config(Some(TestRigConfig { + peer_das_enabled: true, + })); + if r.after_deneb() { + Some(r) + } else { + None + } + } + fn log(&self, msg: &str) { info!(self.log, "TEST_RIG"; "msg" => msg); } @@ -495,7 +529,7 @@ impl TestRig { } } - fn return_empty_sampling_request(&mut self, id: Id) { + fn return_empty_sampling_request(&mut self, id: DataColumnsByRootRequestId) { let peer_id = PeerId::random(); // Send stream termination self.send_sync_message(SyncMessage::RpcDataColumn { @@ -506,6 +540,32 @@ impl TestRig { }); } + fn complete_valid_block_request( + &mut self, + id: SingleLookupReqId, + block: Arc>, + missing_components: bool, + ) { + // Complete download + let peer_id = PeerId::random(); + let slot = block.slot(); + let block_root = block.canonical_root(); + self.single_lookup_block_response(id, peer_id, Some(block)); + self.single_lookup_block_response(id, peer_id, None); + // Expect processing and resolve with import + self.expect_block_process(ResponseType::Block); + self.single_block_component_processed( + id.lookup_id, + if missing_components { + BlockProcessingResult::Ok(AvailabilityProcessingStatus::MissingComponents( + slot, block_root, + )) + } else { + BlockProcessingResult::Ok(AvailabilityProcessingStatus::Imported(block_root)) + }, + ) + } + fn complete_valid_sampling_column_requests( &mut self, sampling_ids: SamplingIds, @@ -522,13 +582,80 @@ impl TestRig { fn complete_valid_sampling_column_request( &mut self, - id: Id, + id: DataColumnsByRootRequestId, data_column: DataColumnSidecar, ) { - let peer_id = PeerId::random(); let block_root = data_column.block_root(); let column_index = data_column.index; + self.complete_data_columns_by_root_request(id, data_column); + + // Expect work event + // TODO(das): worth it to append sender id to the work event for stricter assertion? + self.expect_rpc_sample_verify_work_event(); + + // Respond with valid result + self.send_sync_message(SyncMessage::SampleVerified { + id: SamplingId { + id: SamplingRequester::ImportedBlock(block_root), + column_index, + }, + result: Ok(()), + }) + } + + fn complete_valid_custody_request( + &mut self, + sampling_ids: SamplingIds, + data_columns: Vec>, + missing_components: bool, + ) { + let lookup_id = if let DataColumnsByRootRequester::Custody(id) = + sampling_ids.first().unwrap().0.requester + { + if let CustodyRequester::Lookup(id) = id.id { + id.lookup_id + } else { + panic!("not a lookup requester"); + } + } else { + panic!("not a custody requester") + }; + let first_column = data_columns.first().cloned().unwrap(); + + for (id, column_index) in sampling_ids { + self.log(&format!("return valid data column for {column_index}")); + + let data_column = data_columns[column_index as usize].clone(); + self.complete_data_columns_by_root_request(id, data_column); + } + + // Expect work event + // TODO(das): worth it to append sender id to the work event for stricter assertion? + self.expect_rpc_custody_column_work_event(); + + // Respond with valid result + self.send_sync_message(SyncMessage::BlockComponentProcessed { + process_type: BlockProcessType::SingleCustodyColumn(lookup_id), + result: if missing_components { + BlockProcessingResult::Ok(AvailabilityProcessingStatus::MissingComponents( + first_column.slot(), + first_column.block_root(), + )) + } else { + BlockProcessingResult::Ok(AvailabilityProcessingStatus::Imported( + first_column.block_root(), + )) + }, + }); + } + + fn complete_data_columns_by_root_request( + &mut self, + id: DataColumnsByRootRequestId, + data_column: DataColumnSidecar, + ) { + let peer_id = PeerId::random(); // Send chunk self.send_sync_message(SyncMessage::RpcDataColumn { request_id: SyncRequestId::DataColumnsByRoot(id), @@ -536,7 +663,6 @@ impl TestRig { data_column: Some(Arc::new(data_column)), seen_timestamp: timestamp_now(), }); - // Send stream termination self.send_sync_message(SyncMessage::RpcDataColumn { request_id: SyncRequestId::DataColumnsByRoot(id), @@ -544,19 +670,6 @@ impl TestRig { data_column: None, seen_timestamp: timestamp_now(), }); - - // Expect work event - // TODO(das): worth it to append sender id to the work event for stricter assertion? - self.expect_sample_verify_request(); - - // Respond with valid result - self.send_sync_message(SyncMessage::SampleVerified { - id: SamplingId { - id: SamplingRequester::ImportedBlock(block_root), - column_index, - }, - result: Ok(()), - }) } fn peer_disconnected(&mut self, disconnected_peer_id: PeerId) { @@ -702,7 +815,11 @@ impl TestRig { .unwrap_or_else(|e| panic!("Expected blob parent request for {for_block:?}: {e}")) } - fn expect_sampling_requests(&mut self, for_block: Hash256, count: usize) -> SamplingIds { + fn expect_data_columns_by_root_requests( + &mut self, + for_block: Hash256, + count: usize, + ) -> SamplingIds { (0..count) .map(|i| { self.pop_received_network_event(|ev| match ev { @@ -722,14 +839,18 @@ impl TestRig { _ => None, }) .unwrap_or_else(|e| { - panic!("Expected sampling request {i}/{count} for {for_block:?}: {e}") + panic!("Expected DataColumnsByRoot request {i}/{count} for {for_block:?}: {e}") }) }) .collect() } - fn expect_only_sampling_requests(&mut self, for_block: Hash256, count: usize) -> SamplingIds { - let ids = self.expect_sampling_requests(for_block, count); + fn expect_only_data_columns_by_root_requests( + &mut self, + for_block: Hash256, + count: usize, + ) -> SamplingIds { + let ids = self.expect_data_columns_by_root_requests(for_block, count); self.expect_empty_network(); ids } @@ -769,10 +890,22 @@ impl TestRig { } other => panic!("Expected blob process, found {:?}", other), }, + ResponseType::CustodyColumn => todo!(), } } - fn expect_sample_verify_request(&mut self) { + fn expect_rpc_custody_column_work_event(&mut self) { + self.pop_received_beacon_processor_event(|ev| { + if ev.work_type() == beacon_processor::RPC_CUSTODY_COLUMN { + Some(()) + } else { + None + } + }) + .unwrap_or_else(|e| panic!("Expected sample verify work: {e}")) + } + + fn expect_rpc_sample_verify_work_event(&mut self) { self.pop_received_beacon_processor_event(|ev| { if ev.work_type() == beacon_processor::RPC_VERIFY_DATA_COLUMNS { Some(()) @@ -1453,7 +1586,7 @@ fn test_same_chain_race_condition() { #[test] fn sampling_happy_path() { - let Some(mut r) = TestRig::test_setup_after_deneb() else { + let Some(mut r) = TestRig::test_setup_after_peerdas() else { return; }; r.new_connected_peers(100); // Add enough sampling peers @@ -1461,7 +1594,8 @@ fn sampling_happy_path() { let block_root = block.canonical_root(); r.trigger_sample_block(block_root, block.slot()); // Retrieve all outgoing sample requests for random column indexes - let sampling_ids = r.expect_only_sampling_requests(block_root, SAMPLING_REQUIRED_SUCCESSES); + let sampling_ids = + r.expect_only_data_columns_by_root_requests(block_root, SAMPLING_REQUIRED_SUCCESSES); // Resolve all of them one by one r.complete_valid_sampling_column_requests(sampling_ids, data_columns); r.expect_clean_finished_sampling(); @@ -1469,7 +1603,7 @@ fn sampling_happy_path() { #[test] fn sampling_with_retries() { - let Some(mut r) = TestRig::test_setup_after_deneb() else { + let Some(mut r) = TestRig::test_setup_after_peerdas() else { return; }; r.new_connected_peers(100); // Add enough sampling peers @@ -1477,14 +1611,35 @@ fn sampling_with_retries() { let block_root = block.canonical_root(); r.trigger_sample_block(block_root, block.slot()); // Retrieve all outgoing sample requests for random column indexes, and return empty responses - let sampling_ids = r.expect_only_sampling_requests(block_root, SAMPLING_REQUIRED_SUCCESSES); + let sampling_ids = + r.expect_only_data_columns_by_root_requests(block_root, SAMPLING_REQUIRED_SUCCESSES); r.return_empty_sampling_requests(sampling_ids); // Expect retries for all of them, and resolve them - let sampling_ids = r.expect_only_sampling_requests(block_root, SAMPLING_REQUIRED_SUCCESSES); + let sampling_ids = + r.expect_only_data_columns_by_root_requests(block_root, SAMPLING_REQUIRED_SUCCESSES); r.complete_valid_sampling_column_requests(sampling_ids, data_columns); r.expect_clean_finished_sampling(); } +#[test] +fn custody_lookup_happy_path() { + let Some(mut r) = TestRig::test_setup_after_peerdas() else { + return; + }; + r.new_connected_peers(100); // Add enough sampling peers + let (block, data_columns) = r.rand_block_and_data_columns(); + let block_root = block.canonical_root(); + let peer_id = r.new_connected_peer(); + r.trigger_unknown_block_from_attestation(block_root, peer_id); + // Should not request blobs + let id = r.expect_block_lookup_request(block.canonical_root()); + r.complete_valid_block_request(id, block.into(), true); + // TODO(das): do not hardcode 4 + let custody_ids = r.expect_only_data_columns_by_root_requests(block_root, 4); + r.complete_valid_custody_request(custody_ids, data_columns, false); + r.expect_no_active_lookups(); +} + // TODO(das): Test retries of DataColumnByRoot: // - Expect request for column_index // - Respond with bad data diff --git a/beacon_node/network/src/sync/block_sidecar_coupling.rs b/beacon_node/network/src/sync/block_sidecar_coupling.rs index d159733cbc7..80cfb4eb64b 100644 --- a/beacon_node/network/src/sync/block_sidecar_coupling.rs +++ b/beacon_node/network/src/sync/block_sidecar_coupling.rs @@ -1,4 +1,6 @@ -use beacon_chain::block_verification_types::RpcBlock; +use beacon_chain::{ + block_verification_types::RpcBlock, data_column_verification::CustodyDataColumn, +}; use ssz_types::VariableList; use std::{collections::VecDeque, sync::Arc}; use types::{BlobSidecar, EthSpec, SignedBeaconBlock}; @@ -11,10 +13,12 @@ pub struct BlocksAndBlobsRequestInfo { accumulated_blocks: VecDeque>>, /// Sidecars we have received awaiting for their corresponding block. accumulated_sidecars: VecDeque>>, + accumulated_custody_columns: VecDeque>, /// Whether the individual RPC request for blocks is finished or not. is_blocks_stream_terminated: bool, /// Whether the individual RPC request for sidecars is finished or not. is_sidecars_stream_terminated: bool, + is_custody_columns_stream_terminated: bool, /// Used to determine if this accumulator should wait for a sidecars stream termination request_type: ByRangeRequestType, } @@ -24,8 +28,10 @@ impl BlocksAndBlobsRequestInfo { Self { accumulated_blocks: <_>::default(), accumulated_sidecars: <_>::default(), + accumulated_custody_columns: <_>::default(), is_blocks_stream_terminated: <_>::default(), is_sidecars_stream_terminated: <_>::default(), + is_custody_columns_stream_terminated: <_>::default(), request_type, } } @@ -48,6 +54,13 @@ impl BlocksAndBlobsRequestInfo { } } + pub fn add_custody_column(&mut self, column_opt: Option>) { + match column_opt { + Some(column) => self.accumulated_custody_columns.push_back(column), + None => self.is_custody_columns_stream_terminated = true, + } + } + pub fn into_responses(self) -> Result>, String> { let BlocksAndBlobsRequestInfo { accumulated_blocks, @@ -96,11 +109,12 @@ impl BlocksAndBlobsRequestInfo { } pub fn is_finished(&self) -> bool { - let blobs_requested = match self.request_type { - ByRangeRequestType::Blocks => false, - ByRangeRequestType::BlocksAndBlobs => true, - }; - self.is_blocks_stream_terminated && (!blobs_requested || self.is_sidecars_stream_terminated) + let blobs_requested = matches!(self.request_type, ByRangeRequestType::BlocksAndBlobs); + let custody_columns_requested = + matches!(self.request_type, ByRangeRequestType::BlocksAndColumns); + self.is_blocks_stream_terminated + && (!blobs_requested || self.is_sidecars_stream_terminated) + && (!custody_columns_requested || self.is_custody_columns_stream_terminated) } } diff --git a/beacon_node/network/src/sync/manager.rs b/beacon_node/network/src/sync/manager.rs index d4143774a92..05ef447d828 100644 --- a/beacon_node/network/src/sync/manager.rs +++ b/beacon_node/network/src/sync/manager.rs @@ -35,7 +35,9 @@ use super::backfill_sync::{BackFillSync, ProcessResult, SyncStart}; use super::block_lookups::BlockLookups; -use super::network_context::{BlockOrBlob, RangeRequestId, RpcEvent, SyncNetworkContext}; +use super::network_context::{ + custody::CustodyRequester, BlockOrBlob, CustodyId, RangeRequestId, RpcEvent, SyncNetworkContext, +}; use super::peer_sync_info::{remote_sync_type, PeerSyncType}; use super::range_sync::{RangeSync, RangeSyncType, EPOCHS_PER_BATCH}; use super::sampling::{Sampling, SamplingConfig, SamplingId, SamplingRequester, SamplingResult}; @@ -43,9 +45,10 @@ use crate::network_beacon_processor::{ChainSegmentProcessId, NetworkBeaconProces use crate::service::NetworkMessage; use crate::status::ToStatusMessage; use crate::sync::block_lookups::{ - BlobRequestState, BlockComponent, BlockRequestState, DownloadResult, + BlobRequestState, BlockComponent, BlockRequestState, CustodyRequestState, DownloadResult, }; use crate::sync::block_sidecar_coupling::BlocksAndBlobsRequestInfo; +use crate::sync::network_context::PeerGroup; use beacon_chain::block_verification_types::AsBlock; use beacon_chain::block_verification_types::RpcBlock; use beacon_chain::validator_monitor::timestamp_now; @@ -89,17 +92,23 @@ pub enum RequestId { /// Request searching for a set of blobs given a hash. SingleBlob { id: SingleLookupReqId }, /// Request searching for a set of data columns given a hash and list of column indices. - DataColumnsByRoot(Id), + DataColumnsByRoot(DataColumnsByRootRequestId), /// Range request that is composed by both a block range request and a blob range request. RangeBlockAndBlobs { id: Id }, /// Range request that is composed by both a block range request and a blob range request. RangeBlockAndDataColumns { id: Id }, } +#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] +pub struct DataColumnsByRootRequestId { + pub requester: DataColumnsByRootRequester, + pub req_id: Id, +} + #[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] pub enum DataColumnsByRootRequester { Sampling(SamplingId), - Custody, + Custody(CustodyId), } #[derive(Debug)] @@ -180,12 +189,15 @@ pub enum SyncMessage { pub enum BlockProcessType { SingleBlock { id: Id }, SingleBlob { id: Id }, + SingleCustodyColumn(Id), } impl BlockProcessType { pub fn id(&self) -> Id { match self { - BlockProcessType::SingleBlock { id } | BlockProcessType::SingleBlob { id } => *id, + BlockProcessType::SingleBlock { id } + | BlockProcessType::SingleBlob { id } + | BlockProcessType::SingleCustodyColumn(id) => *id, } } } @@ -668,7 +680,7 @@ impl SyncManager { value: block.block_cloned(), block_root, seen_timestamp: timestamp_now(), - peer_id, + peer_group: PeerGroup::from_single(peer_id), }), ); } @@ -686,7 +698,7 @@ impl SyncManager { value: blob, block_root, seen_timestamp: timestamp_now(), - peer_id, + peer_group: PeerGroup::from_single(peer_id), }), ); } @@ -901,12 +913,13 @@ impl SyncManager { peer_id: PeerId, block: RpcEvent>>, ) { - if let Some(resp) = self.network.on_single_block_response(id, block) { + if let Some(resp) = self.network.on_single_block_response(id, peer_id, block) { self.block_lookups .on_download_response::>( id.lookup_id, - peer_id, - resp, + resp.map(|(value, seen_timestamp)| { + (value, PeerGroup::from_single(peer_id), seen_timestamp) + }), &mut self.network, ) } @@ -973,12 +986,13 @@ impl SyncManager { peer_id: PeerId, blob: RpcEvent>>, ) { - if let Some(resp) = self.network.on_single_blob_response(id, blob) { + if let Some(resp) = self.network.on_single_blob_response(id, peer_id, blob) { self.block_lookups .on_download_response::>( id.lookup_id, - peer_id, - resp, + resp.map(|(value, seen_timestamp)| { + (value, PeerGroup::from_single(peer_id), seen_timestamp) + }), &mut self.network, ) } @@ -986,13 +1000,13 @@ impl SyncManager { fn on_single_data_column_response( &mut self, - id: Id, + id: DataColumnsByRootRequestId, peer_id: PeerId, data_column: RpcEvent>>, ) { - if let Some((requester, resp)) = self - .network - .on_data_columns_by_root_response(id, data_column) + if let Some((requester, resp)) = + self.network + .on_data_columns_by_root_response(id, peer_id, data_column) { match requester { DataColumnsByRootRequester::Sampling(id) => { @@ -1003,8 +1017,29 @@ impl SyncManager { self.on_sampling_result(requester, result) } } - DataColumnsByRootRequester::Custody => { - todo!("TODO(das): handle custody requests"); + DataColumnsByRootRequester::Custody(id) => { + if let Some((requester, custody_columns)) = + self.network.on_custody_by_root_response(id, peer_id, resp) + { + // TODO(das): get proper timestamp + let seen_timestamp = timestamp_now(); + match requester { + CustodyRequester::Lookup(id) => self + .block_lookups + .on_download_response::>( + id.lookup_id, + custody_columns.map(|(columns, peer_group)| { + (columns, peer_group, seen_timestamp) + }), + &mut self.network, + ), + CustodyRequester::RangeSync(_) => { + // TODO(das): this line should be unreachable, no mechanism to make + // custody requests for sync yet + todo!("custody fetching for sync not implemented"); + } + } + } } } } diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index 4f21cb08cf6..bdcebc80516 100644 --- a/beacon_node/network/src/sync/network_context.rs +++ b/beacon_node/network/src/sync/network_context.rs @@ -1,6 +1,8 @@ //! Provides network functionality for the Syncing thread. This fundamentally wraps a network //! channel and stores a global RPC ID to perform requests. +pub use self::custody::CustodyId; +use self::custody::{ActiveCustodyRequest, CustodyRequester, Error as CustodyRequestError}; use self::requests::{ ActiveBlobsByRootRequest, ActiveBlocksByRootRequest, ActiveDataColumnsByRootRequest, }; @@ -9,7 +11,8 @@ pub use self::requests::{ }; use super::block_sidecar_coupling::BlocksAndBlobsRequestInfo; use super::manager::{ - BlockProcessType, DataColumnsByRootRequester, Id, RequestId as SyncRequestId, + BlockProcessType, DataColumnsByRootRequestId, DataColumnsByRootRequester, Id, + RequestId as SyncRequestId, }; use super::range_sync::{BatchId, ByRangeRequestType, ChainId}; use crate::network_beacon_processor::NetworkBeaconProcessor; @@ -18,6 +21,7 @@ use crate::status::ToStatusMessage; use crate::sync::block_lookups::SingleLookupId; use crate::sync::manager::SingleLookupReqId; use beacon_chain::block_verification_types::RpcBlock; +use beacon_chain::data_column_verification::CustodyDataColumn; use beacon_chain::validator_monitor::timestamp_now; use beacon_chain::{BeaconChain, BeaconChainTypes, EngineState}; use fnv::FnvHashMap; @@ -28,6 +32,7 @@ use lighthouse_network::{ }; pub use requests::LookupVerifyError; use slog::{debug, error, trace, warn}; +use slot_clock::SlotClock; use std::collections::hash_map::Entry; use std::sync::Arc; use std::time::Duration; @@ -38,6 +43,7 @@ use types::{ SignedBeaconBlock, }; +pub mod custody; mod requests; pub struct BlocksAndBlobsByRangeResponse { @@ -66,18 +72,11 @@ pub enum RpcEvent { pub type RpcProcessingResult = Result<(T, Duration), LookupFailure>; +#[derive(Debug)] pub enum LookupFailure { RpcError(RPCError), LookupVerifyError(LookupVerifyError), -} - -impl std::fmt::Display for LookupFailure { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match self { - LookupFailure::RpcError(e) => write!(f, "RPC Error: {:?}", e), - LookupFailure::LookupVerifyError(e) => write!(f, "Lookup Verify Error: {:?}", e), - } - } + CustodyRequestError(CustodyRequestError), } impl From for LookupFailure { @@ -92,6 +91,23 @@ impl From for LookupFailure { } } +#[derive(Clone, Debug)] +pub struct PeerGroup { + peers: Vec, +} + +impl PeerGroup { + pub fn from_single(peer: PeerId) -> Self { + Self { peers: vec![peer] } + } + pub fn from_set(peers: Vec) -> Self { + Self { peers } + } + pub fn all(&self) -> &[PeerId] { + &self.peers + } +} + /// Wraps a Network channel to employ various RPC related network functionality for the Sync manager. This includes management of a global RPC request Id. pub struct SyncNetworkContext { /// The network channel to relay messages to the Network service. @@ -105,8 +121,12 @@ pub struct SyncNetworkContext { /// A mapping of active BlobsByRoot requests, including both current slot and parent lookups. blobs_by_root_requests: FnvHashMap>, - data_columns_by_root_requests: - FnvHashMap>, + data_columns_by_root_requests: FnvHashMap< + DataColumnsByRootRequestId, + ActiveDataColumnsByRootRequest, + >, + /// Mapping of active custody column requests for a block root + custody_by_root_requests: FnvHashMap>, /// BlocksByRange requests paired with BlobsByRange range_blocks_and_blobs_requests: @@ -129,6 +149,7 @@ pub struct SyncNetworkContext { pub enum BlockOrBlob { Block(Option>>), Blob(Option>>), + CustodyColumns(Option>), } impl From>>> for BlockOrBlob { @@ -157,6 +178,7 @@ impl SyncNetworkContext { blocks_by_root_requests: <_>::default(), blobs_by_root_requests: <_>::default(), data_columns_by_root_requests: <_>::default(), + custody_by_root_requests: <_>::default(), range_blocks_and_blobs_requests: FnvHashMap::default(), network_beacon_processor, chain, @@ -321,6 +343,7 @@ impl SyncNetworkContext { match block_or_blob { BlockOrBlob::Block(maybe_block) => info.add_block_response(maybe_block), BlockOrBlob::Blob(maybe_sidecar) => info.add_sidecar_response(maybe_sidecar), + BlockOrBlob::CustodyColumns(column) => info.add_custody_column(column), } if info.is_finished() { // If the request is finished, dequeue everything @@ -399,24 +422,32 @@ impl SyncNetworkContext { block_root: Hash256, downloaded_block_expected_blobs: Option, ) -> Result { + // Check if we are into deneb, and before peerdas + if !self + .chain + .data_availability_checker + .blobs_required_for_epoch( + // TODO(das): use the block's slot + self.chain + .slot_clock + .now_or_genesis() + .ok_or("clock not available")? + .epoch(T::EthSpec::slots_per_epoch()), + ) + { + return Ok(false); + } + let expected_blobs = downloaded_block_expected_blobs .or_else(|| { self.chain .data_availability_checker .num_expected_blobs(&block_root) }) - .unwrap_or_else(|| { + .unwrap_or( // If we don't about the block being requested, attempt to fetch all blobs - if self - .chain - .data_availability_checker - .da_check_required_for_current_epoch() - { - T::EthSpec::max_blobs_per_block() - } else { - 0 - } - }); + T::EthSpec::max_blobs_per_block(), + ); let imported_blob_indexes = self .chain @@ -471,8 +502,7 @@ impl SyncNetworkContext { peer_id: PeerId, request: DataColumnsByRootSingleBlockRequest, ) -> Result<(), &'static str> { - let id = self.next_id(); - + let req_id = self.next_id(); debug!( self.log, "Sending DataColumnsByRoot Request"; @@ -481,8 +511,9 @@ impl SyncNetworkContext { "indices" => ?request.indices, "peer" => %peer_id, "requester" => ?requester, - "id" => id, + "id" => req_id, ); + let id = DataColumnsByRootRequestId { requester, req_id }; self.send_network_msg(NetworkMessage::SendRequest { peer_id, @@ -496,6 +527,100 @@ impl SyncNetworkContext { Ok(()) } + pub fn custody_lookup_request( + &mut self, + lookup_id: SingleLookupId, + block_root: Hash256, + downloaded_block_expected_data: Option, + ) -> Result { + // Check if we are into peerdas + if !self + .chain + .data_availability_checker + .data_columns_required_for_epoch( + // TODO(das): use the block's slot + self.chain + .slot_clock + .now_or_genesis() + .ok_or("clock not available")? + .epoch(T::EthSpec::slots_per_epoch()), + ) + { + return Ok(false); + } + + let expects_data = downloaded_block_expected_data + .or_else(|| { + self.chain + .data_availability_checker + .num_expected_blobs(&block_root) + }) + .map(|n| n > 0) + // If we don't know about the block being requested, assume block has data + .unwrap_or(true); + + // No data required for this block + if !expects_data { + return Ok(false); + } + + let custody_indexes_imported = self + .chain + .data_availability_checker + .imported_custody_column_indexes(&block_root) + .unwrap_or_default(); + + // TODO(das): figure out how to pass block.slot if we end up doing rotation + let block_epoch = Epoch::new(0); + let custody_indexes_duty = self.network_globals().custody_columns(block_epoch)?; + + // Include only the blob indexes not yet imported (received through gossip) + let custody_indexes_to_fetch = custody_indexes_duty + .into_iter() + .filter(|index| !custody_indexes_imported.contains(index)) + .collect::>(); + + if custody_indexes_to_fetch.is_empty() { + // No indexes required, do not issue any request + return Ok(false); + } + + let id = SingleLookupReqId { + lookup_id, + req_id: self.next_id(), + }; + + debug!( + self.log, + "Starting custody columns request"; + "block_root" => ?block_root, + "indices" => ?custody_indexes_to_fetch, + "id" => ?id + ); + + let requester = CustodyRequester::Lookup(id); + let mut request = ActiveCustodyRequest::new( + block_root, + requester, + custody_indexes_to_fetch, + self.log.clone(), + ); + + // TODO(das): start request + // Note that you can only send, but not handle a response here + match request.continue_requests(self) { + Ok(_) => { + // Ignoring the result of `continue_requests` is okay. A request that has just been + // created cannot return data immediately, it must send some request to the network + // first. And there must exist some request, `custody_indexes_to_fetch` is not empty. + self.custody_by_root_requests.insert(requester, request); + Ok(true) + } + // TODO(das): handle this error properly + Err(_) => Err("custody_send_error"), + } + } + pub fn is_execution_engine_online(&self) -> bool { self.execution_engine_state == EngineState::Online } @@ -604,6 +729,7 @@ impl SyncNetworkContext { if let Some(data_availability_boundary) = self.chain.data_availability_boundary() { if epoch >= data_availability_boundary { + // TODO(das): After peerdas fork, return `BlocksAndColumns` ByRangeRequestType::BlocksAndBlobs } else { ByRangeRequestType::Blocks @@ -628,6 +754,7 @@ impl SyncNetworkContext { pub fn on_single_block_response( &mut self, request_id: SingleLookupReqId, + peer_id: PeerId, block: RpcEvent>>, ) -> Option>>> { let Entry::Occupied(mut request) = self.blocks_by_root_requests.entry(request_id) else { @@ -654,12 +781,17 @@ impl SyncNetworkContext { Err(e.into()) } }; + + if let Err(ref e) = resp { + self.on_lookup_failure(peer_id, e); + } Some(resp) } pub fn on_single_blob_response( &mut self, request_id: SingleLookupReqId, + peer_id: PeerId, blob: RpcEvent>>, ) -> Option>> { let Entry::Occupied(mut request) = self.blobs_by_root_requests.entry(request_id) else { @@ -691,13 +823,18 @@ impl SyncNetworkContext { Err(e.into()) } }; + + if let Err(ref e) = resp { + self.on_lookup_failure(peer_id, e); + } Some(resp) } #[allow(clippy::type_complexity)] pub fn on_data_columns_by_root_response( &mut self, - id: Id, + id: DataColumnsByRootRequestId, + peer_id: PeerId, item: RpcEvent>>, ) -> Option<( DataColumnsByRootRequester, @@ -731,9 +868,62 @@ impl SyncNetworkContext { Err(e.into()) } }; + + if let Err(ref e) = resp { + self.on_lookup_failure(peer_id, e); + } Some((requester, resp)) } + /// Insert a downloaded column into an active custody request. Then make progress on the + /// entire request. + /// + /// ### Returns + /// + /// - `Some`: Request completed, won't make more progress. Expect requester to act on the result. + /// - `None`: Request still active, requester should do no action + #[allow(clippy::type_complexity)] + pub fn on_custody_by_root_response( + &mut self, + id: CustodyId, + peer_id: PeerId, + resp: RpcProcessingResult>>>, + ) -> Option<( + CustodyRequester, + Result<(Vec>, PeerGroup), LookupFailure>, + )> { + // Note: need to remove the request to borrow self again below. Otherwise we can't + // do nested requests + let Some(mut request) = self.custody_by_root_requests.remove(&id.id) else { + // TOOD(das): This log can happen if the request is error'ed early and dropped + debug!(self.log, "Custody column downloaded event for unknown request"; "id" => ?id); + return None; + }; + + let result = request + .on_data_column_downloaded(peer_id, id.column_index, resp, self) + .map_err(LookupFailure::CustodyRequestError) + .transpose(); + + // Convert a result from internal format of `ActiveCustodyRequest` (error first to use ?) to + // an Option first to use in an `if let Some() { act on result }` block. + if let Some(result) = result { + match result.as_ref() { + Ok((columns, peer_group)) => { + debug!(self.log, "Custody request success, removing"; "id" => ?id, "count" => columns.len(), "peers" => ?peer_group) + } + Err(e) => { + debug!(self.log, "Custody request failure, removing"; "id" => ?id, "error" => ?e) + } + } + + Some((id.id, result)) + } else { + self.custody_by_root_requests.insert(id.id, request); + None + } + } + pub fn send_block_for_processing( &self, block_root: Hash256, @@ -796,6 +986,53 @@ impl SyncNetworkContext { } } } + + pub fn send_custody_columns_for_processing( + &self, + block_root: Hash256, + custody_columns: Vec>, + duration: Duration, + process_type: BlockProcessType, + ) -> Result<(), &'static str> { + match self.beacon_processor_if_enabled() { + Some(beacon_processor) => { + debug!(self.log, "Sending custody columns for processing"; "block" => ?block_root, "process_type" => ?process_type); + if let Err(e) = beacon_processor.send_rpc_custody_columns( + block_root, + custody_columns, + duration, + process_type, + ) { + error!( + self.log, + "Failed to send sync custody columns to processor"; + "error" => ?e + ); + Err("beacon processor send failure") + } else { + Ok(()) + } + } + None => { + trace!(self.log, "Dropping custody columns ready for processing. Beacon processor not available"; "block_root" => %block_root); + Err("beacon processor unavailable") + } + } + } + + /// Downscore peers for lookup errors that originate from sync + pub fn on_lookup_failure(&self, peer_id: PeerId, err: &LookupFailure) { + match err { + // RPCErros are downscored in the network handler + LookupFailure::RpcError(_) => {} + // Only downscore lookup verify errors. RPC errors are downscored in the network handler. + LookupFailure::LookupVerifyError(e) => { + self.report_peer(peer_id, PeerAction::LowToleranceError, e.into()); + } + // CustodyRequestError are downscored in the each data_columns_by_root request + LookupFailure::CustodyRequestError(_) => {} + } + } } fn to_fixed_blob_sidecar_list( diff --git a/beacon_node/network/src/sync/network_context/custody.rs b/beacon_node/network/src/sync/network_context/custody.rs new file mode 100644 index 00000000000..72964d3f362 --- /dev/null +++ b/beacon_node/network/src/sync/network_context/custody.rs @@ -0,0 +1,285 @@ +use crate::sync::manager::{Id, SingleLookupReqId}; + +use self::request::ActiveColumnSampleRequest; +use beacon_chain::data_column_verification::CustodyDataColumn; +use beacon_chain::BeaconChainTypes; +use fnv::FnvHashMap; +use lighthouse_network::PeerId; +use slog::{debug, warn}; +use std::{marker::PhantomData, sync::Arc}; +use types::{data_column_sidecar::ColumnIndex, DataColumnSidecar, Epoch, Hash256}; + +use super::{PeerGroup, RpcProcessingResult, SyncNetworkContext}; + +#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] +pub struct CustodyId { + pub id: CustodyRequester, + pub column_index: ColumnIndex, +} + +#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] +pub enum CustodyRequester { + Lookup(SingleLookupReqId), + RangeSync(Id), +} + +type DataColumnSidecarList = Vec>>; + +pub struct ActiveCustodyRequest { + block_root: Hash256, + block_epoch: Epoch, + requester_id: CustodyRequester, + column_requests: FnvHashMap, + columns: Vec>, + /// Logger for the `SyncNetworkContext`. + pub log: slog::Logger, + _phantom: PhantomData, +} + +#[derive(Debug)] +pub enum Error { + SendFailed(&'static str), + TooManyFailures, + BadState(String), + NoPeers(ColumnIndex), +} + +type CustodyRequestResult = Result>, PeerGroup)>, Error>; + +impl ActiveCustodyRequest { + pub(crate) fn new( + block_root: Hash256, + requester_id: CustodyRequester, + column_indexes: Vec, + log: slog::Logger, + ) -> Self { + Self { + block_root, + // TODO(das): use actual epoch if there's rotation + block_epoch: Epoch::new(0), + requester_id, + column_requests: column_indexes + .into_iter() + .map(|index| (index, ActiveColumnSampleRequest::new(index))) + .collect(), + columns: vec![], + log, + _phantom: PhantomData, + } + } + + /// Insert a downloaded column into an active sampling request. Then make progress on the + /// entire request. + /// + /// ### Returns + /// + /// - `Err`: Sampling request has failed and will be dropped + /// - `Ok(Some)`: Sampling request has successfully completed and will be dropped + /// - `Ok(None)`: Sampling request still active + pub(crate) fn on_data_column_downloaded( + &mut self, + _peer_id: PeerId, + column_index: ColumnIndex, + resp: RpcProcessingResult>, + cx: &mut SyncNetworkContext, + ) -> CustodyRequestResult { + // TODO(das): Should downscore peers for verify errors here + + let Some(request) = self.column_requests.get_mut(&column_index) else { + warn!( + self.log, + "Received sampling response for unrequested column index" + ); + return Ok(None); + }; + + match resp { + Ok((mut data_columns, _seen_timestamp)) => { + debug!(self.log, "Sample download success"; "block_root" => %self.block_root, "column_index" => column_index, "count" => data_columns.len()); + + // No need to check data_columns has len > 1, as the SyncNetworkContext ensure that + // only requested is returned (or none); + if let Some(data_column) = data_columns.pop() { + request.on_download_success()?; + + // If on_download_success is successful, we are expecting a columna for this + // custody requirement. + self.columns + .push(CustodyDataColumn::from_asserted_custody(data_column)); + } else { + // Peer does not have the requested data. + // TODO(das) what to do? + debug!(self.log, "Sampling peer claims to not have the data"; "block_root" => %self.block_root, "column_index" => column_index); + // TODO(das) tolerate this failure if you are not sure the block has data + request.on_download_success()?; + } + } + Err(err) => { + debug!(self.log, "Sample download error"; "block_root" => %self.block_root, "column_index" => column_index, "error" => ?err); + + // Error downloading, maybe penalize peer and retry again. + // TODO(das) with different peer or different peer? + request.on_download_error()?; + } + }; + + self.continue_requests(cx) + } + + pub(crate) fn continue_requests( + &mut self, + cx: &mut SyncNetworkContext, + ) -> CustodyRequestResult { + // First check if sampling is completed, by computing `required_successes` + let mut successes = 0; + + for request in self.column_requests.values() { + if request.is_downloaded() { + successes += 1; + } + } + + // All requests have completed successfully. We may not have all the expected columns if the + // serving peers claim that this block has no data. + if successes == self.column_requests.len() { + let columns = std::mem::take(&mut self.columns); + + let peers = self + .column_requests + .values() + .filter_map(|r| r.peer()) + .collect::>(); + let peer_group = PeerGroup::from_set(peers); + + return Ok(Some((columns, peer_group))); + } + + for (_, request) in self.column_requests.iter_mut() { + request.request(self.block_root, self.block_epoch, self.requester_id, cx)?; + } + + Ok(None) + } +} + +mod request { + use super::{CustodyId, CustodyRequester, Error}; + use crate::sync::{ + manager::DataColumnsByRootRequester, + network_context::{DataColumnsByRootSingleBlockRequest, SyncNetworkContext}, + }; + use beacon_chain::BeaconChainTypes; + use lighthouse_network::PeerId; + use types::{data_column_sidecar::ColumnIndex, Epoch, Hash256}; + + /// TODO(das): this attempt count is nested into the existing lookup request count. + const MAX_CUSTODY_COLUMN_DOWNLOAD_ATTEMPTS: usize = 3; + + pub(crate) struct ActiveColumnSampleRequest { + column_index: ColumnIndex, + status: Status, + download_failures: usize, + } + + #[derive(Debug, Clone)] + enum Status { + NotStarted, + Downloading(PeerId), + Downloaded(PeerId), + } + + impl ActiveColumnSampleRequest { + pub(crate) fn new(column_index: ColumnIndex) -> Self { + Self { + column_index, + status: Status::NotStarted, + download_failures: 0, + } + } + + pub(crate) fn is_downloaded(&self) -> bool { + match self.status { + Status::NotStarted | Status::Downloading(_) => false, + Status::Downloaded(_) => true, + } + } + + pub(crate) fn peer(&self) -> Option { + match self.status { + Status::NotStarted | Status::Downloading(_) => None, + Status::Downloaded(peer) => Some(peer), + } + } + + pub(crate) fn request( + &mut self, + block_root: Hash256, + block_epoch: Epoch, + requester: CustodyRequester, + cx: &mut SyncNetworkContext, + ) -> Result { + match &self.status { + Status::NotStarted => {} // Ok to continue + Status::Downloading(_) => return Ok(false), // Already downloading + Status::Downloaded(_) => return Ok(false), // Already completed + } + + if self.download_failures > MAX_CUSTODY_COLUMN_DOWNLOAD_ATTEMPTS { + return Err(Error::TooManyFailures); + } + + // TODO: When is a fork and only a subset of your peers know about a block, sampling should only + // be queried on the peers on that fork. Should this case be handled? How to handle it? + let peer_ids = cx.get_custodial_peers(block_epoch, self.column_index); + + // TODO(das) randomize custodial peer and avoid failing peers + let Some(peer_id) = peer_ids.first().cloned() else { + // Do not tolerate not having custody peers, hard error. + // TODO(das): we might implement some grace period. The request will pause for X + // seconds expecting the peer manager to find peers before failing the request. + return Err(Error::NoPeers(self.column_index)); + }; + + cx.data_column_lookup_request( + DataColumnsByRootRequester::Custody(CustodyId { + id: requester, + column_index: self.column_index, + }), + peer_id, + DataColumnsByRootSingleBlockRequest { + block_root, + indices: vec![self.column_index], + }, + ) + .map_err(Error::SendFailed)?; + + self.status = Status::Downloading(peer_id); + Ok(true) + } + + pub(crate) fn on_download_error(&mut self) -> Result { + match self.status.clone() { + Status::Downloading(peer_id) => { + self.download_failures += 1; + self.status = Status::NotStarted; + Ok(peer_id) + } + other => Err(Error::BadState(format!( + "bad state on_sampling_error expected Sampling got {other:?}" + ))), + } + } + + pub(crate) fn on_download_success(&mut self) -> Result<(), Error> { + match &self.status { + Status::Downloading(peer) => { + self.status = Status::Downloaded(*peer); + Ok(()) + } + other => Err(Error::BadState(format!( + "bad state on_sampling_success expected Sampling got {other:?}" + ))), + } + } + } +} diff --git a/beacon_node/network/src/sync/range_sync/batch.rs b/beacon_node/network/src/sync/range_sync/batch.rs index 75cb49d176d..273d3248e16 100644 --- a/beacon_node/network/src/sync/range_sync/batch.rs +++ b/beacon_node/network/src/sync/range_sync/batch.rs @@ -19,6 +19,7 @@ const MAX_BATCH_PROCESSING_ATTEMPTS: u8 = 3; #[derive(Debug, Copy, Clone, Display)] #[strum(serialize_all = "snake_case")] pub enum ByRangeRequestType { + BlocksAndColumns, BlocksAndBlobs, Blocks, } diff --git a/beacon_node/network/src/sync/sampling.rs b/beacon_node/network/src/sync/sampling.rs index 9ee8616daac..84ebe69928a 100644 --- a/beacon_node/network/src/sync/sampling.rs +++ b/beacon_node/network/src/sync/sampling.rs @@ -267,7 +267,7 @@ impl ActiveSamplingRequest { }; debug!(self.log, "Sending data_column for verification"; "block" => ?self.block_root, "column_index" => column_index); - if let Err(e) = beacon_processor.send_rpc_data_columns( + if let Err(e) = beacon_processor.send_rpc_validate_data_columns( self.block_root, vec![data_column], seen_timestamp, @@ -288,7 +288,7 @@ impl ActiveSamplingRequest { } } Err(err) => { - debug!(self.log, "Sample download error"; "block_root" => %self.block_root, "column_index" => column_index, "error" => %err); + debug!(self.log, "Sample download error"; "block_root" => %self.block_root, "column_index" => column_index, "error" => ?err); metrics::inc_counter_vec(&metrics::SAMPLE_DOWNLOAD_RESULT, &[metrics::FAILURE]); // Error downloading, maybe penalize peer and retry again. From 42d97d38059d5a084f3a83b94026f1367fe6ed93 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Thu, 9 May 2024 15:17:01 +1000 Subject: [PATCH 15/76] Add data column kzg verification and update `c-kzg`. (#5701) * Add data column kzg verification and update `c-kzg`. * Fix incorrect `Cell` size. * Add kzg verification on rpc blocks. * Add kzg verification on rpc data columns. --- Cargo.lock | 2 +- .../src/block_verification_types.rs | 8 +++ .../src/data_availability_checker.rs | 28 +++++++++- .../src/data_column_verification.rs | 26 ++++----- beacon_node/beacon_chain/src/kzg_utils.rs | 56 ++++++++++++++++++- beacon_node/beacon_chain/src/metrics.rs | 4 ++ beacon_node/beacon_processor/src/lib.rs | 2 +- .../network_beacon_processor/sync_methods.rs | 7 ++- consensus/types/src/data_column_sidecar.rs | 2 +- consensus/types/src/eth_spec.rs | 8 +++ crypto/kzg/src/lib.rs | 44 ++++++++------- 11 files changed, 142 insertions(+), 45 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f17a047e90a..8ee0c68c83e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1132,7 +1132,7 @@ dependencies = [ [[package]] name = "c-kzg" version = "1.0.0" -source = "git+https://github.com/ethereum/c-kzg-4844?branch=das#1124ee930d4d62e3009f7d87edc93cffe7a3e83e" +source = "git+https://github.com/ethereum/c-kzg-4844?branch=das#e08f22ef65a5ba4ea808e0d3a9e845fbd6faea2f" dependencies = [ "blst", "cc", diff --git a/beacon_node/beacon_chain/src/block_verification_types.rs b/beacon_node/beacon_chain/src/block_verification_types.rs index 1a0ff1d6bbc..e7219f6bb07 100644 --- a/beacon_node/beacon_chain/src/block_verification_types.rs +++ b/beacon_node/beacon_chain/src/block_verification_types.rs @@ -73,6 +73,14 @@ impl RpcBlock { RpcBlockInner::BlockAndCustodyColumns(_, _) => None, } } + + pub fn custody_columns(&self) -> Option<&CustodyDataColumnList> { + match &self.block { + RpcBlockInner::Block(_) => None, + RpcBlockInner::BlockAndBlobs(_, _) => None, + RpcBlockInner::BlockAndCustodyColumns(_, data_columns) => Some(data_columns), + } + } } /// Note: This variant is intentionally private because we want to safely construct the diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index 202e733cc13..ce8ade55592 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -282,8 +282,13 @@ impl DataAvailabilityChecker { .kzg .as_ref() .ok_or(AvailabilityCheckError::KzgNotInitialized)?; - verify_kzg_for_data_column_list(data_column_list.iter(), kzg) - .map_err(AvailabilityCheckError::Kzg)?; + verify_kzg_for_data_column_list( + data_column_list + .iter() + .map(|custody_column| custody_column.as_data_column()), + kzg, + ) + .map_err(AvailabilityCheckError::Kzg)?; Ok(MaybeAvailableBlock::Available(AvailableBlock { block_root, block, @@ -343,7 +348,24 @@ impl DataAvailabilityChecker { verify_kzg_for_blob_list(all_blobs.iter(), kzg)?; } - // TODO(das) verify kzg for all data columns + let all_data_columns: DataColumnSidecarList = blocks + .iter() + .filter(|block| self.data_columns_required_for_block(block.as_block())) + // this clone is cheap as it's cloning an Arc + .filter_map(|block| block.custody_columns().cloned()) + .flatten() + .map(CustodyDataColumn::into_inner) + .collect::>() + .into(); + + // verify kzg for all data columns at once + if !all_data_columns.is_empty() { + let kzg = self + .kzg + .as_ref() + .ok_or(AvailabilityCheckError::KzgNotInitialized)?; + verify_kzg_for_data_column_list(all_data_columns.iter(), kzg)?; + } for block in blocks { let (block_root, block, blobs, data_columns) = block.deconstruct(); diff --git a/beacon_node/beacon_chain/src/data_column_verification.rs b/beacon_node/beacon_chain/src/data_column_verification.rs index 136ca037d0b..b5e3b845a4b 100644 --- a/beacon_node/beacon_chain/src/data_column_verification.rs +++ b/beacon_node/beacon_chain/src/data_column_verification.rs @@ -1,8 +1,10 @@ use crate::block_verification::{process_block_slash_info, BlockSlashInfo}; -use crate::{BeaconChain, BeaconChainError, BeaconChainTypes}; +use crate::kzg_utils::validate_data_column; +use crate::{metrics, BeaconChain, BeaconChainError, BeaconChainTypes}; use derivative::Derivative; use kzg::{Error as KzgError, Kzg}; use ssz_derive::{Decode, Encode}; +use std::iter; use std::sync::Arc; use types::data_column_sidecar::{ColumnIndex, DataColumnIdentifier}; use types::{ @@ -196,7 +198,7 @@ impl CustodyDataColumn { pub fn into_inner(self) -> Arc> { self.data } - pub fn as_data_column(&self) -> &DataColumnSidecar { + pub fn as_data_column(&self) -> &Arc> { &self.data } /// This is cheap as we're calling clone on an Arc @@ -253,15 +255,10 @@ impl KzgVerifiedCustodyDataColumn { /// Returns an error if the kzg verification check fails. pub fn verify_kzg_for_data_column( data_column: Arc>, - _kzg: &Kzg, + kzg: &Kzg, ) -> Result, KzgError> { - // TODO(das): validate data column - // validate_blob::( - // kzg, - // &data_column.blob, - // data_column.kzg_commitment, - // data_column.kzg_proof, - // )?; + let _timer = metrics::start_timer(&metrics::KZG_VERIFICATION_DATA_COLUMN_SINGLE_TIMES); + validate_data_column(kzg, iter::once(&data_column))?; Ok(KzgVerifiedDataColumn { data: data_column }) } @@ -271,13 +268,14 @@ pub fn verify_kzg_for_data_column( /// Note: This function should be preferred over calling `verify_kzg_for_data_column` /// in a loop since this function kzg verifies a list of data columns more efficiently. pub fn verify_kzg_for_data_column_list<'a, E: EthSpec, I>( - _data_column_iter: I, - _kzg: &'a Kzg, + data_column_iter: I, + kzg: &'a Kzg, ) -> Result<(), KzgError> where - I: Iterator>, + I: Iterator>> + Clone, { - // TODO(das): implement kzg verification + let _timer = metrics::start_timer(&metrics::KZG_VERIFICATION_DATA_COLUMN_BATCH_TIMES); + validate_data_column(kzg, data_column_iter)?; Ok(()) } diff --git a/beacon_node/beacon_chain/src/kzg_utils.rs b/beacon_node/beacon_chain/src/kzg_utils.rs index b554133875a..69888235365 100644 --- a/beacon_node/beacon_chain/src/kzg_utils.rs +++ b/beacon_node/beacon_chain/src/kzg_utils.rs @@ -1,5 +1,7 @@ -use kzg::{Blob as KzgBlob, Error as KzgError, Kzg}; -use types::{Blob, EthSpec, Hash256, KzgCommitment, KzgProof}; +use kzg::{Blob as KzgBlob, Bytes48, Cell as KzgCell, Error as KzgError, Kzg}; +use std::sync::Arc; +use types::data_column_sidecar::Cell; +use types::{Blob, DataColumnSidecar, EthSpec, Hash256, KzgCommitment, KzgProof}; /// Converts a blob ssz List object to an array to be used with the kzg /// crypto library. @@ -7,6 +9,12 @@ fn ssz_blob_to_crypto_blob(blob: &Blob) -> Result(cell: &Cell) -> Result { + KzgCell::from_bytes(cell.as_ref()).map_err(Into::into) +} + /// Validate a single blob-commitment-proof triplet from a `BlobSidecar`. pub fn validate_blob( kzg: &Kzg, @@ -19,6 +27,50 @@ pub fn validate_blob( kzg.verify_blob_kzg_proof(&kzg_blob, kzg_commitment, kzg_proof) } +/// Validate a batch of `DataColumnSidecar`. +pub fn validate_data_column<'a, E: EthSpec, I>( + kzg: &Kzg, + data_column_iter: I, +) -> Result<(), KzgError> +where + I: Iterator>> + Clone, +{ + let cells = data_column_iter + .clone() + .flat_map(|data_column| data_column.column.iter().map(ssz_cell_to_crypto_cell::)) + .collect::, KzgError>>()?; + + let proofs = data_column_iter + .clone() + .flat_map(|data_column| { + data_column + .kzg_proofs + .iter() + .map(|&proof| Bytes48::from(proof)) + }) + .collect::>(); + + let coordinates = data_column_iter + .clone() + .flat_map(|data_column| { + let col_index = data_column.index; + (0..data_column.column.len()).map(move |row| (row as u64, col_index)) + }) + .collect::>(); + + let commitments = data_column_iter + .clone() + .flat_map(|data_column| { + data_column + .kzg_commitments + .iter() + .map(|&commitment| Bytes48::from(commitment)) + }) + .collect::>(); + + kzg.verify_cell_proof_batch(&cells, &proofs, &coordinates, &commitments) +} + /// Validate a batch of blob-commitment-proof triplets from multiple `BlobSidecars`. pub fn validate_blobs( kzg: &Kzg, diff --git a/beacon_node/beacon_chain/src/metrics.rs b/beacon_node/beacon_chain/src/metrics.rs index fef21d595ab..c4ff0d3bc05 100644 --- a/beacon_node/beacon_chain/src/metrics.rs +++ b/beacon_node/beacon_chain/src/metrics.rs @@ -1145,6 +1145,10 @@ lazy_static! { try_create_histogram("kzg_verification_single_seconds", "Runtime of single kzg verification"); pub static ref KZG_VERIFICATION_BATCH_TIMES: Result = try_create_histogram("kzg_verification_batch_seconds", "Runtime of batched kzg verification"); + pub static ref KZG_VERIFICATION_DATA_COLUMN_SINGLE_TIMES: Result = + try_create_histogram("kzg_verification_data_column_single_seconds", "Runtime of single data column kzg verification"); + pub static ref KZG_VERIFICATION_DATA_COLUMN_BATCH_TIMES: Result = + try_create_histogram("kzg_verification_data_column_batch_seconds", "Runtime of batched data column kzg verification"); pub static ref BLOCK_PRODUCTION_BLOBS_VERIFICATION_TIMES: Result = try_create_histogram( "beacon_block_production_blobs_verification_seconds", diff --git a/beacon_node/beacon_processor/src/lib.rs b/beacon_node/beacon_processor/src/lib.rs index cc6ed245d68..256fcdcbd98 100644 --- a/beacon_node/beacon_processor/src/lib.rs +++ b/beacon_node/beacon_processor/src/lib.rs @@ -1028,7 +1028,7 @@ impl BeaconProcessor { self.spawn_worker(item, idle_tx); } else if let Some(item) = rpc_blob_queue.pop() { self.spawn_worker(item, idle_tx); - // TODO(das): decide proper priorization for sampling columns + // TODO(das): decide proper prioritization for sampling columns } else if let Some(item) = rpc_custody_column_queue.pop() { self.spawn_worker(item, idle_tx); } else if let Some(item) = rpc_verify_data_column_queue.pop() { 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 7c432a4866b..2aa498da857 100644 --- a/beacon_node/network/src/network_beacon_processor/sync_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/sync_methods.rs @@ -8,6 +8,7 @@ use crate::sync::{ use beacon_chain::block_verification_types::{AsBlock, RpcBlock}; use beacon_chain::data_availability_checker::AvailabilityCheckError; use beacon_chain::data_availability_checker::MaybeAvailableBlock; +use beacon_chain::data_column_verification::verify_kzg_for_data_column_list; use beacon_chain::data_column_verification::CustodyDataColumn; use beacon_chain::{ validator_monitor::get_slot_delay_ms, AvailabilityProcessingStatus, BeaconChainError, @@ -360,11 +361,11 @@ impl NetworkBeaconProcessor { pub async fn validate_rpc_data_columns( self: Arc>, _block_root: Hash256, - _data_columns: Vec>>, + data_columns: Vec>>, _seen_timestamp: Duration, ) -> Result<(), String> { - // TODO(das): validate data column sidecar KZG commitment - Ok(()) + let kzg = self.chain.kzg.as_ref().ok_or("Kzg not initialized")?; + verify_kzg_for_data_column_list(data_columns.iter(), kzg).map_err(|err| format!("{err:?}")) } /// Process a sampling completed event, inserting it into fork-choice diff --git a/consensus/types/src/data_column_sidecar.rs b/consensus/types/src/data_column_sidecar.rs index 7e038d81f5c..f92838ceabe 100644 --- a/consensus/types/src/data_column_sidecar.rs +++ b/consensus/types/src/data_column_sidecar.rs @@ -27,7 +27,7 @@ use tree_hash::TreeHash; use tree_hash_derive::TreeHash; pub type ColumnIndex = u64; -pub type Cell = FixedVector::FieldElementsPerCell>; +pub type Cell = FixedVector::BytesPerCell>; pub type DataColumn = VariableList, ::MaxBlobCommitmentsPerBlock>; /// Container of the data that identifies an individual data column. diff --git a/consensus/types/src/eth_spec.rs b/consensus/types/src/eth_spec.rs index 640faeec07e..4ed7bfc9615 100644 --- a/consensus/types/src/eth_spec.rs +++ b/consensus/types/src/eth_spec.rs @@ -144,6 +144,11 @@ pub trait EthSpec: /// Must be set to `BytesPerFieldElement * FieldElementsPerBlob`. type BytesPerBlob: Unsigned + Clone + Sync + Send + Debug + PartialEq; + /// The total length of a data column in bytes. + /// + /// Must be set to `BytesPerFieldElement * FieldElementsPerCell`. + type BytesPerCell: Unsigned + Clone + Sync + Send + Debug + PartialEq; + /* * New in Electra */ @@ -410,6 +415,7 @@ impl EthSpec for MainnetEthSpec { type FieldElementsPerBlob = U4096; type FieldElementsPerCell = U64; type BytesPerBlob = U131072; + type BytesPerCell = U2048; type KzgCommitmentInclusionProofDepth = U17; type MinCustodyRequirement = U1; type DataColumnSubnetCount = U32; @@ -457,6 +463,7 @@ impl EthSpec for MinimalEthSpec { type FieldElementsPerBlob = U4096; type FieldElementsPerCell = U64; type BytesPerBlob = U131072; + type BytesPerCell = U2048; type MaxBlobCommitmentsPerBlock = U16; type KzgCommitmentInclusionProofDepth = U9; type PendingPartialWithdrawalsLimit = U64; @@ -547,6 +554,7 @@ impl EthSpec for GnosisEthSpec { type FieldElementsPerCell = U64; type BytesPerFieldElement = U32; type BytesPerBlob = U131072; + type BytesPerCell = U2048; type KzgCommitmentInclusionProofDepth = U17; type PendingBalanceDepositsLimit = U134217728; type PendingPartialWithdrawalsLimit = U134217728; diff --git a/crypto/kzg/src/lib.rs b/crypto/kzg/src/lib.rs index bf63390d108..b7d4c13a7c4 100644 --- a/crypto/kzg/src/lib.rs +++ b/crypto/kzg/src/lib.rs @@ -13,7 +13,7 @@ pub use c_kzg::{ Blob, Bytes32, Bytes48, KzgSettings, BYTES_PER_BLOB, BYTES_PER_COMMITMENT, BYTES_PER_FIELD_ELEMENT, BYTES_PER_PROOF, FIELD_ELEMENTS_PER_BLOB, }; -use c_kzg::{Cell, CELLS_PER_BLOB}; +pub use c_kzg::{Cell, CELLS_PER_EXT_BLOB}; use mockall::automock; #[derive(Debug)] @@ -151,8 +151,14 @@ impl Kzg { pub fn compute_cells_and_proofs( &self, blob: &Blob, - ) -> Result<(Box<[Cell; CELLS_PER_BLOB]>, Box<[KzgProof; CELLS_PER_BLOB]>), Error> { - let (cells, proofs) = c_kzg::Cell::compute_cells_and_proofs(blob, &self.trusted_setup) + ) -> Result< + ( + Box<[Cell; CELLS_PER_EXT_BLOB]>, + Box<[KzgProof; CELLS_PER_EXT_BLOB]>, + ), + Error, + > { + let (cells, proofs) = c_kzg::Cell::compute_cells_and_kzg_proofs(blob, &self.trusted_setup) .map_err(Into::::into)?; let proofs = Box::new(proofs.map(|proof| KzgProof::from(proof.to_bytes().into_inner()))); Ok((cells, proofs)) @@ -166,25 +172,17 @@ impl Kzg { pub fn verify_cell_proof_batch( &self, cells: &[Cell], - kzg_proofs: &[KzgProof], + kzg_proofs: &[Bytes48], coordinates: &[(u64, u64)], - kzg_commitments: &[KzgCommitment], + kzg_commitments: &[Bytes48], ) -> Result<(), Error> { - let commitments_bytes: Vec = kzg_commitments - .iter() - .map(|comm| Bytes48::from(*comm)) - .collect(); - let proofs_bytes: Vec = kzg_proofs - .iter() - .map(|proof| Bytes48::from(*proof)) - .collect(); let (rows, columns): (Vec, Vec) = coordinates.iter().cloned().unzip(); - if !c_kzg::KzgProof::verify_cell_proof_batch( - &commitments_bytes, + if !c_kzg::KzgProof::verify_cell_kzg_proof_batch( + kzg_commitments, &rows, &columns, cells, - &proofs_bytes, + kzg_proofs, &self.trusted_setup, )? { Err(Error::KzgVerificationFailed) @@ -196,18 +194,24 @@ impl Kzg { pub mod mock { use crate::{Error, KzgProof}; - use c_kzg::{Blob, Cell, CELLS_PER_BLOB}; + use c_kzg::{Blob, Cell, CELLS_PER_EXT_BLOB}; pub const MOCK_KZG_BYTES_PER_CELL: usize = 2048; #[allow(clippy::type_complexity)] pub fn compute_cells_and_proofs( _blob: &Blob, - ) -> Result<(Box<[Cell; CELLS_PER_BLOB]>, Box<[KzgProof; CELLS_PER_BLOB]>), Error> { + ) -> Result< + ( + Box<[Cell; CELLS_PER_EXT_BLOB]>, + Box<[KzgProof; CELLS_PER_EXT_BLOB]>, + ), + Error, + > { let empty_cell = Cell::new([0; MOCK_KZG_BYTES_PER_CELL]); Ok(( - Box::new([empty_cell; CELLS_PER_BLOB]), - Box::new([KzgProof::empty(); CELLS_PER_BLOB]), + Box::new([empty_cell; CELLS_PER_EXT_BLOB]), + Box::new([KzgProof::empty(); CELLS_PER_EXT_BLOB]), )) } } From fe9e5dd4587efb08de872ce292eab2d0da878130 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Thu, 9 May 2024 23:10:24 +1000 Subject: [PATCH 16/76] Rename `PEER_DAS_EPOCH` to `EIP7594_FORK_EPOCH` for client interop. (#5750) --- .../beacon_chain/src/block_verification.rs | 6 ++++-- .../src/data_availability_checker.rs | 8 +++++--- .../overflow_lru_cache.rs | 4 ++-- .../network_beacon_processor/gossip_methods.rs | 6 ++++-- beacon_node/network/src/service.rs | 2 +- .../network/src/sync/block_lookups/tests.rs | 2 +- .../mainnet/config.yaml | 2 +- consensus/types/src/chain_spec.rs | 18 +++++++++--------- 8 files changed, 27 insertions(+), 21 deletions(-) diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 3a0b4daf8ea..77b89b94554 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -738,8 +738,10 @@ impl IntoGossipVerifiedBlockContents for PublishBlockReq let peer_das_enabled = chain .spec - .peer_das_epoch - .map_or(false, |peer_das_epoch| block.epoch() >= peer_das_epoch); + .eip7594_fork_epoch + .map_or(false, |eip7594_fork_epoch| { + block.epoch() >= eip7594_fork_epoch + }); let gossip_verified_data_columns = if peer_das_enabled { build_gossip_verified_data_columns(chain, &block, gossip_verified_blobs.as_ref())? diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index ce8ade55592..b7a56cac164 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -439,11 +439,13 @@ impl DataAvailabilityChecker { block.num_expected_blobs() > 0 && self.data_columns_required_for_epoch(block.epoch()) } - /// Returns true if the given epoch is greater than or equal to the `PEER_DAS_EPOCH`. + /// Returns true if the given epoch is greater than or equal to the `EIP7594_FORK_EPOCH`. fn is_peer_das_enabled_for_epoch(&self, block_epoch: Epoch) -> bool { self.spec - .peer_das_epoch - .map_or(false, |peer_das_epoch| block_epoch >= peer_das_epoch) + .eip7594_fork_epoch + .map_or(false, |eip7594_fork_epoch| { + block_epoch >= eip7594_fork_epoch + }) } /// The epoch at which we require a data availability check in block processing. diff --git a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs index 9d62499711c..5947c521545 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs @@ -792,8 +792,8 @@ impl OverflowLRUCache { let peer_das_enabled = self .spec - .peer_das_epoch - .map_or(false, |peer_das_epoch| epoch >= peer_das_epoch); + .eip7594_fork_epoch + .map_or(false, |eip7594_fork_epoch| epoch >= eip7594_fork_epoch); if peer_das_enabled { Ok(BlockImportRequirement::CustodyColumns( self.custody_column_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 9dd2d240d35..fa03f1c9fc9 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -1277,8 +1277,10 @@ impl NetworkBeaconProcessor { if self .chain .spec - .peer_das_epoch - .map_or(false, |peer_das_epoch| block.epoch() >= peer_das_epoch) + .eip7594_fork_epoch + .map_or(false, |eip7594_fork_epoch| { + block.epoch() >= eip7594_fork_epoch + }) { self.send_sync_message(SyncMessage::SampleBlock(block_root, block.slot())); } diff --git a/beacon_node/network/src/service.rs b/beacon_node/network/src/service.rs index 4a1bd2c6621..fd899f5d45f 100644 --- a/beacon_node/network/src/service.rs +++ b/beacon_node/network/src/service.rs @@ -754,7 +754,7 @@ impl NetworkService { } } } else { - // TODO(das): subscribe after `PEER_DAS_EPOCH` + // TODO(das): subscribe after `EIP7594_FORK_EPOCH` for column_subnet in DataColumnSubnetId::compute_custody_subnets::( self.network_globals.local_enr().node_id().raw().into(), self.network_globals diff --git a/beacon_node/network/src/sync/block_lookups/tests.rs b/beacon_node/network/src/sync/block_lookups/tests.rs index f2fa7bd5be6..497032e7dc0 100644 --- a/beacon_node/network/src/sync/block_lookups/tests.rs +++ b/beacon_node/network/src/sync/block_lookups/tests.rs @@ -102,7 +102,7 @@ impl TestRig { if let Some(config) = config { if config.peer_das_enabled { - spec.peer_das_epoch = Some(Epoch::new(0)); + spec.eip7594_fork_epoch = Some(Epoch::new(0)); } } diff --git a/common/eth2_network_config/built_in_network_configs/mainnet/config.yaml b/common/eth2_network_config/built_in_network_configs/mainnet/config.yaml index 8c3d71d6748..b56793869ee 100644 --- a/common/eth2_network_config/built_in_network_configs/mainnet/config.yaml +++ b/common/eth2_network_config/built_in_network_configs/mainnet/config.yaml @@ -54,7 +54,7 @@ DENEB_FORK_EPOCH: 269568 # March 13, 2024, 01:55:35pm UTC ELECTRA_FORK_VERSION: 0x05000000 ELECTRA_FORK_EPOCH: 18446744073709551615 # PeerDAS -PEER_DAS_EPOCH: 18446744073709551615 +EIP7594_FORK_EPOCH: 18446744073709551615 # Time parameters diff --git a/consensus/types/src/chain_spec.rs b/consensus/types/src/chain_spec.rs index b2e6e3da7e3..d9f9865927d 100644 --- a/consensus/types/src/chain_spec.rs +++ b/consensus/types/src/chain_spec.rs @@ -194,7 +194,7 @@ pub struct ChainSpec { /* * DAS params */ - pub peer_das_epoch: Option, + pub eip7594_fork_epoch: Option, pub custody_requirement: u64, /* @@ -764,7 +764,7 @@ impl ChainSpec { /* * DAS params */ - peer_das_epoch: None, + eip7594_fork_epoch: None, custody_requirement: 1, /* @@ -872,7 +872,7 @@ impl ChainSpec { max_pending_partials_per_withdrawals_sweep: u64::checked_pow(2, 0) .expect("pow does not overflow"), // PeerDAS - peer_das_epoch: None, + eip7594_fork_epoch: None, // Other network_id: 2, // lighthouse testnet network id deposit_chain_id: 5, @@ -1077,7 +1077,7 @@ impl ChainSpec { /* * DAS params */ - peer_das_epoch: None, + eip7594_fork_epoch: None, custody_requirement: 1, /* * Network specific @@ -1216,7 +1216,7 @@ pub struct Config { #[serde(default)] #[serde(serialize_with = "serialize_fork_epoch")] #[serde(deserialize_with = "deserialize_fork_epoch")] - pub peer_das_epoch: Option>, + pub eip7594_fork_epoch: Option>, #[serde(with = "serde_utils::quoted_u64")] seconds_per_slot: u64, @@ -1608,8 +1608,8 @@ impl Config { .electra_fork_epoch .map(|epoch| MaybeQuoted { value: epoch }), - peer_das_epoch: spec - .peer_das_epoch + eip7594_fork_epoch: spec + .eip7594_fork_epoch .map(|epoch| MaybeQuoted { value: epoch }), seconds_per_slot: spec.seconds_per_slot, @@ -1689,7 +1689,7 @@ impl Config { deneb_fork_version, electra_fork_epoch, electra_fork_version, - peer_das_epoch, + eip7594_fork_epoch, seconds_per_slot, seconds_per_eth1_block, min_validator_withdrawability_delay, @@ -1751,7 +1751,7 @@ impl Config { deneb_fork_version, electra_fork_epoch: electra_fork_epoch.map(|q| q.value), electra_fork_version, - peer_das_epoch: peer_das_epoch.map(|q| q.value), + eip7594_fork_epoch: eip7594_fork_epoch.map(|q| q.value), seconds_per_slot, seconds_per_eth1_block, min_validator_withdrawability_delay, From 09d217c1d22941c3bd0146427c2b6a7d8b229eb3 Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Fri, 10 May 2024 11:56:36 +0900 Subject: [PATCH 17/76] Fetch custody columns in range sync (#5747) * Fetch custody columns in range sync * Clean up todos --- .../src/block_verification_types.rs | 6 +- .../src/rpc/codec/ssz_snappy.rs | 26 +- .../lighthouse_network/src/rpc/config.rs | 23 +- .../lighthouse_network/src/rpc/methods.rs | 12 +- .../lighthouse_network/src/rpc/outbound.rs | 4 +- .../lighthouse_network/src/rpc/protocol.rs | 3 +- .../src/rpc/rate_limiter.rs | 31 ++- .../src/service/api_types.rs | 8 +- .../network_beacon_processor/rpc_methods.rs | 120 ++++----- beacon_node/network/src/router.rs | 2 +- .../network/src/sync/backfill_sync/mod.rs | 2 +- .../network/src/sync/block_lookups/tests.rs | 7 +- .../src/sync/block_sidecar_coupling.rs | 246 ++++++++++++++---- beacon_node/network/src/sync/manager.rs | 79 ++---- .../network/src/sync/network_context.rs | 131 +++++++--- .../src/sync/network_context/custody.rs | 11 +- .../network/src/sync/range_sync/chain.rs | 2 +- .../network/src/sync/range_sync/range.rs | 4 +- 18 files changed, 435 insertions(+), 282 deletions(-) diff --git a/beacon_node/beacon_chain/src/block_verification_types.rs b/beacon_node/beacon_chain/src/block_verification_types.rs index e7219f6bb07..e59dadb47fc 100644 --- a/beacon_node/beacon_chain/src/block_verification_types.rs +++ b/beacon_node/beacon_chain/src/block_verification_types.rs @@ -164,11 +164,9 @@ impl RpcBlock { ) -> Result { let block_root = block_root.unwrap_or_else(|| get_block_root(&block)); - if let Ok(block_commitments) = block.message().body().blob_kzg_commitments() { + if block.num_expected_blobs() > 0 && custody_columns.is_empty() { // The number of required custody columns is out of scope here. - if !block_commitments.is_empty() && custody_columns.is_empty() { - return Err(AvailabilityCheckError::MissingCustodyColumns); - } + return Err(AvailabilityCheckError::MissingCustodyColumns); } // Treat empty blob lists as if they are missing. let inner = if custody_columns.is_empty() { diff --git a/beacon_node/lighthouse_network/src/rpc/codec/ssz_snappy.rs b/beacon_node/lighthouse_network/src/rpc/codec/ssz_snappy.rs index 90eca79d98a..74751c604ba 100644 --- a/beacon_node/lighthouse_network/src/rpc/codec/ssz_snappy.rs +++ b/beacon_node/lighthouse_network/src/rpc/codec/ssz_snappy.rs @@ -227,8 +227,8 @@ impl Encoder> for SSZSnappyOutboundCodec { }, OutboundRequest::BlobsByRange(req) => req.as_ssz_bytes(), OutboundRequest::BlobsByRoot(req) => req.blob_ids.as_ssz_bytes(), + OutboundRequest::DataColumnsByRange(req) => req.as_ssz_bytes(), OutboundRequest::DataColumnsByRoot(req) => req.data_column_ids.as_ssz_bytes(), - OutboundRequest::DataColumnsByRange(req) => req.data_column_ids.as_ssz_bytes(), OutboundRequest::Ping(req) => req.as_ssz_bytes(), OutboundRequest::MetaData(_) => return Ok(()), // no metadata to encode }; @@ -521,6 +521,9 @@ fn handle_rpc_request( )?, }))) } + SupportedProtocol::DataColumnsByRangeV1 => Ok(Some(InboundRequest::DataColumnsByRange( + DataColumnsByRangeRequest::from_ssz_bytes(decoded_buffer)?, + ))), SupportedProtocol::DataColumnsByRootV1 => Ok(Some(InboundRequest::DataColumnsByRoot( DataColumnsByRootRequest { data_column_ids: RuntimeVariableList::from_ssz_bytes( @@ -529,9 +532,6 @@ fn handle_rpc_request( )?, }, ))), - SupportedProtocol::DataColumnsByRangeV1 => Ok(Some(InboundRequest::DataColumnsByRange( - DataColumnsByRangeRequest::from_ssz_bytes(decoded_buffer)?, - ))), SupportedProtocol::PingV1 => Ok(Some(InboundRequest::Ping(Ping { data: u64::from_ssz_bytes(decoded_buffer)?, }))), @@ -624,14 +624,14 @@ fn handle_rpc_response( ), )), }, - SupportedProtocol::DataColumnsByRootV1 => match fork_name { + SupportedProtocol::DataColumnsByRangeV1 => match fork_name { // TODO(das): update fork name - Some(ForkName::Deneb) => Ok(Some(RPCResponse::DataColumnsByRoot(Arc::new( + Some(ForkName::Deneb) => Ok(Some(RPCResponse::DataColumnsByRange(Arc::new( DataColumnSidecar::from_ssz_bytes(decoded_buffer)?, )))), Some(_) => Err(RPCError::ErrorResponse( RPCResponseErrorCode::InvalidRequest, - "Invalid fork name for data columns by root".to_string(), + "Invalid fork name for data columns by range".to_string(), )), None => Err(RPCError::ErrorResponse( RPCResponseErrorCode::InvalidRequest, @@ -641,14 +641,14 @@ fn handle_rpc_response( ), )), }, - SupportedProtocol::DataColumnsByRangeV1 => match fork_name { + SupportedProtocol::DataColumnsByRootV1 => match fork_name { // TODO(das): update fork name - Some(ForkName::Deneb) => Ok(Some(RPCResponse::DataColumnsByRange(Arc::new( + Some(ForkName::Deneb) => Ok(Some(RPCResponse::DataColumnsByRoot(Arc::new( DataColumnSidecar::from_ssz_bytes(decoded_buffer)?, )))), Some(_) => Err(RPCError::ErrorResponse( RPCResponseErrorCode::InvalidRequest, - "Invalid fork name for data columns by range".to_string(), + "Invalid fork name for data columns by root".to_string(), )), None => Err(RPCError::ErrorResponse( RPCResponseErrorCode::InvalidRequest, @@ -1066,12 +1066,12 @@ mod tests { OutboundRequest::BlobsByRoot(bbroot) => { assert_eq!(decoded, InboundRequest::BlobsByRoot(bbroot)) } + OutboundRequest::DataColumnsByRange(value) => { + assert_eq!(decoded, InboundRequest::DataColumnsByRange(value)) + } OutboundRequest::DataColumnsByRoot(dcbroot) => { assert_eq!(decoded, InboundRequest::DataColumnsByRoot(dcbroot)) } - OutboundRequest::DataColumnsByRange(dcbrange) => { - assert_eq!(decoded, InboundRequest::DataColumnsByRange(dcbrange)) - } OutboundRequest::Ping(ping) => { assert_eq!(decoded, InboundRequest::Ping(ping)) } diff --git a/beacon_node/lighthouse_network/src/rpc/config.rs b/beacon_node/lighthouse_network/src/rpc/config.rs index a357495e7a8..7f1595d5295 100644 --- a/beacon_node/lighthouse_network/src/rpc/config.rs +++ b/beacon_node/lighthouse_network/src/rpc/config.rs @@ -91,8 +91,8 @@ pub struct RateLimiterConfig { pub(super) blocks_by_root_quota: Quota, pub(super) blobs_by_range_quota: Quota, pub(super) blobs_by_root_quota: Quota, - pub(super) data_columns_by_root_quota: Quota, pub(super) data_columns_by_range_quota: Quota, + pub(super) data_columns_by_root_quota: Quota, pub(super) light_client_bootstrap_quota: Quota, pub(super) light_client_optimistic_update_quota: Quota, pub(super) light_client_finality_update_quota: Quota, @@ -107,8 +107,9 @@ impl RateLimiterConfig { pub const DEFAULT_BLOCKS_BY_ROOT_QUOTA: Quota = Quota::n_every(128, 10); pub const DEFAULT_BLOBS_BY_RANGE_QUOTA: Quota = Quota::n_every(768, 10); pub const DEFAULT_BLOBS_BY_ROOT_QUOTA: Quota = Quota::n_every(128, 10); - pub const DEFAULT_DATA_COLUMNS_BY_ROOT_QUOTA: Quota = Quota::n_every(128, 10); + // TODO(das): random value without thought pub const DEFAULT_DATA_COLUMNS_BY_RANGE_QUOTA: Quota = Quota::n_every(128, 10); + pub const DEFAULT_DATA_COLUMNS_BY_ROOT_QUOTA: Quota = Quota::n_every(128, 10); pub const DEFAULT_LIGHT_CLIENT_BOOTSTRAP_QUOTA: Quota = Quota::one_every(10); pub const DEFAULT_LIGHT_CLIENT_OPTIMISTIC_UPDATE_QUOTA: Quota = Quota::one_every(10); pub const DEFAULT_LIGHT_CLIENT_FINALITY_UPDATE_QUOTA: Quota = Quota::one_every(10); @@ -125,8 +126,8 @@ impl Default for RateLimiterConfig { blocks_by_root_quota: Self::DEFAULT_BLOCKS_BY_ROOT_QUOTA, blobs_by_range_quota: Self::DEFAULT_BLOBS_BY_RANGE_QUOTA, blobs_by_root_quota: Self::DEFAULT_BLOBS_BY_ROOT_QUOTA, - data_columns_by_root_quota: Self::DEFAULT_DATA_COLUMNS_BY_ROOT_QUOTA, data_columns_by_range_quota: Self::DEFAULT_DATA_COLUMNS_BY_RANGE_QUOTA, + data_columns_by_root_quota: Self::DEFAULT_DATA_COLUMNS_BY_ROOT_QUOTA, light_client_bootstrap_quota: Self::DEFAULT_LIGHT_CLIENT_BOOTSTRAP_QUOTA, light_client_optimistic_update_quota: Self::DEFAULT_LIGHT_CLIENT_OPTIMISTIC_UPDATE_QUOTA, @@ -156,6 +157,10 @@ impl Debug for RateLimiterConfig { .field("blocks_by_root", fmt_q!(&self.blocks_by_root_quota)) .field("blobs_by_range", fmt_q!(&self.blobs_by_range_quota)) .field("blobs_by_root", fmt_q!(&self.blobs_by_root_quota)) + .field( + "data_columns_by_range", + fmt_q!(&self.data_columns_by_range_quota), + ) .field( "data_columns_by_root", fmt_q!(&self.data_columns_by_root_quota), @@ -180,8 +185,8 @@ impl FromStr for RateLimiterConfig { let mut blocks_by_root_quota = None; let mut blobs_by_range_quota = None; let mut blobs_by_root_quota = None; - let mut data_columns_by_root_quota = None; let mut data_columns_by_range_quota = None; + let mut data_columns_by_root_quota = None; let mut light_client_bootstrap_quota = None; let mut light_client_optimistic_update_quota = None; let mut light_client_finality_update_quota = None; @@ -196,12 +201,12 @@ impl FromStr for RateLimiterConfig { Protocol::BlocksByRoot => blocks_by_root_quota = blocks_by_root_quota.or(quota), Protocol::BlobsByRange => blobs_by_range_quota = blobs_by_range_quota.or(quota), Protocol::BlobsByRoot => blobs_by_root_quota = blobs_by_root_quota.or(quota), - Protocol::DataColumnsByRoot => { - data_columns_by_root_quota = data_columns_by_root_quota.or(quota) - } Protocol::DataColumnsByRange => { data_columns_by_range_quota = data_columns_by_range_quota.or(quota) } + Protocol::DataColumnsByRoot => { + data_columns_by_root_quota = data_columns_by_root_quota.or(quota) + } Protocol::Ping => ping_quota = ping_quota.or(quota), Protocol::MetaData => meta_data_quota = meta_data_quota.or(quota), Protocol::LightClientBootstrap => { @@ -229,10 +234,10 @@ impl FromStr for RateLimiterConfig { blobs_by_range_quota: blobs_by_range_quota .unwrap_or(Self::DEFAULT_BLOBS_BY_RANGE_QUOTA), blobs_by_root_quota: blobs_by_root_quota.unwrap_or(Self::DEFAULT_BLOBS_BY_ROOT_QUOTA), - data_columns_by_root_quota: data_columns_by_root_quota - .unwrap_or(Self::DEFAULT_DATA_COLUMNS_BY_ROOT_QUOTA), data_columns_by_range_quota: data_columns_by_range_quota .unwrap_or(Self::DEFAULT_DATA_COLUMNS_BY_RANGE_QUOTA), + data_columns_by_root_quota: data_columns_by_root_quota + .unwrap_or(Self::DEFAULT_DATA_COLUMNS_BY_ROOT_QUOTA), light_client_bootstrap_quota: light_client_bootstrap_quota .unwrap_or(Self::DEFAULT_LIGHT_CLIENT_BOOTSTRAP_QUOTA), light_client_optimistic_update_quota: light_client_optimistic_update_quota diff --git a/beacon_node/lighthouse_network/src/rpc/methods.rs b/beacon_node/lighthouse_network/src/rpc/methods.rs index 84add18ee05..a892b7f07cc 100644 --- a/beacon_node/lighthouse_network/src/rpc/methods.rs +++ b/beacon_node/lighthouse_network/src/rpc/methods.rs @@ -300,17 +300,17 @@ impl BlobsByRangeRequest { pub struct DataColumnsByRangeRequest { /// The starting slot to request data columns. pub start_slot: u64, - /// The number of slots from the start slot. pub count: u64, - - /// The list of beacon block roots and column indices being requested. - pub data_column_ids: Vec, + /// The list column indices being requested. + pub columns: Vec, } impl DataColumnsByRangeRequest { - pub fn max_data_columns_requested(&self) -> u64 { - self.count.saturating_mul(E::max_blobs_per_block() as u64) + pub fn max_requested(&self) -> u64 { + self.count + .saturating_mul(E::max_blobs_per_block() as u64) + .saturating_mul(self.columns.len() as u64) } } diff --git a/beacon_node/lighthouse_network/src/rpc/outbound.rs b/beacon_node/lighthouse_network/src/rpc/outbound.rs index bcb76c00081..5c537c49d48 100644 --- a/beacon_node/lighthouse_network/src/rpc/outbound.rs +++ b/beacon_node/lighthouse_network/src/rpc/outbound.rs @@ -36,8 +36,8 @@ pub enum OutboundRequest { BlocksByRoot(BlocksByRootRequest), BlobsByRange(BlobsByRangeRequest), BlobsByRoot(BlobsByRootRequest), - DataColumnsByRoot(DataColumnsByRootRequest), DataColumnsByRange(DataColumnsByRangeRequest), + DataColumnsByRoot(DataColumnsByRootRequest), Ping(Ping), MetaData(MetadataRequest), } @@ -111,7 +111,7 @@ impl OutboundRequest { OutboundRequest::BlobsByRange(req) => req.max_blobs_requested::(), OutboundRequest::BlobsByRoot(req) => req.blob_ids.len() as u64, OutboundRequest::DataColumnsByRoot(req) => req.data_column_ids.len() as u64, - OutboundRequest::DataColumnsByRange(req) => req.data_column_ids.len() as u64, + OutboundRequest::DataColumnsByRange(req) => req.max_requested::(), OutboundRequest::Ping(_) => 1, OutboundRequest::MetaData(_) => 1, } diff --git a/beacon_node/lighthouse_network/src/rpc/protocol.rs b/beacon_node/lighthouse_network/src/rpc/protocol.rs index 1447a57e706..eadeb4d4374 100644 --- a/beacon_node/lighthouse_network/src/rpc/protocol.rs +++ b/beacon_node/lighthouse_network/src/rpc/protocol.rs @@ -382,6 +382,7 @@ impl SupportedProtocol { ProtocolId::new(SupportedProtocol::BlobsByRangeV1, Encoding::SSZSnappy), // TODO(das): add to PeerDAS fork ProtocolId::new(SupportedProtocol::DataColumnsByRootV1, Encoding::SSZSnappy), + ProtocolId::new(SupportedProtocol::DataColumnsByRangeV1, Encoding::SSZSnappy), ]); } supported @@ -704,7 +705,7 @@ impl InboundRequest { InboundRequest::BlobsByRange(req) => req.max_blobs_requested::(), InboundRequest::BlobsByRoot(req) => req.blob_ids.len() as u64, InboundRequest::DataColumnsByRoot(req) => req.data_column_ids.len() as u64, - InboundRequest::DataColumnsByRange(req) => req.data_column_ids.len() as u64, + InboundRequest::DataColumnsByRange(req) => req.max_requested::(), InboundRequest::Ping(_) => 1, InboundRequest::MetaData(_) => 1, InboundRequest::LightClientBootstrap(_) => 1, diff --git a/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs b/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs index 9fb085efd86..de0b1afc8b4 100644 --- a/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs +++ b/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs @@ -97,10 +97,10 @@ pub struct RPCRateLimiter { blbrange_rl: Limiter, /// BlobsByRoot rate limiter. blbroot_rl: Limiter, + /// DataColumnssByRange rate limiter. + dcbrange_rl: Limiter, /// DataColumnssByRoot rate limiter. dcbroot_rl: Limiter, - /// DataColumnsByRange rate limiter. - dcbrange_rl: Limiter, /// LightClientBootstrap rate limiter. lc_bootstrap_rl: Limiter, /// LightClientOptimisticUpdate rate limiter. @@ -137,10 +137,10 @@ pub struct RPCRateLimiterBuilder { blbrange_quota: Option, /// Quota for the BlobsByRoot protocol. blbroot_quota: Option, - /// Quota for the DataColumnsByRoot protocol. - dcbroot_quota: Option, /// Quota for the DataColumnsByRange protocol. dcbrange_quota: Option, + /// Quota for the DataColumnsByRoot protocol. + dcbroot_quota: Option, /// Quota for the LightClientBootstrap protocol. lcbootstrap_quota: Option, /// Quota for the LightClientOptimisticUpdate protocol. @@ -162,8 +162,8 @@ impl RPCRateLimiterBuilder { Protocol::BlocksByRoot => self.bbroots_quota = q, Protocol::BlobsByRange => self.blbrange_quota = q, Protocol::BlobsByRoot => self.blbroot_quota = q, - Protocol::DataColumnsByRoot => self.dcbroot_quota = q, Protocol::DataColumnsByRange => self.dcbrange_quota = q, + Protocol::DataColumnsByRoot => self.dcbroot_quota = q, Protocol::LightClientBootstrap => self.lcbootstrap_quota = q, Protocol::LightClientOptimisticUpdate => self.lc_optimistic_update_quota = q, Protocol::LightClientFinalityUpdate => self.lc_finality_update_quota = q, @@ -196,18 +196,15 @@ impl RPCRateLimiterBuilder { let blbrange_quota = self .blbrange_quota .ok_or("BlobsByRange quota not specified")?; - let blbroots_quota = self .blbroot_quota .ok_or("BlobsByRoot quota not specified")?; - - let dcbroot_quota = self - .dcbroot_quota - .ok_or("DataColumnsByRoot quota not specified")?; - let dcbrange_quota = self .dcbrange_quota .ok_or("DataColumnsByRange quota not specified")?; + let dcbroot_quota = self + .dcbroot_quota + .ok_or("DataColumnsByRoot quota not specified")?; // create the rate limiters let ping_rl = Limiter::from_quota(ping_quota)?; @@ -218,8 +215,8 @@ impl RPCRateLimiterBuilder { let bbrange_rl = Limiter::from_quota(bbrange_quota)?; let blbrange_rl = Limiter::from_quota(blbrange_quota)?; let blbroot_rl = Limiter::from_quota(blbroots_quota)?; - let dcbroot_rl = Limiter::from_quota(dcbroot_quota)?; let dcbrange_rl = Limiter::from_quota(dcbrange_quota)?; + let dcbroot_rl = Limiter::from_quota(dcbroot_quota)?; let lc_bootstrap_rl = Limiter::from_quota(lc_bootstrap_quota)?; let lc_optimistic_update_rl = Limiter::from_quota(lc_optimistic_update_quota)?; let lc_finality_update_rl = Limiter::from_quota(lc_finality_update_quota)?; @@ -238,8 +235,8 @@ impl RPCRateLimiterBuilder { bbrange_rl, blbrange_rl, blbroot_rl, - dcbroot_rl, dcbrange_rl, + dcbroot_rl, lc_bootstrap_rl, lc_optimistic_update_rl, lc_finality_update_rl, @@ -284,8 +281,8 @@ impl RPCRateLimiter { blocks_by_root_quota, blobs_by_range_quota, blobs_by_root_quota, - data_columns_by_root_quota, data_columns_by_range_quota, + data_columns_by_root_quota, light_client_bootstrap_quota, light_client_optimistic_update_quota, light_client_finality_update_quota, @@ -300,8 +297,8 @@ impl RPCRateLimiter { .set_quota(Protocol::BlocksByRoot, blocks_by_root_quota) .set_quota(Protocol::BlobsByRange, blobs_by_range_quota) .set_quota(Protocol::BlobsByRoot, blobs_by_root_quota) - .set_quota(Protocol::DataColumnsByRoot, data_columns_by_root_quota) .set_quota(Protocol::DataColumnsByRange, data_columns_by_range_quota) + .set_quota(Protocol::DataColumnsByRoot, data_columns_by_root_quota) .set_quota(Protocol::LightClientBootstrap, light_client_bootstrap_quota) .set_quota( Protocol::LightClientOptimisticUpdate, @@ -338,8 +335,8 @@ impl RPCRateLimiter { Protocol::BlocksByRoot => &mut self.bbroots_rl, Protocol::BlobsByRange => &mut self.blbrange_rl, Protocol::BlobsByRoot => &mut self.blbroot_rl, - Protocol::DataColumnsByRoot => &mut self.dcbroot_rl, Protocol::DataColumnsByRange => &mut self.dcbrange_rl, + Protocol::DataColumnsByRoot => &mut self.dcbroot_rl, Protocol::LightClientBootstrap => &mut self.lc_bootstrap_rl, Protocol::LightClientOptimisticUpdate => &mut self.lc_optimistic_update_rl, Protocol::LightClientFinalityUpdate => &mut self.lc_finality_update_rl, @@ -357,6 +354,8 @@ impl RPCRateLimiter { self.bbroots_rl.prune(time_since_start); self.blbrange_rl.prune(time_since_start); self.blbroot_rl.prune(time_since_start); + self.dcbrange_rl.prune(time_since_start); + self.dcbroot_rl.prune(time_since_start); } } diff --git a/beacon_node/lighthouse_network/src/service/api_types.rs b/beacon_node/lighthouse_network/src/service/api_types.rs index 0902b6d27ba..86f681fa601 100644 --- a/beacon_node/lighthouse_network/src/service/api_types.rs +++ b/beacon_node/lighthouse_network/src/service/api_types.rs @@ -6,7 +6,9 @@ use types::{ LightClientOptimisticUpdate, SignedBeaconBlock, }; -use crate::rpc::methods::{BlobsByRangeRequest, BlobsByRootRequest, DataColumnsByRootRequest}; +use crate::rpc::methods::{ + BlobsByRangeRequest, BlobsByRootRequest, DataColumnsByRangeRequest, DataColumnsByRootRequest, +}; use crate::rpc::{ methods::{ BlocksByRangeRequest, BlocksByRootRequest, LightClientBootstrapRequest, @@ -16,8 +18,6 @@ use crate::rpc::{ OutboundRequest, SubstreamId, }; -use super::methods::DataColumnsByRangeRequest; - /// Identifier of requests sent by a peer. pub type PeerRequestId = (ConnectionId, SubstreamId); @@ -84,8 +84,8 @@ impl std::convert::From for OutboundRequest { } Request::BlobsByRange(r) => OutboundRequest::BlobsByRange(r), Request::BlobsByRoot(r) => OutboundRequest::BlobsByRoot(r), - Request::DataColumnsByRoot(r) => OutboundRequest::DataColumnsByRoot(r), Request::DataColumnsByRange(r) => OutboundRequest::DataColumnsByRange(r), + Request::DataColumnsByRoot(r) => OutboundRequest::DataColumnsByRoot(r), Request::Status(s) => OutboundRequest::Status(s), } } 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 04329762629..fe69786b522 100644 --- a/beacon_node/network/src/network_beacon_processor/rpc_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/rpc_methods.rs @@ -14,12 +14,13 @@ use lighthouse_network::rpc::*; use lighthouse_network::{PeerId, PeerRequestId, ReportSource, Response, SyncInfo}; use slog::{debug, error, warn}; use slot_clock::SlotClock; +use std::collections::HashSet; use std::collections::{hash_map::Entry, HashMap}; use std::sync::Arc; use tokio::sync::mpsc; use tokio_stream::StreamExt; use types::blob_sidecar::BlobIdentifier; -use types::{Epoch, EthSpec, ForkName, Hash256, Slot}; +use types::{ColumnIndex, Epoch, EthSpec, ForkName, Hash256, Slot}; impl NetworkBeaconProcessor { /* Auxiliary functions */ @@ -936,11 +937,26 @@ impl NetworkBeaconProcessor { /// Handle a `DataColumnsByRange` request from the peer. pub fn handle_data_columns_by_range_request( - self: Arc, + &self, peer_id: PeerId, request_id: PeerRequestId, req: DataColumnsByRangeRequest, ) { + self.terminate_response_stream( + peer_id, + request_id, + self.handle_data_columns_by_range_request_inner(peer_id, request_id, req), + Response::DataColumnsByRange, + ); + } + + /// Handle a `DataColumnsByRange` request from the peer. + pub fn handle_data_columns_by_range_request_inner( + &self, + peer_id: PeerId, + request_id: PeerRequestId, + req: DataColumnsByRangeRequest, + ) -> Result<(), (RPCResponseErrorCode, &'static str)> { debug!(self.log, "Received DataColumnsByRange Request"; "peer_id" => %peer_id, "count" => req.count, @@ -948,15 +964,11 @@ impl NetworkBeaconProcessor { ); // Should not send more than max request data columns - if req.max_data_columns_requested::() - > self.chain.spec.max_request_data_column_sidecars - { - return self.send_error_response( - peer_id, + if req.max_requested::() > self.chain.spec.max_request_data_column_sidecars { + return Err(( RPCResponseErrorCode::InvalidRequest, - "Request exceeded `MAX_REQUEST_DATA_COLUMN_SIDECARS`".into(), - request_id, - ); + "Request exceeded `MAX_REQUEST_BLOBS_SIDECARS`", + )); } let request_start_slot = Slot::from(req.start_slot); @@ -965,13 +977,10 @@ impl NetworkBeaconProcessor { Some(boundary) => boundary.start_slot(T::EthSpec::slots_per_epoch()), None => { debug!(self.log, "Deneb fork is disabled"); - self.send_error_response( - peer_id, + return Err(( RPCResponseErrorCode::InvalidRequest, - "Deneb fork is disabled".into(), - request_id, - ); - return; + "Deneb fork is disabled", + )); } }; @@ -992,19 +1001,15 @@ impl NetworkBeaconProcessor { ); return if data_availability_boundary_slot < oldest_data_column_slot { - self.send_error_response( - peer_id, + Err(( RPCResponseErrorCode::ResourceUnavailable, - "data columns pruned within boundary".into(), - request_id, - ) + "blobs pruned within boundary", + )) } else { - self.send_error_response( - peer_id, + Err(( RPCResponseErrorCode::InvalidRequest, - "Req outside availability period".into(), - request_id, - ) + "Req outside availability period", + )) }; } @@ -1021,25 +1026,15 @@ impl NetworkBeaconProcessor { "requested_slot" => slot, "oldest_known_slot" => oldest_block_slot ); - return self.send_error_response( - peer_id, - RPCResponseErrorCode::ResourceUnavailable, - "Backfilling".into(), - request_id, - ); + return Err((RPCResponseErrorCode::ResourceUnavailable, "Backfilling")); } Err(e) => { - self.send_error_response( - peer_id, - RPCResponseErrorCode::ServerError, - "Database error".into(), - request_id, - ); - return error!(self.log, "Unable to obtain root iter"; + error!(self.log, "Unable to obtain root iter"; "request" => ?req, "peer" => %peer_id, "error" => ?e ); + return Err((RPCResponseErrorCode::ServerError, "Database error")); } }; @@ -1071,11 +1066,12 @@ impl NetworkBeaconProcessor { let block_roots = match maybe_block_roots { Ok(block_roots) => block_roots, Err(e) => { - return error!(self.log, "Error during iteration over blocks"; + error!(self.log, "Error during iteration over blocks"; "request" => ?req, "peer" => %peer_id, "error" => ?e - ) + ); + return Err((RPCResponseErrorCode::ServerError, "Database error")); } }; @@ -1083,23 +1079,22 @@ impl NetworkBeaconProcessor { let block_roots = block_roots.into_iter().flatten(); let mut data_columns_sent = 0; - let mut send_response = true; + let requested_column_indices = + HashSet::::from_iter(req.columns.iter().copied()); for root in block_roots { match self.chain.get_data_columns(&root) { Ok(data_column_sidecar_list) => { for data_column_sidecar in data_column_sidecar_list.iter() { - for &data_column_id in req.data_column_ids.iter() { - if data_column_sidecar.id() == data_column_id { - data_columns_sent += 1; - self.send_network_message(NetworkMessage::SendResponse { - peer_id, - response: Response::DataColumnsByRange(Some( - data_column_sidecar.clone(), - )), - id: request_id, - }); - } + if requested_column_indices.contains(&data_column_sidecar.index) { + data_columns_sent += 1; + self.send_network_message(NetworkMessage::SendResponse { + peer_id, + response: Response::DataColumnsByRange(Some( + data_column_sidecar.clone(), + )), + id: request_id, + }); } } } @@ -1112,14 +1107,10 @@ impl NetworkBeaconProcessor { "block_root" => ?root, "error" => ?e ); - self.send_error_response( - peer_id, + return Err(( RPCResponseErrorCode::ServerError, - "No data columns and failed fetching corresponding block".into(), - request_id, - ); - send_response = false; - break; + "No data columns and failed fetching corresponding block", + )); } } } @@ -1139,14 +1130,7 @@ impl NetworkBeaconProcessor { "returned" => data_columns_sent ); - if send_response { - // send the stream terminator - self.send_network_message(NetworkMessage::SendResponse { - peer_id, - response: Response::DataColumnsByRange(None), - id: request_id, - }); - } + Ok(()) } /// Helper function to ensure single item protocol always end with either a single chunk or an diff --git a/beacon_node/network/src/router.rs b/beacon_node/network/src/router.rs index 944f21a4700..375cef2094d 100644 --- a/beacon_node/network/src/router.rs +++ b/beacon_node/network/src/router.rs @@ -521,7 +521,7 @@ impl Router { ) { let request_id = match request_id { RequestId::Sync(sync_id) => match sync_id { - id @ SyncId::RangeBlockAndBlobs { .. } => id, + id @ SyncId::RangeBlockComponents { .. } => id, other => { crit!(self.log, "BlocksByRange response on incorrect request"; "request" => ?other); return; diff --git a/beacon_node/network/src/sync/backfill_sync/mod.rs b/beacon_node/network/src/sync/backfill_sync/mod.rs index 4be92d59a4b..0eb7b8634ff 100644 --- a/beacon_node/network/src/sync/backfill_sync/mod.rs +++ b/beacon_node/network/src/sync/backfill_sync/mod.rs @@ -929,7 +929,7 @@ impl BackFillSync { ) -> Result<(), BackFillError> { if let Some(batch) = self.batches.get_mut(&batch_id) { let (request, is_blob_batch) = batch.to_blocks_by_range_request(); - match network.blocks_and_blobs_by_range_request( + match network.block_components_by_range_request( peer, is_blob_batch, request, diff --git a/beacon_node/network/src/sync/block_lookups/tests.rs b/beacon_node/network/src/sync/block_lookups/tests.rs index 497032e7dc0..3701fa82c4c 100644 --- a/beacon_node/network/src/sync/block_lookups/tests.rs +++ b/beacon_node/network/src/sync/block_lookups/tests.rs @@ -5,7 +5,6 @@ use crate::sync::manager::{ DataColumnsByRootRequestId, DataColumnsByRootRequester, RequestId as SyncRequestId, SingleLookupReqId, SyncManager, }; -use crate::sync::network_context::custody::CustodyRequester; use crate::sync::sampling::{SamplingConfig, SamplingRequester}; use crate::sync::{SamplingId, SyncMessage}; use crate::NetworkMessage; @@ -612,11 +611,7 @@ impl TestRig { let lookup_id = if let DataColumnsByRootRequester::Custody(id) = sampling_ids.first().unwrap().0.requester { - if let CustodyRequester::Lookup(id) = id.id { - id.lookup_id - } else { - panic!("not a lookup requester"); - } + id.id.0.lookup_id } else { panic!("not a custody requester") }; diff --git a/beacon_node/network/src/sync/block_sidecar_coupling.rs b/beacon_node/network/src/sync/block_sidecar_coupling.rs index 80cfb4eb64b..ca6460f4305 100644 --- a/beacon_node/network/src/sync/block_sidecar_coupling.rs +++ b/beacon_node/network/src/sync/block_sidecar_coupling.rs @@ -1,78 +1,90 @@ use beacon_chain::{ - block_verification_types::RpcBlock, data_column_verification::CustodyDataColumn, + block_verification_types::RpcBlock, data_column_verification::CustodyDataColumn, get_block_root, }; use ssz_types::VariableList; -use std::{collections::VecDeque, sync::Arc}; -use types::{BlobSidecar, EthSpec, SignedBeaconBlock}; - -use super::range_sync::ByRangeRequestType; +use std::{ + collections::{HashMap, VecDeque}, + sync::Arc, +}; +use types::{BlobSidecar, ColumnIndex, DataColumnSidecar, EthSpec, Hash256, SignedBeaconBlock}; #[derive(Debug)] -pub struct BlocksAndBlobsRequestInfo { +pub struct RangeBlockComponentsRequest { /// Blocks we have received awaiting for their corresponding sidecar. - accumulated_blocks: VecDeque>>, + blocks: VecDeque>>, /// Sidecars we have received awaiting for their corresponding block. - accumulated_sidecars: VecDeque>>, - accumulated_custody_columns: VecDeque>, + blobs: VecDeque>>, + data_columns: VecDeque>>, /// Whether the individual RPC request for blocks is finished or not. is_blocks_stream_terminated: bool, /// Whether the individual RPC request for sidecars is finished or not. is_sidecars_stream_terminated: bool, - is_custody_columns_stream_terminated: bool, + custody_columns_streams_terminated: usize, /// Used to determine if this accumulator should wait for a sidecars stream termination - request_type: ByRangeRequestType, + expects_blobs: bool, + expects_custody_columns: Option>, } -impl BlocksAndBlobsRequestInfo { - pub fn new(request_type: ByRangeRequestType) -> Self { +impl RangeBlockComponentsRequest { + pub fn new(expects_blobs: bool, expects_custody_columns: Option>) -> Self { Self { - accumulated_blocks: <_>::default(), - accumulated_sidecars: <_>::default(), - accumulated_custody_columns: <_>::default(), - is_blocks_stream_terminated: <_>::default(), - is_sidecars_stream_terminated: <_>::default(), - is_custody_columns_stream_terminated: <_>::default(), - request_type, + blocks: <_>::default(), + blobs: <_>::default(), + data_columns: <_>::default(), + is_blocks_stream_terminated: false, + is_sidecars_stream_terminated: false, + custody_columns_streams_terminated: 0, + expects_blobs, + expects_custody_columns, } } - pub fn get_request_type(&self) -> ByRangeRequestType { - self.request_type + // TODO: This function should be deprecated when simplying the retry mechanism of this range + // requests. + pub fn get_requirements(&self) -> (bool, Option>) { + (self.expects_blobs, self.expects_custody_columns.clone()) } pub fn add_block_response(&mut self, block_opt: Option>>) { match block_opt { - Some(block) => self.accumulated_blocks.push_back(block), + Some(block) => self.blocks.push_back(block), None => self.is_blocks_stream_terminated = true, } } pub fn add_sidecar_response(&mut self, sidecar_opt: Option>>) { match sidecar_opt { - Some(sidecar) => self.accumulated_sidecars.push_back(sidecar), + Some(sidecar) => self.blobs.push_back(sidecar), None => self.is_sidecars_stream_terminated = true, } } - pub fn add_custody_column(&mut self, column_opt: Option>) { + pub fn add_data_column(&mut self, column_opt: Option>>) { match column_opt { - Some(column) => self.accumulated_custody_columns.push_back(column), - None => self.is_custody_columns_stream_terminated = true, + Some(column) => self.data_columns.push_back(column), + // TODO(das): this mechanism is dangerous, if somehow there are two requests for the + // same column index it can terminate early. This struct should track that all requests + // for all custody columns terminate. + None => self.custody_columns_streams_terminated += 1, } } pub fn into_responses(self) -> Result>, String> { - let BlocksAndBlobsRequestInfo { - accumulated_blocks, - accumulated_sidecars, - .. - } = self; + if let Some(expects_custody_columns) = self.expects_custody_columns.clone() { + self.into_responses_with_custody_columns(expects_custody_columns) + } else { + self.into_responses_with_blobs() + } + } + + fn into_responses_with_blobs(self) -> Result>, String> { + let RangeBlockComponentsRequest { blocks, blobs, .. } = self; // There can't be more more blobs than blocks. i.e. sending any blob (empty // included) for a skipped slot is not permitted. - let mut responses = Vec::with_capacity(accumulated_blocks.len()); - let mut blob_iter = accumulated_sidecars.into_iter().peekable(); - for block in accumulated_blocks.into_iter() { + let mut responses = Vec::with_capacity(blocks.len()); + let mut blob_iter = blobs.into_iter().peekable(); + for block in blocks.into_iter() { let mut blob_list = Vec::with_capacity(E::max_blobs_per_block()); while { let pair_next_blob = blob_iter @@ -108,27 +120,115 @@ impl BlocksAndBlobsRequestInfo { Ok(responses) } + fn into_responses_with_custody_columns( + self, + expects_custody_columns: Vec, + ) -> Result>, String> { + let RangeBlockComponentsRequest { + blocks, + data_columns, + .. + } = self; + + // 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() + { + return Err(format!( + "Repeated column block_root {block_root:?} index {index}" + )); + } + } + + // 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 rpc_blocks = Vec::with_capacity(blocks.len()); + + for block in blocks { + let block_root = get_block_root(&block); + rpc_blocks.push(if block.num_expected_blobs() > 0 { + let Some(mut data_columns_by_index) = data_columns_by_block.remove(&block_root) + else { + // This PR ignores the fix from https://github.com/sigp/lighthouse/pull/5675 + // which allows blobs to not match blocks. + // TODO(das): on the initial version of PeerDAS the beacon chain does not check + // rpc custody requirements and dropping this check can allow the block to have + // an inconsistent DB. + return Err(format!("No columns for block {block_root:?} with data")); + }; + + let mut custody_columns = vec![]; + for index in &expects_custody_columns { + let Some(data_column) = data_columns_by_index.remove(index) else { + return Err(format!("No column for block {block_root:?} index {index}")); + }; + // 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. + custody_columns.push(CustodyDataColumn::from_asserted_custody(data_column)); + } + + // Assert that there are no columns left + if !data_columns_by_index.is_empty() { + let remaining_indices = data_columns_by_index.keys().collect::>(); + return Err(format!( + "Not all columns consumed for block {block_root:?}: {remaining_indices:?}" + )); + } + + RpcBlock::new_with_custody_columns(Some(block_root), block, custody_columns) + .map_err(|e| format!("{e:?}"))? + } else { + RpcBlock::new_without_blobs(Some(block_root), block) + }); + } + + // 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::>(); + return Err(format!("Not all columns consumed: {remaining_roots:?}")); + } + + Ok(rpc_blocks) + } + pub fn is_finished(&self) -> bool { - let blobs_requested = matches!(self.request_type, ByRangeRequestType::BlocksAndBlobs); - let custody_columns_requested = - matches!(self.request_type, ByRangeRequestType::BlocksAndColumns); - self.is_blocks_stream_terminated - && (!blobs_requested || self.is_sidecars_stream_terminated) - && (!custody_columns_requested || self.is_custody_columns_stream_terminated) + if !self.is_blocks_stream_terminated { + return false; + } + if self.expects_blobs && !self.is_sidecars_stream_terminated { + return false; + } + if let Some(expects_custody_columns) = &self.expects_custody_columns { + if self.custody_columns_streams_terminated < expects_custody_columns.len() { + return false; + } + } + true } } #[cfg(test)] mod tests { - use super::BlocksAndBlobsRequestInfo; - use crate::sync::range_sync::ByRangeRequestType; - use beacon_chain::test_utils::{generate_rand_block_and_blobs, NumBlobs}; + use super::RangeBlockComponentsRequest; + use beacon_chain::test_utils::{ + generate_rand_block_and_blobs, generate_rand_block_and_data_columns, NumBlobs, + }; use rand::SeedableRng; use types::{test_utils::XorShiftRng, ForkName, MinimalEthSpec as E}; #[test] fn no_blobs_into_responses() { - let mut info = BlocksAndBlobsRequestInfo::::new(ByRangeRequestType::Blocks); + let mut info = RangeBlockComponentsRequest::::new(false, None); 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) @@ -147,7 +247,7 @@ mod tests { #[test] fn empty_blobs_into_responses() { - let mut info = BlocksAndBlobsRequestInfo::::new(ByRangeRequestType::BlocksAndBlobs); + let mut info = RangeBlockComponentsRequest::::new(true, None); let mut rng = XorShiftRng::from_seed([42; 16]); let blocks = (0..4) .map(|_| { @@ -170,4 +270,58 @@ mod tests { assert!(info.is_finished()); info.into_responses().unwrap(); } + + #[test] + fn rpc_block_with_custody_columns() { + let expects_custody_columns = vec![1, 2, 3, 4]; + let mut info = + RangeBlockComponentsRequest::::new(false, Some(expects_custody_columns.clone())); + let mut rng = XorShiftRng::from_seed([42; 16]); + let blocks = (0..4) + .map(|_| { + generate_rand_block_and_data_columns::( + ForkName::Deneb, + NumBlobs::Number(1), + &mut rng, + ) + }) + .collect::>(); + + // Send blocks and complete terminate response + for block in &blocks { + info.add_block_response(Some(block.0.clone().into())); + } + info.add_block_response(None); + // Assert response is not finished + assert!(!info.is_finished()); + + // Send data columns interleaved + for block in &blocks { + for column in &block.1 { + if expects_custody_columns.contains(&column.index) { + info.add_data_column(Some(column.clone().into())); + } + } + } + + // Terminate the requests + for (i, _column_index) in expects_custody_columns.iter().enumerate() { + info.add_data_column(None); + + if i < expects_custody_columns.len() - 1 { + assert!( + !info.is_finished(), + "requested should not be finished at loop {i}" + ); + } else { + assert!( + info.is_finished(), + "request should be finishied at loop {i}" + ); + } + } + + // All completed construct response + info.into_responses().unwrap(); + } } diff --git a/beacon_node/network/src/sync/manager.rs b/beacon_node/network/src/sync/manager.rs index 05ef447d828..7b8d7850a71 100644 --- a/beacon_node/network/src/sync/manager.rs +++ b/beacon_node/network/src/sync/manager.rs @@ -36,7 +36,7 @@ use super::backfill_sync::{BackFillSync, ProcessResult, SyncStart}; use super::block_lookups::BlockLookups; use super::network_context::{ - custody::CustodyRequester, BlockOrBlob, CustodyId, RangeRequestId, RpcEvent, SyncNetworkContext, + BlockOrBlob, CustodyId, RangeRequestId, RpcEvent, SyncNetworkContext, }; use super::peer_sync_info::{remote_sync_type, PeerSyncType}; use super::range_sync::{RangeSync, RangeSyncType, EPOCHS_PER_BATCH}; @@ -47,7 +47,7 @@ use crate::status::ToStatusMessage; use crate::sync::block_lookups::{ BlobRequestState, BlockComponent, BlockRequestState, CustodyRequestState, DownloadResult, }; -use crate::sync::block_sidecar_coupling::BlocksAndBlobsRequestInfo; +use crate::sync::block_sidecar_coupling::RangeBlockComponentsRequest; use crate::sync::network_context::PeerGroup; use beacon_chain::block_verification_types::AsBlock; use beacon_chain::block_verification_types::RpcBlock; @@ -94,9 +94,7 @@ pub enum RequestId { /// Request searching for a set of data columns given a hash and list of column indices. DataColumnsByRoot(DataColumnsByRootRequestId), /// Range request that is composed by both a block range request and a blob range request. - RangeBlockAndBlobs { id: Id }, - /// Range request that is composed by both a block range request and a blob range request. - RangeBlockAndDataColumns { id: Id }, + RangeBlockComponents(Id), } #[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] @@ -397,30 +395,7 @@ impl SyncManager { RequestId::DataColumnsByRoot(id) => { self.on_single_data_column_response(id, peer_id, RpcEvent::RPCError(error)) } - RequestId::RangeBlockAndBlobs { id } => { - if let Some(sender_id) = self.network.range_request_failed(id) { - match sender_id { - RangeRequestId::RangeSync { chain_id, batch_id } => { - self.range_sync.inject_error( - &mut self.network, - peer_id, - batch_id, - chain_id, - id, - ); - self.update_sync_state(); - } - RangeRequestId::BackfillSync { batch_id } => match self - .backfill_sync - .inject_error(&mut self.network, batch_id, &peer_id, id) - { - Ok(_) => {} - Err(_) => self.update_sync_state(), - }, - } - } - } - RequestId::RangeBlockAndDataColumns { id } => { + RequestId::RangeBlockComponents(id) => { if let Some(sender_id) = self.network.range_request_failed(id) { match sender_id { RangeRequestId::RangeSync { chain_id, batch_id } => { @@ -898,7 +873,7 @@ impl SyncManager { None => RpcEvent::StreamTermination, }, ), - RequestId::RangeBlockAndBlobs { id } => { + RequestId::RangeBlockComponents(id) => { self.range_block_and_blobs_response(id, peer_id, block.into()) } other => { @@ -941,7 +916,7 @@ impl SyncManager { None => RpcEvent::StreamTermination, }, ), - RequestId::RangeBlockAndBlobs { id } => { + RequestId::RangeBlockComponents(id) => { self.range_block_and_blobs_response(id, peer_id, blob.into()) } other => { @@ -971,11 +946,12 @@ impl SyncManager { }, ); } - RequestId::RangeBlockAndBlobs { id } => { - todo!("TODO(das): handle sampling for range sync based on {id}"); - } - RequestId::RangeBlockAndDataColumns { id } => { - todo!("TODO(das): handle sampling for range sync based on {id}"); + RequestId::RangeBlockComponents(id) => { + self.range_block_and_blobs_response( + id, + peer_id, + BlockOrBlob::CustodyColumns(data_column), + ); } } } @@ -1023,22 +999,14 @@ impl SyncManager { { // TODO(das): get proper timestamp let seen_timestamp = timestamp_now(); - match requester { - CustodyRequester::Lookup(id) => self - .block_lookups - .on_download_response::>( - id.lookup_id, - custody_columns.map(|(columns, peer_group)| { - (columns, peer_group, seen_timestamp) - }), - &mut self.network, - ), - CustodyRequester::RangeSync(_) => { - // TODO(das): this line should be unreachable, no mechanism to make - // custody requests for sync yet - todo!("custody fetching for sync not implemented"); - } - } + self.block_lookups + .on_download_response::>( + requester.0.lookup_id, + custody_columns.map(|(columns, peer_group)| { + (columns, peer_group, seen_timestamp) + }), + &mut self.network, + ); } } } @@ -1135,7 +1103,10 @@ impl SyncManager { self.network.insert_range_blocks_and_blobs_request( id, resp.sender_id, - BlocksAndBlobsRequestInfo::new(resp.request_type), + RangeBlockComponentsRequest::new( + resp.expects_blobs, + resp.expects_custody_columns, + ), ); // inform range that the request needs to be treated as failed // With time we will want to downgrade this log @@ -1146,7 +1117,7 @@ impl SyncManager { "sender_id" => ?resp.sender_id, "error" => e.clone() ); - let id = RequestId::RangeBlockAndBlobs { id }; + let id = RequestId::RangeBlockComponents(id); self.network.report_peer( peer_id, PeerAction::MidToleranceError, diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index bdcebc80516..bdd6ca241fd 100644 --- a/beacon_node/network/src/sync/network_context.rs +++ b/beacon_node/network/src/sync/network_context.rs @@ -9,7 +9,7 @@ use self::requests::{ pub use self::requests::{ BlobsByRootSingleBlockRequest, BlocksByRootSingleRequest, DataColumnsByRootSingleBlockRequest, }; -use super::block_sidecar_coupling::BlocksAndBlobsRequestInfo; +use super::block_sidecar_coupling::RangeBlockComponentsRequest; use super::manager::{ BlockProcessType, DataColumnsByRootRequestId, DataColumnsByRootRequester, Id, RequestId as SyncRequestId, @@ -25,7 +25,7 @@ use beacon_chain::data_column_verification::CustodyDataColumn; use beacon_chain::validator_monitor::timestamp_now; use beacon_chain::{BeaconChain, BeaconChainTypes, EngineState}; use fnv::FnvHashMap; -use lighthouse_network::rpc::methods::BlobsByRangeRequest; +use lighthouse_network::rpc::methods::{BlobsByRangeRequest, DataColumnsByRangeRequest}; use lighthouse_network::rpc::{BlocksByRangeRequest, GoodbyeReason, RPCError}; use lighthouse_network::{ Client, Eth2Enr, NetworkGlobals, PeerAction, PeerId, ReportSource, Request, @@ -40,7 +40,7 @@ use tokio::sync::mpsc; use types::blob_sidecar::FixedBlobSidecarList; use types::{ BlobSidecar, ColumnIndex, DataColumnSidecar, DataColumnSubnetId, Epoch, EthSpec, Hash256, - SignedBeaconBlock, + SignedBeaconBlock, Slot, }; pub mod custody; @@ -49,7 +49,8 @@ mod requests; pub struct BlocksAndBlobsByRangeResponse { pub sender_id: RangeRequestId, pub responses: Result>, String>, - pub request_type: ByRangeRequestType, + pub expects_blobs: bool, + pub expects_custody_columns: Option>, } #[derive(Debug, Clone, Copy)] @@ -129,8 +130,8 @@ pub struct SyncNetworkContext { custody_by_root_requests: FnvHashMap>, /// BlocksByRange requests paired with BlobsByRange - range_blocks_and_blobs_requests: - FnvHashMap)>, + range_block_components_requests: + FnvHashMap)>, /// Whether the ee is online. If it's not, we don't allow access to the /// `beacon_processor_send`. @@ -149,7 +150,7 @@ pub struct SyncNetworkContext { pub enum BlockOrBlob { Block(Option>>), Blob(Option>>), - CustodyColumns(Option>), + CustodyColumns(Option>>), } impl From>>> for BlockOrBlob { @@ -179,7 +180,7 @@ impl SyncNetworkContext { blobs_by_root_requests: <_>::default(), data_columns_by_root_requests: <_>::default(), custody_by_root_requests: <_>::default(), - range_blocks_and_blobs_requests: FnvHashMap::default(), + range_block_components_requests: FnvHashMap::default(), network_beacon_processor, chain, log, @@ -253,33 +254,37 @@ impl SyncNetworkContext { } } - /// A blocks by range request for the range sync algorithm. - pub fn blocks_by_range_request( + /// A blocks by range request sent by the range sync algorithm + pub fn block_components_by_range_request( &mut self, peer_id: PeerId, batch_type: ByRangeRequestType, request: BlocksByRangeRequest, + sender_id: RangeRequestId, ) -> Result { + let epoch = Slot::new(*request.start_slot()).epoch(T::EthSpec::slots_per_epoch()); let id = self.next_id(); - trace!( + debug!( self.log, "Sending BlocksByRange request"; "method" => "BlocksByRange", "count" => request.count(), + "epoch" => epoch, "peer" => %peer_id, ); self.send_network_msg(NetworkMessage::SendRequest { peer_id, request: Request::BlocksByRange(request.clone()), - request_id: RequestId::Sync(SyncRequestId::RangeBlockAndBlobs { id }), + request_id: RequestId::Sync(SyncRequestId::RangeBlockComponents(id)), })?; - if matches!(batch_type, ByRangeRequestType::BlocksAndBlobs) { + let expected_blobs = if matches!(batch_type, ByRangeRequestType::BlocksAndBlobs) { debug!( self.log, "Sending BlobsByRange requests"; "method" => "BlobsByRange", "count" => request.count(), + "epoch" => epoch, "peer" => %peer_id, ); @@ -290,30 +295,64 @@ impl SyncNetworkContext { start_slot: *request.start_slot(), count: *request.count(), }), - request_id: RequestId::Sync(SyncRequestId::RangeBlockAndBlobs { id }), + request_id: RequestId::Sync(SyncRequestId::RangeBlockComponents(id)), })?; - } + true + } else { + false + }; - Ok(id) - } + let expects_custody_columns = if matches!(batch_type, ByRangeRequestType::BlocksAndColumns) + { + let custody_indexes = self.network_globals().custody_columns(epoch)?; + + for column_index in &custody_indexes { + let custody_peer_ids = self.get_custodial_peers(epoch, *column_index); + let Some(custody_peer) = custody_peer_ids.first().cloned() else { + // TODO(das): this will be pretty bad UX. To improve we should: + // - Attempt to fetch custody requests first, before requesting blocks + // - Handle the no peers case gracefully, maybe add some timeout and give a few + // minutes / seconds to the peer manager to locate peers on this subnet before + // abandoing progress on the chain completely. + return Err("no custody peer"); + }; + + debug!( + self.log, + "Sending DataColumnsByRange requests"; + "method" => "DataColumnsByRange", + "count" => request.count(), + "epoch" => epoch, + "index" => column_index, + "peer" => %custody_peer, + ); - /// A blocks by range request sent by the range sync algorithm - pub fn blocks_and_blobs_by_range_request( - &mut self, - peer_id: PeerId, - batch_type: ByRangeRequestType, - request: BlocksByRangeRequest, - sender_id: RangeRequestId, - ) -> Result { - let id = self.blocks_by_range_request(peer_id, batch_type, request)?; - self.range_blocks_and_blobs_requests - .insert(id, (sender_id, BlocksAndBlobsRequestInfo::new(batch_type))); + // Create the blob request based on the blocks request. + self.send_network_msg(NetworkMessage::SendRequest { + peer_id: custody_peer, + request: Request::DataColumnsByRange(DataColumnsByRangeRequest { + start_slot: *request.start_slot(), + count: *request.count(), + columns: vec![*column_index], + }), + request_id: RequestId::Sync(SyncRequestId::RangeBlockComponents(id)), + })?; + } + + Some(custody_indexes) + } else { + None + }; + + let info = RangeBlockComponentsRequest::new(expected_blobs, expects_custody_columns); + self.range_block_components_requests + .insert(id, (sender_id, info)); Ok(id) } pub fn range_request_failed(&mut self, request_id: Id) -> Option { let sender_id = self - .range_blocks_and_blobs_requests + .range_block_components_requests .remove(&request_id) .map(|(sender_id, _info)| sender_id); if let Some(sender_id) = sender_id { @@ -337,22 +376,23 @@ impl SyncNetworkContext { request_id: Id, block_or_blob: BlockOrBlob, ) -> Option> { - match self.range_blocks_and_blobs_requests.entry(request_id) { + match self.range_block_components_requests.entry(request_id) { Entry::Occupied(mut entry) => { let (_, info) = entry.get_mut(); match block_or_blob { BlockOrBlob::Block(maybe_block) => info.add_block_response(maybe_block), BlockOrBlob::Blob(maybe_sidecar) => info.add_sidecar_response(maybe_sidecar), - BlockOrBlob::CustodyColumns(column) => info.add_custody_column(column), + BlockOrBlob::CustodyColumns(column) => info.add_data_column(column), } if info.is_finished() { // If the request is finished, dequeue everything let (sender_id, info) = entry.remove(); - let request_type = info.get_request_type(); + let (expects_blobs, expects_custody_columns) = info.get_requirements(); Some(BlocksAndBlobsByRangeResponse { sender_id, - request_type, responses: info.into_responses(), + expects_blobs, + expects_custody_columns, }) } else { None @@ -598,7 +638,7 @@ impl SyncNetworkContext { "id" => ?id ); - let requester = CustodyRequester::Lookup(id); + let requester = CustodyRequester(id); let mut request = ActiveCustodyRequest::new( block_root, requester, @@ -727,13 +767,18 @@ impl SyncNetworkContext { "To deal with alignment with deneb boundaries, batches need to be of just one epoch" ); - if let Some(data_availability_boundary) = self.chain.data_availability_boundary() { - if epoch >= data_availability_boundary { - // TODO(das): After peerdas fork, return `BlocksAndColumns` - ByRangeRequestType::BlocksAndBlobs - } else { - ByRangeRequestType::Blocks - } + if self + .chain + .data_availability_checker + .data_columns_required_for_epoch(epoch) + { + ByRangeRequestType::BlocksAndColumns + } else if self + .chain + .data_availability_checker + .blobs_required_for_epoch(epoch) + { + ByRangeRequestType::BlocksAndBlobs } else { ByRangeRequestType::Blocks } @@ -743,9 +788,9 @@ impl SyncNetworkContext { &mut self, id: Id, sender_id: RangeRequestId, - info: BlocksAndBlobsRequestInfo, + info: RangeBlockComponentsRequest, ) { - self.range_blocks_and_blobs_requests + self.range_block_components_requests .insert(id, (sender_id, info)); } diff --git a/beacon_node/network/src/sync/network_context/custody.rs b/beacon_node/network/src/sync/network_context/custody.rs index 72964d3f362..f20a95415db 100644 --- a/beacon_node/network/src/sync/network_context/custody.rs +++ b/beacon_node/network/src/sync/network_context/custody.rs @@ -1,4 +1,4 @@ -use crate::sync::manager::{Id, SingleLookupReqId}; +use crate::sync::manager::SingleLookupReqId; use self::request::ActiveColumnSampleRequest; use beacon_chain::data_column_verification::CustodyDataColumn; @@ -17,11 +17,10 @@ pub struct CustodyId { pub column_index: ColumnIndex, } +/// Downstream components that perform custody by root requests. +/// Currently, it's only single block lookups, so not using an enum #[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] -pub enum CustodyRequester { - Lookup(SingleLookupReqId), - RangeSync(Id), -} +pub struct CustodyRequester(pub SingleLookupReqId); type DataColumnSidecarList = Vec>>; @@ -109,6 +108,8 @@ impl ActiveCustodyRequest { } else { // Peer does not have the requested data. // TODO(das) what to do? + // TODO(das): If the peer is in the lookup peer set it claims to have imported + // the block AND its custody columns. So in this case we can downscore debug!(self.log, "Sampling peer claims to not have the data"; "block_root" => %self.block_root, "column_index" => column_index); // TODO(das) tolerate this failure if you are not sure the block has data request.on_download_success()?; diff --git a/beacon_node/network/src/sync/range_sync/chain.rs b/beacon_node/network/src/sync/range_sync/chain.rs index 9a6c99ebf6c..809d2d7bb16 100644 --- a/beacon_node/network/src/sync/range_sync/chain.rs +++ b/beacon_node/network/src/sync/range_sync/chain.rs @@ -884,7 +884,7 @@ impl SyncingChain { ) -> ProcessingResult { if let Some(batch) = self.batches.get_mut(&batch_id) { let (request, batch_type) = batch.to_blocks_by_range_request(); - match network.blocks_and_blobs_by_range_request( + match network.block_components_by_range_request( peer, batch_type, request, diff --git a/beacon_node/network/src/sync/range_sync/range.rs b/beacon_node/network/src/sync/range_sync/range.rs index fe48db35b45..eaf5e5f6b1b 100644 --- a/beacon_node/network/src/sync/range_sync/range.rs +++ b/beacon_node/network/src/sync/range_sync/range.rs @@ -554,7 +554,7 @@ mod tests { ) -> (ChainId, BatchId, Id) { if blob_req_opt.is_some() { match block_req { - RequestId::Sync(crate::sync::manager::RequestId::RangeBlockAndBlobs { id }) => { + RequestId::Sync(crate::sync::manager::RequestId::RangeBlockComponents(id)) => { let _ = self .cx .range_block_and_blob_response(id, BlockOrBlob::Block(None)); @@ -570,7 +570,7 @@ mod tests { } } else { match block_req { - RequestId::Sync(crate::sync::manager::RequestId::RangeBlockAndBlobs { id }) => { + RequestId::Sync(crate::sync::manager::RequestId::RangeBlockComponents(id)) => { let response = self .cx .range_block_and_blob_response(id, BlockOrBlob::Block(None)) From 9f495e7f35184b421301cbc83b8dfc212c6eead1 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Fri, 10 May 2024 13:09:40 +1000 Subject: [PATCH 18/76] Remove `BlobSidecar` construction and publish after PeerDAS activated (#5759) * Avoid building and publishing blob sidecars after PeerDAS. * Ignore gossip blobs with a slot greater than peer das activation epoch. * Only attempt to verify blob count and import blobs before PeerDAS. --- beacon_node/beacon_chain/src/beacon_chain.rs | 5 ++ .../beacon_chain/src/blob_verification.rs | 7 +- .../beacon_chain/src/block_verification.rs | 82 +++++++++++-------- .../src/data_availability_checker.rs | 13 +-- .../overflow_lru_cache.rs | 52 ++++++------ .../state_lru_cache.rs | 5 ++ beacon_node/http_api/src/publish_blocks.rs | 4 + .../gossip_methods.rs | 10 +++ consensus/types/src/chain_spec.rs | 7 ++ consensus/types/src/data_column_sidecar.rs | 43 ++++------ 10 files changed, 130 insertions(+), 98 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index b9c1156c478..78d26a9fb5c 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -3006,6 +3006,11 @@ impl BeaconChain { return Err(BlockError::BlockIsAlreadyKnown(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())); + } + if let Some(event_handler) = self.event_handler.as_ref() { if event_handler.has_blob_sidecar_subscribers() { event_handler.register(EventKind::BlobSidecar(SseBlobSidecar::from_blob_sidecar( diff --git a/beacon_node/beacon_chain/src/blob_verification.rs b/beacon_node/beacon_chain/src/blob_verification.rs index fdf8ee2b971..ba875867a00 100644 --- a/beacon_node/beacon_chain/src/blob_verification.rs +++ b/beacon_node/beacon_chain/src/blob_verification.rs @@ -17,7 +17,9 @@ use ssz_types::VariableList; use std::time::Duration; use tree_hash::TreeHash; use types::blob_sidecar::BlobIdentifier; -use types::{BeaconStateError, BlobSidecar, EthSpec, Hash256, SignedBeaconBlockHeader, Slot}; +use types::{ + BeaconStateError, BlobSidecar, Epoch, EthSpec, Hash256, SignedBeaconBlockHeader, Slot, +}; /// An error occurred while validating a gossip blob. #[derive(Debug)] @@ -231,6 +233,9 @@ impl GossipVerifiedBlob { pub fn slot(&self) -> Slot { self.blob.blob.slot() } + pub fn epoch(&self) -> Epoch { + self.slot().epoch(T::EthSpec::slots_per_epoch()) + } pub fn index(&self) -> u64 { self.blob.blob.index } diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 77b89b94554..0a49e107042 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -99,9 +99,10 @@ use task_executor::JoinHandle; use tree_hash::TreeHash; use types::data_column_sidecar::DataColumnSidecarError; use types::{ - BeaconBlockRef, BeaconState, BeaconStateError, BlobSidecarList, ChainSpec, DataColumnSidecar, + BeaconBlockRef, BeaconState, BeaconStateError, BlobsList, ChainSpec, DataColumnSidecar, DataColumnSubnetId, Epoch, EthSpec, ExecutionBlockHash, FullPayload, Hash256, InconsistentFork, - PublicKey, PublicKeyBytes, RelativeEpoch, SignedBeaconBlock, SignedBeaconBlockHeader, Slot, + KzgProofs, PublicKey, PublicKeyBytes, RelativeEpoch, SignedBeaconBlock, + SignedBeaconBlockHeader, Slot, }; use types::{BlobSidecar, ExecPayload}; @@ -308,6 +309,14 @@ 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), + /// 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. + /// + /// ## Peer scoring + /// + /// This indicates the peer is sending an unexpected gossip blob and should be penalised. + BlobNotRequired(Slot), } impl From for BlockError { @@ -717,36 +726,15 @@ impl IntoGossipVerifiedBlockContents for PublishBlockReq chain: &BeaconChain, ) -> Result, BlockContentsError> { let (block, blobs) = self.deconstruct(); + let peer_das_enabled = chain.spec.is_peer_das_enabled_for_epoch(block.epoch()); - let gossip_verified_blobs = blobs - .map(|(kzg_proofs, blobs)| { - let mut gossip_verified_blobs = vec![]; - for (i, (kzg_proof, blob)) in kzg_proofs.iter().zip(blobs).enumerate() { - let _timer = - metrics::start_timer(&metrics::BLOB_SIDECAR_INCLUSION_PROOF_COMPUTATION); - let blob = BlobSidecar::new(i, blob, &block, *kzg_proof) - .map_err(BlockContentsError::BlobSidecarError)?; - drop(_timer); - let gossip_verified_blob = - GossipVerifiedBlob::new(Arc::new(blob), i as u64, chain)?; - gossip_verified_blobs.push(gossip_verified_blob); - } - let gossip_verified_blobs = VariableList::from(gossip_verified_blobs); - Ok::<_, BlockContentsError>(gossip_verified_blobs) - }) - .transpose()?; - - let peer_das_enabled = chain - .spec - .eip7594_fork_epoch - .map_or(false, |eip7594_fork_epoch| { - block.epoch() >= eip7594_fork_epoch - }); - - let gossip_verified_data_columns = if peer_das_enabled { - build_gossip_verified_data_columns(chain, &block, gossip_verified_blobs.as_ref())? + let (gossip_verified_blobs, gossip_verified_data_columns) = if peer_das_enabled { + let gossip_verified_data_columns = + build_gossip_verified_data_columns(chain, &block, blobs.map(|(_, blobs)| blobs))?; + (None, gossip_verified_data_columns) } else { - None + let gossip_verified_blobs = build_gossip_verified_blobs(chain, &block, blobs)?; + (gossip_verified_blobs, None) }; let gossip_verified_block = GossipVerifiedBlock::new(block, chain)?; @@ -763,12 +751,37 @@ impl IntoGossipVerifiedBlockContents for PublishBlockReq } } +#[allow(clippy::type_complexity)] +fn build_gossip_verified_blobs( + chain: &BeaconChain, + block: &Arc>>, + blobs: Option<(KzgProofs, BlobsList)>, +) -> Result>, BlockContentsError> { + blobs + .map(|(kzg_proofs, blobs)| { + let mut gossip_verified_blobs = vec![]; + for (i, (kzg_proof, blob)) in kzg_proofs.iter().zip(blobs).enumerate() { + let _timer = + metrics::start_timer(&metrics::BLOB_SIDECAR_INCLUSION_PROOF_COMPUTATION); + let blob = BlobSidecar::new(i, blob, block, *kzg_proof) + .map_err(BlockContentsError::BlobSidecarError)?; + drop(_timer); + let gossip_verified_blob = + GossipVerifiedBlob::new(Arc::new(blob), i as u64, chain)?; + gossip_verified_blobs.push(gossip_verified_blob); + } + let gossip_verified_blobs = VariableList::from(gossip_verified_blobs); + Ok::<_, BlockContentsError>(gossip_verified_blobs) + }) + .transpose() +} + fn build_gossip_verified_data_columns( chain: &BeaconChain, block: &SignedBeaconBlock>, - gossip_verified_blobs: Option<&GossipVerifiedBlobList>, + blobs: Option>, ) -> Result>, BlockContentsError> { - gossip_verified_blobs + blobs // Only attempt to build data columns if blobs is non empty to avoid skewing the metrics. .filter(|b| !b.is_empty()) .map(|blobs| { @@ -780,11 +793,8 @@ fn build_gossip_verified_data_columns( GossipDataColumnError::::KzgNotInitialized, ))?; - let blob_sidecar_list: Vec<_> = blobs.iter().map(|blob| blob.clone_blob()).collect(); - let blob_sidecar_list = BlobSidecarList::new(blob_sidecar_list) - .map_err(DataColumnSidecarError::SszError)?; let timer = metrics::start_timer(&metrics::DATA_COLUMN_SIDECAR_COMPUTATION); - let sidecars = DataColumnSidecar::build_sidecars(&blob_sidecar_list, block, kzg)?; + let sidecars = DataColumnSidecar::build_sidecars(&blobs, block, kzg)?; drop(timer); let mut gossip_verified_data_columns = vec![]; for sidecar in sidecars { diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index b7a56cac164..1b171508ba6 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -419,14 +419,14 @@ impl DataAvailabilityChecker { /// Determines the blob requirements for a block. If the block is pre-deneb, no blobs are required. /// If the epoch is from prior to the data availability boundary, no blobs are required. pub fn blobs_required_for_epoch(&self, epoch: Epoch) -> bool { - self.da_check_required_for_epoch(epoch) && !self.is_peer_das_enabled_for_epoch(epoch) + self.da_check_required_for_epoch(epoch) && !self.spec.is_peer_das_enabled_for_epoch(epoch) } /// Determines the data column requirements for an epoch. /// - If the epoch is pre-peerdas, no data columns are required. /// - If the epoch is from prior to the data availability boundary, no data columns are required. pub fn data_columns_required_for_epoch(&self, epoch: Epoch) -> bool { - self.da_check_required_for_epoch(epoch) && self.is_peer_das_enabled_for_epoch(epoch) + self.da_check_required_for_epoch(epoch) && self.spec.is_peer_das_enabled_for_epoch(epoch) } /// See `Self::blobs_required_for_epoch` @@ -439,15 +439,6 @@ impl DataAvailabilityChecker { block.num_expected_blobs() > 0 && self.data_columns_required_for_epoch(block.epoch()) } - /// Returns true if the given epoch is greater than or equal to the `EIP7594_FORK_EPOCH`. - fn is_peer_das_enabled_for_epoch(&self, block_epoch: Epoch) -> bool { - self.spec - .eip7594_fork_epoch - .map_or(false, |eip7594_fork_epoch| { - block_epoch >= eip7594_fork_epoch - }) - } - /// 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 { diff --git a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs index 5947c521545..7e3e65d0790 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs @@ -39,7 +39,7 @@ use crate::store::{DBColumn, KeyValueStore}; use crate::BeaconChainTypes; use lru::LruCache; use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard}; -use slog::{debug, trace, Logger}; +use slog::{trace, Logger}; use ssz::{Decode, Encode}; use ssz_derive::{Decode, Encode}; use ssz_types::{FixedVector, VariableList}; @@ -235,7 +235,7 @@ impl PendingComponents { ) -> bool { match block_import_requirement { BlockImportRequirement::AllBlobs => { - debug!( + trace!( log, "Checking block and blob importability"; "block_root" => %self.block_root, @@ -250,7 +250,6 @@ impl PendingComponents { } BlockImportRequirement::CustodyColumns(num_expected_columns) => { let num_received_data_columns = self.num_received_data_columns(); - trace!( log, "Checking block and data column importability"; @@ -284,7 +283,11 @@ impl PendingComponents { /// /// 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, recover: R) -> Result, AvailabilityCheckError> + pub fn make_available( + self, + spec: &ChainSpec, + recover: R, + ) -> Result, AvailabilityCheckError> where R: FnOnce( DietAvailabilityPendingExecutedBlock, @@ -306,17 +309,23 @@ impl PendingComponents { let Some(diet_executed_block) = executed_block else { return Err(AvailabilityCheckError::Unexpected); }; - let num_blobs_expected = diet_executed_block.num_blobs_expected(); - let Some(verified_blobs) = verified_blobs - .into_iter() - .cloned() - .map(|b| b.map(|b| b.to_blob())) - .take(num_blobs_expected) - .collect::>>() - else { - return Err(AvailabilityCheckError::Unexpected); - }; - let verified_blobs = VariableList::new(verified_blobs)?; + + let is_before_peer_das = !spec.is_peer_das_enabled_for_epoch(diet_executed_block.epoch()); + let blobs = is_before_peer_das + .then(|| { + let num_blobs_expected = diet_executed_block.num_blobs_expected(); + let Some(verified_blobs) = verified_blobs + .into_iter() + .cloned() + .map(|b| b.map(|b| b.to_blob())) + .take(num_blobs_expected) + .collect::>>() + else { + return Err(AvailabilityCheckError::Unexpected); + }; + Ok(VariableList::new(verified_blobs)?) + }) + .transpose()?; let executed_block = recover(diet_executed_block)?; @@ -329,7 +338,7 @@ impl PendingComponents { let available_block = AvailableBlock { block_root, block, - blobs: Some(verified_blobs), + blobs, blobs_available_timestamp, // TODO(das) Do we need a check here for number of expected custody columns? // TODO(das): Update store types to prevent this conversion @@ -790,10 +799,7 @@ impl OverflowLRUCache { .epoch() .ok_or(AvailabilityCheckError::UnableToDetermineImportRequirement)?; - let peer_das_enabled = self - .spec - .eip7594_fork_epoch - .map_or(false, |eip7594_fork_epoch| epoch >= eip7594_fork_epoch); + let peer_das_enabled = self.spec.is_peer_das_enabled_for_epoch(epoch); if peer_das_enabled { Ok(BlockImportRequirement::CustodyColumns( self.custody_column_count, @@ -824,7 +830,7 @@ impl OverflowLRUCache { if pending_components.is_available(block_import_requirement, &self.log) { // No need to hold the write lock anymore drop(write_lock); - pending_components.make_available(|diet_block| { + pending_components.make_available(&self.spec, |diet_block| { self.state_cache.recover_pending_executed_block(diet_block) }) } else { @@ -864,7 +870,7 @@ impl OverflowLRUCache { if pending_components.is_available(block_import_requirement, &self.log) { // No need to hold the write lock anymore drop(write_lock); - pending_components.make_available(|diet_block| { + pending_components.make_available(&self.spec, |diet_block| { self.state_cache.recover_pending_executed_block(diet_block) }) } else { @@ -904,7 +910,7 @@ impl OverflowLRUCache { if pending_components.is_available(block_import_requirement, &self.log) { // No need to hold the write lock anymore drop(write_lock); - pending_components.make_available(|diet_block| { + pending_components.make_available(&self.spec, |diet_block| { self.state_cache.recover_pending_executed_block(diet_block) }) } else { diff --git a/beacon_node/beacon_chain/src/data_availability_checker/state_lru_cache.rs b/beacon_node/beacon_chain/src/data_availability_checker/state_lru_cache.rs index f8a243bd9e8..71b81bcddc6 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker/state_lru_cache.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker/state_lru_cache.rs @@ -53,6 +53,11 @@ impl DietAvailabilityPendingExecutedBlock { .cloned() .unwrap_or_default() } + + /// Returns the epoch corresponding to `self.slot()`. + pub fn epoch(&self) -> Epoch { + self.block.slot().epoch(E::slots_per_epoch()) + } } /// This LRU cache holds BeaconStates used for block import. If the cache overflows, diff --git a/beacon_node/http_api/src/publish_blocks.rs b/beacon_node/http_api/src/publish_blocks.rs index 067136919e7..896448176a0 100644 --- a/beacon_node/http_api/src/publish_blocks.rs +++ b/beacon_node/http_api/src/publish_blocks.rs @@ -173,6 +173,10 @@ pub async fn publish_block NetworkBeaconProcessor { ); return None; } + Err(e @ BlockError::BlobNotRequired(_)) => { + // TODO(das): penalty not implemented yet as other clients may still send us blobs + // during early stage of implementation. + debug!(self.log, "Received blobs for slot after PeerDAS epoch from peer"; + "error" => %e, + "peer_id" => %peer_id, + ); + 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/consensus/types/src/chain_spec.rs b/consensus/types/src/chain_spec.rs index d9f9865927d..b166e547dbb 100644 --- a/consensus/types/src/chain_spec.rs +++ b/consensus/types/src/chain_spec.rs @@ -397,6 +397,13 @@ impl ChainSpec { } } + /// Returns true if the given epoch is greater than or equal to the `EIP7594_FORK_EPOCH`. + pub fn is_peer_das_enabled_for_epoch(&self, block_epoch: Epoch) -> bool { + self.eip7594_fork_epoch.map_or(false, |eip7594_fork_epoch| { + block_epoch >= eip7594_fork_epoch + }) + } + /// Returns a full `Fork` struct for a given epoch. pub fn fork_at_epoch(&self, epoch: Epoch) -> Fork { let current_fork_name = self.fork_name_at_epoch(epoch); diff --git a/consensus/types/src/data_column_sidecar.rs b/consensus/types/src/data_column_sidecar.rs index f92838ceabe..7924a32f7b5 100644 --- a/consensus/types/src/data_column_sidecar.rs +++ b/consensus/types/src/data_column_sidecar.rs @@ -1,10 +1,10 @@ use crate::beacon_block_body::KzgCommitments; use crate::test_utils::TestRandom; -use crate::BeaconStateError; use crate::{ - BeaconBlockHeader, BlobSidecarList, EthSpec, Hash256, KzgProofs, SignedBeaconBlock, - SignedBeaconBlockHeader, Slot, + BeaconBlockHeader, EthSpec, Hash256, KzgProofs, SignedBeaconBlock, SignedBeaconBlockHeader, + Slot, }; +use crate::{BeaconStateError, BlobsList}; use bls::Signature; use derivative::Derivative; #[cfg_attr(test, double)] @@ -83,7 +83,7 @@ impl DataColumnSidecar { } pub fn build_sidecars( - blobs: &BlobSidecarList, + blobs: &BlobsList, block: &SignedBeaconBlock, kzg: &Kzg, ) -> Result, DataColumnSidecarError> { @@ -106,7 +106,7 @@ impl DataColumnSidecar { // NOTE: assumes blob sidecars are ordered by index for blob in blobs { - let blob = KzgBlob::from_bytes(&blob.blob).map_err(KzgError::from)?; + let blob = KzgBlob::from_bytes(blob).map_err(KzgError::from)?; let (blob_cells, blob_cell_proofs) = kzg.compute_cells_and_proofs(&blob)?; // we iterate over each column, and we construct the column from "top to bottom", @@ -279,11 +279,11 @@ mod test { use crate::beacon_block_body::KzgCommitments; use crate::eth_spec::EthSpec; use crate::{ - BeaconBlock, BeaconBlockDeneb, Blob, BlobSidecar, BlobSidecarList, ChainSpec, - DataColumnSidecar, MainnetEthSpec, SignedBeaconBlock, + BeaconBlock, BeaconBlockDeneb, Blob, ChainSpec, DataColumnSidecar, MainnetEthSpec, + SignedBeaconBlock, }; use bls::Signature; - use kzg::{KzgCommitment, KzgProof}; + use kzg::KzgCommitment; use std::sync::Arc; #[test] @@ -291,8 +291,7 @@ mod test { type E = MainnetEthSpec; let num_of_blobs = 0; let spec = E::default_spec(); - let (signed_block, blob_sidecars) = - create_test_block_and_blob_sidecars::(num_of_blobs, &spec); + let (signed_block, blob_sidecars) = create_test_block_and_blobs::(num_of_blobs, &spec); let mock_kzg = Arc::new(Kzg::default()); let column_sidecars = @@ -306,8 +305,7 @@ mod test { type E = MainnetEthSpec; let num_of_blobs = 6; let spec = E::default_spec(); - let (signed_block, blob_sidecars) = - create_test_block_and_blob_sidecars::(num_of_blobs, &spec); + let (signed_block, blob_sidecars) = create_test_block_and_blobs::(num_of_blobs, &spec); let mut mock_kzg = Kzg::default(); mock_kzg @@ -345,10 +343,10 @@ mod test { } } - fn create_test_block_and_blob_sidecars( + fn create_test_block_and_blobs( num_of_blobs: usize, spec: &ChainSpec, - ) -> (SignedBeaconBlock, BlobSidecarList) { + ) -> (SignedBeaconBlock, BlobsList) { let mut block = BeaconBlock::Deneb(BeaconBlockDeneb::empty(spec)); let mut body = block.body_mut(); let blob_kzg_commitments = body.blob_kzg_commitments_mut().unwrap(); @@ -358,20 +356,11 @@ mod test { let signed_block = SignedBeaconBlock::from_block(block, Signature::empty()); - let sidecars = (0..num_of_blobs) - .map(|index| { - BlobSidecar::new( - index, - Blob::::default(), - &signed_block, - KzgProof::empty(), - ) - .map(Arc::new) - }) - .collect::, _>>() - .unwrap() + let blobs = (0..num_of_blobs) + .map(|_| Blob::::default()) + .collect::>() .into(); - (signed_block, sidecars) + (signed_block, blobs) } } From c8ea589cf4b04d7bff95fab5aa20e1233ef9f6a3 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Fri, 10 May 2024 13:53:56 +1000 Subject: [PATCH 19/76] #5684 review comments (#5748) * #5684 review comments. * Doc and message update only. * Fix incorrect condition when constructing `RpcBlock` with `DataColumn`s --- beacon_node/beacon_chain/src/beacon_chain.rs | 16 +++++++++++----- .../beacon_chain/src/block_verification_types.rs | 15 ++++----------- .../src/data_availability_checker.rs | 3 ++- .../overflow_lru_cache.rs | 3 +-- .../beacon_chain/src/data_column_verification.rs | 4 ++-- beacon_node/beacon_chain/src/errors.rs | 3 +-- .../network/src/network_beacon_processor/mod.rs | 3 ++- .../network/src/sync/block_lookups/tests.rs | 6 +++--- 8 files changed, 26 insertions(+), 27 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 78d26a9fb5c..0a6a4666d13 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -3081,7 +3081,7 @@ impl BeaconChain { self.remove_notified(&block_root, r) } - /// Cache the blobs in the processing cache, process it, then evict it from the cache if it was + /// Cache the columns in the processing cache, process it, then evict it from the cache if it was /// imported or errors. pub async fn process_rpc_custody_columns( self: &Arc, @@ -3089,7 +3089,7 @@ impl BeaconChain { custody_columns: Vec>, ) -> 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. + // we don't need to process its columns again. if self .canonical_head .fork_choice_read_lock() @@ -3099,9 +3099,13 @@ impl BeaconChain { } // TODO(das): custody column SSE event - // TODO(das): Why is the slot necessary here? - let slot = Slot::new(0); - + let slot = custody_columns + .first() + .ok_or(BeaconChainError::EmptyRpcCustodyColumns)? + .as_data_column() + .signed_block_header + .message + .slot; let r = self .check_rpc_custody_columns_availability_and_import(slot, block_root, custody_columns) .await; @@ -3494,6 +3498,8 @@ impl BeaconChain { ); } + // TODO(das) record custody column available timestamp + // import let chain = self.clone(); let block_root = self diff --git a/beacon_node/beacon_chain/src/block_verification_types.rs b/beacon_node/beacon_chain/src/block_verification_types.rs index e59dadb47fc..ef079fb196c 100644 --- a/beacon_node/beacon_chain/src/block_verification_types.rs +++ b/beacon_node/beacon_chain/src/block_verification_types.rs @@ -96,10 +96,7 @@ enum RpcBlockInner { BlockAndBlobs(Arc>, BlobSidecarList), /// This variant is used with parent lookups and by-range responses. It should have all /// requested data columns, all block roots matching for this block. - BlockAndCustodyColumns( - Arc>, - VariableList, ::DataColumnCount>, - ), + BlockAndCustodyColumns(Arc>, CustodyDataColumnList), } impl RpcBlock { @@ -168,13 +165,9 @@ impl RpcBlock { // The number of required custody columns is out of scope here. return Err(AvailabilityCheckError::MissingCustodyColumns); } - // Treat empty blob lists as if they are missing. - let inner = if custody_columns.is_empty() { - RpcBlockInner::BlockAndCustodyColumns( - block, - VariableList::new(custody_columns) - .expect("TODO(das): custody vec should never exceed len"), - ) + // Treat empty data column lists as if they are missing. + let inner = if !custody_columns.is_empty() { + RpcBlockInner::BlockAndCustodyColumns(block, VariableList::new(custody_columns)?) } else { RpcBlockInner::Block(block) }; diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index 1b171508ba6..1dff39737e1 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -196,9 +196,10 @@ impl DataAvailabilityChecker { }; // TODO(das): report which column is invalid for proper peer scoring + // TODO(das): batch KZG verification here let verified_custody_columns = custody_columns .into_iter() - .map(|c| KzgVerifiedCustodyDataColumn::new(c, kzg).map_err(AvailabilityCheckError::Kzg)) + .map(|c| KzgVerifiedCustodyDataColumn::new(c, kzg)) .collect::, _>>()?; self.availability_cache diff --git a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs index 7e3e65d0790..9baa5cd3656 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs @@ -81,7 +81,7 @@ impl PendingComponents { &self.verified_blobs } - /// Returns a mutable reference to the cached data column. + /// Returns an immutable reference to the cached data column. pub fn get_cached_data_column( &self, data_column_index: u64, @@ -340,7 +340,6 @@ impl PendingComponents { block, blobs, blobs_available_timestamp, - // TODO(das) Do we need a check here for number of expected custody columns? // TODO(das): Update store types to prevent this conversion data_columns: Some( VariableList::new( diff --git a/beacon_node/beacon_chain/src/data_column_verification.rs b/beacon_node/beacon_chain/src/data_column_verification.rs index b5e3b845a4b..7951c3dde9e 100644 --- a/beacon_node/beacon_chain/src/data_column_verification.rs +++ b/beacon_node/beacon_chain/src/data_column_verification.rs @@ -228,8 +228,8 @@ impl KzgVerifiedCustodyDataColumn { } /// Verify a column already marked as custody column - pub fn new(data_column: CustodyDataColumn, _kzg: &Kzg) -> Result { - // TODO(das): verify kzg proof + pub fn new(data_column: CustodyDataColumn, kzg: &Kzg) -> Result { + verify_kzg_for_data_column(data_column.clone_arc(), kzg)?; Ok(Self { data: data_column.data, }) diff --git a/beacon_node/beacon_chain/src/errors.rs b/beacon_node/beacon_chain/src/errors.rs index cc564861641..22a823fd324 100644 --- a/beacon_node/beacon_chain/src/errors.rs +++ b/beacon_node/beacon_chain/src/errors.rs @@ -83,8 +83,6 @@ pub enum BeaconChainError { max_task_runtime: Duration, }, MissingFinalizedStateRoot(Slot), - /// Returned when an internal check fails, indicating corrupt data. - InvariantViolated(String), SszTypesError(SszTypesError), NoProposerForSlot(Slot), CanonicalHeadLockTimeout, @@ -227,6 +225,7 @@ pub enum BeaconChainError { LightClientError(LightClientError), UnsupportedFork, MilhouseError(MilhouseError), + EmptyRpcCustodyColumns, } easy_from_to!(SlotProcessingError, BeaconChainError); diff --git a/beacon_node/network/src/network_beacon_processor/mod.rs b/beacon_node/network/src/network_beacon_processor/mod.rs index 55a07d78e5b..3d64e1da708 100644 --- a/beacon_node/network/src/network_beacon_processor/mod.rs +++ b/beacon_node/network/src/network_beacon_processor/mod.rs @@ -504,7 +504,8 @@ impl NetworkBeaconProcessor { }) } - /// Create a new `Work` event for some data_columns from ReqResp + /// Create a new `Work` event for some sampling columns, and reports the verification result + /// back to sync. pub fn send_rpc_validate_data_columns( self: &Arc, block_root: Hash256, diff --git a/beacon_node/network/src/sync/block_lookups/tests.rs b/beacon_node/network/src/sync/block_lookups/tests.rs index 3701fa82c4c..63f00138d67 100644 --- a/beacon_node/network/src/sync/block_lookups/tests.rs +++ b/beacon_node/network/src/sync/block_lookups/tests.rs @@ -897,7 +897,7 @@ impl TestRig { None } }) - .unwrap_or_else(|e| panic!("Expected sample verify work: {e}")) + .unwrap_or_else(|e| panic!("Expected RPC custody column work: {e}")) } fn expect_rpc_sample_verify_work_event(&mut self) { @@ -1629,8 +1629,8 @@ fn custody_lookup_happy_path() { // Should not request blobs let id = r.expect_block_lookup_request(block.canonical_root()); r.complete_valid_block_request(id, block.into(), true); - // TODO(das): do not hardcode 4 - let custody_ids = r.expect_only_data_columns_by_root_requests(block_root, 4); + let custody_column_count = E::min_custody_requirement() * E::data_columns_per_subnet(); + let custody_ids = r.expect_only_data_columns_by_root_requests(block_root, custody_column_count); r.complete_valid_custody_request(custody_ids, data_columns, false); r.expect_no_active_lookups(); } From 500915f22b9fb51aa5c56f5b970f1911f2419d8f Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Mon, 13 May 2024 23:53:10 +0300 Subject: [PATCH 20/76] Make sampling tests deterministic (#5775) --- .../src/peer_manager/peerdb.rs | 20 ++++++++++++-- .../network/src/sync/block_lookups/tests.rs | 26 ++++++++++++++----- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs b/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs index c3e77ae225e..76dbd483b57 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs @@ -1,9 +1,11 @@ +use crate::discovery::enr::PEERDAS_CUSTODY_SUBNET_COUNT_ENR_KEY; use crate::discovery::CombinedKey; use crate::{metrics, multiaddr::Multiaddr, types::Subnet, Enr, Gossipsub, PeerId}; use peer_info::{ConnectionDirection, PeerConnectionStatus, PeerInfo}; use rand::seq::SliceRandom; use score::{PeerAction, ReportSource, Score, ScoreState}; use slog::{crit, debug, error, trace, warn}; +use ssz::Encode; use std::net::IpAddr; use std::time::Instant; use std::{cmp::Ordering, fmt::Display}; @@ -673,9 +675,23 @@ impl PeerDB { } /// Updates the connection state. MUST ONLY BE USED IN TESTS. - pub fn __add_connected_peer_testing_only(&mut self, peer_id: &PeerId) -> Option { + pub fn __add_connected_peer_testing_only( + &mut self, + peer_id: &PeerId, + supernode: bool, + ) -> Option { let enr_key = CombinedKey::generate_secp256k1(); - let enr = Enr::builder().build(&enr_key).unwrap(); + let mut enr = Enr::builder().build(&enr_key).unwrap(); + + if supernode { + enr.insert( + PEERDAS_CUSTODY_SUBNET_COUNT_ENR_KEY, + &(E::data_column_subnet_count() as u64).as_ssz_bytes(), + &enr_key, + ) + .expect("u64 can be encoded"); + } + self.update_connection_state( peer_id, NewConnectionState::Connected { diff --git a/beacon_node/network/src/sync/block_lookups/tests.rs b/beacon_node/network/src/sync/block_lookups/tests.rs index 63f00138d67..388c0c09e32 100644 --- a/beacon_node/network/src/sync/block_lookups/tests.rs +++ b/beacon_node/network/src/sync/block_lookups/tests.rs @@ -356,12 +356,26 @@ impl TestRig { self.network_globals .peers .write() - .__add_connected_peer_testing_only(&peer_id); + .__add_connected_peer_testing_only(&peer_id, false); peer_id } - fn new_connected_peers(&mut self, count: usize) -> Vec { - (0..count).map(|_| self.new_connected_peer()).collect() + fn new_connected_supernode_peer(&mut self) -> PeerId { + let peer_id = PeerId::random(); + self.network_globals + .peers + .write() + .__add_connected_peer_testing_only(&peer_id, true); + peer_id + } + + fn new_connected_peers_for_peerdas(&mut self) { + // Enough sampling peers with few columns + for _ in 0..100 { + self.new_connected_peer(); + } + // One supernode peer to ensure all columns have at least one peer + self.new_connected_supernode_peer(); } fn parent_chain_processed_success( @@ -1584,7 +1598,7 @@ fn sampling_happy_path() { let Some(mut r) = TestRig::test_setup_after_peerdas() else { return; }; - r.new_connected_peers(100); // Add enough sampling peers + r.new_connected_peers_for_peerdas(); let (block, data_columns) = r.rand_block_and_data_columns(); let block_root = block.canonical_root(); r.trigger_sample_block(block_root, block.slot()); @@ -1601,7 +1615,7 @@ fn sampling_with_retries() { let Some(mut r) = TestRig::test_setup_after_peerdas() else { return; }; - r.new_connected_peers(100); // Add enough sampling peers + r.new_connected_peers_for_peerdas(); let (block, data_columns) = r.rand_block_and_data_columns(); let block_root = block.canonical_root(); r.trigger_sample_block(block_root, block.slot()); @@ -1621,7 +1635,7 @@ fn custody_lookup_happy_path() { let Some(mut r) = TestRig::test_setup_after_peerdas() else { return; }; - r.new_connected_peers(100); // Add enough sampling peers + r.new_connected_peers_for_peerdas(); let (block, data_columns) = r.rand_block_and_data_columns(); let block_root = block.canonical_root(); let peer_id = r.new_connected_peer(); From 4957347910b473ef46952d7f5321b9f078454507 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Tue, 14 May 2024 04:34:49 +0300 Subject: [PATCH 21/76] PeerDAS spec tests (#5772) * Add get_custody_columns spec tests. * Add kzg merkle proof spec tests. * Add SSZ spec tests. * Add remaining KZG tests * Load KZG only once per process, exclude electra tests and add missing SSZ tests. * Fix lint and missing changes. * Ignore macOS generated file. --- Cargo.lock | 1 + consensus/types/src/data_column_subnet_id.rs | 17 ++- consensus/types/src/lib.rs | 4 +- testing/ef_tests/Cargo.toml | 1 + testing/ef_tests/Makefile | 2 +- testing/ef_tests/check_all_files_accessed.py | 7 ++ testing/ef_tests/src/cases.rs | 28 ++++- .../ef_tests/src/cases/get_custody_columns.rs | 39 ++++++ .../src/cases/kzg_blob_to_kzg_commitment.rs | 8 +- .../src/cases/kzg_compute_blob_kzg_proof.rs | 7 +- .../cases/kzg_compute_cells_and_kzg_proofs.rs | 73 +++++++++++ .../src/cases/kzg_compute_kzg_proof.rs | 7 +- .../src/cases/kzg_verify_blob_kzg_proof.rs | 50 ++++++-- .../cases/kzg_verify_blob_kzg_proof_batch.rs | 8 +- .../cases/kzg_verify_cell_kzg_proof_batch.rs | 76 +++++++++++ .../src/cases/kzg_verify_kzg_proof.rs | 7 +- .../src/cases/merkle_proof_validity.rs | 65 ++++++++-- testing/ef_tests/src/handler.rs | 119 +++++++++++++++++- testing/ef_tests/src/lib.rs | 8 +- testing/ef_tests/src/type_name.rs | 3 +- testing/ef_tests/tests/tests.rs | 43 ++++++- 21 files changed, 523 insertions(+), 50 deletions(-) create mode 100644 testing/ef_tests/src/cases/get_custody_columns.rs create mode 100644 testing/ef_tests/src/cases/kzg_compute_cells_and_kzg_proofs.rs create mode 100644 testing/ef_tests/src/cases/kzg_verify_cell_kzg_proof_batch.rs diff --git a/Cargo.lock b/Cargo.lock index 8ee0c68c83e..ce71a00af37 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2158,6 +2158,7 @@ dependencies = [ "fs2", "hex", "kzg", + "lazy_static", "logging", "rayon", "serde", diff --git a/consensus/types/src/data_column_subnet_id.rs b/consensus/types/src/data_column_subnet_id.rs index 94f06317360..79b427ee63f 100644 --- a/consensus/types/src/data_column_subnet_id.rs +++ b/consensus/types/src/data_column_subnet_id.rs @@ -2,6 +2,7 @@ use crate::data_column_sidecar::ColumnIndex; use crate::EthSpec; use ethereum_types::U256; +use itertools::Itertools; use safe_arith::{ArithError, SafeArith}; use serde::{Deserialize, Serialize}; use smallvec::SmallVec; @@ -60,15 +61,15 @@ impl DataColumnSubnetId { node_id: U256, custody_subnet_count: u64, ) -> impl Iterator { - // NOTE: we could perform check on `custody_subnet_count` here to ensure that it is a valid + // TODO(das): we could perform check on `custody_subnet_count` here to ensure that it is a valid // value, but here we assume it is valid. let mut subnets = SmallVec::<[u64; 32]>::new(); - let mut offset = 0; + let mut current_id = node_id; while (subnets.len() as u64) < custody_subnet_count { - let offset_node_id = node_id + U256::from(offset); - let offset_node_id = offset_node_id.low_u64().to_le_bytes(); - let hash: [u8; 32] = ethereum_hashing::hash_fixed(&offset_node_id); + let mut node_id_bytes = [0u8; 32]; + current_id.to_little_endian(&mut node_id_bytes); + let hash = ethereum_hashing::hash_fixed(&node_id_bytes); let hash_prefix = [ hash[0], hash[1], hash[2], hash[3], hash[4], hash[5], hash[6], hash[7], ]; @@ -79,7 +80,10 @@ impl DataColumnSubnetId { subnets.push(subnet); } - offset += 1 + if current_id == U256::MAX { + current_id = U256::zero() + } + current_id += U256::one() } subnets.into_iter().map(DataColumnSubnetId::new) } @@ -90,6 +94,7 @@ impl DataColumnSubnetId { ) -> impl Iterator { Self::compute_custody_subnets::(node_id, custody_subnet_count) .flat_map(|subnet| subnet.columns::()) + .sorted() } } diff --git a/consensus/types/src/lib.rs b/consensus/types/src/lib.rs index 66e8cdd2914..98c0944d2c5 100644 --- a/consensus/types/src/lib.rs +++ b/consensus/types/src/lib.rs @@ -134,7 +134,7 @@ pub use crate::beacon_block_body::{ pub use crate::beacon_block_header::BeaconBlockHeader; pub use crate::beacon_committee::{BeaconCommittee, OwnedBeaconCommittee}; pub use crate::beacon_state::{Error as BeaconStateError, *}; -pub use crate::blob_sidecar::{BlobSidecar, BlobSidecarList, BlobsList}; +pub use crate::blob_sidecar::{BlobIdentifier, BlobSidecar, BlobSidecarList, BlobsList}; pub use crate::bls_to_execution_change::BlsToExecutionChange; pub use crate::chain_spec::{ChainSpec, Config, Domain}; pub use crate::checkpoint::Checkpoint; @@ -143,7 +143,7 @@ pub use crate::config_and_preset::{ }; pub use crate::consolidation::Consolidation; pub use crate::contribution_and_proof::ContributionAndProof; -pub use crate::data_column_sidecar::{ColumnIndex, DataColumnSidecar}; +pub use crate::data_column_sidecar::{ColumnIndex, DataColumnIdentifier, DataColumnSidecar}; pub use crate::data_column_subnet_id::DataColumnSubnetId; pub use crate::deposit::{Deposit, DEPOSIT_TREE_DEPTH}; pub use crate::deposit_data::DepositData; diff --git a/testing/ef_tests/Cargo.toml b/testing/ef_tests/Cargo.toml index f3d00fa035c..c1b34a2dc87 100644 --- a/testing/ef_tests/Cargo.toml +++ b/testing/ef_tests/Cargo.toml @@ -40,3 +40,4 @@ store = { workspace = true } fork_choice = { workspace = true } execution_layer = { workspace = true } logging = { workspace = true } +lazy_static = { workspace = true } diff --git a/testing/ef_tests/Makefile b/testing/ef_tests/Makefile index 508c284275a..5dc3d2a0404 100644 --- a/testing/ef_tests/Makefile +++ b/testing/ef_tests/Makefile @@ -1,4 +1,4 @@ -TESTS_TAG := v1.4.0-beta.6 +TESTS_TAG := v1.5.0-alpha.2 TESTS = general minimal mainnet TARBALLS = $(patsubst %,%-$(TESTS_TAG).tar.gz,$(TESTS)) diff --git a/testing/ef_tests/check_all_files_accessed.py b/testing/ef_tests/check_all_files_accessed.py index 7629d61827f..8c7891a9030 100755 --- a/testing/ef_tests/check_all_files_accessed.py +++ b/testing/ef_tests/check_all_files_accessed.py @@ -20,6 +20,8 @@ # following regular expressions, we will assume they are to be ignored (i.e., we are purposefully # *not* running the spec tests). excluded_paths = [ + # TODO(das): remove once electra tests are on unstable + "tests/.*/electra/", # Eth1Block and PowBlock # # Intentionally omitted, as per https://github.com/sigp/lighthouse/issues/1835 @@ -31,10 +33,15 @@ "tests/.*/.*/ssz_static/LightClientStore", # LightClientSnapshot "tests/.*/.*/ssz_static/LightClientSnapshot", + # Unused kzg methods + "tests/.*/.*/kzg/compute_cells", + "tests/.*/.*/kzg/recover_all_cells", + "tests/.*/.*/kzg/verify_cell_kzg_proof", # One of the EF researchers likes to pack the tarballs on a Mac ".*\.DS_Store.*", # More Mac weirdness. "tests/mainnet/bellatrix/operations/deposit/pyspec_tests/deposit_with_previous_fork_version__valid_ineffective/._meta.yaml", + "tests/mainnet/eip7594/networking/get_custody_columns/pyspec_tests/get_custody_columns__short_node_id/._meta.yaml", # bls tests are moved to bls12-381-tests directory "tests/general/phase0/bls", # some bls tests are not included now diff --git a/testing/ef_tests/src/cases.rs b/testing/ef_tests/src/cases.rs index f328fa64047..2137452d46d 100644 --- a/testing/ef_tests/src/cases.rs +++ b/testing/ef_tests/src/cases.rs @@ -1,6 +1,6 @@ use super::*; use rayon::prelude::*; -use std::fmt::Debug; +use std::fmt::{Debug, Display, Formatter}; use std::path::{Path, PathBuf}; use types::ForkName; @@ -18,11 +18,14 @@ mod fork; mod fork_choice; mod genesis_initialization; mod genesis_validity; +mod get_custody_columns; mod kzg_blob_to_kzg_commitment; mod kzg_compute_blob_kzg_proof; +mod kzg_compute_cells_and_kzg_proofs; mod kzg_compute_kzg_proof; mod kzg_verify_blob_kzg_proof; mod kzg_verify_blob_kzg_proof_batch; +mod kzg_verify_cell_kzg_proof_batch; mod kzg_verify_kzg_proof; mod merkle_proof_validity; mod operations; @@ -48,11 +51,14 @@ pub use epoch_processing::*; pub use fork::ForkTest; pub use genesis_initialization::*; pub use genesis_validity::*; +pub use get_custody_columns::*; pub use kzg_blob_to_kzg_commitment::*; pub use kzg_compute_blob_kzg_proof::*; +pub use kzg_compute_cells_and_kzg_proofs::*; pub use kzg_compute_kzg_proof::*; pub use kzg_verify_blob_kzg_proof::*; pub use kzg_verify_blob_kzg_proof_batch::*; +pub use kzg_verify_cell_kzg_proof_batch::*; pub use kzg_verify_kzg_proof::*; pub use merkle_proof_validity::*; pub use operations::*; @@ -64,6 +70,19 @@ pub use ssz_generic::*; pub use ssz_static::*; pub use transition::TransitionTest; +#[derive(Debug, PartialEq)] +pub enum FeatureName { + Eip7594, +} + +impl Display for FeatureName { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + FeatureName::Eip7594 => f.write_str("eip7594"), + } + } +} + pub trait LoadCase: Sized { /// Load the test case from a test case directory. fn load_from_dir(_path: &Path, _fork_name: ForkName) -> Result; @@ -84,6 +103,13 @@ pub trait Case: Debug + Sync { true } + /// Whether or not this test exists for the given `feature_name`. + /// + /// Returns `true` by default. + fn is_enabled_for_feature(_feature_name: FeatureName) -> bool { + true + } + /// Execute a test and return the result. /// /// `case_index` reports the index of the case in the set of test cases. It is not strictly diff --git a/testing/ef_tests/src/cases/get_custody_columns.rs b/testing/ef_tests/src/cases/get_custody_columns.rs new file mode 100644 index 00000000000..608362bbf0d --- /dev/null +++ b/testing/ef_tests/src/cases/get_custody_columns.rs @@ -0,0 +1,39 @@ +use super::*; +use ethereum_types::U256; +use serde::Deserialize; +use std::marker::PhantomData; +use types::DataColumnSubnetId; + +#[derive(Debug, Clone, Deserialize)] +#[serde(bound = "E: EthSpec", deny_unknown_fields)] +pub struct GetCustodyColumns { + pub node_id: String, + pub custody_subnet_count: u64, + pub result: Vec, + #[serde(skip)] + _phantom: PhantomData, +} + +impl LoadCase for GetCustodyColumns { + fn load_from_dir(path: &Path, _fork_name: ForkName) -> Result { + decode::yaml_decode_file(path.join("meta.yaml").as_path()) + } +} + +impl Case for GetCustodyColumns { + fn result(&self, _case_index: usize, _fork_name: ForkName) -> Result<(), Error> { + let node_id = U256::from_dec_str(&self.node_id) + .map_err(|e| Error::FailedToParseTest(format!("{e:?}")))?; + let computed = + DataColumnSubnetId::compute_custody_columns::(node_id, self.custody_subnet_count) + .collect::>(); + let expected = &self.result; + if computed == *expected { + Ok(()) + } else { + Err(Error::NotEqual(format!( + "Got {computed:?}\nExpected {expected:?}" + ))) + } + } +} diff --git a/testing/ef_tests/src/cases/kzg_blob_to_kzg_commitment.rs b/testing/ef_tests/src/cases/kzg_blob_to_kzg_commitment.rs index aa48c127b20..78889cc0271 100644 --- a/testing/ef_tests/src/cases/kzg_blob_to_kzg_commitment.rs +++ b/testing/ef_tests/src/cases/kzg_blob_to_kzg_commitment.rs @@ -31,11 +31,13 @@ impl Case for KZGBlobToKZGCommitment { fork_name == ForkName::Deneb } - fn result(&self, _case_index: usize, _fork_name: ForkName) -> Result<(), Error> { - let kzg = get_kzg()?; + fn is_enabled_for_feature(feature_name: FeatureName) -> bool { + feature_name != FeatureName::Eip7594 + } + fn result(&self, _case_index: usize, _fork_name: ForkName) -> Result<(), Error> { let commitment = parse_blob::(&self.input.blob).and_then(|blob| { - blob_to_kzg_commitment::(&kzg, &blob).map_err(|e| { + blob_to_kzg_commitment::(&KZG, &blob).map_err(|e| { Error::InternalError(format!("Failed to compute kzg commitment: {:?}", e)) }) }); diff --git a/testing/ef_tests/src/cases/kzg_compute_blob_kzg_proof.rs b/testing/ef_tests/src/cases/kzg_compute_blob_kzg_proof.rs index 71e1ff8e23d..15bcc6f7d0b 100644 --- a/testing/ef_tests/src/cases/kzg_compute_blob_kzg_proof.rs +++ b/testing/ef_tests/src/cases/kzg_compute_blob_kzg_proof.rs @@ -32,6 +32,10 @@ impl Case for KZGComputeBlobKZGProof { fork_name == ForkName::Deneb } + fn is_enabled_for_feature(feature_name: FeatureName) -> bool { + feature_name != FeatureName::Eip7594 + } + fn result(&self, _case_index: usize, _fork_name: ForkName) -> Result<(), Error> { let parse_input = |input: &KZGComputeBlobKZGProofInput| -> Result<_, Error> { let blob = parse_blob::(&input.blob)?; @@ -39,9 +43,8 @@ impl Case for KZGComputeBlobKZGProof { Ok((blob, commitment)) }; - let kzg = get_kzg()?; let proof = parse_input(&self.input).and_then(|(blob, commitment)| { - compute_blob_kzg_proof::(&kzg, &blob, commitment) + compute_blob_kzg_proof::(&KZG, &blob, commitment) .map_err(|e| Error::InternalError(format!("Failed to compute kzg proof: {:?}", e))) }); diff --git a/testing/ef_tests/src/cases/kzg_compute_cells_and_kzg_proofs.rs b/testing/ef_tests/src/cases/kzg_compute_cells_and_kzg_proofs.rs new file mode 100644 index 00000000000..63d528467bd --- /dev/null +++ b/testing/ef_tests/src/cases/kzg_compute_cells_and_kzg_proofs.rs @@ -0,0 +1,73 @@ +use super::*; +use crate::case_result::compare_result; +use kzg::{Blob as KzgBlob, Cell}; +use kzg::{KzgProof, CELLS_PER_EXT_BLOB}; +use serde::Deserialize; +use std::marker::PhantomData; + +#[derive(Debug, Clone, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct KZGComputeCellsAndKzgProofsInput { + pub blob: String, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(bound = "E: EthSpec", deny_unknown_fields)] +pub struct KZGComputeCellsAndKZGProofs { + pub input: KZGComputeCellsAndKzgProofsInput, + pub output: Option<(Vec, Vec)>, + #[serde(skip)] + _phantom: PhantomData, +} + +impl LoadCase for KZGComputeCellsAndKZGProofs { + fn load_from_dir(path: &Path, _fork_name: ForkName) -> Result { + decode::yaml_decode_file(path.join("data.yaml").as_path()) + } +} + +impl Case for KZGComputeCellsAndKZGProofs { + fn is_enabled_for_fork(fork_name: ForkName) -> bool { + fork_name == ForkName::Deneb + } + + fn result(&self, _case_index: usize, _fork_name: ForkName) -> Result<(), Error> { + let cells_and_proofs = parse_blob::(&self.input.blob).and_then(|blob| { + let blob = KzgBlob::from_bytes(&blob).map_err(|e| { + Error::InternalError(format!("Failed to convert blob to kzg blob: {e:?}")) + })?; + KZG.compute_cells_and_proofs(&blob).map_err(|e| { + Error::InternalError(format!("Failed to compute cells and kzg proofs: {e:?}")) + }) + }); + + let expected = self.output.as_ref().and_then(|(cells, proofs)| { + parse_cells_and_proofs(cells, proofs) + .map(|(cells, proofs)| { + ( + cells + .try_into() + .map_err(|e| { + Error::FailedToParseTest(format!("Failed to parse cells: {e:?}")) + }) + .unwrap(), + proofs + .try_into() + .map_err(|e| { + Error::FailedToParseTest(format!("Failed to parse proofs: {e:?}")) + }) + .unwrap(), + ) + }) + .ok() + }); + + compare_result::< + ( + Box<[Cell; CELLS_PER_EXT_BLOB]>, + Box<[KzgProof; CELLS_PER_EXT_BLOB]>, + ), + _, + >(&cells_and_proofs, &expected) + } +} diff --git a/testing/ef_tests/src/cases/kzg_compute_kzg_proof.rs b/testing/ef_tests/src/cases/kzg_compute_kzg_proof.rs index 98bb7492491..88e90dbf1f2 100644 --- a/testing/ef_tests/src/cases/kzg_compute_kzg_proof.rs +++ b/testing/ef_tests/src/cases/kzg_compute_kzg_proof.rs @@ -39,6 +39,10 @@ impl Case for KZGComputeKZGProof { fork_name == ForkName::Deneb } + fn is_enabled_for_feature(feature_name: FeatureName) -> bool { + feature_name != FeatureName::Eip7594 + } + fn result(&self, _case_index: usize, _fork_name: ForkName) -> Result<(), Error> { let parse_input = |input: &KZGComputeKZGProofInput| -> Result<_, Error> { let blob = parse_blob::(&input.blob)?; @@ -46,9 +50,8 @@ impl Case for KZGComputeKZGProof { Ok((blob, z)) }; - let kzg = get_kzg()?; let proof = parse_input(&self.input).and_then(|(blob, z)| { - compute_kzg_proof::(&kzg, &blob, z) + compute_kzg_proof::(&KZG, &blob, z) .map_err(|e| Error::InternalError(format!("Failed to compute kzg proof: {:?}", e))) }); diff --git a/testing/ef_tests/src/cases/kzg_verify_blob_kzg_proof.rs b/testing/ef_tests/src/cases/kzg_verify_blob_kzg_proof.rs index f68f0fd7ed0..dca9af96161 100644 --- a/testing/ef_tests/src/cases/kzg_verify_blob_kzg_proof.rs +++ b/testing/ef_tests/src/cases/kzg_verify_blob_kzg_proof.rs @@ -2,16 +2,47 @@ use super::*; use crate::case_result::compare_result; use beacon_chain::kzg_utils::validate_blob; use eth2_network_config::TRUSTED_SETUP_BYTES; -use kzg::{Error as KzgError, Kzg, KzgCommitment, KzgProof, TrustedSetup}; +use kzg::{Cell, Error as KzgError, Kzg, KzgCommitment, KzgProof, TrustedSetup}; +use lazy_static::lazy_static; use serde::Deserialize; use std::marker::PhantomData; +use std::sync::Arc; use types::Blob; -pub fn get_kzg() -> Result { - let trusted_setup: TrustedSetup = serde_json::from_reader(TRUSTED_SETUP_BYTES) - .map_err(|e| Error::InternalError(format!("Failed to initialize kzg: {:?}", e)))?; - Kzg::new_from_trusted_setup(trusted_setup) - .map_err(|e| Error::InternalError(format!("Failed to initialize kzg: {:?}", e))) +lazy_static! { + pub static ref KZG: Arc = { + let trusted_setup: TrustedSetup = serde_json::from_reader(TRUSTED_SETUP_BYTES) + .map_err(|e| format!("Unable to read trusted setup file: {}", e)) + .expect("should have trusted setup"); + let kzg = Kzg::new_from_trusted_setup(trusted_setup).expect("should create kzg"); + Arc::new(kzg) + }; +} + +pub fn parse_cells_and_proofs( + cells: &[String], + proofs: &[String], +) -> Result<(Vec, Vec), Error> { + let cells = cells + .iter() + .map(|s| parse_cell(s.as_str())) + .collect::, Error>>()?; + + let proofs = proofs + .iter() + .map(|s| parse_proof(s.as_str())) + .collect::, Error>>()?; + + Ok((cells, proofs)) +} + +pub fn parse_cell(cell: &str) -> Result { + hex::decode(strip_0x(cell)?) + .map_err(|e| Error::FailedToParseTest(format!("Failed to parse cell: {:?}", e))) + .and_then(|bytes| { + Cell::from_bytes(bytes.as_ref()) + .map_err(|e| Error::FailedToParseTest(format!("Failed to parse proof: {:?}", e))) + }) } pub fn parse_proof(proof: &str) -> Result { @@ -80,6 +111,10 @@ impl Case for KZGVerifyBlobKZGProof { fork_name == ForkName::Deneb } + fn is_enabled_for_feature(feature_name: FeatureName) -> bool { + feature_name != FeatureName::Eip7594 + } + fn result(&self, _case_index: usize, _fork_name: ForkName) -> Result<(), Error> { let parse_input = |input: &KZGVerifyBlobKZGProofInput| -> Result<(Blob, KzgCommitment, KzgProof), Error> { let blob = parse_blob::(&input.blob)?; @@ -88,9 +123,8 @@ impl Case for KZGVerifyBlobKZGProof { Ok((blob, commitment, proof)) }; - let kzg = get_kzg()?; let result = parse_input(&self.input).and_then(|(blob, commitment, proof)| { - match validate_blob::(&kzg, &blob, commitment, proof) { + match validate_blob::(&KZG, &blob, commitment, proof) { Ok(_) => Ok(true), Err(KzgError::KzgVerificationFailed) => Ok(false), Err(e) => Err(Error::InternalError(format!( diff --git a/testing/ef_tests/src/cases/kzg_verify_blob_kzg_proof_batch.rs b/testing/ef_tests/src/cases/kzg_verify_blob_kzg_proof_batch.rs index ae5caedf069..d4cb581c634 100644 --- a/testing/ef_tests/src/cases/kzg_verify_blob_kzg_proof_batch.rs +++ b/testing/ef_tests/src/cases/kzg_verify_blob_kzg_proof_batch.rs @@ -33,6 +33,10 @@ impl Case for KZGVerifyBlobKZGProofBatch { fork_name == ForkName::Deneb } + fn is_enabled_for_feature(feature_name: FeatureName) -> bool { + feature_name != FeatureName::Eip7594 + } + fn result(&self, _case_index: usize, _fork_name: ForkName) -> Result<(), Error> { let parse_input = |input: &KZGVerifyBlobKZGProofBatchInput| -> Result<_, Error> { let blobs = input @@ -53,12 +57,10 @@ impl Case for KZGVerifyBlobKZGProofBatch { Ok((commitments, blobs, proofs)) }; - let kzg = get_kzg()?; - let result = parse_input(&self.input).and_then( |(commitments, blobs, proofs)| match validate_blobs::( - &kzg, + &KZG, &commitments, blobs.iter().collect(), &proofs, diff --git a/testing/ef_tests/src/cases/kzg_verify_cell_kzg_proof_batch.rs b/testing/ef_tests/src/cases/kzg_verify_cell_kzg_proof_batch.rs new file mode 100644 index 00000000000..150cc47770f --- /dev/null +++ b/testing/ef_tests/src/cases/kzg_verify_cell_kzg_proof_batch.rs @@ -0,0 +1,76 @@ +use super::*; +use crate::case_result::compare_result; +use kzg::{Bytes48, Error as KzgError}; +use serde::Deserialize; +use std::marker::PhantomData; + +#[derive(Debug, Clone, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct KZGVerifyCellKZGProofBatchInput { + pub row_commitments: Vec, + pub row_indices: Vec, + pub column_indices: Vec, + pub cells: Vec, + pub proofs: Vec, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(bound = "E: EthSpec", deny_unknown_fields)] +pub struct KZGVerifyCellKZGProofBatch { + pub input: KZGVerifyCellKZGProofBatchInput, + pub output: Option, + #[serde(skip)] + _phantom: PhantomData, +} + +impl LoadCase for KZGVerifyCellKZGProofBatch { + fn load_from_dir(path: &Path, _fork_name: ForkName) -> Result { + decode::yaml_decode_file(path.join("data.yaml").as_path()) + } +} + +impl Case for KZGVerifyCellKZGProofBatch { + fn is_enabled_for_fork(fork_name: ForkName) -> bool { + fork_name == ForkName::Deneb + } + + fn result(&self, _case_index: usize, _fork_name: ForkName) -> Result<(), Error> { + let parse_input = |input: &KZGVerifyCellKZGProofBatchInput| -> Result<_, Error> { + let (cells, proofs) = parse_cells_and_proofs(&input.cells, &input.proofs)?; + let row_commitments = input + .row_commitments + .iter() + .map(|s| parse_commitment(s)) + .collect::, _>>()?; + let coordinates = input + .row_indices + .iter() + .zip(&input.column_indices) + .map(|(&row, &col)| (row as u64, col as u64)) + .collect::>(); + + Ok((cells, proofs, coordinates, row_commitments)) + }; + + let result = + parse_input(&self.input).and_then(|(cells, proofs, coordinates, commitments)| { + let proofs: Vec = proofs.iter().map(|&proof| proof.into()).collect(); + let commitments: Vec = commitments.iter().map(|&c| c.into()).collect(); + match KZG.verify_cell_proof_batch( + cells.as_slice(), + &proofs, + &coordinates, + &commitments, + ) { + Ok(_) => Ok(true), + Err(KzgError::KzgVerificationFailed) => Ok(false), + Err(e) => Err(Error::InternalError(format!( + "Failed to validate cells: {:?}", + e + ))), + } + }); + + compare_result::(&result, &self.output) + } +} diff --git a/testing/ef_tests/src/cases/kzg_verify_kzg_proof.rs b/testing/ef_tests/src/cases/kzg_verify_kzg_proof.rs index e395558e0e1..1839333fa53 100644 --- a/testing/ef_tests/src/cases/kzg_verify_kzg_proof.rs +++ b/testing/ef_tests/src/cases/kzg_verify_kzg_proof.rs @@ -33,6 +33,10 @@ impl Case for KZGVerifyKZGProof { fork_name == ForkName::Deneb } + fn is_enabled_for_feature(feature_name: FeatureName) -> bool { + feature_name != FeatureName::Eip7594 + } + fn result(&self, _case_index: usize, _fork_name: ForkName) -> Result<(), Error> { let parse_input = |input: &KZGVerifyKZGProofInput| -> Result<_, Error> { let commitment = parse_commitment(&input.commitment)?; @@ -42,9 +46,8 @@ impl Case for KZGVerifyKZGProof { Ok((commitment, z, y, proof)) }; - let kzg = get_kzg()?; let result = parse_input(&self.input).and_then(|(commitment, z, y, proof)| { - verify_kzg_proof::(&kzg, commitment, proof, z, y) + verify_kzg_proof::(&KZG, commitment, proof, z, y) .map_err(|e| Error::InternalError(format!("Failed to validate proof: {:?}", e))) }); diff --git a/testing/ef_tests/src/cases/merkle_proof_validity.rs b/testing/ef_tests/src/cases/merkle_proof_validity.rs index 8d5c0687753..b68bbdc5d39 100644 --- a/testing/ef_tests/src/cases/merkle_proof_validity.rs +++ b/testing/ef_tests/src/cases/merkle_proof_validity.rs @@ -3,7 +3,8 @@ use crate::decode::{ssz_decode_file, ssz_decode_state, yaml_decode_file}; use serde::Deserialize; use tree_hash::Hash256; use types::{ - BeaconBlockBody, BeaconBlockBodyDeneb, BeaconBlockBodyElectra, BeaconState, FullPayload, + BeaconBlockBody, BeaconBlockBodyDeneb, BeaconBlockBodyElectra, BeaconState, FixedVector, + FullPayload, Unsigned, }; #[derive(Debug, Clone, Deserialize)] @@ -81,12 +82,18 @@ impl Case for MerkleProofValidity { } } -#[derive(Debug, Clone, Deserialize)] -#[serde(bound = "E: EthSpec")] +#[derive(Debug, Clone)] pub struct KzgInclusionMerkleProofValidity { pub metadata: Option, pub block: BeaconBlockBody, pub merkle_proof: MerkleProof, + pub proof_type: KzgInclusionProofType, +} + +#[derive(Debug, Clone)] +pub enum KzgInclusionProofType { + Single, + List, } impl LoadCase for KzgInclusionMerkleProofValidity { @@ -115,21 +122,33 @@ impl LoadCase for KzgInclusionMerkleProofValidity { None }; + let file_name = path + .file_name() + .and_then(|file_name| file_name.to_str()) + .ok_or(Error::InternalError( + "failed to read file name from path".to_string(), + ))?; + + let proof_type = if file_name.starts_with("blob_kzg_commitments") { + KzgInclusionProofType::List + } else { + KzgInclusionProofType::Single + }; + Ok(Self { metadata, block, merkle_proof, + proof_type, }) } } -impl Case for KzgInclusionMerkleProofValidity { - fn result(&self, _case_index: usize, _fork_name: ForkName) -> Result<(), Error> { - let Ok(proof) = self.block.to_ref().kzg_commitment_merkle_proof(0) else { - return Err(Error::FailedToParseTest( - "Could not retrieve merkle proof".to_string(), - )); - }; +impl KzgInclusionMerkleProofValidity { + fn verify_kzg_inclusion_proof( + &self, + proof: FixedVector, + ) -> Result<(), Error> { let proof_len = proof.len(); let branch_len = self.merkle_proof.branch.len(); if proof_len != branch_len { @@ -153,3 +172,29 @@ impl Case for KzgInclusionMerkleProofValidity { Ok(()) } } +impl Case for KzgInclusionMerkleProofValidity { + fn result(&self, _case_index: usize, _fork_name: ForkName) -> Result<(), Error> { + match self.proof_type { + KzgInclusionProofType::Single => { + let proof = self + .block + .to_ref() + .kzg_commitment_merkle_proof(0) + .map_err(|e| { + Error::FailedToParseTest(format!("Could not retrieve merkle proof: {e:?}")) + })?; + self.verify_kzg_inclusion_proof(proof) + } + KzgInclusionProofType::List => { + let proof = self + .block + .to_ref() + .kzg_commitments_merkle_proof() + .map_err(|e| { + Error::FailedToParseTest(format!("Could not retrieve merkle proof: {e:?}")) + })?; + self.verify_kzg_inclusion_proof(proof) + } + } + } +} diff --git a/testing/ef_tests/src/handler.rs b/testing/ef_tests/src/handler.rs index 2d5ea4149ef..39dbc6ed88b 100644 --- a/testing/ef_tests/src/handler.rs +++ b/testing/ef_tests/src/handler.rs @@ -1,12 +1,15 @@ use crate::cases::{self, Case, Cases, EpochTransition, LoadCase, Operation}; -use crate::type_name; use crate::type_name::TypeName; +use crate::{type_name, FeatureName}; use derivative::Derivative; use std::fs::{self, DirEntry}; use std::marker::PhantomData; use std::path::PathBuf; use types::{BeaconState, EthSpec, ForkName}; +const EIP7594_FORK: ForkName = ForkName::Deneb; +const EIP7594_TESTS: [&str; 4] = ["ssz_static", "merkle_proof", "networking", "kzg"]; + pub trait Handler { type Case: Case + LoadCase; @@ -29,10 +32,21 @@ pub trait Handler { Self::Case::is_enabled_for_fork(fork_name) } + fn is_enabled_for_feature(&self, feature_name: FeatureName) -> bool { + Self::Case::is_enabled_for_feature(feature_name) + } + fn run(&self) { for fork_name in ForkName::list_all() { if !self.disabled_forks().contains(&fork_name) && self.is_enabled_for_fork(fork_name) { - self.run_for_fork(fork_name) + self.run_for_fork(fork_name); + + if fork_name == EIP7594_FORK + && EIP7594_TESTS.contains(&Self::runner_name()) + && self.is_enabled_for_feature(FeatureName::Eip7594) + { + self.run_for_feature(EIP7594_FORK, FeatureName::Eip7594); + } } } } @@ -82,6 +96,47 @@ pub trait Handler { ); crate::results::assert_tests_pass(&name, &handler_path, &results); } + + fn run_for_feature(&self, fork_name: ForkName, feature_name: FeatureName) { + let feature_name_str = feature_name.to_string(); + + let handler_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("consensus-spec-tests") + .join("tests") + .join(Self::config_name()) + .join(&feature_name_str) + .join(Self::runner_name()) + .join(self.handler_name()); + + // Iterate through test suites + let as_directory = |entry: Result| -> Option { + entry + .ok() + .filter(|e| e.file_type().map(|ty| ty.is_dir()).unwrap()) + }; + + let test_cases = fs::read_dir(&handler_path) + .unwrap_or_else(|e| panic!("handler dir {} exists: {:?}", handler_path.display(), e)) + .filter_map(as_directory) + .flat_map(|suite| fs::read_dir(suite.path()).expect("suite dir exists")) + .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) + }) + .collect(); + + let results = Cases { test_cases }.test_results(fork_name, Self::use_rayon()); + + let name = format!( + "{}/{}/{}", + feature_name_str, + Self::runner_name(), + self.handler_name() + ); + crate::results::assert_tests_pass(&name, &handler_path, &results); + } } macro_rules! bls_eth_handler { @@ -771,6 +826,66 @@ impl Handler for KZGVerifyKZGProofHandler { } } +#[derive(Derivative)] +#[derivative(Default(bound = ""))] +pub struct GetCustodyColumnsHandler(PhantomData); + +impl Handler for GetCustodyColumnsHandler { + type Case = cases::GetCustodyColumns; + + fn config_name() -> &'static str { + E::name() + } + + fn runner_name() -> &'static str { + "networking" + } + + fn handler_name(&self) -> String { + "get_custody_columns".into() + } +} + +#[derive(Derivative)] +#[derivative(Default(bound = ""))] +pub struct KZGComputeCellsAndKZGProofHandler(PhantomData); + +impl Handler for KZGComputeCellsAndKZGProofHandler { + type Case = cases::KZGComputeCellsAndKZGProofs; + + fn config_name() -> &'static str { + "general" + } + + fn runner_name() -> &'static str { + "kzg" + } + + fn handler_name(&self) -> String { + "compute_cells_and_kzg_proofs".into() + } +} + +#[derive(Derivative)] +#[derivative(Default(bound = ""))] +pub struct KZGVerifyCellKZGProofBatchHandler(PhantomData); + +impl Handler for KZGVerifyCellKZGProofBatchHandler { + type Case = cases::KZGVerifyCellKZGProofBatch; + + fn config_name() -> &'static str { + "general" + } + + fn runner_name() -> &'static str { + "kzg" + } + + fn handler_name(&self) -> String { + "verify_cell_kzg_proof_batch".into() + } +} + #[derive(Derivative)] #[derivative(Default(bound = ""))] pub struct MerkleProofValidityHandler(PhantomData); diff --git a/testing/ef_tests/src/lib.rs b/testing/ef_tests/src/lib.rs index 5ab2b4b7b43..430607c16c0 100644 --- a/testing/ef_tests/src/lib.rs +++ b/testing/ef_tests/src/lib.rs @@ -1,10 +1,10 @@ pub use case_result::CaseResult; pub use cases::WithdrawalsPayload; pub use cases::{ - Case, EffectiveBalanceUpdates, Eth1DataReset, HistoricalRootsUpdate, HistoricalSummariesUpdate, - InactivityUpdates, JustificationAndFinalization, ParticipationFlagUpdates, - ParticipationRecordUpdates, RandaoMixesReset, RegistryUpdates, RewardsAndPenalties, Slashings, - SlashingsReset, SyncCommitteeUpdates, + Case, EffectiveBalanceUpdates, Eth1DataReset, FeatureName, HistoricalRootsUpdate, + HistoricalSummariesUpdate, InactivityUpdates, JustificationAndFinalization, + ParticipationFlagUpdates, ParticipationRecordUpdates, RandaoMixesReset, RegistryUpdates, + RewardsAndPenalties, Slashings, SlashingsReset, SyncCommitteeUpdates, }; pub use decode::log_file_access; pub use error::Error; diff --git a/testing/ef_tests/src/type_name.rs b/testing/ef_tests/src/type_name.rs index 49ebbe81909..bde05d779ce 100644 --- a/testing/ef_tests/src/type_name.rs +++ b/testing/ef_tests/src/type_name.rs @@ -1,5 +1,4 @@ //! Mapping from types to canonical string identifiers used in testing. -use types::blob_sidecar::BlobIdentifier; use types::historical_summary::HistoricalSummary; use types::*; @@ -52,7 +51,9 @@ type_name_generic!(BeaconBlockBodyDeneb, "BeaconBlockBody"); type_name!(BeaconBlockHeader); type_name_generic!(BeaconState); type_name!(BlobIdentifier); +type_name!(DataColumnIdentifier); type_name_generic!(BlobSidecar); +type_name_generic!(DataColumnSidecar); type_name!(Checkpoint); type_name_generic!(ContributionAndProof); type_name!(Deposit); diff --git a/testing/ef_tests/tests/tests.rs b/testing/ef_tests/tests/tests.rs index 5226c7ac2b0..11c504c75b0 100644 --- a/testing/ef_tests/tests/tests.rs +++ b/testing/ef_tests/tests/tests.rs @@ -214,10 +214,11 @@ macro_rules! ssz_static_test_no_run { #[cfg(feature = "fake_crypto")] mod ssz_static { - use ef_tests::{Handler, SszStaticHandler, SszStaticTHCHandler, SszStaticWithSpecHandler}; - use types::blob_sidecar::BlobIdentifier; + use ef_tests::{ + FeatureName, Handler, SszStaticHandler, SszStaticTHCHandler, SszStaticWithSpecHandler, + }; use types::historical_summary::HistoricalSummary; - use types::{LightClientBootstrapAltair, *}; + use types::*; ssz_static_test!(aggregate_and_proof, AggregateAndProof<_>); ssz_static_test!(attestation, Attestation<_>); @@ -516,6 +517,22 @@ mod ssz_static { SszStaticHandler::::capella_and_later().run(); SszStaticHandler::::capella_and_later().run(); } + + #[test] + fn data_column_sidecar() { + SszStaticHandler::, MinimalEthSpec>::deneb_only() + .run_for_feature(ForkName::Deneb, FeatureName::Eip7594); + SszStaticHandler::, MainnetEthSpec>::deneb_only() + .run_for_feature(ForkName::Deneb, FeatureName::Eip7594); + } + + #[test] + fn data_column_identifier() { + SszStaticHandler::::deneb_only() + .run_for_feature(ForkName::Deneb, FeatureName::Eip7594); + SszStaticHandler::::deneb_only() + .run_for_feature(ForkName::Deneb, FeatureName::Eip7594); + } } #[test] @@ -726,6 +743,18 @@ fn kzg_verify_kzg_proof() { KZGVerifyKZGProofHandler::::default().run(); } +#[test] +fn kzg_compute_cells_and_proofs() { + KZGComputeCellsAndKZGProofHandler::::default() + .run_for_feature(ForkName::Deneb, FeatureName::Eip7594); +} + +#[test] +fn kzg_verify_cell_proof_batch() { + KZGVerifyCellKZGProofBatchHandler::::default() + .run_for_feature(ForkName::Deneb, FeatureName::Eip7594); +} + #[test] fn merkle_proof_validity() { MerkleProofValidityHandler::::default().run(); @@ -745,3 +774,11 @@ fn rewards() { RewardsHandler::::new(handler).run(); } } + +#[test] +fn get_custody_columns() { + GetCustodyColumnsHandler::::default() + .run_for_feature(ForkName::Deneb, FeatureName::Eip7594); + GetCustodyColumnsHandler::::default() + .run_for_feature(ForkName::Deneb, FeatureName::Eip7594); +} From 55706335262b1b6a29601cf9e6e7572b16f4f6e7 Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Tue, 14 May 2024 14:39:47 +0300 Subject: [PATCH 22/76] Merge remote branch 'sigp/unstable' into das --- .../src/block_verification_types.rs | 20 + .../src/data_availability_checker.rs | 7 +- .../overflow_lru_cache.rs | 13 +- .../gossip_methods.rs | 20 +- .../src/network_beacon_processor/mod.rs | 12 +- .../network_beacon_processor/sync_methods.rs | 18 +- .../network/src/sync/block_lookups/common.rs | 21 +- .../network/src/sync/block_lookups/mod.rs | 29 +- .../sync/block_lookups/single_block_lookup.rs | 49 +- .../network/src/sync/block_lookups/tests.rs | 706 ++++++++++-------- beacon_node/network/src/sync/manager.rs | 11 + .../network/src/sync/network_context.rs | 79 +- 12 files changed, 588 insertions(+), 397 deletions(-) diff --git a/beacon_node/beacon_chain/src/block_verification_types.rs b/beacon_node/beacon_chain/src/block_verification_types.rs index ef079fb196c..022c4f97e71 100644 --- a/beacon_node/beacon_chain/src/block_verification_types.rs +++ b/beacon_node/beacon_chain/src/block_verification_types.rs @@ -366,6 +366,26 @@ pub struct BlockImportData { pub consensus_context: ConsensusContext, } +impl BlockImportData { + pub fn __new_for_test( + block_root: Hash256, + state: BeaconState, + parent_block: SignedBeaconBlock>, + ) -> Self { + Self { + block_root, + state, + parent_block, + parent_eth1_finalization_data: Eth1FinalizationData { + eth1_data: <_>::default(), + eth1_deposit_index: 0, + }, + confirmed_state_roots: vec![], + consensus_context: ConsensusContext::new(Slot::new(0)), + } + } +} + pub type GossipVerifiedBlockContents = ( GossipVerifiedBlock, Option>, diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index 1dff39737e1..70f72b80122 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -106,10 +106,11 @@ impl DataAvailabilityChecker { }) } - /// Checks if the block root is currenlty in the availability cache awaiting processing because + /// Checks if the block root is currenlty in the availability cache awaiting import because /// of missing components. - pub fn has_block(&self, block_root: &Hash256) -> bool { - self.availability_cache.has_block(block_root) + pub fn has_execution_valid_block(&self, block_root: &Hash256) -> bool { + self.availability_cache + .has_execution_valid_block(block_root) } /// Return the required blobs `block_root` expects if the block is currenlty in the cache. diff --git a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs index 9baa5cd3656..62fe3674122 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs @@ -608,11 +608,6 @@ impl Critical { Ok(()) } - /// Returns true if the block root is known, without altering the LRU ordering - pub fn has_block(&self, block_root: &Hash256) -> bool { - self.in_memory.peek(block_root).is_some() || self.store_keys.contains(block_root) - } - /// This only checks for the blobs in memory pub fn peek_blob( &self, @@ -746,8 +741,12 @@ impl OverflowLRUCache { } /// Returns true if the block root is known, without altering the LRU ordering - pub fn has_block(&self, block_root: &Hash256) -> bool { - self.critical.read().has_block(block_root) + pub fn has_execution_valid_block(&self, block_root: &Hash256) -> bool { + if let Some(pending_components) = self.critical.read().peek_pending_components(block_root) { + pending_components.executed_block.is_some() + } else { + false + } } /// Fetch a blob from the cache without affecting the LRU ordering 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 9259e6826b1..1a55216efe6 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -1342,19 +1342,18 @@ impl NetworkBeaconProcessor { "block_root" => %block_root, ); } - Err(BlockError::ParentUnknown(block)) => { - // Inform the sync manager to find parents for this block - // This should not occur. It should be checked by `should_forward_block` + Err(BlockError::ParentUnknown(_)) => { + // This should not occur. It should be checked by `should_forward_block`. + // Do not send sync message UnknownParentBlock to prevent conflicts with the + // BlockComponentProcessed message below. If this error ever happens, lookup sync + // can recover by receiving another block / blob / attestation referencing the + // chain that includes this block. error!( self.log, "Block with unknown parent attempted to be processed"; + "block_root" => %block_root, "peer_id" => %peer_id ); - self.send_sync_message(SyncMessage::UnknownParentBlock( - peer_id, - block.clone(), - block_root, - )); } Err(ref e @ BlockError::ExecutionPayloadError(ref epe)) if !epe.penalize_peer() => { debug!( @@ -1418,6 +1417,11 @@ impl NetworkBeaconProcessor { &self.log, ); } + + self.send_sync_message(SyncMessage::GossipBlockProcessResult { + block_root, + imported: matches!(result, Ok(AvailabilityProcessingStatus::Imported(_))), + }); } pub fn process_gossip_voluntary_exit( diff --git a/beacon_node/network/src/network_beacon_processor/mod.rs b/beacon_node/network/src/network_beacon_processor/mod.rs index 3d64e1da708..969c604da49 100644 --- a/beacon_node/network/src/network_beacon_processor/mod.rs +++ b/beacon_node/network/src/network_beacon_processor/mod.rs @@ -1,10 +1,8 @@ -use crate::{ - service::NetworkMessage, - sync::{manager::BlockProcessType, SamplingId, SyncMessage}, -}; -use beacon_chain::{ - block_verification_types::RpcBlock, data_column_verification::CustodyDataColumn, -}; +use crate::sync::manager::BlockProcessType; +use crate::sync::SamplingId; +use crate::{service::NetworkMessage, sync::manager::SyncMessage}; +use beacon_chain::block_verification_types::RpcBlock; +use beacon_chain::data_column_verification::CustodyDataColumn; use beacon_chain::{builder::Witness, eth1_chain::CachingEth1Backend, BeaconChain}; use beacon_chain::{BeaconChainTypes, NotifyExecutionLayer}; use beacon_processor::{ 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 2aa498da857..2a9b42be38d 100644 --- a/beacon_node/network/src/network_beacon_processor/sync_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/sync_methods.rs @@ -172,17 +172,15 @@ impl NetworkBeaconProcessor { if reprocess_tx.try_send(reprocess_msg).is_err() { error!(self.log, "Failed to inform block import"; "source" => "rpc", "block_root" => %hash) }; - if matches!(process_type, BlockProcessType::SingleBlock { .. }) { - self.chain.block_times_cache.write().set_time_observed( - hash, - slot, - seen_timestamp, - None, - None, - ); + self.chain.block_times_cache.write().set_time_observed( + hash, + slot, + seen_timestamp, + None, + None, + ); - self.chain.recompute_head_at_current_slot().await; - } + self.chain.recompute_head_at_current_slot().await; } // Sync handles these results self.send_sync_message(SyncMessage::BlockComponentProcessed { diff --git a/beacon_node/network/src/sync/block_lookups/common.rs b/beacon_node/network/src/sync/block_lookups/common.rs index 5491bd7120f..c393f052bab 100644 --- a/beacon_node/network/src/sync/block_lookups/common.rs +++ b/beacon_node/network/src/sync/block_lookups/common.rs @@ -3,7 +3,7 @@ use crate::sync::block_lookups::single_block_lookup::{ }; use crate::sync::block_lookups::{BlobRequestState, BlockRequestState, PeerId}; use crate::sync::manager::{BlockProcessType, Id, SLOT_IMPORT_TOLERANCE}; -use crate::sync::network_context::SyncNetworkContext; +use crate::sync::network_context::{LookupRequestResult, SyncNetworkContext}; use beacon_chain::block_verification_types::RpcBlock; use beacon_chain::data_column_verification::CustodyDataColumn; use beacon_chain::BeaconChainTypes; @@ -49,7 +49,7 @@ pub trait RequestState { peer_id: PeerId, downloaded_block_expected_blobs: Option, cx: &mut SyncNetworkContext, - ) -> Result; + ) -> Result; /* Response handling methods */ @@ -84,7 +84,7 @@ impl RequestState for BlockRequestState { peer_id: PeerId, _: Option, cx: &mut SyncNetworkContext, - ) -> Result { + ) -> Result { cx.block_lookup_request(id, peer_id, self.requested_block_root) .map_err(LookupRequestError::SendFailed) } @@ -101,10 +101,10 @@ impl RequestState for BlockRequestState { .. } = download_result; cx.send_block_for_processing( + id, block_root, RpcBlock::new_without_blobs(Some(block_root), value), seen_timestamp, - BlockProcessType::SingleBlock { id }, ) .map_err(LookupRequestError::SendFailed) } @@ -132,7 +132,7 @@ impl RequestState for BlobRequestState { peer_id: PeerId, downloaded_block_expected_blobs: Option, cx: &mut SyncNetworkContext, - ) -> Result { + ) -> Result { cx.blob_lookup_request( id, peer_id, @@ -153,13 +153,8 @@ impl RequestState for BlobRequestState { seen_timestamp, .. } = download_result; - cx.send_blobs_for_processing( - block_root, - value, - seen_timestamp, - BlockProcessType::SingleBlob { id }, - ) - .map_err(LookupRequestError::SendFailed) + cx.send_blobs_for_processing(id, block_root, value, seen_timestamp) + .map_err(LookupRequestError::SendFailed) } fn response_type() -> ResponseType { @@ -186,7 +181,7 @@ impl RequestState for CustodyRequestState { _peer_id: PeerId, downloaded_block_expected_blobs: Option, cx: &mut SyncNetworkContext, - ) -> Result { + ) -> Result { cx.custody_lookup_request(id, self.block_root, downloaded_block_expected_blobs) .map_err(LookupRequestError::SendFailed) } diff --git a/beacon_node/network/src/sync/block_lookups/mod.rs b/beacon_node/network/src/sync/block_lookups/mod.rs index 1b6f449ec37..34e3df41783 100644 --- a/beacon_node/network/src/sync/block_lookups/mod.rs +++ b/beacon_node/network/src/sync/block_lookups/mod.rs @@ -133,7 +133,10 @@ impl BlockLookups { block_root, Some(block_component), Some(parent_root), - &[peer_id], + // 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. + &[], cx, ); } @@ -566,6 +569,30 @@ impl BlockLookups { } } + pub fn on_external_processing_result( + &mut self, + block_root: Hash256, + imported: bool, + cx: &mut SyncNetworkContext, + ) { + let Some((id, lookup)) = self + .single_block_lookups + .iter_mut() + .find(|(_, lookup)| lookup.is_for_block(block_root)) + else { + // Ok to ignore gossip process events + return; + }; + + let lookup_result = if imported { + Ok(LookupResult::Completed) + } else { + lookup.continue_requests(cx) + }; + let id = *id; + self.on_lookup_result(id, lookup_result, "external_processing_result", cx); + } + /// Makes progress on the immediate children of `block_root` pub fn continue_child_lookups(&mut self, block_root: Hash256, cx: &mut SyncNetworkContext) { let mut lookup_results = vec![]; // < need to buffer lookup results to not re-borrow &mut self 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 9020aea52e8..5daef5c4e93 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 @@ -2,7 +2,7 @@ use super::common::ResponseType; use super::{BlockComponent, PeerId, SINGLE_BLOCK_LOOKUP_MAX_ATTEMPTS}; use crate::sync::block_lookups::common::RequestState; use crate::sync::block_lookups::Id; -use crate::sync::network_context::{PeerGroup, SyncNetworkContext}; +use crate::sync::network_context::{LookupRequestResult, PeerGroup, SyncNetworkContext}; use beacon_chain::data_column_verification::CustodyDataColumn; use beacon_chain::BeaconChainTypes; use rand::seq::IteratorRandom; @@ -161,9 +161,17 @@ impl SingleBlockLookup { .state .peek_downloaded_data() .map(|block| block.num_expected_blobs()); - let peer_id = self - .get_rand_available_peer() - .ok_or(LookupRequestError::NoPeers)?; + + let Some(peer_id) = self.get_rand_available_peer() else { + if awaiting_parent { + // Allow lookups awaiting for a parent to have zero peers. If when the parent + // resolve they still have zero peers the lookup will fail gracefully. + return Ok(()); + } else { + return Err(LookupRequestError::NoPeers); + } + }; + let request = R::request_state_mut(self); // Verify the current request has not exceeded the maximum number of attempts. @@ -173,11 +181,13 @@ impl SingleBlockLookup { return Err(LookupRequestError::TooManyAttempts { cannot_process }); } - // make_request returns true only if a request needs to be made - if request.make_request(id, peer_id, downloaded_block_expected_blobs, cx)? { - request.get_state_mut().on_download_start()?; - } else { - request.get_state_mut().on_completed_request()?; + match request.make_request(id, peer_id, downloaded_block_expected_blobs, cx)? { + LookupRequestResult::RequestSent => request.get_state_mut().on_download_start()?, + LookupRequestResult::NoRequestNeeded => { + request.get_state_mut().on_completed_request()? + } + // Sync will receive a future event to make progress on the request, do nothing now + LookupRequestResult::Pending => return Ok(()), } // Otherwise, attempt to progress awaiting processing @@ -279,12 +289,16 @@ pub struct DownloadResult { pub peer_group: PeerGroup, } -#[derive(Debug)] +#[derive(Debug, IntoStaticStr)] pub enum State { AwaitingDownload, Downloading, AwaitingProcess(DownloadResult), + /// Request is processing, sent by lookup sync Processing(DownloadResult), + /// Request is processed: + /// - `Processed(Some)` if lookup sync downloaded and sent to process this request + /// - `Processed(None)` if another source (i.e. gossip) sent this component for processing Processed(Option), } @@ -434,12 +448,11 @@ impl SingleLookupRequestState { } } - pub fn on_processing_success(&mut self) -> Result { + pub fn on_processing_success(&mut self) -> Result<(), LookupRequestError> { match &self.state { State::Processing(result) => { - let peer_group = result.peer_group.clone(); - self.state = State::Processed(Some(peer_group.clone())); - Ok(peer_group) + self.state = State::Processed(Some(result.peer_group.clone())); + Ok(()) } other => Err(LookupRequestError::BadState(format!( "Bad state on_processing_success expected Processing got {other}" @@ -488,12 +501,6 @@ impl SingleLookupRequestState { impl std::fmt::Display for State { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - State::AwaitingDownload => write!(f, "AwaitingDownload"), - State::Downloading { .. } => write!(f, "Downloading"), - State::AwaitingProcess { .. } => write!(f, "AwaitingProcessing"), - State::Processing { .. } => write!(f, "Processing"), - State::Processed { .. } => write!(f, "Processed"), - } + write!(f, "{}", Into::<&'static str>::into(self)) } } diff --git a/beacon_node/network/src/sync/block_lookups/tests.rs b/beacon_node/network/src/sync/block_lookups/tests.rs index 388c0c09e32..38e74ba0d57 100644 --- a/beacon_node/network/src/sync/block_lookups/tests.rs +++ b/beacon_node/network/src/sync/block_lookups/tests.rs @@ -13,14 +13,19 @@ use std::sync::Arc; use super::*; use crate::sync::block_lookups::common::{ResponseType, PARENT_DEPTH_TOLERANCE}; -use beacon_chain::block_verification_types::RpcBlock; +use beacon_chain::blob_verification::GossipVerifiedBlob; +use beacon_chain::block_verification_types::{BlockImportData, RpcBlock}; use beacon_chain::builder::Witness; +use beacon_chain::data_availability_checker::Availability; use beacon_chain::eth1_chain::CachingEth1Backend; use beacon_chain::test_utils::{ build_log, generate_rand_block_and_blobs, generate_rand_block_and_data_columns, test_spec, BeaconChainHarness, EphemeralHarnessType, NumBlobs, }; use beacon_chain::validator_monitor::timestamp_now; +use beacon_chain::{ + AvailabilityPendingExecutedBlock, PayloadVerificationOutcome, PayloadVerificationStatus, +}; use beacon_processor::WorkEvent; use lighthouse_network::rpc::{RPCError, RPCResponseErrorCode}; use lighthouse_network::types::SyncState; @@ -30,10 +35,12 @@ use slot_clock::{ManualSlotClock, SlotClock, TestingSlotClock}; use store::MemoryStore; use tokio::sync::mpsc; use types::data_column_sidecar::ColumnIndex; +use types::test_utils::TestRandom; use types::{ test_utils::{SeedableRng, XorShiftRng}, BlobSidecar, ForkName, MinimalEthSpec as E, SignedBeaconBlock, Slot, }; +use types::{BeaconState, BeaconStateBase}; use types::{DataColumnSidecar, Epoch}; type T = Witness, E, MemoryStore, MemoryStore>; @@ -75,6 +82,8 @@ struct TestRig { sync_manager: SyncManager, /// To manipulate sync state and peer connection status network_globals: Arc>, + /// Beacon chain harness + harness: BeaconChainHarness>, /// `rng` for generating test blocks and blobs. rng: XorShiftRng, fork_name: ForkName, @@ -158,6 +167,7 @@ impl TestRig { }, log.clone(), ), + harness, fork_name, log, } @@ -342,15 +352,6 @@ impl TestRig { self.expect_no_active_single_lookups(); } - fn expect_lookups(&self, expected_block_roots: &[Hash256]) { - let block_roots = self - .active_single_lookups() - .iter() - .map(|(_, b, _)| *b) - .collect::>(); - assert_eq!(&block_roots, expected_block_roots); - } - fn new_connected_peer(&mut self) -> PeerId { let peer_id = PeerId::random(); self.network_globals @@ -508,6 +509,63 @@ impl TestRig { }); } + fn complete_single_lookup_blob_download( + &mut self, + id: SingleLookupReqId, + peer_id: PeerId, + blobs: Vec>, + ) { + for blob in blobs { + self.single_lookup_blob_response(id, peer_id, Some(blob.into())); + } + self.single_lookup_blob_response(id, peer_id, None); + } + + fn complete_single_lookup_blob_lookup_valid( + &mut self, + id: SingleLookupReqId, + peer_id: PeerId, + blobs: Vec>, + import: bool, + ) { + let block_root = blobs.first().unwrap().block_root(); + let block_slot = blobs.first().unwrap().slot(); + self.complete_single_lookup_blob_download(id, peer_id, blobs); + self.expect_block_process(ResponseType::Blob); + self.single_blob_component_processed( + id.lookup_id, + if import { + BlockProcessingResult::Ok(AvailabilityProcessingStatus::Imported(block_root)) + } else { + BlockProcessingResult::Ok(AvailabilityProcessingStatus::MissingComponents( + block_slot, block_root, + )) + }, + ); + } + + fn complete_single_lookup_block_valid(&mut self, block: SignedBeaconBlock, import: bool) { + let block_root = block.canonical_root(); + let block_slot = block.slot(); + let id = self.expect_block_lookup_request(block_root); + self.expect_empty_network(); + let peer_id = self.new_connected_peer(); + self.single_lookup_block_response(id, peer_id, Some(block.into())); + self.single_lookup_block_response(id, peer_id, None); + self.expect_block_process(ResponseType::Block); + let id = self.find_single_lookup_for(block_root); + self.single_block_component_processed( + id, + if import { + BlockProcessingResult::Ok(AvailabilityProcessingStatus::Imported(block_root)) + } else { + BlockProcessingResult::Ok(AvailabilityProcessingStatus::MissingComponents( + block_slot, block_root, + )) + }, + ) + } + fn parent_lookup_failed(&mut self, id: SingleLookupReqId, peer_id: PeerId, error: RPCError) { self.send_sync_message(SyncMessage::RpcError { peer_id, @@ -708,6 +766,12 @@ impl TestRig { } } + fn drain_processor_rx(&mut self) { + while let Ok(event) = self.beacon_processor_rx.try_recv() { + self.beacon_processor_rx_queue.push(event); + } + } + fn pop_received_network_event) -> Option>( &mut self, predicate_transform: F, @@ -728,17 +792,11 @@ impl TestRig { } } - fn drain_beacon_processor_rx(&mut self) { - while let Ok(event) = self.beacon_processor_rx.try_recv() { - self.beacon_processor_rx_queue.push(event); - } - } - - fn pop_received_beacon_processor_event) -> Option>( + fn pop_received_processor_event) -> Option>( &mut self, predicate_transform: F, ) -> Result { - self.drain_beacon_processor_rx(); + self.drain_processor_rx(); if let Some(index) = self .beacon_processor_rx_queue @@ -751,15 +809,17 @@ impl TestRig { Ok(transformed) } else { Err(format!( - "current beacon processor messages {:?}", + "current processor messages {:?}", self.beacon_processor_rx_queue ) .to_string()) } } - #[track_caller] - fn expect_block_lookup_request(&mut self, for_block: Hash256) -> SingleLookupReqId { + fn find_block_lookup_request( + &mut self, + for_block: Hash256, + ) -> Result { self.pop_received_network_event(|ev| match ev { NetworkMessage::SendRequest { peer_id: _, @@ -768,11 +828,18 @@ impl TestRig { } if request.block_roots().to_vec().contains(&for_block) => Some(*id), _ => None, }) - .unwrap_or_else(|e| panic!("Expected block request for {for_block:?}: {e}")) } #[track_caller] - fn expect_blob_lookup_request(&mut self, for_block: Hash256) -> SingleLookupReqId { + fn expect_block_lookup_request(&mut self, for_block: Hash256) -> SingleLookupReqId { + self.find_block_lookup_request(for_block) + .unwrap_or_else(|e| panic!("Expected block request for {for_block:?}: {e}")) + } + + fn find_blob_lookup_request( + &mut self, + for_block: Hash256, + ) -> Result { self.pop_received_network_event(|ev| match ev { NetworkMessage::SendRequest { peer_id: _, @@ -788,7 +855,12 @@ impl TestRig { } _ => None, }) - .unwrap_or_else(|e| panic!("Expected blob request for {for_block:?}: {e}")) + } + + #[track_caller] + fn expect_blob_lookup_request(&mut self, for_block: Hash256) -> SingleLookupReqId { + self.find_blob_lookup_request(for_block) + .unwrap_or_else(|e| panic!("Expected blob request for {for_block:?}: {e}")) } #[track_caller] @@ -804,6 +876,15 @@ impl TestRig { .unwrap_or_else(|e| panic!("Expected block parent request for {for_block:?}: {e}")) } + fn expect_no_requests_for(&mut self, block_root: Hash256) { + if let Ok(request) = self.find_block_lookup_request(block_root) { + panic!("Expected no block request for {block_root:?} found {request:?}"); + } + if let Ok(request) = self.find_blob_lookup_request(block_root) { + panic!("Expected no blob request for {block_root:?} found {request:?}"); + } + } + #[track_caller] fn expect_blob_parent_request(&mut self, for_block: Hash256) -> SingleLookupReqId { self.pop_received_network_event(|ev| match ev { @@ -887,24 +968,26 @@ impl TestRig { #[track_caller] fn expect_block_process(&mut self, response_type: ResponseType) { match response_type { - ResponseType::Block => match self.beacon_processor_rx.try_recv() { - Ok(work) => { - assert_eq!(work.work_type(), beacon_processor::RPC_BLOCK); - } - other => panic!("Expected block process, found {:?}", other), - }, - ResponseType::Blob => match self.beacon_processor_rx.try_recv() { - Ok(work) => { - assert_eq!(work.work_type(), beacon_processor::RPC_BLOBS); - } - other => panic!("Expected blob process, found {:?}", other), - }, - ResponseType::CustodyColumn => todo!(), + ResponseType::Block => self + .pop_received_processor_event(|ev| { + (ev.work_type() == beacon_processor::RPC_BLOCK).then_some(()) + }) + .unwrap_or_else(|e| panic!("Expected block work event: {e}")), + ResponseType::Blob => self + .pop_received_processor_event(|ev| { + (ev.work_type() == beacon_processor::RPC_BLOBS).then_some(()) + }) + .unwrap_or_else(|e| panic!("Expected blobs work event: {e}")), + ResponseType::CustodyColumn => self + .pop_received_processor_event(|ev| { + (ev.work_type() == beacon_processor::RPC_CUSTODY_COLUMN).then_some(()) + }) + .unwrap_or_else(|e| panic!("Expected column work event: {e}")), } } fn expect_rpc_custody_column_work_event(&mut self) { - self.pop_received_beacon_processor_event(|ev| { + self.pop_received_processor_event(|ev| { if ev.work_type() == beacon_processor::RPC_CUSTODY_COLUMN { Some(()) } else { @@ -915,7 +998,7 @@ impl TestRig { } fn expect_rpc_sample_verify_work_event(&mut self) { - self.pop_received_beacon_processor_event(|ev| { + self.pop_received_processor_event(|ev| { if ev.work_type() == beacon_processor::RPC_VERIFY_DATA_COLUMNS { Some(()) } else { @@ -926,7 +1009,7 @@ impl TestRig { } fn expect_sampling_result_work(&mut self) { - self.pop_received_beacon_processor_event(|ev| { + self.pop_received_processor_event(|ev| { if ev.work_type() == beacon_processor::SAMPLING_RESULT { Some(()) } else { @@ -1053,6 +1136,89 @@ impl TestRig { )); blocks } + + fn insert_block_to_da_checker(&mut self, block: Arc>) { + let state = BeaconState::Base(BeaconStateBase::random_for_test(&mut self.rng)); + let parent_block = self.rand_block(); + let import_data = BlockImportData::::__new_for_test( + block.canonical_root(), + state, + parent_block.into(), + ); + let payload_verification_outcome = PayloadVerificationOutcome { + payload_verification_status: PayloadVerificationStatus::Verified, + is_valid_merge_transition_block: false, + }; + let executed_block = + AvailabilityPendingExecutedBlock::new(block, import_data, payload_verification_outcome); + match self + .harness + .chain + .data_availability_checker + .put_pending_executed_block(executed_block) + .unwrap() + { + Availability::Available(_) => panic!("block removed from da_checker, available"), + Availability::MissingComponents(block_root) => { + self.log(&format!("inserted block to da_checker {block_root:?}")) + } + }; + } + + fn insert_blob_to_da_checker(&mut self, blob: BlobSidecar) { + match self + .harness + .chain + .data_availability_checker + .put_gossip_blob(GossipVerifiedBlob::__assumed_valid(blob.into())) + .unwrap() + { + Availability::Available(_) => panic!("blob removed from da_checker, available"), + Availability::MissingComponents(block_root) => { + self.log(&format!("inserted blob to da_checker {block_root:?}")) + } + }; + } + + fn insert_block_to_processing_cache(&mut self, block: Arc>) { + self.harness + .chain + .reqresp_pre_import_cache + .write() + .insert(block.canonical_root(), block); + } + + fn simulate_block_gossip_processing_becomes_invalid(&mut self, block_root: Hash256) { + self.harness + .chain + .reqresp_pre_import_cache + .write() + .remove(&block_root); + + self.send_sync_message(SyncMessage::GossipBlockProcessResult { + block_root, + imported: false, + }); + } + + fn simulate_block_gossip_processing_becomes_valid_missing_components( + &mut self, + block: Arc>, + ) { + let block_root = block.canonical_root(); + self.harness + .chain + .reqresp_pre_import_cache + .write() + .remove(&block_root); + + self.insert_block_to_da_checker(block); + + self.send_sync_message(SyncMessage::GossipBlockProcessResult { + block_root, + imported: false, + }); + } } #[test] @@ -1096,22 +1262,29 @@ fn test_single_block_lookup_happy_path() { rig.expect_no_active_lookups(); } +// Tests that if a peer does not respond with a block, we downscore and retry the block only #[test] fn test_single_block_lookup_empty_response() { - let mut rig = TestRig::test_setup(); + let mut r = TestRig::test_setup(); - let block_hash = Hash256::random(); - let peer_id = rig.new_connected_peer(); + let block = r.rand_block(); + let block_root = block.canonical_root(); + let peer_id = r.new_connected_peer(); // Trigger the request - rig.trigger_unknown_block_from_attestation(block_hash, peer_id); - let id = rig.expect_lookup_request_block_and_blobs(block_hash); + r.trigger_unknown_block_from_attestation(block_root, peer_id); + let id = r.expect_lookup_request_block_and_blobs(block_root); // The peer does not have the block. It should be penalized. - rig.single_lookup_block_response(id, peer_id, None); - rig.expect_penalty(peer_id, "NoResponseReturned"); - - rig.expect_block_lookup_request(block_hash); // it should be retried + r.single_lookup_block_response(id, peer_id, None); + r.expect_penalty(peer_id, "NoResponseReturned"); + // it should be retried + let id = r.expect_block_lookup_request(block_root); + // Send the right block this time. + r.single_lookup_block_response(id, peer_id, Some(block.into())); + r.expect_block_process(ResponseType::Block); + r.single_block_component_processed_imported(block_root); + r.expect_no_active_lookups(); } #[test] @@ -1203,6 +1376,8 @@ fn test_parent_lookup_happy_path() { rig.expect_block_process(ResponseType::Block); rig.expect_empty_network(); + // Add peer to child lookup to prevent it being dropped + rig.trigger_unknown_block_from_attestation(block_root, peer_id); // Processing succeeds, now the rest of the chain should be sent for processing. rig.parent_block_processed( block_root, @@ -1238,6 +1413,8 @@ fn test_parent_lookup_wrong_response() { rig.parent_lookup_block_response(id2, peer_id, Some(parent.into())); rig.expect_block_process(ResponseType::Block); + // Add peer to child lookup to prevent it being dropped + rig.trigger_unknown_block_from_attestation(block_root, peer_id); // Processing succeeds, now the rest of the chain should be sent for processing. rig.parent_block_processed_imported(block_root); rig.expect_parent_chain_process(); @@ -1245,33 +1422,6 @@ fn test_parent_lookup_wrong_response() { rig.expect_no_active_lookups(); } -#[test] -fn test_parent_lookup_empty_response() { - let mut rig = TestRig::test_setup(); - - let (parent, block, parent_root, block_root) = rig.rand_block_and_parent(); - let peer_id = rig.new_connected_peer(); - - // Trigger the request - rig.trigger_unknown_parent_block(peer_id, block.into()); - let id1 = rig.expect_parent_request_block_and_blobs(parent_root); - - // Peer sends an empty response, peer should be penalized and the block re-requested. - rig.parent_lookup_block_response(id1, peer_id, None); - rig.expect_penalty(peer_id, "NoResponseReturned"); - let id2 = rig.expect_block_parent_request(parent_root); - - // Send the right block this time. - rig.parent_lookup_block_response(id2, peer_id, Some(parent.into())); - rig.expect_block_process(ResponseType::Block); - - // Processing succeeds, now the rest of the chain should be sent for processing. - rig.parent_block_processed_imported(block_root); - - rig.single_block_component_processed_imported(block_root); - rig.expect_no_active_lookups(); -} - #[test] fn test_parent_lookup_rpc_failure() { let mut rig = TestRig::test_setup(); @@ -1291,6 +1441,8 @@ fn test_parent_lookup_rpc_failure() { rig.parent_lookup_block_response(id2, peer_id, Some(parent.into())); rig.expect_block_process(ResponseType::Block); + // Add peer to child lookup to prevent it being dropped + rig.trigger_unknown_block_from_attestation(block_root, peer_id); // Processing succeeds, now the rest of the chain should be sent for processing. rig.parent_block_processed_imported(block_root); rig.expect_parent_chain_process(); @@ -1450,17 +1602,17 @@ fn test_parent_lookup_disconnection_no_peers_left() { } #[test] -fn test_parent_lookup_disconnection_peer_left() { +fn test_lookup_disconnection_peer_left() { let mut rig = TestRig::test_setup(); let peer_ids = (0..2).map(|_| rig.new_connected_peer()).collect::>(); - let trigger_block = rig.rand_block(); + let block_root = Hash256::random(); // lookup should have two peers associated with the same block for peer_id in peer_ids.iter() { - rig.trigger_unknown_parent_block(*peer_id, trigger_block.clone().into()); + rig.trigger_unknown_block_from_attestation(block_root, *peer_id); } // Disconnect the first peer only, which is the one handling the request rig.peer_disconnected(*peer_ids.first().unwrap()); - rig.assert_parent_lookups_count(1); + rig.assert_single_lookups_count(1); } #[test] @@ -1476,19 +1628,6 @@ fn test_skip_creating_failed_parent_lookup() { rig.expect_no_active_lookups(); } -#[test] -fn test_skip_creating_failed_current_lookup() { - let mut rig = TestRig::test_setup(); - let (_, block, parent_root, block_root) = rig.rand_block_and_parent(); - let peer_id = rig.new_connected_peer(); - rig.insert_failed_chain(block_root); - rig.trigger_unknown_parent_block(peer_id, block.into()); - // Expect single penalty for peer - rig.expect_single_penalty(peer_id, "failed_chain"); - // Only the current lookup should be rejected - rig.expect_lookups(&[parent_root]); -} - #[test] fn test_single_block_lookup_ignored_response() { let mut rig = TestRig::test_setup(); @@ -1585,7 +1724,10 @@ fn test_same_chain_race_condition() { rig.trigger_unknown_parent_block(peer_id, trigger_block.clone()); rig.expect_empty_network(); - // Processing succeeds, now the rest of the chain should be sent for processing. + // Add a peer to the tip child lookup which has zero peers + rig.trigger_unknown_block_from_attestation(trigger_block.canonical_root(), peer_id); + + rig.log("Processing succeeds, now the rest of the chain should be sent for processing."); for block in blocks.iter().skip(1).chain(&[trigger_block]) { rig.expect_parent_chain_process(); rig.single_block_component_processed_imported(block.canonical_root()); @@ -1593,6 +1735,87 @@ fn test_same_chain_race_condition() { rig.expect_no_active_lookups(); } +#[test] +fn block_in_da_checker_skips_download() { + let Some(mut r) = TestRig::test_setup_after_deneb() else { + return; + }; + let (block, blobs) = r.rand_block_and_blobs(NumBlobs::Number(1)); + let block_root = block.canonical_root(); + let peer_id = r.new_connected_peer(); + r.insert_block_to_da_checker(block.into()); + r.trigger_unknown_block_from_attestation(block_root, peer_id); + // Should not trigger block request + let id = r.expect_blob_lookup_request(block_root); + r.expect_empty_network(); + // Resolve blob and expect lookup completed + r.complete_single_lookup_blob_lookup_valid(id, peer_id, blobs, true); + r.expect_no_active_lookups(); +} + +#[test] +fn block_in_processing_cache_becomes_invalid() { + let Some(mut r) = TestRig::test_setup_after_deneb() else { + return; + }; + let (block, blobs) = r.rand_block_and_blobs(NumBlobs::Number(1)); + let block_root = block.canonical_root(); + let peer_id = r.new_connected_peer(); + r.insert_block_to_processing_cache(block.clone().into()); + r.trigger_unknown_block_from_attestation(block_root, peer_id); + // Should not trigger block request + let id = r.expect_blob_lookup_request(block_root); + r.expect_empty_network(); + // Simulate invalid block, removing it from processing cache + r.simulate_block_gossip_processing_becomes_invalid(block_root); + // Should download and process the block + r.complete_single_lookup_block_valid(block, false); + // Resolve blob and expect lookup completed + r.complete_single_lookup_blob_lookup_valid(id, peer_id, blobs, true); + r.expect_no_active_lookups(); +} + +#[test] +fn block_in_processing_cache_becomes_valid_imported() { + let Some(mut r) = TestRig::test_setup_after_deneb() else { + return; + }; + let (block, blobs) = r.rand_block_and_blobs(NumBlobs::Number(1)); + let block_root = block.canonical_root(); + let peer_id = r.new_connected_peer(); + r.insert_block_to_processing_cache(block.clone().into()); + r.trigger_unknown_block_from_attestation(block_root, peer_id); + // Should not trigger block request + let id = r.expect_blob_lookup_request(block_root); + r.expect_empty_network(); + // Resolve the block from processing step + r.simulate_block_gossip_processing_becomes_valid_missing_components(block.into()); + // Resolve blob and expect lookup completed + r.complete_single_lookup_blob_lookup_valid(id, peer_id, blobs, true); + r.expect_no_active_lookups(); +} + +// IGNORE: wait for change that delays blob fetching to knowing the block +#[ignore] +#[test] +fn blobs_in_da_checker_skip_download() { + let Some(mut r) = TestRig::test_setup_after_deneb() else { + return; + }; + let (block, blobs) = r.rand_block_and_blobs(NumBlobs::Number(1)); + let block_root = block.canonical_root(); + let peer_id = r.new_connected_peer(); + for blob in blobs { + r.insert_blob_to_da_checker(blob); + } + r.trigger_unknown_block_from_attestation(block_root, peer_id); + // Should download and process the block + r.complete_single_lookup_block_valid(block, true); + // Should not trigger blob request + r.expect_empty_network(); + r.expect_no_active_lookups(); +} + #[test] fn sampling_happy_path() { let Some(mut r) = TestRig::test_setup_after_peerdas() else { @@ -1670,6 +1893,7 @@ mod deneb_only { rig: TestRig, block: Arc>, blobs: Vec>>, + parent_block_roots: Vec, parent_block: VecDeque>>, parent_blobs: VecDeque>>>, unknown_parent_block: Option>>, @@ -1685,16 +1909,16 @@ mod deneb_only { enum RequestTrigger { AttestationUnknownBlock, - GossipUnknownParentBlock { num_parents: usize }, - GossipUnknownParentBlob { num_parents: usize }, + GossipUnknownParentBlock(usize), + GossipUnknownParentBlob(usize), } impl RequestTrigger { fn num_parents(&self) -> usize { match self { RequestTrigger::AttestationUnknownBlock => 0, - RequestTrigger::GossipUnknownParentBlock { num_parents } => *num_parents, - RequestTrigger::GossipUnknownParentBlob { num_parents } => *num_parents, + RequestTrigger::GossipUnknownParentBlock(num_parents) => *num_parents, + RequestTrigger::GossipUnknownParentBlob(num_parents) => *num_parents, } } } @@ -1712,6 +1936,7 @@ mod deneb_only { let num_parents = request_trigger.num_parents(); let mut parent_block_chain = VecDeque::with_capacity(num_parents); let mut parent_blobs_chain = VecDeque::with_capacity(num_parents); + let mut parent_block_roots = vec![]; for _ in 0..num_parents { // Set the current block as the parent. let parent_root = block.canonical_root(); @@ -1719,6 +1944,7 @@ mod deneb_only { let parent_blobs = blobs.clone(); parent_block_chain.push_front(parent_block); parent_blobs_chain.push_front(parent_blobs); + parent_block_roots.push(parent_root); // Create the next block. let (child_block, child_blobs) = @@ -1753,13 +1979,12 @@ mod deneb_only { )); let parent_root = block.parent_root(); - let blob_req_id = rig.expect_blob_lookup_request(block_root); let parent_block_req_id = rig.expect_block_parent_request(parent_root); let parent_blob_req_id = rig.expect_blob_parent_request(parent_root); rig.expect_empty_network(); // expect no more requests ( None, - Some(blob_req_id), + None, Some(parent_block_req_id), Some(parent_blob_req_id), ) @@ -1769,14 +1994,12 @@ mod deneb_only { let parent_root = single_blob.block_parent_root(); rig.send_sync_message(SyncMessage::UnknownParentBlob(peer_id, single_blob)); - let block_req_id = rig.expect_block_lookup_request(block_root); - let blobs_req_id = rig.expect_blob_lookup_request(block_root); let parent_block_req_id = rig.expect_block_parent_request(parent_root); let parent_blob_req_id = rig.expect_blob_parent_request(parent_root); rig.expect_empty_network(); // expect no more requests ( - Some(block_req_id), - Some(blobs_req_id), + None, + None, Some(parent_block_req_id), Some(parent_blob_req_id), ) @@ -1789,6 +2012,7 @@ mod deneb_only { blobs, parent_block: parent_block_chain, parent_blobs: parent_blobs_chain, + parent_block_roots, unknown_parent_block: None, unknown_parent_blobs: None, peer_id, @@ -1806,6 +2030,13 @@ mod deneb_only { self } + fn trigger_unknown_block_from_attestation(mut self) -> Self { + let block_root = self.block.canonical_root(); + self.rig + .trigger_unknown_block_from_attestation(block_root, self.peer_id); + self + } + fn parent_block_response(mut self) -> Self { self.rig.expect_empty_network(); let block = self.parent_block.pop_front().unwrap().clone(); @@ -1916,15 +2147,6 @@ mod deneb_only { self } - fn empty_parent_block_response(mut self) -> Self { - self.rig.parent_lookup_block_response( - self.parent_block_req_id.expect("block request id"), - self.peer_id, - None, - ); - self - } - fn empty_parent_blobs_response(mut self) -> Self { self.rig.parent_lookup_blob_response( self.parent_blob_req_id.expect("blob request id"), @@ -1973,23 +2195,28 @@ mod deneb_only { } fn parent_block_imported(mut self) -> Self { - self.rig.log("parent_block_imported"); + let parent_root = *self.parent_block_roots.first().unwrap(); + self.rig + .log(&format!("parent_block_imported {parent_root:?}")); self.rig.parent_block_processed( self.block_root, - BlockProcessingResult::Ok(AvailabilityProcessingStatus::Imported(self.block_root)), + BlockProcessingResult::Ok(AvailabilityProcessingStatus::Imported(parent_root)), ); - self.rig.expect_empty_network(); + self.rig.expect_no_requests_for(parent_root); self.rig.assert_parent_lookups_count(0); self } fn parent_blob_imported(mut self) -> Self { - self.rig.log("parent_blob_imported"); + let parent_root = *self.parent_block_roots.first().unwrap(); + self.rig + .log(&format!("parent_blob_imported {parent_root:?}")); self.rig.parent_blob_processed( self.block_root, - BlockProcessingResult::Ok(AvailabilityProcessingStatus::Imported(self.block_root)), + BlockProcessingResult::Ok(AvailabilityProcessingStatus::Imported(parent_root)), ); - self.rig.expect_empty_network(); + + self.rig.expect_no_requests_for(parent_root); self.rig.assert_parent_lookups_count(0); self } @@ -2087,6 +2314,34 @@ mod deneb_only { self } + fn complete_current_block_and_blobs_lookup(self) -> Self { + self.expect_block_request() + .expect_blobs_request() + .block_response() + .blobs_response() + // TODO: Should send blobs for processing + .expect_block_process() + .block_imported() + } + + fn empty_parent_blobs_then_parent_block(self) -> Self { + self.log( + " Return empty blobs for parent, block errors with missing components, downscore", + ) + .empty_parent_blobs_response() + .expect_no_penalty_and_no_requests() + .parent_block_response() + .parent_block_missing_components() + .expect_penalty("sent_incomplete_blobs") + .log("Re-request parent blobs, succeed and import parent") + .expect_parent_blobs_request() + .parent_blob_response() + .expect_block_process() + // Insert new peer into child request before completing parent + .trigger_unknown_block_from_attestation() + .parent_blob_imported() + } + fn expect_penalty(mut self, expect_penalty_msg: &'static str) -> Self { self.rig.expect_penalty(self.peer_id, expect_penalty_msg); self @@ -2144,10 +2399,6 @@ mod deneb_only { self.blobs.push(first_blob); self } - fn expect_parent_chain_process(mut self) -> Self { - self.rig.expect_parent_chain_process(); - self - } fn expect_block_process(mut self) -> Self { self.rig.expect_block_process(ResponseType::Block); self @@ -2168,7 +2419,6 @@ mod deneb_only { let Some(tester) = DenebTester::new(RequestTrigger::AttestationUnknownBlock) else { return; }; - tester .block_response_triggering_process() .blobs_response() @@ -2182,7 +2432,6 @@ mod deneb_only { let Some(tester) = DenebTester::new(RequestTrigger::AttestationUnknownBlock) else { return; }; - tester .blobs_response() // hold blobs for processing .block_response_triggering_process() @@ -2196,7 +2445,6 @@ mod deneb_only { let Some(tester) = DenebTester::new(RequestTrigger::AttestationUnknownBlock) else { return; }; - tester .empty_block_response() .expect_penalty("NoResponseReturned") @@ -2248,7 +2496,6 @@ mod deneb_only { let Some(tester) = DenebTester::new(RequestTrigger::AttestationUnknownBlock) else { return; }; - tester .block_response_triggering_process() .invalid_block_processed() @@ -2265,7 +2512,6 @@ mod deneb_only { let Some(tester) = DenebTester::new(RequestTrigger::AttestationUnknownBlock) else { return; }; - tester .block_response_triggering_process() .missing_components_from_block_request() @@ -2281,7 +2527,6 @@ mod deneb_only { let Some(tester) = DenebTester::new(RequestTrigger::AttestationUnknownBlock) else { return; }; - tester .block_response_triggering_process() .missing_components_from_block_request() @@ -2298,7 +2543,6 @@ mod deneb_only { let Some(tester) = DenebTester::new(RequestTrigger::AttestationUnknownBlock) else { return; }; - tester .block_response_triggering_process() .invalidate_blobs_too_many() @@ -2313,7 +2557,6 @@ mod deneb_only { let Some(tester) = DenebTester::new(RequestTrigger::AttestationUnknownBlock) else { return; }; - tester .invalidate_blobs_too_few() .blobs_response() // blobs are not sent until the block is processed @@ -2326,7 +2569,6 @@ mod deneb_only { let Some(tester) = DenebTester::new(RequestTrigger::AttestationUnknownBlock) else { return; }; - tester .invalidate_blobs_too_many() .blobs_response() @@ -2336,16 +2578,13 @@ mod deneb_only { .block_response_triggering_process(); } + // Test peer returning block that has unknown parent, and a new lookup is created #[test] fn parent_block_unknown_parent() { - let Some(tester) = - DenebTester::new(RequestTrigger::GossipUnknownParentBlock { num_parents: 1 }) - else { + let Some(tester) = DenebTester::new(RequestTrigger::GossipUnknownParentBlock(1)) else { return; }; - tester - .blobs_response() .expect_empty_beacon_processor() .parent_block_response() .parent_blob_response() @@ -2356,17 +2595,13 @@ mod deneb_only { .expect_empty_beacon_processor(); } + // Test peer returning invalid (processing) block, expect retry #[test] fn parent_block_invalid_parent() { - let Some(tester) = - DenebTester::new(RequestTrigger::GossipUnknownParentBlock { num_parents: 1 }) - else { + let Some(tester) = DenebTester::new(RequestTrigger::GossipUnknownParentBlock(1)) else { return; }; - tester - .blobs_response() - .expect_empty_beacon_processor() .parent_block_response() .parent_blob_response() .expect_block_process() @@ -2376,99 +2611,44 @@ mod deneb_only { .expect_empty_beacon_processor(); } + // Tests that if a peer does not respond with a block, we downscore and retry the block only #[test] - fn parent_block_and_blob_lookup_parent_returned_first() { - let Some(tester) = - DenebTester::new(RequestTrigger::GossipUnknownParentBlock { num_parents: 1 }) - else { - return; - }; - - tester - .parent_block_response() - .parent_blob_response() - .expect_block_process() - .parent_block_imported() - .blobs_response() - .expect_parent_chain_process(); - } - - #[test] - fn parent_block_and_blob_lookup_child_returned_first() { - let Some(tester) = - DenebTester::new(RequestTrigger::GossipUnknownParentBlock { num_parents: 1 }) - else { - return; - }; - - tester - .blobs_response() - .expect_no_penalty_and_no_requests() - .parent_block_response() - .parent_blob_response() - .expect_block_process() - .parent_block_imported() - .expect_parent_chain_process(); - } - - #[test] - fn empty_parent_block_then_parent_blob() { - let Some(tester) = - DenebTester::new(RequestTrigger::GossipUnknownParentBlock { num_parents: 1 }) - else { + fn empty_block_is_retried() { + let Some(tester) = DenebTester::new(RequestTrigger::AttestationUnknownBlock) else { return; }; - tester - .empty_parent_block_response() + .empty_block_response() .expect_penalty("NoResponseReturned") - .expect_parent_block_request() + .expect_block_request() .expect_no_blobs_request() - .parent_blob_response() - .expect_empty_beacon_processor() - .parent_block_response() - .expect_block_process() - .parent_block_imported() + .block_response() .blobs_response() - .expect_parent_chain_process(); + .block_imported() + .expect_no_active_lookups(); } #[test] fn empty_parent_blobs_then_parent_block() { - let Some(tester) = - DenebTester::new(RequestTrigger::GossipUnknownParentBlock { num_parents: 1 }) - else { + let Some(tester) = DenebTester::new(RequestTrigger::GossipUnknownParentBlock(1)) else { return; }; - tester - .blobs_response() - .log(" Return empty blobs for parent, block errors with missing components, downscore") - .empty_parent_blobs_response() - .expect_no_penalty_and_no_requests() - .parent_block_response() - .parent_block_missing_components() - .expect_penalty("sent_incomplete_blobs") - .log("Re-request parent blobs, succeed and import parent") - .expect_parent_blobs_request() - .parent_blob_response() - .expect_block_process() - .parent_blob_imported() + .empty_parent_blobs_then_parent_block() .log("resolve original block trigger blobs request and import") + // Should not have block request, it is cached + .expect_blobs_request() + // TODO: Should send blobs for processing .block_imported() .expect_no_active_lookups(); } #[test] fn parent_blob_unknown_parent() { - let Some(tester) = - DenebTester::new(RequestTrigger::GossipUnknownParentBlob { num_parents: 1 }) - else { + let Some(tester) = DenebTester::new(RequestTrigger::GossipUnknownParentBlob(1)) else { return; }; - tester - .block_response() .expect_empty_beacon_processor() .parent_block_response() .parent_blob_response() @@ -2481,14 +2661,10 @@ mod deneb_only { #[test] fn parent_blob_invalid_parent() { - let Some(tester) = - DenebTester::new(RequestTrigger::GossipUnknownParentBlob { num_parents: 1 }) - else { + let Some(tester) = DenebTester::new(RequestTrigger::GossipUnknownParentBlob(1)) else { return; }; - tester - .block_response() .expect_empty_beacon_processor() .parent_block_response() .parent_blob_response() @@ -2502,106 +2678,37 @@ mod deneb_only { #[test] fn parent_block_and_blob_lookup_parent_returned_first_blob_trigger() { - let Some(tester) = - DenebTester::new(RequestTrigger::GossipUnknownParentBlob { num_parents: 1 }) - else { + let Some(tester) = DenebTester::new(RequestTrigger::GossipUnknownParentBlob(1)) else { return; }; - tester .parent_block_response() .parent_blob_response() .expect_block_process() + .trigger_unknown_block_from_attestation() .parent_block_imported() - .block_response() - .blobs_response() - .expect_parent_chain_process() - .block_imported() - .expect_no_active_lookups(); - } - - #[test] - fn parent_block_and_blob_lookup_child_returned_first_blob_trigger() { - let Some(tester) = - DenebTester::new(RequestTrigger::GossipUnknownParentBlob { num_parents: 1 }) - else { - return; - }; - - tester - .block_response() - .expect_no_penalty_and_no_requests() - .parent_block_response() - .parent_blob_response() - .expect_block_process() - .parent_block_imported() - .blobs_response() - .expect_parent_chain_process() - .block_imported() - .expect_no_active_lookups(); - } - - #[test] - fn empty_parent_block_then_parent_blob_blob_trigger() { - let Some(tester) = - DenebTester::new(RequestTrigger::GossipUnknownParentBlob { num_parents: 1 }) - else { - return; - }; - - tester - .empty_parent_block_response() - .expect_penalty("NoResponseReturned") - .expect_parent_block_request() - .expect_no_blobs_request() - .parent_blob_response() - .expect_empty_beacon_processor() - .parent_block_response() - .expect_block_process() - .parent_block_imported() - .blobs_response() - .block_response() - .block_imported() + .complete_current_block_and_blobs_lookup() .expect_no_active_lookups(); } #[test] fn empty_parent_blobs_then_parent_block_blob_trigger() { - let Some(tester) = - DenebTester::new(RequestTrigger::GossipUnknownParentBlob { num_parents: 1 }) - else { + let Some(tester) = DenebTester::new(RequestTrigger::GossipUnknownParentBlob(1)) else { return; }; - tester - .block_response() - .log(" Return empty blobs for parent, block errors with missing components, downscore") - .empty_parent_blobs_response() - .expect_no_penalty_and_no_requests() - .parent_block_response() - .parent_block_missing_components() - .expect_penalty("sent_incomplete_blobs") - .log("Re-request parent blobs, succeed and import parent") - .expect_parent_blobs_request() - .parent_blob_response() - .expect_block_process() - .parent_blob_imported() + .empty_parent_blobs_then_parent_block() .log("resolve original block trigger blobs request and import") - .blobs_response() - .block_imported() + .complete_current_block_and_blobs_lookup() .expect_no_active_lookups(); } #[test] fn parent_blob_unknown_parent_chain() { - let Some(tester) = - DenebTester::new(RequestTrigger::GossipUnknownParentBlob { num_parents: 2 }) - else { + let Some(tester) = DenebTester::new(RequestTrigger::GossipUnknownParentBlob(2)) else { return; }; - tester - .block_response() .expect_empty_beacon_processor() .parent_block_response() .parent_blob_response() @@ -2619,12 +2726,9 @@ mod deneb_only { #[test] fn unknown_parent_block_dup() { - let Some(tester) = - DenebTester::new(RequestTrigger::GossipUnknownParentBlock { num_parents: 1 }) - else { + let Some(tester) = DenebTester::new(RequestTrigger::GossipUnknownParentBlock(1)) else { return; }; - tester .search_parent_dup() .expect_no_blobs_request() @@ -2633,18 +2737,18 @@ mod deneb_only { #[test] fn unknown_parent_blob_dup() { - let Some(tester) = - DenebTester::new(RequestTrigger::GossipUnknownParentBlob { num_parents: 1 }) - else { + let Some(tester) = DenebTester::new(RequestTrigger::GossipUnknownParentBlob(1)) else { return; }; - tester .search_parent_dup() .expect_no_blobs_request() .expect_no_block_request(); } + // This test no longer applies, we don't issue requests for child lookups + // Keep for after updating rules on fetching blocks only first + #[ignore] #[test] fn no_peer_penalty_when_rpc_response_already_known_from_gossip() { let Some(mut r) = TestRig::test_setup_after_deneb() else { diff --git a/beacon_node/network/src/sync/manager.rs b/beacon_node/network/src/sync/manager.rs index 7b8d7850a71..ca15d6fe048 100644 --- a/beacon_node/network/src/sync/manager.rs +++ b/beacon_node/network/src/sync/manager.rs @@ -180,6 +180,9 @@ pub enum SyncMessage { id: SamplingId, result: Result<(), String>, }, + + /// A block from gossip has completed processing, + GossipBlockProcessResult { block_root: Hash256, imported: bool }, } /// The type of processing specified for a received block. @@ -704,6 +707,14 @@ impl SyncManager { } => self .block_lookups .on_processing_result(process_type, result, &mut self.network), + SyncMessage::GossipBlockProcessResult { + block_root, + imported, + } => self.block_lookups.on_external_processing_result( + block_root, + imported, + &mut self.network, + ), SyncMessage::BatchProcessed { sync_type, result } => match sync_type { ChainSegmentProcessId::RangeBatchId(chain_id, epoch) => { self.range_sync.handle_block_process_result( diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index bdd6ca241fd..2ea15c23303 100644 --- a/beacon_node/network/src/sync/network_context.rs +++ b/beacon_node/network/src/sync/network_context.rs @@ -3,12 +3,10 @@ pub use self::custody::CustodyId; use self::custody::{ActiveCustodyRequest, CustodyRequester, Error as CustodyRequestError}; -use self::requests::{ - ActiveBlobsByRootRequest, ActiveBlocksByRootRequest, ActiveDataColumnsByRootRequest, -}; -pub use self::requests::{ - BlobsByRootSingleBlockRequest, BlocksByRootSingleRequest, DataColumnsByRootSingleBlockRequest, -}; +use self::requests::ActiveDataColumnsByRootRequest; +pub use self::requests::DataColumnsByRootSingleBlockRequest; +use self::requests::{ActiveBlobsByRootRequest, ActiveBlocksByRootRequest}; +pub use self::requests::{BlobsByRootSingleBlockRequest, BlocksByRootSingleRequest}; use super::block_sidecar_coupling::RangeBlockComponentsRequest; use super::manager::{ BlockProcessType, DataColumnsByRootRequestId, DataColumnsByRootRequester, Id, @@ -109,6 +107,19 @@ impl PeerGroup { } } +pub enum LookupRequestResult { + /// A request is sent. Sync MUST receive an event from the network in the future for either: + /// completed response or failed request + RequestSent, + /// No request is sent, and no further action is necessary to consider this request completed + NoRequestNeeded, + /// No request is sent, but the request is not completed. Sync MUST receive some future event + /// that makes progress on the request. For example: request is processing from a different + /// source (i.e. block received from gossip) and sync MUST receive an event with that processing + /// result. + Pending, +} + /// Wraps a Network channel to employ various RPC related network functionality for the Sync manager. This includes management of a global RPC request Id. pub struct SyncNetworkContext { /// The network channel to relay messages to the Network service. @@ -411,14 +422,27 @@ impl SyncNetworkContext { lookup_id: SingleLookupId, peer_id: PeerId, block_root: Hash256, - ) -> Result { + ) -> Result { + // da_checker includes block that are execution verified, but are missing components + if self + .chain + .data_availability_checker + .has_execution_valid_block(&block_root) + { + return Ok(LookupRequestResult::NoRequestNeeded); + } + + // reqresp_pre_import_cache includes blocks that may not be yet execution verified if self .chain .reqresp_pre_import_cache .read() .contains_key(&block_root) { - return Ok(false); + // A block is on the `reqresp_pre_import_cache` but NOT in the + // `data_availability_checker` only if it is actively processing. We can expect a future + // event with the result of processing + return Ok(LookupRequestResult::Pending); } let id = SingleLookupReqId { @@ -446,7 +470,7 @@ impl SyncNetworkContext { self.blocks_by_root_requests .insert(id, ActiveBlocksByRootRequest::new(request)); - Ok(true) + Ok(LookupRequestResult::RequestSent) } /// Request necessary blobs for `block_root`. Requests only the necessary blobs by checking: @@ -461,7 +485,7 @@ impl SyncNetworkContext { peer_id: PeerId, block_root: Hash256, downloaded_block_expected_blobs: Option, - ) -> Result { + ) -> Result { // Check if we are into deneb, and before peerdas if !self .chain @@ -475,7 +499,7 @@ impl SyncNetworkContext { .epoch(T::EthSpec::slots_per_epoch()), ) { - return Ok(false); + return Ok(LookupRequestResult::NoRequestNeeded); } let expected_blobs = downloaded_block_expected_blobs @@ -501,7 +525,7 @@ impl SyncNetworkContext { if indices.is_empty() { // No blobs required, do not issue any request - return Ok(false); + return Ok(LookupRequestResult::NoRequestNeeded); } let id = SingleLookupReqId { @@ -533,7 +557,7 @@ impl SyncNetworkContext { self.blobs_by_root_requests .insert(id, ActiveBlobsByRootRequest::new(request)); - Ok(true) + Ok(LookupRequestResult::RequestSent) } pub fn data_column_lookup_request( @@ -572,7 +596,7 @@ impl SyncNetworkContext { lookup_id: SingleLookupId, block_root: Hash256, downloaded_block_expected_data: Option, - ) -> Result { + ) -> Result { // Check if we are into peerdas if !self .chain @@ -586,7 +610,7 @@ impl SyncNetworkContext { .epoch(T::EthSpec::slots_per_epoch()), ) { - return Ok(false); + return Ok(LookupRequestResult::NoRequestNeeded); } let expects_data = downloaded_block_expected_data @@ -601,7 +625,7 @@ impl SyncNetworkContext { // No data required for this block if !expects_data { - return Ok(false); + return Ok(LookupRequestResult::NoRequestNeeded); } let custody_indexes_imported = self @@ -622,7 +646,7 @@ impl SyncNetworkContext { if custody_indexes_to_fetch.is_empty() { // No indexes required, do not issue any request - return Ok(false); + return Ok(LookupRequestResult::NoRequestNeeded); } let id = SingleLookupReqId { @@ -654,7 +678,7 @@ impl SyncNetworkContext { // created cannot return data immediately, it must send some request to the network // first. And there must exist some request, `custody_indexes_to_fetch` is not empty. self.custody_by_root_requests.insert(requester, request); - Ok(true) + Ok(LookupRequestResult::RequestSent) } // TODO(das): handle this error properly Err(_) => Err("custody_send_error"), @@ -971,19 +995,19 @@ impl SyncNetworkContext { pub fn send_block_for_processing( &self, + id: Id, block_root: Hash256, block: RpcBlock, duration: Duration, - process_type: BlockProcessType, ) -> Result<(), &'static str> { match self.beacon_processor_if_enabled() { Some(beacon_processor) => { - debug!(self.log, "Sending block for processing"; "block" => ?block_root, "process" => ?process_type); + debug!(self.log, "Sending block for processing"; "block" => ?block_root, "id" => id); if let Err(e) = beacon_processor.send_rpc_beacon_block( block_root, block, duration, - process_type, + BlockProcessType::SingleBlock { id }, ) { error!( self.log, @@ -1004,17 +1028,20 @@ impl SyncNetworkContext { pub fn send_blobs_for_processing( &self, + id: Id, block_root: Hash256, blobs: FixedBlobSidecarList, duration: Duration, - process_type: BlockProcessType, ) -> Result<(), &'static str> { match self.beacon_processor_if_enabled() { Some(beacon_processor) => { - debug!(self.log, "Sending blobs for processing"; "block" => ?block_root, "process_type" => ?process_type); - if let Err(e) = - beacon_processor.send_rpc_blobs(block_root, blobs, duration, process_type) - { + debug!(self.log, "Sending blobs for processing"; "block" => ?block_root, "id" => id); + if let Err(e) = beacon_processor.send_rpc_blobs( + block_root, + blobs, + duration, + BlockProcessType::SingleBlob { id }, + ) { error!( self.log, "Failed to send sync blobs to processor"; From 562e9d04495417a166486d848af851ae8a83e825 Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Wed, 15 May 2024 09:18:45 +0300 Subject: [PATCH 23/76] Implement unconditional reconstruction for supernodes (#5781) * Implement unconditional reconstruction for supernodes * Move code into KzgVerifiedCustodyDataColumn * Remove expect * Add test * Thanks justin --- Cargo.lock | 4 +- Cargo.toml | 2 +- beacon_node/beacon_chain/src/beacon_chain.rs | 66 +++++++++-- .../beacon_chain/src/block_verification.rs | 7 +- .../src/data_availability_checker.rs | 22 +++- .../overflow_lru_cache.rs | 93 +++++++++++++-- .../src/data_column_verification.rs | 25 ++++ beacon_node/beacon_chain/src/kzg_utils.rs | 59 ++++++++++ beacon_node/beacon_chain/src/metrics.rs | 8 ++ beacon_node/http_api/src/publish_blocks.rs | 7 +- .../gossip_methods.rs | 73 ++++++++---- .../network_beacon_processor/sync_methods.rs | 38 +++--- consensus/types/src/data_column_sidecar.rs | 108 +++++++++++++++++- consensus/types/src/data_column_subnet_id.rs | 9 +- crypto/kzg/src/lib.rs | 19 +++ 15 files changed, 460 insertions(+), 80 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ce71a00af37..d1d7d3bf773 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1131,8 +1131,8 @@ dependencies = [ [[package]] name = "c-kzg" -version = "1.0.0" -source = "git+https://github.com/ethereum/c-kzg-4844?branch=das#e08f22ef65a5ba4ea808e0d3a9e845fbd6faea2f" +version = "1.0.2" +source = "git+https://github.com/ethereum/c-kzg-4844?rev=114fa0382990e9b74b1f90f3b0dc5f97c2f8a7ad#114fa0382990e9b74b1f90f3b0dc5f97c2f8a7ad" dependencies = [ "blst", "cc", diff --git a/Cargo.toml b/Cargo.toml index a43db3dfc53..a6f311f58ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -92,7 +92,7 @@ bytes = "1" # TODO(das): switch to c-kzg crate before merging back to unstable (and disable default-features) if possible # Turn off c-kzg's default features which include `blst/portable`. We can turn on blst's portable # feature ourselves when desired. -c-kzg = { git = "https://github.com/ethereum/c-kzg-4844", branch = "das" } +c-kzg = { git = "https://github.com/ethereum/c-kzg-4844", rev = "114fa0382990e9b74b1f90f3b0dc5f97c2f8a7ad" } clap = "2" compare_fields_derive = { path = "common/compare_fields_derive" } criterion = "0.3" diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 0a6a4666d13..69a0b0c6229 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -22,6 +22,7 @@ pub use crate::canonical_head::CanonicalHead; use crate::chain_config::ChainConfig; use crate::data_availability_checker::{ Availability, AvailabilityCheckError, AvailableBlock, DataAvailabilityChecker, + DataColumnsToPublish, }; use crate::data_column_verification::{ CustodyDataColumn, GossipDataColumnError, GossipVerifiedDataColumn, @@ -3028,7 +3029,13 @@ impl BeaconChain { pub async fn process_gossip_data_column( self: &Arc, data_column: GossipVerifiedDataColumn, - ) -> Result> { + ) -> Result< + ( + AvailabilityProcessingStatus, + DataColumnsToPublish, + ), + BlockError, + > { let block_root = data_column.block_root(); // If this block has already been imported to forkchoice it must have been available, so @@ -3044,7 +3051,7 @@ impl BeaconChain { let r = self .check_gossip_data_column_availability_and_import(data_column) .await; - self.remove_notified(&block_root, r) + self.remove_notified_custody_columns(&block_root, r) } /// Cache the blobs in the processing cache, process it, then evict it from the cache if it was @@ -3087,7 +3094,13 @@ impl BeaconChain { self: &Arc, block_root: Hash256, custody_columns: Vec>, - ) -> Result> { + ) -> Result< + ( + AvailabilityProcessingStatus, + DataColumnsToPublish, + ), + BlockError, + > { // If this block has already been imported to forkchoice it must have been available, so // we don't need to process its columns again. if self @@ -3109,7 +3122,7 @@ impl BeaconChain { let r = self .check_rpc_custody_columns_availability_and_import(slot, block_root, custody_columns) .await; - self.remove_notified(&block_root, r) + self.remove_notified_custody_columns(&block_root, r) } /// Remove any block components from the *processing cache* if we no longer require them. If the @@ -3127,6 +3140,23 @@ impl BeaconChain { r } + /// Remove any block components from the *processing cache* if we no longer require them. If the + /// block was imported full or erred, we no longer require them. + fn remove_notified_custody_columns

( + &self, + block_root: &Hash256, + r: Result<(AvailabilityProcessingStatus, P), BlockError>, + ) -> Result<(AvailabilityProcessingStatus, P), BlockError> { + let has_missing_components = matches!( + r, + Ok((AvailabilityProcessingStatus::MissingComponents(_, _), _)) + ); + if !has_missing_components { + self.reqresp_pre_import_cache.write().remove(block_root); + } + r + } + /// Wraps `process_block` in logic to cache the block's commitments in the processing cache /// and evict if the block was imported or errored. pub async fn process_block_with_early_caching>( @@ -3362,16 +3392,24 @@ impl BeaconChain { async fn check_gossip_data_column_availability_and_import( self: &Arc, data_column: GossipVerifiedDataColumn, - ) -> Result> { + ) -> Result< + ( + AvailabilityProcessingStatus, + DataColumnsToPublish, + ), + BlockError, + > { let slot = data_column.slot(); if let Some(slasher) = self.slasher.as_ref() { slasher.accept_block_header(data_column.signed_block_header()); } - let availability = self + let (availability, data_columns_to_publish) = self .data_availability_checker .put_gossip_data_column(data_column)?; - self.process_availability(slot, availability).await + self.process_availability(slot, availability) + .await + .map(|result| (result, data_columns_to_publish)) } /// Checks if the provided blobs can make any cached blocks available, and imports immediately @@ -3419,7 +3457,13 @@ impl BeaconChain { slot: Slot, block_root: Hash256, custody_columns: Vec>, - ) -> Result> { + ) -> Result< + ( + AvailabilityProcessingStatus, + DataColumnsToPublish, + ), + BlockError, + > { // Need to scope this to ensure the lock is dropped before calling `process_availability` // Even an explicit drop is not enough to convince the borrow checker. { @@ -3443,11 +3487,13 @@ impl BeaconChain { } } } - let availability = self + let (availability, data_columns_to_publish) = self .data_availability_checker .put_rpc_custody_columns(block_root, custody_columns)?; - self.process_availability(slot, availability).await + self.process_availability(slot, availability) + .await + .map(|result| (result, data_columns_to_publish)) } /// Imports a fully available block. Otherwise, returns `AvailabilityProcessingStatus::MissingComponents` diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 0a49e107042..39760e860f4 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -799,12 +799,7 @@ fn build_gossip_verified_data_columns( let mut gossip_verified_data_columns = vec![]; for sidecar in sidecars { let subnet = - DataColumnSubnetId::try_from_column_index::(sidecar.index as usize) - .map_err(|_| { - BlockContentsError::::DataColumnSidecarError( - DataColumnSidecarError::DataColumnIndexOutOfBounds, - ) - })?; + DataColumnSubnetId::from_column_index::(sidecar.index as usize); let column = GossipVerifiedDataColumn::new(sidecar, subnet.into(), chain)?; gossip_verified_data_columns.push(column); } diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index 70f72b80122..0ccee82641c 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -31,6 +31,8 @@ pub use error::{Error as AvailabilityCheckError, ErrorCategory as AvailabilityCh use types::data_column_sidecar::{DataColumnIdentifier, DataColumnSidecarList}; use types::non_zero_usize::new_non_zero_usize; +pub use self::overflow_lru_cache::DataColumnsToPublish; + /// The LRU Cache stores `PendingComponents` which can store up to /// `MAX_BLOBS_PER_BLOCK = 6` blobs each. A `BlobSidecar` is 0.131256 MB. So /// the maximum size of a `PendingComponents` is ~ 0.787536 MB. Setting this @@ -187,11 +189,13 @@ impl DataAvailabilityChecker { /// 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)] pub fn put_rpc_custody_columns( &self, block_root: Hash256, custody_columns: Vec>, - ) -> Result, AvailabilityCheckError> { + ) -> Result<(Availability, DataColumnsToPublish), AvailabilityCheckError> + { let Some(kzg) = self.kzg.as_ref() else { return Err(AvailabilityCheckError::KzgNotInitialized); }; @@ -203,8 +207,11 @@ impl DataAvailabilityChecker { .map(|c| KzgVerifiedCustodyDataColumn::new(c, kzg)) .collect::, _>>()?; - self.availability_cache - .put_kzg_verified_data_columns(block_root, verified_custody_columns) + self.availability_cache.put_kzg_verified_data_columns( + kzg, + block_root, + verified_custody_columns, + ) } /// Check if we've cached other blobs for this block. If it completes a set and we also @@ -225,10 +232,15 @@ impl DataAvailabilityChecker { /// Otherwise cache the data column sidecar. /// /// This should only accept gossip verified data columns, so we should not have to worry about dupes. + #[allow(clippy::type_complexity)] pub fn put_gossip_data_column( &self, gossip_data_column: GossipVerifiedDataColumn, - ) -> Result, AvailabilityCheckError> { + ) -> Result<(Availability, DataColumnsToPublish), AvailabilityCheckError> + { + let Some(kzg) = self.kzg.as_ref() else { + return Err(AvailabilityCheckError::KzgNotInitialized); + }; let block_root = gossip_data_column.block_root(); // TODO(das): ensure that our custody requirements include this column @@ -236,7 +248,7 @@ impl DataAvailabilityChecker { KzgVerifiedCustodyDataColumn::from_asserted_custody(gossip_data_column.into_inner()); self.availability_cache - .put_kzg_verified_data_columns(block_root, vec![custody_column]) + .put_kzg_verified_data_columns(kzg, block_root, vec![custody_column]) } /// Check if we have all the blobs for a block. Returns `Availability` which has information diff --git a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs index 62fe3674122..8c7f1a77d51 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs @@ -35,11 +35,13 @@ use crate::block_verification_types::{ }; use crate::data_availability_checker::{Availability, AvailabilityCheckError}; use crate::data_column_verification::{KzgVerifiedCustodyDataColumn, KzgVerifiedDataColumn}; +use crate::metrics; use crate::store::{DBColumn, KeyValueStore}; use crate::BeaconChainTypes; +use kzg::Kzg; use lru::LruCache; use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard}; -use slog::{trace, Logger}; +use slog::{debug, trace, Logger}; use ssz::{Decode, Encode}; use ssz_derive::{Decode, Encode}; use ssz_types::{FixedVector, VariableList}; @@ -49,6 +51,8 @@ use types::blob_sidecar::BlobIdentifier; use types::data_column_sidecar::DataColumnIdentifier; use types::{BlobSidecar, ChainSpec, ColumnIndex, DataColumnSidecar, Epoch, EthSpec, Hash256}; +pub type DataColumnsToPublish = Option>>>; + /// This represents the components of a partially available block /// /// The blobs are all gossip and kzg verified. @@ -230,7 +234,7 @@ impl PendingComponents { /// matches the number of expected blobs / custody columns. pub fn is_available( &self, - block_import_requirement: BlockImportRequirement, + block_import_requirement: &BlockImportRequirement, log: &Logger, ) -> bool { match block_import_requirement { @@ -259,7 +263,7 @@ impl PendingComponents { if let Some(num_expected_blobs) = self.num_expected_blobs() { // No data columns when there are 0 blobs - num_expected_blobs == 0 || num_expected_columns == num_received_data_columns + num_expected_blobs == 0 || *num_expected_columns == num_received_data_columns } else { false } @@ -807,13 +811,16 @@ impl OverflowLRUCache { } } + #[allow(clippy::type_complexity)] pub fn put_kzg_verified_data_columns< I: IntoIterator>, >( &self, + kzg: &Kzg, block_root: Hash256, kzg_verified_data_columns: I, - ) -> Result, AvailabilityCheckError> { + ) -> Result<(Availability, DataColumnsToPublish), AvailabilityCheckError> + { let mut write_lock = self.critical.write(); // Grab existing entry or create a new entry. @@ -825,22 +832,86 @@ impl OverflowLRUCache { pending_components.merge_data_columns(kzg_verified_data_columns)?; let block_import_requirement = self.block_import_requirement(&pending_components)?; - if pending_components.is_available(block_import_requirement, &self.log) { + + // Potentially trigger reconstruction if: + // - Our custody requirement is all columns + // - We >= 50% of columns + let data_columns_to_publish = + if self.should_reconstruct(&block_import_requirement, &pending_components) { + let timer = metrics::start_timer(&metrics::DATA_AVAILABILITY_RECONSTRUCTION_TIME); + + let existing_column_indices = pending_components + .verified_data_columns + .iter() + .map(|d| d.index()) + .collect::>(); + + // Will only return an error if: + // - < 50% of columns + // - There are duplicates + let all_data_columns = KzgVerifiedCustodyDataColumn::reconstruct_columns( + kzg, + &pending_components.verified_data_columns, + )?; + + let data_columns_to_publish = all_data_columns + .iter() + .filter(|d| !existing_column_indices.contains(&d.index())) + .map(|d| d.clone_arc()) + .collect::>(); + + pending_components.verified_data_columns = all_data_columns.into(); + + metrics::stop_timer(timer); + metrics::inc_counter_by( + &metrics::DATA_AVAILABILITY_RECONSTRUCTED_COLUMNS, + data_columns_to_publish.len() as u64, + ); + debug!(self.log, "Reconstructed columns"; "count" => data_columns_to_publish.len()); + + Some(data_columns_to_publish) + } else { + None + }; + + if pending_components.is_available(&block_import_requirement, &self.log) { // No need to hold the write lock anymore drop(write_lock); - pending_components.make_available(&self.spec, |diet_block| { - self.state_cache.recover_pending_executed_block(diet_block) - }) + pending_components + .make_available(&self.spec, |diet_block| { + self.state_cache.recover_pending_executed_block(diet_block) + }) + .map(|availability| (availability, data_columns_to_publish)) } else { write_lock.put_pending_components( block_root, pending_components, &self.overflow_store, )?; - Ok(Availability::MissingComponents(block_root)) + Ok(( + Availability::MissingComponents(block_root), + data_columns_to_publish, + )) } } + /// Potentially trigger reconstruction if: + /// - Our custody requirement is all columns + /// - We >= 50% of columns + fn should_reconstruct( + &self, + block_import_requirement: &BlockImportRequirement, + pending_components: &PendingComponents, + ) -> bool { + let BlockImportRequirement::CustodyColumns(num_expected_columns) = block_import_requirement + else { + return false; + }; + + *num_expected_columns == T::EthSpec::number_of_columns() + && pending_components.verified_data_columns.len() >= T::EthSpec::number_of_columns() / 2 + } + pub fn put_kzg_verified_blobs>>( &self, block_root: Hash256, @@ -865,7 +936,7 @@ impl OverflowLRUCache { pending_components.merge_blobs(fixed_blobs); let block_import_requirement = self.block_import_requirement(&pending_components)?; - if pending_components.is_available(block_import_requirement, &self.log) { + if pending_components.is_available(&block_import_requirement, &self.log) { // No need to hold the write lock anymore drop(write_lock); pending_components.make_available(&self.spec, |diet_block| { @@ -905,7 +976,7 @@ impl OverflowLRUCache { // Check if we have all components and entire set is consistent. let block_import_requirement = self.block_import_requirement(&pending_components)?; - if pending_components.is_available(block_import_requirement, &self.log) { + if pending_components.is_available(&block_import_requirement, &self.log) { // No need to hold the write lock anymore drop(write_lock); pending_components.make_available(&self.spec, |diet_block| { diff --git a/beacon_node/beacon_chain/src/data_column_verification.rs b/beacon_node/beacon_chain/src/data_column_verification.rs index 7951c3dde9e..b872896a4b0 100644 --- a/beacon_node/beacon_chain/src/data_column_verification.rs +++ b/beacon_node/beacon_chain/src/data_column_verification.rs @@ -235,6 +235,31 @@ impl KzgVerifiedCustodyDataColumn { }) } + pub fn reconstruct_columns( + kzg: &Kzg, + partial_set_of_columns: &[Self], + ) -> Result, KzgError> { + // Will only return an error if: + // - < 50% of columns + // - There are duplicates + let all_data_columns = DataColumnSidecar::reconstruct( + kzg, + &partial_set_of_columns + .iter() + .map(|d| d.clone_arc()) + .collect::>(), + )?; + + Ok(all_data_columns + .into_iter() + .map(|d| { + KzgVerifiedCustodyDataColumn::from_asserted_custody(KzgVerifiedDataColumn { + data: d, + }) + }) + .collect::>()) + } + pub fn into_inner(self) -> Arc> { self.data } diff --git a/beacon_node/beacon_chain/src/kzg_utils.rs b/beacon_node/beacon_chain/src/kzg_utils.rs index 69888235365..3f5aa46203e 100644 --- a/beacon_node/beacon_chain/src/kzg_utils.rs +++ b/beacon_node/beacon_chain/src/kzg_utils.rs @@ -128,3 +128,62 @@ pub fn verify_kzg_proof( ) -> Result { kzg.verify_kzg_proof(kzg_commitment, &z.0.into(), &y.0.into(), kzg_proof) } + +#[cfg(test)] +mod test { + use bls::Signature; + use eth2_network_config::TRUSTED_SETUP_BYTES; + use kzg::{Kzg, KzgCommitment, TrustedSetup}; + use types::{ + beacon_block_body::KzgCommitments, BeaconBlock, BeaconBlockDeneb, Blob, BlobsList, + ChainSpec, DataColumnSidecar, EmptyBlock, EthSpec, MainnetEthSpec, SignedBeaconBlock, + }; + + #[test] + fn build_and_reconstruct() { + type E = MainnetEthSpec; + let num_of_blobs = 6; + let spec = E::default_spec(); + let (signed_block, blob_sidecars) = create_test_block_and_blobs::(num_of_blobs, &spec); + + let trusted_setup: TrustedSetup = serde_json::from_reader(TRUSTED_SETUP_BYTES) + .map_err(|e| format!("Unable to read trusted setup file: {}", e)) + .expect("should have trusted setup"); + let kzg = Kzg::new_from_trusted_setup(trusted_setup).expect("should create kzg"); + + let column_sidecars = + DataColumnSidecar::build_sidecars(&blob_sidecars, &signed_block, &kzg).unwrap(); + + // Now reconstruct + let reconstructed_columns = DataColumnSidecar::reconstruct( + &kzg, + &column_sidecars.iter().as_slice()[0..column_sidecars.len() / 2], + ) + .unwrap(); + + for i in 0..E::number_of_columns() { + assert_eq!(reconstructed_columns.get(i), column_sidecars.get(i), "{i}"); + } + } + + fn create_test_block_and_blobs( + num_of_blobs: usize, + spec: &ChainSpec, + ) -> (SignedBeaconBlock, BlobsList) { + let mut block = BeaconBlock::Deneb(BeaconBlockDeneb::empty(spec)); + let mut body = block.body_mut(); + let blob_kzg_commitments = body.blob_kzg_commitments_mut().unwrap(); + *blob_kzg_commitments = + KzgCommitments::::new(vec![KzgCommitment::empty_for_testing(); num_of_blobs]) + .unwrap(); + + let signed_block = SignedBeaconBlock::from_block(block, Signature::empty()); + + let blobs = (0..num_of_blobs) + .map(|_| Blob::::default()) + .collect::>() + .into(); + + (signed_block, blobs) + } +} diff --git a/beacon_node/beacon_chain/src/metrics.rs b/beacon_node/beacon_chain/src/metrics.rs index c4ff0d3bc05..b6eb6e0e4d7 100644 --- a/beacon_node/beacon_chain/src/metrics.rs +++ b/beacon_node/beacon_chain/src/metrics.rs @@ -1180,6 +1180,14 @@ lazy_static! { "data_availability_overflow_store_cache_size", "Number of entries in the data availability overflow store cache." ); + pub static ref DATA_AVAILABILITY_RECONSTRUCTION_TIME: Result = try_create_histogram( + "data_availability_reconstruction_time_seconds", + "Time taken to reconstruct columns" + ); + pub static ref DATA_AVAILABILITY_RECONSTRUCTED_COLUMNS: Result = try_create_int_counter( + "data_availability_reconstructed_columns_total", + "Total count of reconstructed columns" + ); /* * light_client server metrics diff --git a/beacon_node/http_api/src/publish_blocks.rs b/beacon_node/http_api/src/publish_blocks.rs index 896448176a0..8219f97b2a5 100644 --- a/beacon_node/http_api/src/publish_blocks.rs +++ b/beacon_node/http_api/src/publish_blocks.rs @@ -118,12 +118,9 @@ pub async fn publish_block( + let subnet = DataColumnSubnetId::from_column_index::( data_col.index as usize, - ) - .map_err(|e| { - BeaconChainError::UnableToBuildColumnSidecar(format!("{e:?}")) - })?; + ); pubsub_messages.push(PubsubMessage::DataColumnSidecar(Box::new(( subnet, data_col, )))); 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 1a55216efe6..d439f142f31 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -4,7 +4,6 @@ use crate::{ service::NetworkMessage, sync::SyncMessage, }; -use beacon_chain::blob_verification::{GossipBlobError, GossipVerifiedBlob}; use beacon_chain::block_verification_types::AsBlock; use beacon_chain::data_column_verification::GossipVerifiedDataColumn; use beacon_chain::store::Error; @@ -19,7 +18,13 @@ use beacon_chain::{ AvailabilityProcessingStatus, BeaconChainError, BeaconChainTypes, BlockError, ForkChoiceError, GossipVerifiedBlock, NotifyExecutionLayer, }; -use lighthouse_network::{Client, MessageAcceptance, MessageId, PeerAction, PeerId, ReportSource}; +use beacon_chain::{ + blob_verification::{GossipBlobError, GossipVerifiedBlob}, + data_availability_checker::DataColumnsToPublish, +}; +use lighthouse_network::{ + Client, MessageAcceptance, MessageId, PeerAction, PeerId, PubsubMessage, ReportSource, +}; use operation_pool::ReceivedPreCapella; use slog::{crit, debug, error, info, trace, warn, Logger}; use slot_clock::SlotClock; @@ -165,6 +170,24 @@ impl NetworkBeaconProcessor { }) } + pub(crate) fn handle_data_columns_to_publish( + &self, + data_columns_to_publish: DataColumnsToPublish, + ) { + if let Some(data_columns_to_publish) = data_columns_to_publish { + self.send_network_message(NetworkMessage::Publish { + messages: data_columns_to_publish + .iter() + .map(|d| { + let subnet = + DataColumnSubnetId::from_column_index::(d.index as usize); + PubsubMessage::DataColumnSidecar(Box::new((subnet, d.clone()))) + }) + .collect(), + }); + } + } + /// Send a message on `message_tx` that the `message_id` sent by `peer_id` should be propagated on /// the gossip network. /// @@ -890,24 +913,34 @@ impl NetworkBeaconProcessor { .process_gossip_data_column(verified_data_column) .await { - Ok(AvailabilityProcessingStatus::Imported(block_root)) => { - // Note: Reusing block imported metric here - metrics::inc_counter(&metrics::BEACON_PROCESSOR_GOSSIP_BLOCK_IMPORTED_TOTAL); - info!( - self.log, - "Gossipsub data column processed, imported fully available block"; - "block_root" => %block_root - ); - self.chain.recompute_head_at_current_slot().await; - } - Ok(AvailabilityProcessingStatus::MissingComponents(slot, block_root)) => { - trace!( - self.log, - "Processed data column, waiting for other components"; - "slot" => %slot, - "data_column_index" => %data_column_index, - "block_root" => %block_root, - ); + Ok((availability, data_columns_to_publish)) => { + self.handle_data_columns_to_publish(data_columns_to_publish); + + match availability { + AvailabilityProcessingStatus::Imported(block_root) => { + // Note: Reusing block imported metric here + metrics::inc_counter( + &metrics::BEACON_PROCESSOR_GOSSIP_BLOCK_IMPORTED_TOTAL, + ); + info!( + self.log, + "Gossipsub data column processed, imported fully available block"; + "block_root" => %block_root + ); + self.chain.recompute_head_at_current_slot().await; + } + AvailabilityProcessingStatus::MissingComponents(slot, block_root) => { + trace!( + self.log, + "Processed data column, waiting for other components"; + "slot" => %slot, + "data_column_index" => %data_column_index, + "block_root" => %block_root, + ); + + // Potentially trigger reconstruction + } + } } Err(err) => { debug!( 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 2a9b42be38d..a487157caaf 100644 --- a/beacon_node/network/src/network_beacon_processor/sync_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/sync_methods.rs @@ -316,21 +316,27 @@ impl NetworkBeaconProcessor { .await; match &result { - Ok(AvailabilityProcessingStatus::Imported(hash)) => { - debug!( - self.log, - "Block components retrieved"; - "result" => "imported block and custody columns", - "block_hash" => %hash, - ); - self.chain.recompute_head_at_current_slot().await; - } - Ok(AvailabilityProcessingStatus::MissingComponents(_, _)) => { - debug!( - self.log, - "Missing components over rpc"; - "block_hash" => %block_root, - ); + Ok((availability, data_columns_to_publish)) => { + self.handle_data_columns_to_publish(data_columns_to_publish.clone()); + + match availability { + AvailabilityProcessingStatus::Imported(hash) => { + debug!( + self.log, + "Block components retrieved"; + "result" => "imported block and custody columns", + "block_hash" => %hash, + ); + self.chain.recompute_head_at_current_slot().await; + } + AvailabilityProcessingStatus::MissingComponents(_, _) => { + debug!( + self.log, + "Missing components over rpc"; + "block_hash" => %block_root, + ); + } + } } Err(BlockError::BlockIsAlreadyKnown(_)) => { debug!( @@ -351,7 +357,7 @@ impl NetworkBeaconProcessor { self.send_sync_message(SyncMessage::BlockComponentProcessed { process_type, - result: result.into(), + result: result.map(|(r, _)| r).into(), }); } diff --git a/consensus/types/src/data_column_sidecar.rs b/consensus/types/src/data_column_sidecar.rs index 7924a32f7b5..88094b946b2 100644 --- a/consensus/types/src/data_column_sidecar.rs +++ b/consensus/types/src/data_column_sidecar.rs @@ -9,7 +9,7 @@ use bls::Signature; use derivative::Derivative; #[cfg_attr(test, double)] use kzg::Kzg; -use kzg::{Blob as KzgBlob, Error as KzgError}; +use kzg::{Blob as KzgBlob, Cell as KzgCell, Error as KzgError}; use kzg::{KzgCommitment, KzgProof}; use merkle_proof::MerkleTreeError; #[cfg(test)] @@ -169,6 +169,106 @@ impl DataColumnSidecar { Ok(sidecars) } + pub fn reconstruct(kzg: &Kzg, data_columns: &[Arc]) -> Result>, KzgError> { + let mut columns = + vec![Vec::with_capacity(E::max_blobs_per_block()); E::number_of_columns()]; + let mut column_kzg_proofs = + vec![Vec::with_capacity(E::max_blobs_per_block()); E::number_of_columns()]; + + let first_data_column = data_columns + .first() + .ok_or(KzgError::InconsistentArrayLength( + "data_columns should have at least one element".to_string(), + ))?; + let num_of_blobs = first_data_column.kzg_commitments.len(); + + for row_index in 0..num_of_blobs { + let mut cells: Vec = vec![]; + let mut cell_ids: Vec = vec![]; + for data_column in data_columns { + let cell = + data_column + .column + .get(row_index) + .ok_or(KzgError::InconsistentArrayLength(format!( + "Missing data column at index {row_index}" + )))?; + + cells.push(ssz_cell_to_crypto_cell::(cell)?); + cell_ids.push(data_column.index); + } + // recover_all_cells does not expect sorted + let all_cells = kzg.recover_all_cells(&cell_ids, &cells)?; + let blob = kzg.cells_to_blob(&all_cells)?; + + // Note: This function computes all cells and proofs. According to Justin this is okay, + // computing a partial set may be more expensive and requires code paths that don't exist. + // Computing the blobs cells is technically unnecessary but very cheap. It's done here again + // for simplicity. + let (blob_cells, blob_cell_proofs) = kzg.compute_cells_and_proofs(&blob)?; + + // we iterate over each column, and we construct the column from "top to bottom", + // pushing on the cell and the corresponding proof at each column index. we do this for + // each blob (i.e. the outer loop). + for col in 0..E::number_of_columns() { + let cell = blob_cells + .get(col) + .ok_or(KzgError::InconsistentArrayLength(format!( + "Missing blob cell at index {col}" + )))?; + let cell: Vec = cell + .into_inner() + .into_iter() + .flat_map(|data| (*data).into_iter()) + .collect(); + let cell = Cell::::from(cell); + + let proof = blob_cell_proofs + .get(col) + .ok_or(KzgError::InconsistentArrayLength(format!( + "Missing blob cell KZG proof at index {col}" + )))?; + + let column = columns + .get_mut(col) + .ok_or(KzgError::InconsistentArrayLength(format!( + "Missing data column at index {col}" + )))?; + let column_proofs = + column_kzg_proofs + .get_mut(col) + .ok_or(KzgError::InconsistentArrayLength(format!( + "Missing data column proofs at index {col}" + )))?; + + column.push(cell); + column_proofs.push(*proof); + } + } + + // Clone sidecar elements from existing data column, no need to re-compute + let kzg_commitments = &first_data_column.kzg_commitments; + let signed_block_header = &first_data_column.signed_block_header; + let kzg_commitments_inclusion_proof = &first_data_column.kzg_commitments_inclusion_proof; + + let sidecars: Vec>> = columns + .into_iter() + .zip(column_kzg_proofs) + .enumerate() + .map(|(index, (col, proofs))| { + Arc::new(DataColumnSidecar { + index: index as u64, + column: DataColumn::::from(col), + kzg_commitments: kzg_commitments.clone(), + kzg_proofs: KzgProofs::::from(proofs), + signed_block_header: signed_block_header.clone(), + kzg_commitments_inclusion_proof: kzg_commitments_inclusion_proof.clone(), + }) + }) + .collect(); + Ok(sidecars) + } + pub fn min_size() -> usize { // min size is one cell Self { @@ -272,6 +372,12 @@ pub type DataColumnSidecarList = pub type FixedDataColumnSidecarList = FixedVector>>, ::DataColumnCount>; +/// Converts a cell ssz List object to an array to be used with the kzg +/// crypto library. +fn ssz_cell_to_crypto_cell(cell: &Cell) -> Result { + KzgCell::from_bytes(cell.as_ref()).map_err(Into::into) +} + #[cfg(test)] mod test { use super::*; diff --git a/consensus/types/src/data_column_subnet_id.rs b/consensus/types/src/data_column_subnet_id.rs index 79b427ee63f..787a0db332c 100644 --- a/consensus/types/src/data_column_subnet_id.rs +++ b/consensus/types/src/data_column_subnet_id.rs @@ -41,9 +41,12 @@ impl DataColumnSubnetId { id.into() } - pub fn try_from_column_index(column_index: usize) -> Result { - let id = column_index.safe_rem(E::data_column_subnet_count())? as u64; - Ok(id.into()) + pub fn from_column_index(column_index: usize) -> Self { + (column_index + .safe_rem(E::data_column_subnet_count()) + .expect("data_column_subnet_count should never be zero if this function is called") + as u64) + .into() } #[allow(clippy::arithmetic_side_effects)] diff --git a/crypto/kzg/src/lib.rs b/crypto/kzg/src/lib.rs index b7d4c13a7c4..1c4bca05166 100644 --- a/crypto/kzg/src/lib.rs +++ b/crypto/kzg/src/lib.rs @@ -22,6 +22,8 @@ pub enum Error { Kzg(c_kzg::Error), /// The kzg verification failed KzgVerificationFailed, + /// Misc indexing error + InconsistentArrayLength(String), } impl From for Error { @@ -44,6 +46,7 @@ impl Kzg { trusted_setup: KzgSettings::load_trusted_setup( &trusted_setup.g1_points(), &trusted_setup.g2_points(), + 0, )?, }) } @@ -190,6 +193,22 @@ impl Kzg { Ok(()) } } + + pub fn cells_to_blob(&self, cells: &[Cell; c_kzg::CELLS_PER_EXT_BLOB]) -> Result { + Ok(Blob::cells_to_blob(cells)?) + } + + pub fn recover_all_cells( + &self, + cell_ids: &[u64], + cells: &[Cell], + ) -> Result, Error> { + Ok(c_kzg::Cell::recover_all_cells( + cell_ids, + cells, + &self.trusted_setup, + )?) + } } pub mod mock { From 178253a1e27d5b2daf690967ae78b5d82e890dd3 Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Wed, 15 May 2024 14:07:50 +0300 Subject: [PATCH 24/76] Add withhold attack mode for interop (#5788) * Add withhold attack mode * Update readme * Drop added readmes * Undo styling changes --- Cargo.lock | 1 + beacon_node/beacon_chain/src/chain_config.rs | 3 +++ beacon_node/http_api/Cargo.toml | 1 + beacon_node/http_api/src/publish_blocks.rs | 15 +++++++++++++++ beacon_node/src/cli.rs | 6 ++++++ beacon_node/src/config.rs | 6 +++--- book/src/help_bn.md | 5 ++++- lighthouse/tests/beacon_node.rs | 7 +++++++ 8 files changed, 40 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d1d7d3bf773..d041c005187 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3732,6 +3732,7 @@ dependencies = [ "operation_pool", "parking_lot 0.12.2", "proto_array", + "rand", "safe_arith", "sensitive_url", "serde", diff --git a/beacon_node/beacon_chain/src/chain_config.rs b/beacon_node/beacon_chain/src/chain_config.rs index 255b8f00497..b6b42c8dd9b 100644 --- a/beacon_node/beacon_chain/src/chain_config.rs +++ b/beacon_node/beacon_chain/src/chain_config.rs @@ -86,6 +86,8 @@ pub struct ChainConfig { pub epochs_per_migration: u64, /// When set to true Light client server computes and caches state proofs for serving updates pub enable_light_client_server: bool, + /// Enable malicious PeerDAS mode where node withholds data columns when publishing a block + pub malicious_withhold_count: usize, } impl Default for ChainConfig { @@ -118,6 +120,7 @@ impl Default for ChainConfig { always_prepare_payload: false, epochs_per_migration: crate::migrate::DEFAULT_EPOCHS_PER_MIGRATION, enable_light_client_server: false, + malicious_withhold_count: 0, } } } diff --git a/beacon_node/http_api/Cargo.toml b/beacon_node/http_api/Cargo.toml index b58e0442f7c..37f6222a480 100644 --- a/beacon_node/http_api/Cargo.toml +++ b/beacon_node/http_api/Cargo.toml @@ -43,6 +43,7 @@ sensitive_url = { workspace = true } store = { workspace = true } bytes = { workspace = true } beacon_processor = { workspace = true } +rand = { workspace = true } [dev-dependencies] environment = { workspace = true } diff --git a/beacon_node/http_api/src/publish_blocks.rs b/beacon_node/http_api/src/publish_blocks.rs index 8219f97b2a5..29f28c3ba2c 100644 --- a/beacon_node/http_api/src/publish_blocks.rs +++ b/beacon_node/http_api/src/publish_blocks.rs @@ -11,6 +11,7 @@ use eth2::types::{FullPayloadContents, PublishBlockRequest}; use execution_layer::ProvenancedPayload; use lighthouse_network::{NetworkGlobals, PubsubMessage}; use network::NetworkMessage; +use rand::seq::SliceRandom; use slog::{debug, error, info, warn, Logger}; use slot_clock::SlotClock; use std::marker::PhantomData; @@ -71,6 +72,7 @@ pub async fn publish_block block.slot()); + let malicious_withhold_count = chain.config.malicious_withhold_count; /* actually publish a block */ let publish_block = move |block: Arc>, @@ -117,6 +119,19 @@ pub async fn publish_block 0 { + let columns_to_keep = data_col_sidecars + .len() + .saturating_sub(malicious_withhold_count); + // Randomize columns before dropping the last malicious_withhold_count items + data_col_sidecars.shuffle(&mut rand::thread_rng()); + data_col_sidecars = data_col_sidecars + .into_iter() + .take(columns_to_keep) + .collect::>(); + } + for data_col in data_col_sidecars { let subnet = DataColumnSubnetId::from_column_index::( data_col.index as usize, diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index 9c0972d4bbd..d4a733f8ef9 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -46,6 +46,12 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { subscribed to all data column subnets.") .takes_value(false), ) + .arg( + Arg::with_name("malicious-withhold-count") + .long("malicious-withhold-count") + .help("TESTING ONLY do not use this") + .takes_value(true), + ) .arg( Arg::with_name("subscribe-all-subnets") .long("subscribe-all-subnets") diff --git a/beacon_node/src/config.rs b/beacon_node/src/config.rs index 32c279601d2..3211c26d76d 100644 --- a/beacon_node/src/config.rs +++ b/beacon_node/src/config.rs @@ -443,10 +443,10 @@ pub fn get_config( client_config.store.epochs_per_blob_prune = epochs_per_blob_prune; } - if let Some(blob_prune_margin_epochs) = - clap_utils::parse_optional(cli_args, "blob-prune-margin-epochs")? + if let Some(malicious_withhold_count) = + clap_utils::parse_optional(cli_args, "malicious-withhold-count")? { - client_config.store.blob_prune_margin_epochs = blob_prune_margin_epochs; + client_config.chain.malicious_withhold_count = malicious_withhold_count; } /* diff --git a/book/src/help_bn.md b/book/src/help_bn.md index 894369ca9f6..89d037d48ae 100644 --- a/book/src/help_bn.md +++ b/book/src/help_bn.md @@ -259,7 +259,7 @@ OPTIONS: --graffiti Specify your custom graffiti to be included in blocks. Defaults to the current version and commit, truncated - to fit in 32 bytes. + to fit in 32 bytes. --historic-state-cache-size Specifies how many states from the freezer database should cache in memory [default: 1] @@ -324,6 +324,9 @@ OPTIONS: --logfile-max-size The maximum size (in MB) each log file can grow to before rotating. If set to 0, background file logging is disabled. [default: 200] + --malicious-withhold-count + TESTING ONLY do not use this + --max-skip-slots Refuse to skip more than this many slots when processing an attestation. This prevents nodes on minority forks from wasting our time and disk space, but could also cause unnecessary consensus failures, so is diff --git a/lighthouse/tests/beacon_node.rs b/lighthouse/tests/beacon_node.rs index f589da439a6..e814ef77db7 100644 --- a/lighthouse/tests/beacon_node.rs +++ b/lighthouse/tests/beacon_node.rs @@ -1987,6 +1987,13 @@ fn epochs_per_migration_override() { .run_with_zero_port() .with_config(|config| assert_eq!(config.chain.epochs_per_migration, 128)); } +#[test] +fn malicious_withhold_count_flag() { + CommandLineTest::new() + .flag("malicious-withhold-count", Some("128")) + .run_with_zero_port() + .with_config(|config| assert_eq!(config.chain.malicious_withhold_count, 128)); +} // Tests for Slasher flags. // Using `--slasher-max-db-size` to work around https://github.com/sigp/lighthouse/issues/2342 From 4332207f814c7acbd769437da143985841838de2 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Wed, 15 May 2024 14:08:28 +0300 Subject: [PATCH 25/76] Add column gossip verification and handle unknown parent block (#5783) * Add column gossip verification and handle missing parent for columns. * Review PR * Fix rebase issue * more lint issues :) --------- Co-authored-by: dapplion <35266934+dapplion@users.noreply.github.com> --- beacon_node/beacon_chain/src/beacon_chain.rs | 6 +- .../beacon_chain/src/blob_verification.rs | 4 +- .../beacon_chain/src/block_verification.rs | 2 +- beacon_node/beacon_chain/src/builder.rs | 1 + .../src/data_column_verification.rs | 341 ++++++++++++++++-- beacon_node/beacon_chain/src/errors.rs | 2 +- beacon_node/beacon_chain/src/lib.rs | 2 +- beacon_node/beacon_chain/src/metrics.rs | 4 + ..._sidecars.rs => observed_data_sidecars.rs} | 123 ++++--- .../gossip_methods.rs | 83 ++++- .../network/src/sync/block_lookups/mod.rs | 5 +- .../sync/block_lookups/single_block_lookup.rs | 4 +- beacon_node/network/src/sync/manager.rs | 21 ++ .../src/sync/network_context/requests.rs | 2 +- consensus/types/src/data_column_sidecar.rs | 25 +- 15 files changed, 540 insertions(+), 85 deletions(-) rename beacon_node/beacon_chain/src/{observed_blob_sidecars.rs => observed_data_sidecars.rs} (78%) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 69a0b0c6229..7353fda794b 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -55,8 +55,8 @@ use crate::observed_aggregates::{ use crate::observed_attesters::{ ObservedAggregators, ObservedAttesters, ObservedSyncAggregators, ObservedSyncContributors, }; -use crate::observed_blob_sidecars::ObservedBlobSidecars; use crate::observed_block_producers::ObservedBlockProducers; +use crate::observed_data_sidecars::ObservedDataSidecars; use crate::observed_operations::{ObservationOutcome, ObservedOperations}; use crate::observed_slashable::ObservedSlashable; use crate::persisted_beacon_chain::{PersistedBeaconChain, DUMMY_CANONICAL_HEAD_BLOCK_ROOT}; @@ -415,7 +415,9 @@ pub struct BeaconChain { /// Maintains a record of which validators have proposed blocks for each slot. pub observed_block_producers: RwLock>, /// Maintains a record of blob sidecars seen over the gossip network. - pub observed_blob_sidecars: RwLock>, + pub observed_blob_sidecars: RwLock>>, + /// Maintains a record of column sidecars seen over the gossip network. + pub observed_column_sidecars: RwLock>>, /// Maintains a record of slashable message seen over the gossip network or RPC. pub observed_slashable: RwLock>, /// Maintains a record of which validators have submitted voluntary exits. diff --git a/beacon_node/beacon_chain/src/blob_verification.rs b/beacon_node/beacon_chain/src/blob_verification.rs index ba875867a00..a15ebef897f 100644 --- a/beacon_node/beacon_chain/src/blob_verification.rs +++ b/beacon_node/beacon_chain/src/blob_verification.rs @@ -417,8 +417,8 @@ pub fn validate_blob_sidecar_for_gossip( // Verify that the blob_sidecar was received on the correct subnet. if blob_index != subnet { return Err(GossipBlobError::InvalidSubnet { - expected: blob_index, - received: subnet, + expected: subnet, + received: blob_index, }); } diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 39760e860f4..dc989595f63 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -2075,7 +2075,7 @@ impl BlockBlobError for GossipBlobError { impl BlockBlobError for GossipDataColumnError { fn not_later_than_parent_error(data_column_slot: Slot, parent_slot: Slot) -> Self { - GossipDataColumnError::DataColumnIsNotLaterThanParent { + GossipDataColumnError::IsNotLaterThanParent { data_column_slot, parent_slot, } diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index 2d2b9c9a124..d5b408af526 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -936,6 +936,7 @@ where observed_sync_aggregators: <_>::default(), // TODO: allow for persisting and loading the pool from disk. observed_block_producers: <_>::default(), + observed_column_sidecars: <_>::default(), observed_blob_sidecars: <_>::default(), observed_slashable: <_>::default(), observed_voluntary_exits: <_>::default(), diff --git a/beacon_node/beacon_chain/src/data_column_verification.rs b/beacon_node/beacon_chain/src/data_column_verification.rs index b872896a4b0..fa43fa170eb 100644 --- a/beacon_node/beacon_chain/src/data_column_verification.rs +++ b/beacon_node/beacon_chain/src/data_column_verification.rs @@ -1,15 +1,23 @@ -use crate::block_verification::{process_block_slash_info, BlockSlashInfo}; +use crate::block_verification::{ + cheap_state_advance_to_obtain_committees, get_validator_pubkey_cache, process_block_slash_info, + BlockSlashInfo, +}; use crate::kzg_utils::validate_data_column; use crate::{metrics, BeaconChain, BeaconChainError, BeaconChainTypes}; use derivative::Derivative; +use fork_choice::ProtoBlock; use kzg::{Error as KzgError, Kzg}; +use proto_array::Block; +use slasher::test_utils::E; +use slog::debug; +use slot_clock::SlotClock; use ssz_derive::{Decode, Encode}; use std::iter; use std::sync::Arc; use types::data_column_sidecar::{ColumnIndex, DataColumnIdentifier}; use types::{ - BeaconStateError, DataColumnSidecar, EthSpec, Hash256, SignedBeaconBlockHeader, Slot, - VariableList, + BeaconStateError, DataColumnSidecar, DataColumnSubnetId, EthSpec, Hash256, + SignedBeaconBlockHeader, Slot, VariableList, }; /// An error occurred while validating a gossip data column. @@ -23,51 +31,108 @@ pub enum GossipDataColumnError { /// We were unable to process this data column due to an internal error. It's /// unclear if the data column is valid. BeaconChainError(BeaconChainError), - /// The proposal signature in invalid. /// /// ## Peer scoring /// /// The data column is invalid and the peer is faulty. ProposalSignatureInvalid, - /// The proposal_index corresponding to data column.beacon_block_root is not known. /// /// ## Peer scoring /// /// The data column is invalid and the peer is faulty. UnknownValidator(u64), - /// The provided data column is not from a later slot than its parent. /// /// ## Peer scoring /// /// The data column is invalid and the peer is faulty. - DataColumnIsNotLaterThanParent { + IsNotLaterThanParent { data_column_slot: Slot, parent_slot: Slot, }, - /// `Kzg` struct hasn't been initialized. This is an internal error. /// /// ## Peer scoring /// /// The peer isn't faulty, This is an internal error. KzgNotInitialized, - /// The kzg verification failed. /// /// ## Peer scoring /// /// The data column sidecar is invalid and the peer is faulty. - KzgError(kzg::Error), - - /// The provided data column's parent block is unknown. + InvalidKzgProof(kzg::Error), + /// The column was gossiped over an incorrect subnet. + /// + /// ## Peer scoring + /// + /// The column is invalid or the peer is faulty. + InvalidSubnetId { received: u64, expected: u64 }, + /// The column sidecar is from a slot that is later than the current slot (with respect to the + /// gossip clock disparity). + /// + /// ## Peer scoring + /// + /// Assuming the local clock is correct, the peer has sent an invalid message. + FutureSlot { + message_slot: Slot, + latest_permissible_slot: Slot, + }, + /// The sidecar corresponds to a slot older than the finalized head slot. + /// + /// ## Peer scoring + /// + /// It's unclear if this column is valid, but this column is for a finalized slot and is + /// therefore useless to us. + PastFinalizedSlot { + column_slot: Slot, + finalized_slot: Slot, + }, + /// The pubkey cache timed out. + /// + /// ## Peer scoring + /// + /// The column sidecar may be valid, this is an internal error. + PubkeyCacheTimeout, + /// The proposer index specified in the sidecar does not match the locally computed + /// proposer index. /// /// ## Peer scoring /// - /// We cannot process the data column without validating its parent, the peer isn't necessarily faulty. - DataColumnParentUnknown(Arc>), + /// The column is invalid and the peer is faulty. + ProposerIndexMismatch { sidecar: usize, local: usize }, + /// The provided columns's parent block is unknown. + /// + /// ## Peer scoring + /// + /// We cannot process the columns without validating its parent, the peer isn't necessarily faulty. + ParentUnknown(Arc>), + /// The column conflicts with finalization, no need to propagate. + /// + /// ## Peer scoring + /// + /// It's unclear if this column is valid, but it conflicts with finality and shouldn't be + /// imported. + NotFinalizedDescendant { block_parent_root: Hash256 }, + /// Invalid kzg commitment inclusion proof + /// + /// ## Peer scoring + /// + /// The column sidecar is invalid and the peer is faulty + InvalidInclusionProof, + /// A column has already been seen for the given `(sidecar.block_root, sidecar.index)` tuple + /// over gossip or no gossip sources. + /// + /// ## Peer scoring + /// + /// The peer isn't faulty, but we do not forward it over gossip. + PriorKnown { + proposer: u64, + slot: Slot, + index: ColumnIndex, + }, } impl From for GossipDataColumnError { @@ -306,22 +371,254 @@ where pub fn validate_data_column_sidecar_for_gossip( data_column: Arc>, - _subnet: u64, + subnet: u64, chain: &BeaconChain, ) -> Result, GossipDataColumnError> { - // TODO(das): validate gossip rules - let block_root = data_column.block_root(); + let column_slot = data_column.slot(); + + verify_index_matches_subnet(&data_column, subnet)?; + verify_sidecar_not_from_future_slot(chain, column_slot)?; + verify_slot_greater_than_latest_finalized_slot(chain, column_slot)?; + verify_is_first_sidecar(chain, &data_column)?; + verify_column_inclusion_proof(&data_column)?; + let parent_block = verify_parent_block_and_finalized_descendant(data_column.clone(), chain)?; + verify_slot_higher_than_parent(&parent_block, column_slot)?; + verify_proposer_and_signature(&data_column, &parent_block, chain)?; - // Kzg verification for gossip data column sidecar let kzg = chain .kzg - .as_ref() + .clone() .ok_or(GossipDataColumnError::KzgNotInitialized)?; - let kzg_verified_data_column = - KzgVerifiedDataColumn::new(data_column, kzg).map_err(GossipDataColumnError::KzgError)?; + let kzg_verified_data_column = verify_kzg_for_data_column(data_column.clone(), &kzg) + .map_err(GossipDataColumnError::InvalidKzgProof)?; + + chain + .observed_slashable + .write() + .observe_slashable( + column_slot, + data_column.block_proposer_index(), + data_column.block_root(), + ) + .map_err(|e| GossipDataColumnError::BeaconChainError(e.into()))?; Ok(GossipVerifiedDataColumn { - block_root, + block_root: data_column.block_root(), data_column: kzg_verified_data_column, }) } + +// Verify that this is the first column sidecar received for the tuple: +// (block_header.slot, block_header.proposer_index, column_sidecar.index) +fn verify_is_first_sidecar( + chain: &BeaconChain, + data_column: &DataColumnSidecar, +) -> Result<(), GossipDataColumnError> { + if chain + .observed_column_sidecars + .read() + .proposer_is_known(data_column) + .map_err(|e| GossipDataColumnError::BeaconChainError(e.into()))? + { + return Err(GossipDataColumnError::PriorKnown { + proposer: data_column.block_proposer_index(), + slot: data_column.slot(), + index: data_column.index, + }); + } + Ok(()) +} + +fn verify_column_inclusion_proof( + data_column: &DataColumnSidecar, +) -> Result<(), GossipDataColumnError> { + let _timer = metrics::start_timer(&metrics::DATA_COLUMN_SIDECAR_INCLUSION_PROOF_VERIFICATION); + if !data_column.verify_inclusion_proof() { + return Err(GossipDataColumnError::InvalidInclusionProof); + } + + Ok(()) +} + +fn verify_slot_higher_than_parent( + parent_block: &Block, + data_column_slot: Slot, +) -> Result<(), GossipDataColumnError> { + if parent_block.slot >= data_column_slot { + return Err(GossipDataColumnError::IsNotLaterThanParent { + data_column_slot, + parent_slot: parent_block.slot, + }); + } + Ok(()) +} + +fn verify_parent_block_and_finalized_descendant( + data_column: Arc>, + chain: &BeaconChain, +) -> Result> { + let fork_choice = chain.canonical_head.fork_choice_read_lock(); + + // We have already verified that the column is past finalization, so we can + // just check fork choice for the block's parent. + let block_parent_root = data_column.block_parent_root(); + let Some(parent_block) = fork_choice.get_block(&block_parent_root) else { + return Err(GossipDataColumnError::ParentUnknown(data_column.clone())); + }; + + // Do not process a column that does not descend from the finalized root. + // We just loaded the parent_block, so we can be sure that it exists in fork choice. + if !fork_choice.is_finalized_checkpoint_or_descendant(block_parent_root) { + return Err(GossipDataColumnError::NotFinalizedDescendant { block_parent_root }); + } + + Ok(parent_block) +} + +fn verify_proposer_and_signature( + data_column: &DataColumnSidecar, + parent_block: &ProtoBlock, + chain: &BeaconChain, +) -> Result<(), GossipDataColumnError> { + let column_slot = data_column.slot(); + let column_epoch = column_slot.epoch(E::slots_per_epoch()); + let column_index = data_column.index; + let block_root = data_column.block_root(); + let block_parent_root = data_column.block_parent_root(); + + let proposer_shuffling_root = + if parent_block.slot.epoch(T::EthSpec::slots_per_epoch()) == column_epoch { + parent_block + .next_epoch_shuffling_id + .shuffling_decision_block + } else { + parent_block.root + }; + + let proposer_opt = chain + .beacon_proposer_cache + .lock() + .get_slot::(proposer_shuffling_root, column_slot); + + let (proposer_index, fork) = if let Some(proposer) = proposer_opt { + (proposer.index, proposer.fork) + } else { + debug!( + chain.log, + "Proposer shuffling cache miss for column verification"; + "block_root" => %block_root, + "index" => %column_index, + ); + let (parent_state_root, mut parent_state) = chain + .store + .get_advanced_hot_state(block_parent_root, column_slot, parent_block.state_root) + .map_err(|e| GossipDataColumnError::BeaconChainError(e.into()))? + .ok_or_else(|| { + BeaconChainError::DBInconsistent(format!( + "Missing state for parent block {block_parent_root:?}", + )) + })?; + + let state = cheap_state_advance_to_obtain_committees::<_, GossipDataColumnError>( + &mut parent_state, + Some(parent_state_root), + column_slot, + &chain.spec, + )?; + + let proposers = state.get_beacon_proposer_indices(&chain.spec)?; + let proposer_index = *proposers + .get(column_slot.as_usize() % T::EthSpec::slots_per_epoch() as usize) + .ok_or_else(|| BeaconChainError::NoProposerForSlot(column_slot))?; + + // Prime the proposer shuffling cache with the newly-learned value. + chain.beacon_proposer_cache.lock().insert( + column_epoch, + proposer_shuffling_root, + proposers, + state.fork(), + )?; + (proposer_index, state.fork()) + }; + + // Signature verify the signed block header. + let signature_is_valid = { + let pubkey_cache = get_validator_pubkey_cache(chain) + .map_err(|_| GossipDataColumnError::PubkeyCacheTimeout)?; + + let pubkey = pubkey_cache + .get(proposer_index) + .ok_or_else(|| GossipDataColumnError::UnknownValidator(proposer_index as u64))?; + let signed_block_header = &data_column.signed_block_header; + signed_block_header.verify_signature::( + pubkey, + &fork, + chain.genesis_validators_root, + &chain.spec, + ) + }; + + if !signature_is_valid { + return Err(GossipDataColumnError::ProposalSignatureInvalid); + } + + let column_proposer_index = data_column.block_proposer_index(); + if proposer_index != column_proposer_index as usize { + return Err(GossipDataColumnError::ProposerIndexMismatch { + sidecar: column_proposer_index as usize, + local: proposer_index, + }); + } + + Ok(()) +} + +fn verify_index_matches_subnet( + data_column: &DataColumnSidecar, + subnet: u64, +) -> Result<(), GossipDataColumnError> { + let expected_subnet: u64 = + DataColumnSubnetId::from_column_index::(data_column.index as usize).into(); + if expected_subnet != subnet { + return Err(GossipDataColumnError::InvalidSubnetId { + received: subnet, + expected: expected_subnet, + }); + } + Ok(()) +} + +fn verify_slot_greater_than_latest_finalized_slot( + chain: &BeaconChain, + column_slot: Slot, +) -> Result<(), GossipDataColumnError> { + let latest_finalized_slot = chain + .head() + .finalized_checkpoint() + .epoch + .start_slot(T::EthSpec::slots_per_epoch()); + if column_slot <= latest_finalized_slot { + return Err(GossipDataColumnError::PastFinalizedSlot { + column_slot, + finalized_slot: latest_finalized_slot, + }); + } + Ok(()) +} + +fn verify_sidecar_not_from_future_slot( + chain: &BeaconChain, + column_slot: Slot, +) -> Result<(), GossipDataColumnError> { + let latest_permissible_slot = chain + .slot_clock + .now_with_future_tolerance(chain.spec.maximum_gossip_clock_disparity()) + .ok_or(BeaconChainError::UnableToReadSlot)?; + if column_slot > latest_permissible_slot { + return Err(GossipDataColumnError::FutureSlot { + message_slot: column_slot, + latest_permissible_slot, + }); + } + Ok(()) +} diff --git a/beacon_node/beacon_chain/src/errors.rs b/beacon_node/beacon_chain/src/errors.rs index 22a823fd324..ddca76ed6b8 100644 --- a/beacon_node/beacon_chain/src/errors.rs +++ b/beacon_node/beacon_chain/src/errors.rs @@ -9,8 +9,8 @@ use crate::migrate::PruningError; use crate::naive_aggregation_pool::Error as NaiveAggregationError; use crate::observed_aggregates::Error as ObservedAttestationsError; use crate::observed_attesters::Error as ObservedAttestersError; -use crate::observed_blob_sidecars::Error as ObservedBlobSidecarsError; use crate::observed_block_producers::Error as ObservedBlockProducersError; +use crate::observed_data_sidecars::Error as ObservedBlobSidecarsError; use execution_layer::PayloadStatus; use fork_choice::ExecutionStatus; use futures::channel::mpsc::TrySendError; diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index 2fcde392fdc..b9b4afd3a4c 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -42,8 +42,8 @@ pub mod migrate; mod naive_aggregation_pool; mod observed_aggregates; mod observed_attesters; -mod observed_blob_sidecars; pub mod observed_block_producers; +mod observed_data_sidecars; pub mod observed_operations; mod observed_slashable; pub mod otb_verification_service; diff --git a/beacon_node/beacon_chain/src/metrics.rs b/beacon_node/beacon_chain/src/metrics.rs index b6eb6e0e4d7..49bf9e9db47 100644 --- a/beacon_node/beacon_chain/src/metrics.rs +++ b/beacon_node/beacon_chain/src/metrics.rs @@ -1061,6 +1061,10 @@ lazy_static! { "data_column_sidecar_computation_seconds", "Time taken to compute data column sidecar, including cells, proofs and inclusion proof" ); + pub static ref DATA_COLUMN_SIDECAR_INCLUSION_PROOF_VERIFICATION: Result = try_create_histogram( + "data_column_sidecar_inclusion_proof_verification_seconds", + "Time taken to verify data_column sidecar inclusion proof" + ); } // Fifth lazy-static block is used to account for macro recursion limit. diff --git a/beacon_node/beacon_chain/src/observed_blob_sidecars.rs b/beacon_node/beacon_chain/src/observed_data_sidecars.rs similarity index 78% rename from beacon_node/beacon_chain/src/observed_blob_sidecars.rs rename to beacon_node/beacon_chain/src/observed_data_sidecars.rs index 7d7f490ebb9..9485ef47692 100644 --- a/beacon_node/beacon_chain/src/observed_blob_sidecars.rs +++ b/beacon_node/beacon_chain/src/observed_data_sidecars.rs @@ -6,20 +6,63 @@ use crate::observed_block_producers::ProposalKey; use std::collections::{HashMap, HashSet}; use std::marker::PhantomData; -use types::{BlobSidecar, EthSpec, Slot}; +use types::{BlobSidecar, DataColumnSidecar, EthSpec, Slot}; #[derive(Debug, PartialEq)] pub enum Error { - /// The slot of the provided `BlobSidecar` is prior to finalization and should not have been provided + /// The slot of the provided `ObservableSidecar` is prior to finalization and should not have been provided /// to this function. This is an internal error. - FinalizedBlob { slot: Slot, finalized_slot: Slot }, - /// The blob sidecar contains an invalid blob index, the blob sidecar is invalid. - /// Note: The invalid blob should have been caught and flagged as an error much before reaching + FinalizedDataSidecar { slot: Slot, finalized_slot: Slot }, + /// The data sidecar contains an invalid index, the data sidecar is invalid. + /// Note: The invalid data should have been caught and flagged as an error much before reaching /// here. - InvalidBlobIndex(u64), + InvalidDataIndex(u64), } -/// Maintains a cache of seen `BlobSidecar`s that are received over gossip +pub trait ObservableDataSidecar { + fn slot(&self) -> Slot; + fn block_proposer_index(&self) -> u64; + fn index(&self) -> u64; + fn max_num_of_items() -> usize; +} + +impl ObservableDataSidecar for BlobSidecar { + fn slot(&self) -> Slot { + self.slot() + } + + fn block_proposer_index(&self) -> u64 { + self.block_proposer_index() + } + + fn index(&self) -> u64 { + self.index + } + + fn max_num_of_items() -> usize { + E::max_blobs_per_block() + } +} + +impl ObservableDataSidecar for DataColumnSidecar { + fn slot(&self) -> Slot { + self.slot() + } + + fn block_proposer_index(&self) -> u64 { + self.block_proposer_index() + } + + fn index(&self) -> u64 { + self.index + } + + fn max_num_of_items() -> usize { + E::number_of_columns() + } +} + +/// Maintains a cache of seen `ObservableSidecar`s that are received over gossip /// and have been gossip verified. /// /// The cache supports pruning based upon the finalized epoch. It does not automatically prune, you @@ -27,14 +70,14 @@ pub enum Error { /// /// Note: To prevent DoS attacks, this cache must include only items that have received some DoS resistance /// like checking the proposer signature. -pub struct ObservedBlobSidecars { +pub struct ObservedDataSidecars { finalized_slot: Slot, - /// Stores all received blob indices for a given `(ValidatorIndex, Slot)` tuple. + /// Stores all received data indices for a given `(ValidatorIndex, Slot)` tuple. items: HashMap>, - _phantom: PhantomData, + _phantom: PhantomData, } -impl Default for ObservedBlobSidecars { +impl Default for ObservedDataSidecars { /// Instantiates `Self` with `finalized_slot == 0`. fn default() -> Self { Self { @@ -45,49 +88,47 @@ impl Default for ObservedBlobSidecars { } } -impl ObservedBlobSidecars { - /// Observe the `blob_sidecar` at (`blob_sidecar.block_proposer_index, blob_sidecar.slot`). - /// This will update `self` so future calls to it indicate that this `blob_sidecar` is known. +impl ObservedDataSidecars { + /// Observe the `data_sidecar` at (`data_sidecar.block_proposer_index, data_sidecar.slot`). + /// This will update `self` so future calls to it indicate that this `data_sidecar` is known. /// - /// The supplied `blob_sidecar` **MUST** have completed proposer signature verification. - pub fn observe_sidecar(&mut self, blob_sidecar: &BlobSidecar) -> Result { - self.sanitize_blob_sidecar(blob_sidecar)?; + /// The supplied `data_sidecar` **MUST** have completed proposer signature verification. + pub fn observe_sidecar(&mut self, data_sidecar: &T) -> Result { + self.sanitize_data_sidecar(data_sidecar)?; - let blob_indices = self + let data_indices = self .items .entry(ProposalKey { - slot: blob_sidecar.slot(), - proposer: blob_sidecar.block_proposer_index(), + slot: data_sidecar.slot(), + proposer: data_sidecar.block_proposer_index(), }) - .or_insert_with(|| HashSet::with_capacity(E::max_blobs_per_block())); - let did_not_exist = blob_indices.insert(blob_sidecar.index); + .or_insert_with(|| HashSet::with_capacity(T::max_num_of_items())); + let did_not_exist = data_indices.insert(data_sidecar.index()); Ok(!did_not_exist) } - /// Returns `true` if the `blob_sidecar` has already been observed in the cache within the prune window. - pub fn proposer_is_known(&self, blob_sidecar: &BlobSidecar) -> Result { - self.sanitize_blob_sidecar(blob_sidecar)?; + /// Returns `true` if the `data_sidecar` has already been observed in the cache within the prune window. + pub fn proposer_is_known(&self, data_sidecar: &T) -> Result { + self.sanitize_data_sidecar(data_sidecar)?; let is_known = self .items .get(&ProposalKey { - slot: blob_sidecar.slot(), - proposer: blob_sidecar.block_proposer_index(), + slot: data_sidecar.slot(), + proposer: data_sidecar.block_proposer_index(), }) - .map_or(false, |blob_indices| { - blob_indices.contains(&blob_sidecar.index) - }); + .map_or(false, |indices| indices.contains(&data_sidecar.index())); Ok(is_known) } - fn sanitize_blob_sidecar(&self, blob_sidecar: &BlobSidecar) -> Result<(), Error> { - if blob_sidecar.index >= E::max_blobs_per_block() as u64 { - return Err(Error::InvalidBlobIndex(blob_sidecar.index)); + fn sanitize_data_sidecar(&self, data_sidecar: &T) -> Result<(), Error> { + if data_sidecar.index() >= T::max_num_of_items() as u64 { + return Err(Error::InvalidDataIndex(data_sidecar.index())); } let finalized_slot = self.finalized_slot; - if finalized_slot > 0 && blob_sidecar.slot() <= finalized_slot { - return Err(Error::FinalizedBlob { - slot: blob_sidecar.slot(), + if finalized_slot > 0 && data_sidecar.slot() <= finalized_slot { + return Err(Error::FinalizedDataSidecar { + slot: data_sidecar.slot(), finalized_slot, }); } @@ -95,7 +136,7 @@ impl ObservedBlobSidecars { Ok(()) } - /// Prune `blob_sidecar` observations for slots less than or equal to the given slot. + /// Prune `data_sidecar` observations for slots less than or equal to the given slot. pub fn prune(&mut self, finalized_slot: Slot) { if finalized_slot == 0 { return; @@ -125,7 +166,7 @@ mod tests { #[test] fn pruning() { - let mut cache = ObservedBlobSidecars::default(); + let mut cache = ObservedDataSidecars::>::default(); assert_eq!(cache.finalized_slot, 0, "finalized slot is zero"); assert_eq!(cache.items.len(), 0, "no slots should be present"); @@ -200,7 +241,7 @@ mod tests { assert_eq!( cache.observe_sidecar(&block_b), - Err(Error::FinalizedBlob { + Err(Error::FinalizedDataSidecar { slot: E::slots_per_epoch().into(), finalized_slot: E::slots_per_epoch().into(), }), @@ -263,7 +304,7 @@ mod tests { #[test] fn simple_observations() { - let mut cache = ObservedBlobSidecars::default(); + let mut cache = ObservedDataSidecars::>::default(); // Slot 0, index 0 let proposer_index_a = 420; @@ -423,7 +464,7 @@ mod tests { let sidecar_d = get_blob_sidecar(0, proposer_index_a, invalid_index); assert_eq!( cache.observe_sidecar(&sidecar_d), - Err(Error::InvalidBlobIndex(invalid_index)), + Err(Error::InvalidDataIndex(invalid_index)), "cannot add an index > MaxBlobsPerBlock" ); } 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 d439f142f31..8007ad25735 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -5,7 +5,7 @@ use crate::{ sync::SyncMessage, }; use beacon_chain::block_verification_types::AsBlock; -use beacon_chain::data_column_verification::GossipVerifiedDataColumn; +use beacon_chain::data_column_verification::{GossipDataColumnError, GossipVerifiedDataColumn}; use beacon_chain::store::Error; use beacon_chain::{ attestation_verification::{self, Error as AttnError, VerifiedAttestation}, @@ -683,11 +683,82 @@ impl NetworkBeaconProcessor { .await } Err(err) => { - error!( - self.log, - "Internal error when verifying data column sidecar"; - "error" => ?err, - ) + match err { + GossipDataColumnError::ParentUnknown(column) => { + debug!( + self.log, + "Unknown parent hash for column"; + "action" => "requesting parent", + "block_root" => %column.block_root(), + "parent_root" => %column.block_parent_root(), + ); + self.send_sync_message(SyncMessage::UnknownParentDataColumn( + peer_id, column, + )); + } + GossipDataColumnError::KzgNotInitialized + | GossipDataColumnError::PubkeyCacheTimeout + | GossipDataColumnError::BeaconChainError(_) => { + crit!( + self.log, + "Internal error when verifying column sidecar"; + "error" => ?err, + ) + } + GossipDataColumnError::ProposalSignatureInvalid + | GossipDataColumnError::UnknownValidator(_) + | GossipDataColumnError::ProposerIndexMismatch { .. } + | GossipDataColumnError::IsNotLaterThanParent { .. } + | GossipDataColumnError::InvalidSubnetId { .. } + | GossipDataColumnError::InvalidInclusionProof { .. } + | GossipDataColumnError::InvalidKzgProof { .. } + | GossipDataColumnError::NotFinalizedDescendant { .. } => { + // TODO(das): downgrade log to debug after interop + warn!( + self.log, + "Could not verify column sidecar for gossip. Rejecting the column sidecar"; + "error" => ?err, + "slot" => %slot, + "block_root" => %block_root, + "index" => %index, + ); + // Prevent recurring behaviour by penalizing the peer slightly. + self.gossip_penalize_peer( + peer_id, + PeerAction::LowToleranceError, + "gossip_data_column_low", + ); + self.propagate_validation_result( + message_id, + peer_id, + MessageAcceptance::Reject, + ); + } + GossipDataColumnError::FutureSlot { .. } + | GossipDataColumnError::PriorKnown { .. } + | GossipDataColumnError::PastFinalizedSlot { .. } => { + // TODO(das): downgrade log to debug after interop + warn!( + self.log, + "Could not verify column sidecar for gossip. Ignoring the column sidecar"; + "error" => ?err, + "slot" => %slot, + "block_root" => %block_root, + "index" => %index, + ); + // Prevent recurring behaviour by penalizing the peer slightly. + self.gossip_penalize_peer( + peer_id, + PeerAction::HighToleranceError, + "gossip_data_column_high", + ); + self.propagate_validation_result( + message_id, + peer_id, + MessageAcceptance::Ignore, + ); + } + } } } } diff --git a/beacon_node/network/src/sync/block_lookups/mod.rs b/beacon_node/network/src/sync/block_lookups/mod.rs index f298dc1e4bb..44c9ab86b3b 100644 --- a/beacon_node/network/src/sync/block_lookups/mod.rs +++ b/beacon_node/network/src/sync/block_lookups/mod.rs @@ -20,7 +20,7 @@ use std::collections::hash_map::Entry; use std::sync::Arc; use std::time::Duration; use store::Hash256; -use types::{BlobSidecar, EthSpec, SignedBeaconBlock}; +use types::{BlobSidecar, DataColumnSidecar, EthSpec, SignedBeaconBlock}; pub mod common; pub mod parent_chain; @@ -34,6 +34,7 @@ pub const SINGLE_BLOCK_LOOKUP_MAX_ATTEMPTS: u8 = 4; pub enum BlockComponent { Block(DownloadResult>>), Blob(DownloadResult>>), + DataColumn(DownloadResult>>), } impl BlockComponent { @@ -41,12 +42,14 @@ impl BlockComponent { match self { BlockComponent::Block(block) => block.value.parent_root(), BlockComponent::Blob(blob) => blob.value.block_parent_root(), + BlockComponent::DataColumn(column) => column.value.block_parent_root(), } } fn get_type(&self) -> &'static str { match self { BlockComponent::Block(_) => "block", BlockComponent::Blob(_) => "blob", + BlockComponent::DataColumn(_) => "data_column", } } } 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 5d6b329312a..c33ad6fecf7 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 @@ -107,8 +107,8 @@ impl SingleBlockLookup { .block_request_state .state .insert_verified_response(block), - BlockComponent::Blob(_) => { - // For now ignore single blobs, as the blob request state assumes all blobs are + BlockComponent::Blob(_) | BlockComponent::DataColumn(_) => { + // For now ignore single blobs and columns, as the blob request state assumes all blobs are // attributed to the same peer = the peer serving the remaining blobs. Ignoring this // block component has a minor effect, causing the node to re-request this blob // once the parent chain is successfully resolved diff --git a/beacon_node/network/src/sync/manager.rs b/beacon_node/network/src/sync/manager.rs index 8b115e89880..a81fe42cc83 100644 --- a/beacon_node/network/src/sync/manager.rs +++ b/beacon_node/network/src/sync/manager.rs @@ -145,6 +145,9 @@ pub enum SyncMessage { /// A blob with an unknown parent has been received. UnknownParentBlob(PeerId, Arc>), + /// A data column with an unknown parent has been received. + UnknownParentDataColumn(PeerId, Arc>), + /// 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), @@ -680,6 +683,24 @@ impl SyncManager { }), ); } + SyncMessage::UnknownParentDataColumn(peer_id, data_column) => { + let data_column_slot = data_column.slot(); + let block_root = data_column.block_root(); + let parent_root = data_column.block_parent_root(); + debug!(self.log, "Received unknown parent data column message"; "block_root" => %block_root, "parent_root" => %parent_root); + self.handle_unknown_parent( + peer_id, + block_root, + parent_root, + data_column_slot, + BlockComponent::DataColumn(DownloadResult { + value: data_column, + block_root, + seen_timestamp: timestamp_now(), + peer_group: PeerGroup::from_single(peer_id), + }), + ); + } SyncMessage::UnknownBlockHashFromAttestation(peer_id, block_root) => { self.handle_unknown_block_root(peer_id, block_root); } diff --git a/beacon_node/network/src/sync/network_context/requests.rs b/beacon_node/network/src/sync/network_context/requests.rs index 2fc239b49b2..e2cdb37b1df 100644 --- a/beacon_node/network/src/sync/network_context/requests.rs +++ b/beacon_node/network/src/sync/network_context/requests.rs @@ -207,7 +207,7 @@ impl ActiveDataColumnsByRootRequest { "un-requested block root {block_root:?}" ))); } - if !data_column.verify_inclusion_proof().unwrap_or(false) { + if !data_column.verify_inclusion_proof() { return Err(RPCError::InvalidData("invalid inclusion proof".to_string())); } if !self.request.indices.contains(&data_column.index) { diff --git a/consensus/types/src/data_column_sidecar.rs b/consensus/types/src/data_column_sidecar.rs index 88094b946b2..62324fdef62 100644 --- a/consensus/types/src/data_column_sidecar.rs +++ b/consensus/types/src/data_column_sidecar.rs @@ -1,4 +1,4 @@ -use crate::beacon_block_body::KzgCommitments; +use crate::beacon_block_body::{KzgCommitments, BLOB_KZG_COMMITMENTS_INDEX}; use crate::test_utils::TestRandom; use crate::{ BeaconBlockHeader, EthSpec, Hash256, KzgProofs, SignedBeaconBlock, SignedBeaconBlockHeader, @@ -11,7 +11,7 @@ use derivative::Derivative; use kzg::Kzg; use kzg::{Blob as KzgBlob, Cell as KzgCell, Error as KzgError}; use kzg::{KzgCommitment, KzgProof}; -use merkle_proof::MerkleTreeError; +use merkle_proof::verify_merkle_proof; #[cfg(test)] use mockall_double::double; use safe_arith::ArithError; @@ -76,10 +76,25 @@ impl DataColumnSidecar { self.signed_block_header.message.tree_hash_root() } + pub fn block_parent_root(&self) -> Hash256 { + self.signed_block_header.message.parent_root + } + + pub fn block_proposer_index(&self) -> u64 { + self.signed_block_header.message.proposer_index + } + /// Verifies the kzg commitment inclusion merkle proof. - pub fn verify_inclusion_proof(&self) -> Result { - // TODO(das): implement - Ok(true) + pub fn verify_inclusion_proof(&self) -> bool { + let blob_kzg_commitments_root = self.kzg_commitments.tree_hash_root(); + + verify_merkle_proof( + blob_kzg_commitments_root, + &self.kzg_commitments_inclusion_proof, + E::kzg_commitments_inclusion_proof_depth(), + BLOB_KZG_COMMITMENTS_INDEX, + self.signed_block_header.message.body_root, + ) } pub fn build_sidecars( From 07df74c83f53adeabac1d466007894cb94b553f2 Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Wed, 15 May 2024 14:11:54 +0300 Subject: [PATCH 26/76] Trigger sampling on sync events (#5776) * Trigger sampling on sync events * Update beacon_chain.rs * Fix tests * Fix tests --- beacon_node/beacon_chain/src/beacon_chain.rs | 22 ++++++++++---- .../beacon_chain/src/block_verification.rs | 4 +++ .../beacon_chain/tests/block_verification.rs | 4 +-- .../gossip_methods.rs | 12 +++----- .../network_beacon_processor/sync_methods.rs | 30 ++++++++++++++++--- 5 files changed, 52 insertions(+), 20 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 7353fda794b..2ebf14dd322 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -220,11 +220,13 @@ impl TryInto for AvailabilityProcessingStatus { /// The result of a chain segment processing. pub enum ChainSegmentResult { /// Processing this chain segment finished successfully. - Successful { imported_blocks: usize }, + Successful { + imported_blocks: Vec<(Hash256, Slot)>, + }, /// There was an error processing this chain segment. Before the error, some blocks could /// have been imported. Failed { - imported_blocks: usize, + imported_blocks: Vec<(Hash256, Slot)>, error: BlockError, }, } @@ -2712,7 +2714,7 @@ impl BeaconChain { chain_segment: Vec>, ) -> Result>, ChainSegmentResult> { // This function will never import any blocks. - let imported_blocks = 0; + let imported_blocks = vec![]; let mut filtered_chain_segment = Vec::with_capacity(chain_segment.len()); // Produce a list of the parent root and slot of the child of each block. @@ -2818,7 +2820,7 @@ impl BeaconChain { chain_segment: Vec>, notify_execution_layer: NotifyExecutionLayer, ) -> ChainSegmentResult { - let mut imported_blocks = 0; + let mut imported_blocks = vec![]; // Filter uninteresting blocks from the chain segment in a blocking task. let chain = self.clone(); @@ -2878,6 +2880,7 @@ impl BeaconChain { // Import the blocks into the chain. for signature_verified_block in signature_verified_blocks { + let block_slot = signature_verified_block.slot(); match self .process_block( signature_verified_block.block_root(), @@ -2889,9 +2892,9 @@ impl BeaconChain { { Ok(status) => { match status { - AvailabilityProcessingStatus::Imported(_) => { + AvailabilityProcessingStatus::Imported(block_root) => { // The block was imported successfully. - imported_blocks += 1; + imported_blocks.push((block_root, block_slot)); } AvailabilityProcessingStatus::MissingComponents(slot, block_root) => { warn!(self.log, "Blobs missing in response to range request"; @@ -6871,6 +6874,13 @@ impl BeaconChain { self.data_availability_checker.data_availability_boundary() } + /// Returns true if we should issue a sampling request for this block + /// TODO(das): check if the block is still within the da_window + pub fn should_sample_slot(&self, slot: Slot) -> bool { + self.spec + .is_peer_das_enabled_for_epoch(slot.epoch(T::EthSpec::slots_per_epoch())) + } + pub fn logger(&self) -> &Logger { &self.log } diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index dc989595f63..9dc3ee7029f 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -1220,6 +1220,10 @@ impl SignatureVerifiedBlock { pub fn block_root(&self) -> Hash256 { self.block_root } + + pub fn slot(&self) -> Slot { + self.block.slot() + } } impl IntoExecutionPendingBlock for SignatureVerifiedBlock { diff --git a/beacon_node/beacon_chain/tests/block_verification.rs b/beacon_node/beacon_chain/tests/block_verification.rs index 98a112daffe..c1b7261fc21 100644 --- a/beacon_node/beacon_chain/tests/block_verification.rs +++ b/beacon_node/beacon_chain/tests/block_verification.rs @@ -1362,7 +1362,7 @@ async fn add_base_block_to_altair_chain() { ) .await, ChainSegmentResult::Failed { - imported_blocks: 0, + .. error: BlockError::InconsistentFork(InconsistentFork { fork_at_slot: ForkName::Altair, object_fork: ForkName::Base, @@ -1497,7 +1497,7 @@ async fn add_altair_block_to_base_chain() { ) .await, ChainSegmentResult::Failed { - imported_blocks: 0, + .. error: BlockError::InconsistentFork(InconsistentFork { fork_at_slot: ForkName::Base, object_fork: ForkName::Altair, 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 8007ad25735..4eefb473c15 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -1382,20 +1382,16 @@ impl NetworkBeaconProcessor { let block = verified_block.block.block_cloned(); let block_root = verified_block.block_root; + // TODO(das) Might be too early to issue a request here. We haven't checked that the block + // actually includes blob transactions and thus has data. A peer could send a block is + // garbage commitments, and make us trigger sampling for a block that does not have data. if block.num_expected_blobs() > 0 { // Trigger sampling for block not yet execution valid. At this point column custodials are // unlikely to have received their columns. Triggering sampling so early is only viable with // either: // - Sync delaying sampling until some latter window // - Re-processing early sampling requests: https://github.com/sigp/lighthouse/pull/5569 - if self - .chain - .spec - .eip7594_fork_epoch - .map_or(false, |eip7594_fork_epoch| { - block.epoch() >= eip7594_fork_epoch - }) - { + if self.chain.should_sample_slot(block.slot()) { self.send_sync_message(SyncMessage::SampleBlock(block_root, block.slot())); } } 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 a487157caaf..598f9b4f303 100644 --- a/beacon_node/network/src/network_beacon_processor/sync_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/sync_methods.rs @@ -140,6 +140,7 @@ impl NetworkBeaconProcessor { }; let slot = block.slot(); + let block_has_data = block.as_block().num_expected_blobs() > 0; let parent_root = block.message().parent_root(); let commitments_formatted = block.as_block().commitments_formatted(); @@ -182,6 +183,18 @@ impl NetworkBeaconProcessor { self.chain.recompute_head_at_current_slot().await; } + + // RPC block imported or execution validated. If the block was already imported by gossip we + // receive Err(BlockError::AlreadyKnown). + if result.is_ok() && + // Block has at least one blob, so it produced columns + block_has_data && + // Block slot is within the DA boundary (should always be the case) and PeerDAS is activated + self.chain.should_sample_slot(slot) + { + self.send_sync_message(SyncMessage::SampleBlock(block_root, slot)); + } + // Sync handles these results self.send_sync_message(SyncMessage::BlockComponentProcessed { process_type, @@ -495,10 +508,19 @@ impl NetworkBeaconProcessor { { ChainSegmentResult::Successful { imported_blocks } => { metrics::inc_counter(&metrics::BEACON_PROCESSOR_CHAIN_SEGMENT_SUCCESS_TOTAL); - if imported_blocks > 0 { + if !imported_blocks.is_empty() { self.chain.recompute_head_at_current_slot().await; + + for (block_root, block_slot) in &imported_blocks { + if self.chain.should_sample_slot(*block_slot) { + self.send_sync_message(SyncMessage::SampleBlock( + *block_root, + *block_slot, + )); + } + } } - (imported_blocks, Ok(())) + (imported_blocks.len(), Ok(())) } ChainSegmentResult::Failed { imported_blocks, @@ -506,10 +528,10 @@ impl NetworkBeaconProcessor { } => { metrics::inc_counter(&metrics::BEACON_PROCESSOR_CHAIN_SEGMENT_FAILED_TOTAL); let r = self.handle_failed_chain_segment(error); - if imported_blocks > 0 { + if !imported_blocks.is_empty() { self.chain.recompute_head_at_current_slot().await; } - (imported_blocks, r) + (imported_blocks.len(), r) } } } From aa8095086c43745faac9efce2b1d3eb7aeb7e886 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Wed, 15 May 2024 14:12:41 +0300 Subject: [PATCH 27/76] PeerDAS parameter changes for devnet-0 (#5779) * Update PeerDAS parameters to latest values. * Lint fix * Fix lint. --- .../lighthouse_network/src/discovery/enr.rs | 17 +++++++---------- .../src/discovery/subnet_predicate.rs | 12 ++++-------- .../lighthouse_network/src/rpc/config.rs | 9 ++++++--- .../lighthouse_network/src/types/globals.rs | 5 ++--- beacon_node/network/src/service.rs | 3 +-- beacon_node/network/src/sync/network_context.rs | 5 +---- consensus/types/src/chain_spec.rs | 5 +---- consensus/types/src/data_column_subnet_id.rs | 9 ++++----- consensus/types/src/eth_spec.rs | 6 +++--- 9 files changed, 29 insertions(+), 42 deletions(-) diff --git a/beacon_node/lighthouse_network/src/discovery/enr.rs b/beacon_node/lighthouse_network/src/discovery/enr.rs index 00ce790da28..9235b2bfe70 100644 --- a/beacon_node/lighthouse_network/src/discovery/enr.rs +++ b/beacon_node/lighthouse_network/src/discovery/enr.rs @@ -38,7 +38,7 @@ pub trait Eth2Enr { ) -> Result, &'static str>; /// The peerdas custody subnet count associated with the ENR. - fn custody_subnet_count(&self) -> Result; + fn custody_subnet_count(&self) -> u64; fn eth2(&self) -> Result; } @@ -64,15 +64,12 @@ impl Eth2Enr for Enr { .map_err(|_| "Could not decode the ENR syncnets bitfield") } - fn custody_subnet_count(&self) -> Result { - // NOTE: if the custody value is non-existent in the ENR, then we assume the minimum - // custody value defined in the spec. - let min_custody_bytes = E::min_custody_requirement().as_ssz_bytes(); - let custody_bytes = self - .get(PEERDAS_CUSTODY_SUBNET_COUNT_ENR_KEY) - .unwrap_or(&min_custody_bytes); - u64::from_ssz_bytes(custody_bytes) - .map_err(|_| "Could not decode the ENR custody subnet count") + /// if the custody value is non-existent in the ENR, then we assume the minimum custody value + /// defined in the spec. + fn custody_subnet_count(&self) -> u64 { + self.get(PEERDAS_CUSTODY_SUBNET_COUNT_ENR_KEY) + .and_then(|custody_bytes| u64::from_ssz_bytes(custody_bytes).ok()) + .unwrap_or(E::min_custody_requirement() as u64) } fn eth2(&self) -> Result { diff --git a/beacon_node/lighthouse_network/src/discovery/subnet_predicate.rs b/beacon_node/lighthouse_network/src/discovery/subnet_predicate.rs index f1dd44c6969..8f737eec6ec 100644 --- a/beacon_node/lighthouse_network/src/discovery/subnet_predicate.rs +++ b/beacon_node/lighthouse_network/src/discovery/subnet_predicate.rs @@ -25,11 +25,7 @@ where let sync_committee_bitfield: Result, _> = enr.sync_committee_bitfield::(); - // Pre-fork/fork-boundary enrs may not contain a peerdas custody field. - // Don't return early here. - // - // NOTE: we could map to minimum custody requirement here. - let custody_subnet_count: Result = enr.custody_subnet_count::(); + let custody_subnet_count = enr.custody_subnet_count::(); let predicate = subnets.iter().any(|subnet| match subnet { Subnet::Attestation(s) => attestation_bitfield @@ -38,13 +34,13 @@ where Subnet::SyncCommittee(s) => sync_committee_bitfield .as_ref() .map_or(false, |b| b.get(*s.deref() as usize).unwrap_or(false)), - Subnet::DataColumn(s) => custody_subnet_count.map_or(false, |count| { + Subnet::DataColumn(s) => { let mut subnets = DataColumnSubnetId::compute_custody_subnets::( enr.node_id().raw().into(), - count, + custody_subnet_count, ); subnets.contains(s) - }), + } }); if !predicate { diff --git a/beacon_node/lighthouse_network/src/rpc/config.rs b/beacon_node/lighthouse_network/src/rpc/config.rs index 7f1595d5295..169ea234092 100644 --- a/beacon_node/lighthouse_network/src/rpc/config.rs +++ b/beacon_node/lighthouse_network/src/rpc/config.rs @@ -107,9 +107,12 @@ impl RateLimiterConfig { pub const DEFAULT_BLOCKS_BY_ROOT_QUOTA: Quota = Quota::n_every(128, 10); pub const DEFAULT_BLOBS_BY_RANGE_QUOTA: Quota = Quota::n_every(768, 10); pub const DEFAULT_BLOBS_BY_ROOT_QUOTA: Quota = Quota::n_every(128, 10); - // TODO(das): random value without thought - pub const DEFAULT_DATA_COLUMNS_BY_RANGE_QUOTA: Quota = Quota::n_every(128, 10); - pub const DEFAULT_DATA_COLUMNS_BY_ROOT_QUOTA: Quota = Quota::n_every(128, 10); + // 320 blocks worth of columns for regular node, or 40 blocks for supernode. + // Range sync load balances when requesting blocks, and each batch is 32 blocks. + pub const DEFAULT_DATA_COLUMNS_BY_RANGE_QUOTA: Quota = Quota::n_every(5120, 10); + // 512 columns per request from spec. This should be plenty as peers are unlikely to send all + // sampling requests to a single peer. + pub const DEFAULT_DATA_COLUMNS_BY_ROOT_QUOTA: Quota = Quota::n_every(512, 10); pub const DEFAULT_LIGHT_CLIENT_BOOTSTRAP_QUOTA: Quota = Quota::one_every(10); pub const DEFAULT_LIGHT_CLIENT_OPTIMISTIC_UPDATE_QUOTA: Quota = Quota::one_every(10); pub const DEFAULT_LIGHT_CLIENT_FINALITY_UPDATE_QUOTA: Quota = Quota::one_every(10); diff --git a/beacon_node/lighthouse_network/src/types/globals.rs b/beacon_node/lighthouse_network/src/types/globals.rs index 5dd74f25726..263755ca6cb 100644 --- a/beacon_node/lighthouse_network/src/types/globals.rs +++ b/beacon_node/lighthouse_network/src/types/globals.rs @@ -115,8 +115,7 @@ impl NetworkGlobals { pub fn custody_columns(&self, _epoch: Epoch) -> Result, &'static str> { let enr = self.local_enr(); let node_id = enr.node_id().raw().into(); - // TODO(das): cache this number at start-up to not make this fallible - let custody_subnet_count = enr.custody_subnet_count::()?; + let custody_subnet_count = enr.custody_subnet_count::(); Ok( DataColumnSubnetId::compute_custody_columns::(node_id, custody_subnet_count) .collect(), @@ -152,7 +151,7 @@ mod test { fn test_custody_count_default() { let log = logging::test_logger(); let default_custody_requirement_column_count = - E::number_of_columns() / E::data_column_subnet_count(); + E::number_of_columns() / E::data_column_subnet_count() * E::min_custody_requirement(); let globals = NetworkGlobals::::new_test_globals(vec![], &log); let any_epoch = Epoch::new(0); let columns = globals.custody_columns(any_epoch).unwrap(); diff --git a/beacon_node/network/src/service.rs b/beacon_node/network/src/service.rs index fd899f5d45f..5f922e25c9d 100644 --- a/beacon_node/network/src/service.rs +++ b/beacon_node/network/src/service.rs @@ -759,8 +759,7 @@ impl NetworkService { self.network_globals.local_enr().node_id().raw().into(), self.network_globals .local_enr() - .custody_subnet_count::<::EthSpec>() - .unwrap_or(self.beacon_chain.spec.custody_requirement), + .custody_subnet_count::<::EthSpec>(), ) { for fork_digest in self.required_gossip_fork_digests() { let gossip_kind = Subnet::DataColumn(column_subnet).into(); diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index d516221e941..d67ad63cc3a 100644 --- a/beacon_node/network/src/sync/network_context.rs +++ b/beacon_node/network/src/sync/network_context.rs @@ -208,10 +208,7 @@ impl SyncNetworkContext { for (peer_id, peer_info) in self.network_globals().peers.read().connected_peers() { if let Some(enr) = peer_info.enr() { - // TODO(das): ignores decode errors - let custody_subnet_count = enr - .custody_subnet_count::() - .unwrap_or(T::EthSpec::min_custody_requirement() as u64); + let custody_subnet_count = enr.custody_subnet_count::(); // TODO(das): consider caching a map of subnet -> Vec and invalidating // whenever a peer connected or disconnect event in received let mut subnets = DataColumnSubnetId::compute_custody_subnets::( diff --git a/consensus/types/src/chain_spec.rs b/consensus/types/src/chain_spec.rs index b166e547dbb..3ce2b5e8edc 100644 --- a/consensus/types/src/chain_spec.rs +++ b/consensus/types/src/chain_spec.rs @@ -195,7 +195,6 @@ pub struct ChainSpec { * DAS params */ pub eip7594_fork_epoch: Option, - pub custody_requirement: u64, /* * Networking @@ -772,7 +771,6 @@ impl ChainSpec { * DAS params */ eip7594_fork_epoch: None, - custody_requirement: 1, /* * Network specific @@ -1085,7 +1083,6 @@ impl ChainSpec { * DAS params */ eip7594_fork_epoch: None, - custody_requirement: 1, /* * Network specific */ @@ -1436,7 +1433,7 @@ const fn default_max_request_blob_sidecars() -> u64 { } const fn default_max_request_data_column_sidecars() -> u64 { - 16384 + 512 } const fn default_min_epochs_for_blob_sidecars_requests() -> u64 { diff --git a/consensus/types/src/data_column_subnet_id.rs b/consensus/types/src/data_column_subnet_id.rs index 787a0db332c..d57f47352e2 100644 --- a/consensus/types/src/data_column_subnet_id.rs +++ b/consensus/types/src/data_column_subnet_id.rs @@ -160,7 +160,7 @@ impl From for Error { mod test { use crate::data_column_subnet_id::DataColumnSubnetId; use crate::EthSpec; - use crate::{ChainSpec, MainnetEthSpec}; + use crate::MainnetEthSpec; type E = MainnetEthSpec; @@ -182,15 +182,14 @@ mod test { .map(|v| ethereum_types::U256::from_dec_str(v).unwrap()) .collect::>(); - let spec = ChainSpec::mainnet(); - + let custody_requirement = 4; for node_id in node_ids { let computed_subnets = - DataColumnSubnetId::compute_custody_subnets::(node_id, spec.custody_requirement); + DataColumnSubnetId::compute_custody_subnets::(node_id, custody_requirement); let computed_subnets: Vec<_> = computed_subnets.collect(); // the number of subnets is equal to the custody requirement - assert_eq!(computed_subnets.len() as u64, spec.custody_requirement); + assert_eq!(computed_subnets.len() as u64, custody_requirement); let subnet_count = E::data_column_subnet_count(); for subnet in computed_subnets { diff --git a/consensus/types/src/eth_spec.rs b/consensus/types/src/eth_spec.rs index 4ed7bfc9615..973d4cd693c 100644 --- a/consensus/types/src/eth_spec.rs +++ b/consensus/types/src/eth_spec.rs @@ -417,7 +417,7 @@ impl EthSpec for MainnetEthSpec { type BytesPerBlob = U131072; type BytesPerCell = U2048; type KzgCommitmentInclusionProofDepth = U17; - type MinCustodyRequirement = U1; + type MinCustodyRequirement = U4; type DataColumnSubnetCount = U32; type DataColumnCount = U128; type DataColumnsPerSubnet = U4; @@ -471,7 +471,7 @@ impl EthSpec for MinimalEthSpec { type MaxDepositReceiptsPerPayload = U4; type MaxWithdrawalRequestsPerPayload = U2; // DAS spec values copied from `MainnetEthSpec` - type MinCustodyRequirement = U1; + type MinCustodyRequirement = U4; type DataColumnSubnetCount = U32; type DataColumnCount = U128; type DataColumnsPerSubnet = U4; @@ -565,7 +565,7 @@ impl EthSpec for GnosisEthSpec { type MaxAttestationsElectra = U8; type MaxWithdrawalRequestsPerPayload = U16; // DAS spec values copied from `MainnetEthSpec` - type MinCustodyRequirement = U1; + type MinCustodyRequirement = U4; type DataColumnSubnetCount = U32; type DataColumnCount = U128; type DataColumnsPerSubnet = U4; From 6f7e4e99310e9bb4e7f3f0c8b825ad8e727bbddf Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Wed, 15 May 2024 18:42:44 +0300 Subject: [PATCH 28/76] Update hardcoded subnet count to 64 (#5791) --- consensus/types/src/eth_spec.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/consensus/types/src/eth_spec.rs b/consensus/types/src/eth_spec.rs index 973d4cd693c..d6481a1ecde 100644 --- a/consensus/types/src/eth_spec.rs +++ b/consensus/types/src/eth_spec.rs @@ -418,7 +418,7 @@ impl EthSpec for MainnetEthSpec { type BytesPerCell = U2048; type KzgCommitmentInclusionProofDepth = U17; type MinCustodyRequirement = U4; - type DataColumnSubnetCount = U32; + type DataColumnSubnetCount = U64; type DataColumnCount = U128; type DataColumnsPerSubnet = U4; type KzgCommitmentsInclusionProofDepth = U4; // inclusion of the whole list of commitments @@ -472,7 +472,7 @@ impl EthSpec for MinimalEthSpec { type MaxWithdrawalRequestsPerPayload = U2; // DAS spec values copied from `MainnetEthSpec` type MinCustodyRequirement = U4; - type DataColumnSubnetCount = U32; + type DataColumnSubnetCount = U64; type DataColumnCount = U128; type DataColumnsPerSubnet = U4; type KzgCommitmentsInclusionProofDepth = U4; @@ -566,7 +566,7 @@ impl EthSpec for GnosisEthSpec { type MaxWithdrawalRequestsPerPayload = U16; // DAS spec values copied from `MainnetEthSpec` type MinCustodyRequirement = U4; - type DataColumnSubnetCount = U32; + type DataColumnSubnetCount = U64; type DataColumnCount = U128; type DataColumnsPerSubnet = U4; type KzgCommitmentsInclusionProofDepth = U4; From b64dd9d310baec5a42f5cf5d9dbdb4973202eb79 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Wed, 15 May 2024 21:54:20 +0300 Subject: [PATCH 29/76] Fix incorrect columns per subnet and config cleanup (#5792) * Tidy up PeerDAS preset and config values. * Fix broken config --- .../src/data_availability_checker.rs | 2 +- .../overflow_lru_cache.rs | 2 +- .../src/data_column_verification.rs | 4 +- .../lighthouse_network/src/discovery/enr.rs | 4 +- .../lighthouse_network/src/types/globals.rs | 2 +- .../network/src/sync/block_lookups/tests.rs | 2 +- consensus/types/presets/gnosis/deneb.yaml | 9 --- consensus/types/presets/gnosis/eip7594.yaml | 10 +++ consensus/types/presets/mainnet/deneb.yaml | 9 --- consensus/types/presets/mainnet/eip7594.yaml | 10 +++ consensus/types/presets/minimal/deneb.yaml | 9 --- consensus/types/presets/minimal/eip7594.yaml | 10 +++ consensus/types/src/data_column_sidecar.rs | 4 +- consensus/types/src/eth_spec.rs | 68 +++++++++++-------- consensus/types/src/preset.rs | 36 +++++++--- 15 files changed, 106 insertions(+), 75 deletions(-) create mode 100644 consensus/types/presets/gnosis/eip7594.yaml create mode 100644 consensus/types/presets/mainnet/eip7594.yaml create mode 100644 consensus/types/presets/minimal/eip7594.yaml diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index 0ccee82641c..7e11a163d5a 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -87,7 +87,7 @@ impl DataAvailabilityChecker { let custody_subnet_count = if import_all_data_columns { T::EthSpec::data_column_subnet_count() } else { - T::EthSpec::min_custody_requirement() + T::EthSpec::custody_requirement() }; let custody_column_count = diff --git a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs index 8c7f1a77d51..759ccc9a2d5 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs @@ -63,7 +63,7 @@ pub type DataColumnsToPublish = Option>>>; pub struct PendingComponents { pub block_root: Hash256, pub verified_blobs: FixedVector>, E::MaxBlobsPerBlock>, - pub verified_data_columns: VariableList, E::DataColumnCount>, + pub verified_data_columns: VariableList, E::NumberOfColumns>, pub executed_block: Option>, } diff --git a/beacon_node/beacon_chain/src/data_column_verification.rs b/beacon_node/beacon_chain/src/data_column_verification.rs index fa43fa170eb..1ae59da6ed5 100644 --- a/beacon_node/beacon_chain/src/data_column_verification.rs +++ b/beacon_node/beacon_chain/src/data_column_verification.rs @@ -149,7 +149,7 @@ impl From for GossipDataColumnError { pub type GossipVerifiedDataColumnList = VariableList< GossipVerifiedDataColumn, - <::EthSpec as EthSpec>::DataColumnCount, + <::EthSpec as EthSpec>::NumberOfColumns, >; /// A wrapper around a `DataColumnSidecar` that indicates it has been approved for re-gossiping on @@ -243,7 +243,7 @@ impl KzgVerifiedDataColumn { } pub type CustodyDataColumnList = - VariableList, ::DataColumnCount>; + VariableList, ::NumberOfColumns>; /// Data column that we must custody #[derive(Debug, Derivative, Clone, Encode, Decode)] diff --git a/beacon_node/lighthouse_network/src/discovery/enr.rs b/beacon_node/lighthouse_network/src/discovery/enr.rs index 9235b2bfe70..d6214423212 100644 --- a/beacon_node/lighthouse_network/src/discovery/enr.rs +++ b/beacon_node/lighthouse_network/src/discovery/enr.rs @@ -69,7 +69,7 @@ impl Eth2Enr for Enr { fn custody_subnet_count(&self) -> u64 { self.get(PEERDAS_CUSTODY_SUBNET_COUNT_ENR_KEY) .and_then(|custody_bytes| u64::from_ssz_bytes(custody_bytes).ok()) - .unwrap_or(E::min_custody_requirement() as u64) + .unwrap_or(E::custody_requirement() as u64) } fn eth2(&self) -> Result { @@ -238,7 +238,7 @@ pub fn build_enr( let custody_subnet_count = if config.subscribe_all_data_column_subnets { E::data_column_subnet_count() as u64 } else { - E::min_custody_requirement() as u64 + E::custody_requirement() as u64 }; builder.add_value( diff --git a/beacon_node/lighthouse_network/src/types/globals.rs b/beacon_node/lighthouse_network/src/types/globals.rs index 263755ca6cb..fa1501cbf18 100644 --- a/beacon_node/lighthouse_network/src/types/globals.rs +++ b/beacon_node/lighthouse_network/src/types/globals.rs @@ -151,7 +151,7 @@ mod test { fn test_custody_count_default() { let log = logging::test_logger(); let default_custody_requirement_column_count = - E::number_of_columns() / E::data_column_subnet_count() * E::min_custody_requirement(); + E::number_of_columns() / E::data_column_subnet_count() * E::custody_requirement(); let globals = NetworkGlobals::::new_test_globals(vec![], &log); let any_epoch = Epoch::new(0); let columns = globals.custody_columns(any_epoch).unwrap(); diff --git a/beacon_node/network/src/sync/block_lookups/tests.rs b/beacon_node/network/src/sync/block_lookups/tests.rs index 38e74ba0d57..173ae761f65 100644 --- a/beacon_node/network/src/sync/block_lookups/tests.rs +++ b/beacon_node/network/src/sync/block_lookups/tests.rs @@ -1866,7 +1866,7 @@ fn custody_lookup_happy_path() { // Should not request blobs let id = r.expect_block_lookup_request(block.canonical_root()); r.complete_valid_block_request(id, block.into(), true); - let custody_column_count = E::min_custody_requirement() * E::data_columns_per_subnet(); + let custody_column_count = E::custody_requirement() * E::data_columns_per_subnet(); let custody_ids = r.expect_only_data_columns_by_root_requests(block_root, custody_column_count); r.complete_valid_custody_request(custody_ids, data_columns, false); r.expect_no_active_lookups(); diff --git a/consensus/types/presets/gnosis/deneb.yaml b/consensus/types/presets/gnosis/deneb.yaml index bef51470e89..d2d7d0abed3 100644 --- a/consensus/types/presets/gnosis/deneb.yaml +++ b/consensus/types/presets/gnosis/deneb.yaml @@ -12,12 +12,3 @@ MAX_BLOB_COMMITMENTS_PER_BLOCK: 4096 MAX_BLOBS_PER_BLOCK: 6 # `floorlog2(BLOB_KZG_COMMITMENTS_GINDEX) + 1 + ceillog2(MAX_BLOB_COMMITMENTS_PER_BLOCK)` = 4 + 1 + 12 = 17 KZG_COMMITMENT_INCLUSION_PROOF_DEPTH: 17 - -# EIP-7594 (temporary in Deneb for the purpose of prototyping) -# --------------------------------------------------------------- -# `uint64(2**6)` (= 64) -FIELD_ELEMENTS_PER_CELL: 64 -# uint64(floorlog2(get_generalized_index(BeaconBlockBody, 'blob_kzg_commitments')) -KZG_COMMITMENTS_INCLUSION_PROOF_DEPTH: 4 -# `uint64((FIELD_ELEMENTS_PER_BLOB * 2) // FIELD_ELEMENTS_PER_CELL)` (= 128) -NUMBER_OF_COLUMNS: 128 \ No newline at end of file diff --git a/consensus/types/presets/gnosis/eip7594.yaml b/consensus/types/presets/gnosis/eip7594.yaml new file mode 100644 index 00000000000..813febf26d5 --- /dev/null +++ b/consensus/types/presets/gnosis/eip7594.yaml @@ -0,0 +1,10 @@ +# Mainnet preset - EIP7594 + +# Misc +# --------------------------------------------------------------- +# `uint64(2**6)` (= 64) +FIELD_ELEMENTS_PER_CELL: 64 +# `uint64(2 * 4096)` (= 8192) +FIELD_ELEMENTS_PER_EXT_BLOB: 8192 +# uint64(floorlog2(get_generalized_index(BeaconBlockBody, 'blob_kzg_commitments')) +KZG_COMMITMENTS_INCLUSION_PROOF_DEPTH: 4 diff --git a/consensus/types/presets/mainnet/deneb.yaml b/consensus/types/presets/mainnet/deneb.yaml index 44bc5034294..0f56b8bdfac 100644 --- a/consensus/types/presets/mainnet/deneb.yaml +++ b/consensus/types/presets/mainnet/deneb.yaml @@ -10,12 +10,3 @@ MAX_BLOB_COMMITMENTS_PER_BLOCK: 4096 MAX_BLOBS_PER_BLOCK: 6 # `floorlog2(get_generalized_index(BeaconBlockBody, 'blob_kzg_commitments')) + 1 + ceillog2(MAX_BLOB_COMMITMENTS_PER_BLOCK)` = 4 + 1 + 12 = 17 KZG_COMMITMENT_INCLUSION_PROOF_DEPTH: 17 - -# EIP-7594 (temporary in Deneb for the purpose of prototyping) -# --------------------------------------------------------------- -# `uint64(2**6)` (= 64) -FIELD_ELEMENTS_PER_CELL: 64 -# uint64(floorlog2(get_generalized_index(BeaconBlockBody, 'blob_kzg_commitments')) -KZG_COMMITMENTS_INCLUSION_PROOF_DEPTH: 4 -# `uint64((FIELD_ELEMENTS_PER_BLOB * 2) // FIELD_ELEMENTS_PER_CELL)` (= 128) -NUMBER_OF_COLUMNS: 128 \ No newline at end of file diff --git a/consensus/types/presets/mainnet/eip7594.yaml b/consensus/types/presets/mainnet/eip7594.yaml new file mode 100644 index 00000000000..813febf26d5 --- /dev/null +++ b/consensus/types/presets/mainnet/eip7594.yaml @@ -0,0 +1,10 @@ +# Mainnet preset - EIP7594 + +# Misc +# --------------------------------------------------------------- +# `uint64(2**6)` (= 64) +FIELD_ELEMENTS_PER_CELL: 64 +# `uint64(2 * 4096)` (= 8192) +FIELD_ELEMENTS_PER_EXT_BLOB: 8192 +# uint64(floorlog2(get_generalized_index(BeaconBlockBody, 'blob_kzg_commitments')) +KZG_COMMITMENTS_INCLUSION_PROOF_DEPTH: 4 diff --git a/consensus/types/presets/minimal/deneb.yaml b/consensus/types/presets/minimal/deneb.yaml index 8bb3e0b66bd..be2b9fadfa5 100644 --- a/consensus/types/presets/minimal/deneb.yaml +++ b/consensus/types/presets/minimal/deneb.yaml @@ -10,12 +10,3 @@ MAX_BLOB_COMMITMENTS_PER_BLOCK: 16 MAX_BLOBS_PER_BLOCK: 6 # [customized] `floorlog2(BLOB_KZG_COMMITMENTS_GINDEX) + 1 + ceillog2(MAX_BLOB_COMMITMENTS_PER_BLOCK)` = 4 + 1 + 4 = 9 KZG_COMMITMENT_INCLUSION_PROOF_DEPTH: 9 - -# EIP-7594 (temporary in Deneb for the purpose of prototyping) -# --------------------------------------------------------------- -# `uint64(2**6)` (= 64) -FIELD_ELEMENTS_PER_CELL: 64 -# uint64(floorlog2(get_generalized_index(BeaconBlockBody, 'blob_kzg_commitments')) -KZG_COMMITMENTS_INCLUSION_PROOF_DEPTH: 4 -# `uint64((FIELD_ELEMENTS_PER_BLOB * 2) // FIELD_ELEMENTS_PER_CELL)` (= 128) -NUMBER_OF_COLUMNS: 128 \ No newline at end of file diff --git a/consensus/types/presets/minimal/eip7594.yaml b/consensus/types/presets/minimal/eip7594.yaml new file mode 100644 index 00000000000..847719a4210 --- /dev/null +++ b/consensus/types/presets/minimal/eip7594.yaml @@ -0,0 +1,10 @@ +# Minimal preset - EIP7594 + +# Misc +# --------------------------------------------------------------- +# `uint64(2**6)` (= 64) +FIELD_ELEMENTS_PER_CELL: 64 +# `uint64(2 * 4096)` (= 8192) +FIELD_ELEMENTS_PER_EXT_BLOB: 8192 +# uint64(floorlog2(get_generalized_index(BeaconBlockBody, 'blob_kzg_commitments')) +KZG_COMMITMENTS_INCLUSION_PROOF_DEPTH: 4 diff --git a/consensus/types/src/data_column_sidecar.rs b/consensus/types/src/data_column_sidecar.rs index 62324fdef62..c6183414c5d 100644 --- a/consensus/types/src/data_column_sidecar.rs +++ b/consensus/types/src/data_column_sidecar.rs @@ -383,9 +383,9 @@ impl From for DataColumnSidecarError { } pub type DataColumnSidecarList = - VariableList>, ::DataColumnCount>; + VariableList>, ::NumberOfColumns>; pub type FixedDataColumnSidecarList = - FixedVector>>, ::DataColumnCount>; + FixedVector>>, ::NumberOfColumns>; /// Converts a cell ssz List object to an array to be used with the kzg /// crypto library. diff --git a/consensus/types/src/eth_spec.rs b/consensus/types/src/eth_spec.rs index d6481a1ecde..157287c4836 100644 --- a/consensus/types/src/eth_spec.rs +++ b/consensus/types/src/eth_spec.rs @@ -110,17 +110,20 @@ pub trait EthSpec: type MaxBlobsPerBlock: Unsigned + Clone + Sync + Send + Debug + PartialEq + Unpin; type MaxBlobCommitmentsPerBlock: Unsigned + Clone + Sync + Send + Debug + PartialEq + Unpin; type FieldElementsPerBlob: Unsigned + Clone + Sync + Send + Debug + PartialEq; - type FieldElementsPerCell: Unsigned + Clone + Sync + Send + Debug + PartialEq; type BytesPerFieldElement: Unsigned + Clone + Sync + Send + Debug + PartialEq; type KzgCommitmentInclusionProofDepth: Unsigned + Clone + Sync + Send + Debug + PartialEq; /* * New in PeerDAS */ - type MinCustodyRequirement: Unsigned + Clone + Sync + Send + Debug + PartialEq; - type DataColumnSubnetCount: Unsigned + Clone + Sync + Send + Debug + PartialEq; - type DataColumnCount: Unsigned + Clone + Sync + Send + Debug + PartialEq; - type DataColumnsPerSubnet: Unsigned + Clone + Sync + Send + Debug + PartialEq; + type FieldElementsPerCell: Unsigned + Clone + Sync + Send + Debug + PartialEq; + type FieldElementsPerExtBlob: Unsigned + Clone + Sync + Send + Debug + PartialEq; type KzgCommitmentsInclusionProofDepth: Unsigned + Clone + Sync + Send + Debug + PartialEq; + /* + * Config values in PeerDAS + */ + type CustodyRequirement: Unsigned + Clone + Sync + Send + Debug + PartialEq; + type DataColumnSidecarSubnetCount: Unsigned + Clone + Sync + Send + Debug + PartialEq; + type NumberOfColumns: Unsigned + Clone + Sync + Send + Debug + PartialEq; /* * Derived values (set these CAREFULLY) */ @@ -149,6 +152,11 @@ pub trait EthSpec: /// Must be set to `BytesPerFieldElement * FieldElementsPerCell`. type BytesPerCell: Unsigned + Clone + Sync + Send + Debug + PartialEq; + /// Number of data columns per subnet. + /// + /// Must be set to `NumberOfColumns / DataColumnSidecarSubnetCount` + type DataColumnsPerSubnet: Unsigned + Clone + Sync + Send + Debug + PartialEq; + /* * New in Electra */ @@ -296,6 +304,11 @@ pub trait EthSpec: Self::FieldElementsPerBlob::to_usize() } + /// Returns the `FIELD_ELEMENTS_PER_EXT_BLOB` constant for this specification. + fn field_elements_per_ext_blob() -> usize { + Self::FieldElementsPerExtBlob::to_usize() + } + /// Returns the `FIELD_ELEMENTS_PER_CELL` constant for this specification. fn field_elements_per_cell() -> usize { Self::FieldElementsPerCell::to_usize() @@ -352,19 +365,19 @@ pub trait EthSpec: } fn number_of_columns() -> usize { - Self::DataColumnCount::to_usize() + Self::NumberOfColumns::to_usize() } fn data_columns_per_subnet() -> usize { Self::DataColumnsPerSubnet::to_usize() } - fn min_custody_requirement() -> usize { - Self::MinCustodyRequirement::to_usize() + fn custody_requirement() -> usize { + Self::CustodyRequirement::to_usize() } fn data_column_subnet_count() -> usize { - Self::DataColumnSubnetCount::to_usize() + Self::DataColumnSidecarSubnetCount::to_usize() } fn kzg_commitments_inclusion_proof_depth() -> usize { @@ -414,13 +427,14 @@ impl EthSpec for MainnetEthSpec { type BytesPerFieldElement = U32; type FieldElementsPerBlob = U4096; type FieldElementsPerCell = U64; + type FieldElementsPerExtBlob = U8192; type BytesPerBlob = U131072; type BytesPerCell = U2048; type KzgCommitmentInclusionProofDepth = U17; - type MinCustodyRequirement = U4; - type DataColumnSubnetCount = U64; - type DataColumnCount = U128; - type DataColumnsPerSubnet = U4; + type CustodyRequirement = U4; + type DataColumnSidecarSubnetCount = U64; + type NumberOfColumns = U128; + type DataColumnsPerSubnet = U2; type KzgCommitmentsInclusionProofDepth = U4; // inclusion of the whole list of commitments type SyncSubcommitteeSize = U128; // 512 committee size / 4 sync committee subnet count type MaxPendingAttestations = U4096; // 128 max attestations * 32 slots per epoch @@ -461,20 +475,20 @@ impl EthSpec for MinimalEthSpec { type SlotsPerEth1VotingPeriod = U32; // 4 epochs * 8 slots per epoch type MaxWithdrawalsPerPayload = U4; type FieldElementsPerBlob = U4096; - type FieldElementsPerCell = U64; type BytesPerBlob = U131072; - type BytesPerCell = U2048; type MaxBlobCommitmentsPerBlock = U16; type KzgCommitmentInclusionProofDepth = U9; type PendingPartialWithdrawalsLimit = U64; type PendingConsolidationsLimit = U64; type MaxDepositReceiptsPerPayload = U4; type MaxWithdrawalRequestsPerPayload = U2; - // DAS spec values copied from `MainnetEthSpec` - type MinCustodyRequirement = U4; - type DataColumnSubnetCount = U64; - type DataColumnCount = U128; - type DataColumnsPerSubnet = U4; + type FieldElementsPerCell = U64; + type FieldElementsPerExtBlob = U8192; + type BytesPerCell = U2048; + type CustodyRequirement = U4; + type DataColumnSidecarSubnetCount = U64; + type NumberOfColumns = U128; + type DataColumnsPerSubnet = U2; type KzgCommitmentsInclusionProofDepth = U4; params_from_eth_spec!(MainnetEthSpec { @@ -551,10 +565,8 @@ impl EthSpec for GnosisEthSpec { type MaxBlobsPerBlock = U6; type MaxBlobCommitmentsPerBlock = U4096; type FieldElementsPerBlob = U4096; - type FieldElementsPerCell = U64; type BytesPerFieldElement = U32; type BytesPerBlob = U131072; - type BytesPerCell = U2048; type KzgCommitmentInclusionProofDepth = U17; type PendingBalanceDepositsLimit = U134217728; type PendingPartialWithdrawalsLimit = U134217728; @@ -564,11 +576,13 @@ impl EthSpec for GnosisEthSpec { type MaxAttesterSlashingsElectra = U1; type MaxAttestationsElectra = U8; type MaxWithdrawalRequestsPerPayload = U16; - // DAS spec values copied from `MainnetEthSpec` - type MinCustodyRequirement = U4; - type DataColumnSubnetCount = U64; - type DataColumnCount = U128; - type DataColumnsPerSubnet = U4; + type FieldElementsPerCell = U64; + type FieldElementsPerExtBlob = U8192; + type BytesPerCell = U2048; + type CustodyRequirement = U4; + type DataColumnSidecarSubnetCount = U64; + type NumberOfColumns = U128; + type DataColumnsPerSubnet = U2; type KzgCommitmentsInclusionProofDepth = U4; fn default_spec() -> ChainSpec { diff --git a/consensus/types/src/preset.rs b/consensus/types/src/preset.rs index a0cd61e0251..b0f67beec0d 100644 --- a/consensus/types/src/preset.rs +++ b/consensus/types/src/preset.rs @@ -214,13 +214,6 @@ pub struct DenebPreset { pub max_blob_commitments_per_block: u64, #[serde(with = "serde_utils::quoted_u64")] pub field_elements_per_blob: u64, - // EIP-7594 DAS presets - to be moved to the next fork - #[serde(with = "serde_utils::quoted_u64")] - pub field_elements_per_cell: u64, - #[serde(with = "serde_utils::quoted_u64")] - pub kzg_commitments_inclusion_proof_depth: u64, - #[serde(with = "serde_utils::quoted_u64")] - pub number_of_columns: u64, } impl DenebPreset { @@ -229,10 +222,6 @@ impl DenebPreset { max_blobs_per_block: E::max_blobs_per_block() as u64, max_blob_commitments_per_block: E::max_blob_commitments_per_block() as u64, field_elements_per_blob: E::field_elements_per_blob() as u64, - field_elements_per_cell: E::field_elements_per_cell() as u64, - kzg_commitments_inclusion_proof_depth: E::kzg_commitments_inclusion_proof_depth() - as u64, - number_of_columns: E::number_of_columns() as u64, } } } @@ -289,6 +278,28 @@ impl ElectraPreset { } } +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[serde(rename_all = "UPPERCASE")] +pub struct Eip7594Preset { + #[serde(with = "serde_utils::quoted_u64")] + pub field_elements_per_cell: u64, + #[serde(with = "serde_utils::quoted_u64")] + pub field_elements_per_ext_blob: u64, + #[serde(with = "serde_utils::quoted_u64")] + pub kzg_commitments_inclusion_proof_depth: u64, +} + +impl Eip7594Preset { + pub fn from_chain_spec(_spec: &ChainSpec) -> Self { + Self { + field_elements_per_cell: E::field_elements_per_cell() as u64, + field_elements_per_ext_blob: E::field_elements_per_ext_blob() as u64, + kzg_commitments_inclusion_proof_depth: E::kzg_commitments_inclusion_proof_depth() + as u64, + } + } +} + #[cfg(test)] mod test { use super::*; @@ -333,6 +344,9 @@ mod test { let electra: ElectraPreset = preset_from_file(&preset_name, "electra.yaml"); assert_eq!(electra, ElectraPreset::from_chain_spec::(&spec)); + + let eip7594: Eip7594Preset = preset_from_file(&preset_name, "eip7594.yaml"); + assert_eq!(eip7594, Eip7594Preset::from_chain_spec::(&spec)); } #[test] From 80892e634d16814d0d63e1fa9f337cb40327c0fe Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Thu, 16 May 2024 00:35:38 +0300 Subject: [PATCH 30/76] Fix DAS branch CI (#5793) * Fix invalid syntax. * Update cli doc. Ignore get_custody_columns test temporarily. * Fix failing test and add verify inclusion test. * Undo accidentally removed code. --- beacon_node/beacon_chain/src/test_utils.rs | 24 +++++-------------- .../beacon_chain/tests/block_verification.rs | 4 ++-- .../network/src/sync/block_lookups/tests.rs | 14 ++++++----- .../src/sync/block_sidecar_coupling.rs | 2 +- beacon_node/src/cli.rs | 2 ++ beacon_node/src/config.rs | 6 +++++ book/src/help_bn.md | 5 +--- consensus/types/src/data_column_sidecar.rs | 1 + testing/ef_tests/check_all_files_accessed.py | 2 ++ testing/ef_tests/tests/tests.rs | 2 ++ 10 files changed, 31 insertions(+), 31 deletions(-) diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index b7db2462ca9..aa7ecc054e4 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -2578,32 +2578,20 @@ pub fn generate_rand_block_and_blobs( (block, blob_sidecars) } +#[allow(clippy::type_complexity)] pub fn generate_rand_block_and_data_columns( fork_name: ForkName, num_blobs: NumBlobs, rng: &mut impl Rng, ) -> ( SignedBeaconBlock>, - Vec>, + Vec>>, ) { let (block, blobs) = generate_rand_block_and_blobs(fork_name, num_blobs, rng); - let blob = blobs.first().expect("should have at least 1 blob"); - - let data_columns = (0..E::number_of_columns()) - .map(|index| DataColumnSidecar { - index: index as u64, - column: <_>::default(), - kzg_commitments: block - .message() - .body() - .blob_kzg_commitments() - .unwrap() - .clone(), - kzg_proofs: (vec![]).into(), - signed_block_header: blob.signed_block_header.clone(), - kzg_commitments_inclusion_proof: <_>::default(), - }) - .collect::>(); + let blob: BlobsList = blobs.into_iter().map(|b| b.blob).collect::>().into(); + let data_columns = DataColumnSidecar::build_sidecars(&blob, &block, &KZG) + .unwrap() + .into(); (block, data_columns) } diff --git a/beacon_node/beacon_chain/tests/block_verification.rs b/beacon_node/beacon_chain/tests/block_verification.rs index c1b7261fc21..92a873ff3bc 100644 --- a/beacon_node/beacon_chain/tests/block_verification.rs +++ b/beacon_node/beacon_chain/tests/block_verification.rs @@ -1362,7 +1362,7 @@ async fn add_base_block_to_altair_chain() { ) .await, ChainSegmentResult::Failed { - .. + imported_blocks: _, error: BlockError::InconsistentFork(InconsistentFork { fork_at_slot: ForkName::Altair, object_fork: ForkName::Base, @@ -1497,7 +1497,7 @@ async fn add_altair_block_to_base_chain() { ) .await, ChainSegmentResult::Failed { - .. + imported_blocks: _, error: BlockError::InconsistentFork(InconsistentFork { fork_at_slot: ForkName::Base, object_fork: ForkName::Altair, diff --git a/beacon_node/network/src/sync/block_lookups/tests.rs b/beacon_node/network/src/sync/block_lookups/tests.rs index 173ae761f65..222d58604da 100644 --- a/beacon_node/network/src/sync/block_lookups/tests.rs +++ b/beacon_node/network/src/sync/block_lookups/tests.rs @@ -241,7 +241,9 @@ impl TestRig { generate_rand_block_and_blobs::(fork_name, num_blobs, rng) } - fn rand_block_and_data_columns(&mut self) -> (SignedBeaconBlock, Vec>) { + fn rand_block_and_data_columns( + &mut self, + ) -> (SignedBeaconBlock, Vec>>) { let num_blobs = NumBlobs::Number(1); generate_rand_block_and_data_columns::(self.fork_name, num_blobs, &mut self.rng) } @@ -640,7 +642,7 @@ impl TestRig { fn complete_valid_sampling_column_requests( &mut self, sampling_ids: SamplingIds, - data_columns: Vec>, + data_columns: Vec>>, ) { for (id, column_index) in sampling_ids { self.log(&format!("return valid data column for {column_index}")); @@ -654,7 +656,7 @@ impl TestRig { fn complete_valid_sampling_column_request( &mut self, id: DataColumnsByRootRequestId, - data_column: DataColumnSidecar, + data_column: Arc>, ) { let block_root = data_column.block_root(); let column_index = data_column.index; @@ -677,7 +679,7 @@ impl TestRig { fn complete_valid_custody_request( &mut self, sampling_ids: SamplingIds, - data_columns: Vec>, + data_columns: Vec>>, missing_components: bool, ) { let lookup_id = if let DataColumnsByRootRequester::Custody(id) = @@ -720,14 +722,14 @@ impl TestRig { fn complete_data_columns_by_root_request( &mut self, id: DataColumnsByRootRequestId, - data_column: DataColumnSidecar, + data_column: Arc>, ) { let peer_id = PeerId::random(); // Send chunk self.send_sync_message(SyncMessage::RpcDataColumn { request_id: SyncRequestId::DataColumnsByRoot(id), peer_id, - data_column: Some(Arc::new(data_column)), + data_column: Some(data_column), seen_timestamp: timestamp_now(), }); // Send stream termination diff --git a/beacon_node/network/src/sync/block_sidecar_coupling.rs b/beacon_node/network/src/sync/block_sidecar_coupling.rs index ca6460f4305..f2f22d1ecc1 100644 --- a/beacon_node/network/src/sync/block_sidecar_coupling.rs +++ b/beacon_node/network/src/sync/block_sidecar_coupling.rs @@ -299,7 +299,7 @@ mod tests { for block in &blocks { for column in &block.1 { if expects_custody_columns.contains(&column.index) { - info.add_data_column(Some(column.clone().into())); + info.add_data_column(Some(column.clone())); } } } diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index d4a733f8ef9..e44910e0c0a 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -47,9 +47,11 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .takes_value(false), ) .arg( + // TODO(das): remove this before release Arg::with_name("malicious-withhold-count") .long("malicious-withhold-count") .help("TESTING ONLY do not use this") + .hidden(true) .takes_value(true), ) .arg( diff --git a/beacon_node/src/config.rs b/beacon_node/src/config.rs index 3211c26d76d..f02b742f280 100644 --- a/beacon_node/src/config.rs +++ b/beacon_node/src/config.rs @@ -443,6 +443,12 @@ pub fn get_config( client_config.store.epochs_per_blob_prune = epochs_per_blob_prune; } + if let Some(blob_prune_margin_epochs) = + clap_utils::parse_optional(cli_args, "blob-prune-margin-epochs")? + { + client_config.store.blob_prune_margin_epochs = blob_prune_margin_epochs; + } + if let Some(malicious_withhold_count) = clap_utils::parse_optional(cli_args, "malicious-withhold-count")? { diff --git a/book/src/help_bn.md b/book/src/help_bn.md index 89d037d48ae..894369ca9f6 100644 --- a/book/src/help_bn.md +++ b/book/src/help_bn.md @@ -259,7 +259,7 @@ OPTIONS: --graffiti Specify your custom graffiti to be included in blocks. Defaults to the current version and commit, truncated - to fit in 32 bytes. + to fit in 32 bytes. --historic-state-cache-size Specifies how many states from the freezer database should cache in memory [default: 1] @@ -324,9 +324,6 @@ OPTIONS: --logfile-max-size The maximum size (in MB) each log file can grow to before rotating. If set to 0, background file logging is disabled. [default: 200] - --malicious-withhold-count - TESTING ONLY do not use this - --max-skip-slots Refuse to skip more than this many slots when processing an attestation. This prevents nodes on minority forks from wasting our time and disk space, but could also cause unnecessary consensus failures, so is diff --git a/consensus/types/src/data_column_sidecar.rs b/consensus/types/src/data_column_sidecar.rs index c6183414c5d..75687a43dfa 100644 --- a/consensus/types/src/data_column_sidecar.rs +++ b/consensus/types/src/data_column_sidecar.rs @@ -461,6 +461,7 @@ mod test { col_sidecar.kzg_commitments_inclusion_proof, block_kzg_commitments_inclusion_proof ); + assert!(col_sidecar.verify_inclusion_proof()); } } diff --git a/testing/ef_tests/check_all_files_accessed.py b/testing/ef_tests/check_all_files_accessed.py index 8c7891a9030..67cdbe9b969 100755 --- a/testing/ef_tests/check_all_files_accessed.py +++ b/testing/ef_tests/check_all_files_accessed.py @@ -22,6 +22,8 @@ excluded_paths = [ # TODO(das): remove once electra tests are on unstable "tests/.*/electra/", + # TODO(das): ignore until new spec test release with column subnet count = 64. + "tests/.*/.*/.*/get_custody_columns/", # Eth1Block and PowBlock # # Intentionally omitted, as per https://github.com/sigp/lighthouse/issues/1835 diff --git a/testing/ef_tests/tests/tests.rs b/testing/ef_tests/tests/tests.rs index 11c504c75b0..8775c6d3bd2 100644 --- a/testing/ef_tests/tests/tests.rs +++ b/testing/ef_tests/tests/tests.rs @@ -775,7 +775,9 @@ fn rewards() { } } +// TODO(das): ignore until new spec test release with column subnet count = 64. #[test] +#[ignore] fn get_custody_columns() { GetCustodyColumnsHandler::::default() .run_for_feature(ForkName::Deneb, FeatureName::Eip7594); From 163e17f75aa875e994d04bafa2ed4562adc1b6c9 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Thu, 16 May 2024 00:36:19 +0300 Subject: [PATCH 31/76] Only attempt reconstruct columns once. (#5794) --- .../data_availability_checker/overflow_lru_cache.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs index 759ccc9a2d5..d6fd796158d 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs @@ -65,6 +65,7 @@ pub struct PendingComponents { pub verified_blobs: FixedVector>, E::MaxBlobsPerBlock>, pub verified_data_columns: VariableList, E::NumberOfColumns>, pub executed_block: Option>, + pub reconstruction_started: bool, } pub enum BlockImportRequirement { @@ -278,6 +279,7 @@ impl PendingComponents { verified_blobs: FixedVector::default(), verified_data_columns: VariableList::default(), executed_block: None, + reconstruction_started: false, } } @@ -302,6 +304,7 @@ impl PendingComponents { verified_blobs, verified_data_columns, executed_block, + .. } = self; let blobs_available_timestamp = verified_blobs @@ -360,6 +363,10 @@ impl PendingComponents { ))) } + pub fn reconstruction_started(&mut self) { + self.reconstruction_started = true; + } + /// Returns the epoch of the block if it is cached, otherwise returns the epoch of the first blob. pub fn epoch(&self) -> Option { self.executed_block @@ -838,6 +845,8 @@ impl OverflowLRUCache { // - We >= 50% of columns let data_columns_to_publish = if self.should_reconstruct(&block_import_requirement, &pending_components) { + pending_components.reconstruction_started(); + let timer = metrics::start_timer(&metrics::DATA_AVAILABILITY_RECONSTRUCTION_TIME); let existing_column_indices = pending_components @@ -908,7 +917,8 @@ impl OverflowLRUCache { return false; }; - *num_expected_columns == T::EthSpec::number_of_columns() + !pending_components.reconstruction_started + && *num_expected_columns == T::EthSpec::number_of_columns() && pending_components.verified_data_columns.len() >= T::EthSpec::number_of_columns() / 2 } From 500b828315514f66a96afae1a9e855a55e44d02c Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Thu, 16 May 2024 10:38:23 +0300 Subject: [PATCH 32/76] Re-enable precompute table for peerdas kzg (#5795) --- crypto/kzg/src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crypto/kzg/src/lib.rs b/crypto/kzg/src/lib.rs index 1c4bca05166..3d5461628e6 100644 --- a/crypto/kzg/src/lib.rs +++ b/crypto/kzg/src/lib.rs @@ -46,7 +46,8 @@ impl Kzg { trusted_setup: KzgSettings::load_trusted_setup( &trusted_setup.g1_points(), &trusted_setup.g2_points(), - 0, + // Enable precomputed table for 8 bits + 8, )?, }) } From 4946e724bd70ee8d6f07c0ff97e524de85debfe9 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Thu, 16 May 2024 11:29:44 +0300 Subject: [PATCH 33/76] Update subscription filter. (#5797) --- .../lighthouse_network/src/service/mod.rs | 7 ++-- consensus/types/src/chain_spec.rs | 13 -------- consensus/types/src/data_column_subnet_id.rs | 13 ++++++++ consensus/types/src/eth_spec.rs | 32 +++---------------- 4 files changed, 21 insertions(+), 44 deletions(-) diff --git a/beacon_node/lighthouse_network/src/service/mod.rs b/beacon_node/lighthouse_network/src/service/mod.rs index 4509027c7c9..ba2d7a25ce9 100644 --- a/beacon_node/lighthouse_network/src/service/mod.rs +++ b/beacon_node/lighthouse_network/src/service/mod.rs @@ -240,7 +240,8 @@ impl Network { let max_topics = ctx.chain_spec.attestation_subnet_count as usize + SYNC_COMMITTEE_SUBNET_COUNT as usize + ctx.chain_spec.blob_sidecar_subnet_count as usize - + ctx.chain_spec.data_column_sidecar_subnet_count as usize + // TODO: move to chainspec + + E::data_column_subnet_count() + BASE_CORE_TOPICS.len() + ALTAIR_CORE_TOPICS.len() + CAPELLA_CORE_TOPICS.len() @@ -254,11 +255,11 @@ impl Network { ctx.chain_spec.attestation_subnet_count, SYNC_COMMITTEE_SUBNET_COUNT, ctx.chain_spec.blob_sidecar_subnet_count, - ctx.chain_spec.data_column_sidecar_subnet_count, + E::data_column_subnet_count() as u64, ), // during a fork we subscribe to both the old and new topics max_subscribed_topics: max_topics * 4, - // 162 in theory = (64 attestation + 4 sync committee + 7 core topics + 6 blob topics) * 2 + // 209 in theory = (64 attestation + 4 sync committee + 7 core topics + 6 blob topics + 64 column topics) * 2 max_subscriptions_per_request: max_topics * 2, }; diff --git a/consensus/types/src/chain_spec.rs b/consensus/types/src/chain_spec.rs index 3ce2b5e8edc..facca8faddb 100644 --- a/consensus/types/src/chain_spec.rs +++ b/consensus/types/src/chain_spec.rs @@ -227,7 +227,6 @@ pub struct ChainSpec { pub max_request_data_column_sidecars: u64, pub min_epochs_for_blob_sidecars_requests: u64, pub blob_sidecar_subnet_count: u64, - pub data_column_sidecar_subnet_count: u64, /* * Networking Derived @@ -804,7 +803,6 @@ impl ChainSpec { max_request_data_column_sidecars: default_max_request_data_column_sidecars(), min_epochs_for_blob_sidecars_requests: default_min_epochs_for_blob_sidecars_requests(), blob_sidecar_subnet_count: default_blob_sidecar_subnet_count(), - data_column_sidecar_subnet_count: default_data_column_sidecar_subnet_count(), /* * Derived Deneb Specific @@ -1115,7 +1113,6 @@ impl ChainSpec { max_request_data_column_sidecars: default_max_request_data_column_sidecars(), min_epochs_for_blob_sidecars_requests: 16384, blob_sidecar_subnet_count: default_blob_sidecar_subnet_count(), - data_column_sidecar_subnet_count: default_data_column_sidecar_subnet_count(), /* * Derived Deneb Specific @@ -1316,9 +1313,6 @@ pub struct Config { #[serde(default = "default_blob_sidecar_subnet_count")] #[serde(with = "serde_utils::quoted_u64")] blob_sidecar_subnet_count: u64, - #[serde(default = "default_data_column_sidecar_subnet_count")] - #[serde(with = "serde_utils::quoted_u64")] - data_column_sidecar_subnet_count: u64, #[serde(default = "default_min_per_epoch_churn_limit_electra")] #[serde(with = "serde_utils::quoted_u64")] @@ -1444,10 +1438,6 @@ const fn default_blob_sidecar_subnet_count() -> u64 { 6 } -const fn default_data_column_sidecar_subnet_count() -> u64 { - 32 -} - const fn default_min_per_epoch_churn_limit_electra() -> u64 { 128_000_000_000 } @@ -1655,7 +1645,6 @@ impl Config { max_request_data_column_sidecars: spec.max_request_data_column_sidecars, min_epochs_for_blob_sidecars_requests: spec.min_epochs_for_blob_sidecars_requests, blob_sidecar_subnet_count: spec.blob_sidecar_subnet_count, - data_column_sidecar_subnet_count: spec.data_column_sidecar_subnet_count, min_per_epoch_churn_limit_electra: spec.min_per_epoch_churn_limit_electra, max_per_epoch_activation_exit_churn_limit: spec @@ -1729,7 +1718,6 @@ impl Config { max_request_data_column_sidecars, min_epochs_for_blob_sidecars_requests, blob_sidecar_subnet_count, - data_column_sidecar_subnet_count, min_per_epoch_churn_limit_electra, max_per_epoch_activation_exit_churn_limit, @@ -1795,7 +1783,6 @@ impl Config { max_request_data_column_sidecars, min_epochs_for_blob_sidecars_requests, blob_sidecar_subnet_count, - data_column_sidecar_subnet_count, min_per_epoch_churn_limit_electra, max_per_epoch_activation_exit_churn_limit, diff --git a/consensus/types/src/data_column_subnet_id.rs b/consensus/types/src/data_column_subnet_id.rs index d57f47352e2..4813f20aa34 100644 --- a/consensus/types/src/data_column_subnet_id.rs +++ b/consensus/types/src/data_column_subnet_id.rs @@ -204,4 +204,17 @@ mod test { } } } + + #[test] + fn test_columns_subnet_conversion() { + for subnet in 0..E::data_column_subnet_count() as u64 { + let subnet_id = DataColumnSubnetId::new(subnet); + for column_index in subnet_id.columns::() { + assert_eq!( + subnet_id, + DataColumnSubnetId::from_column_index::(column_index as usize) + ); + } + } + } } diff --git a/consensus/types/src/eth_spec.rs b/consensus/types/src/eth_spec.rs index 157287c4836..0afdcdb9f34 100644 --- a/consensus/types/src/eth_spec.rs +++ b/consensus/types/src/eth_spec.rs @@ -120,6 +120,7 @@ pub trait EthSpec: type KzgCommitmentsInclusionProofDepth: Unsigned + Clone + Sync + Send + Debug + PartialEq; /* * Config values in PeerDAS + * TODO(das) move to `ChainSpec` */ type CustodyRequirement: Unsigned + Clone + Sync + Send + Debug + PartialEq; type DataColumnSidecarSubnetCount: Unsigned + Clone + Sync + Send + Debug + PartialEq; @@ -152,11 +153,6 @@ pub trait EthSpec: /// Must be set to `BytesPerFieldElement * FieldElementsPerCell`. type BytesPerCell: Unsigned + Clone + Sync + Send + Debug + PartialEq; - /// Number of data columns per subnet. - /// - /// Must be set to `NumberOfColumns / DataColumnSidecarSubnetCount` - type DataColumnsPerSubnet: Unsigned + Clone + Sync + Send + Debug + PartialEq; - /* * New in Electra */ @@ -369,7 +365,9 @@ pub trait EthSpec: } fn data_columns_per_subnet() -> usize { - Self::DataColumnsPerSubnet::to_usize() + Self::number_of_columns() + .safe_div(Self::data_column_subnet_count()) + .expect("Subnet count must be greater than 0") } fn custody_requirement() -> usize { @@ -434,7 +432,6 @@ impl EthSpec for MainnetEthSpec { type CustodyRequirement = U4; type DataColumnSidecarSubnetCount = U64; type NumberOfColumns = U128; - type DataColumnsPerSubnet = U2; type KzgCommitmentsInclusionProofDepth = U4; // inclusion of the whole list of commitments type SyncSubcommitteeSize = U128; // 512 committee size / 4 sync committee subnet count type MaxPendingAttestations = U4096; // 128 max attestations * 32 slots per epoch @@ -488,7 +485,6 @@ impl EthSpec for MinimalEthSpec { type CustodyRequirement = U4; type DataColumnSidecarSubnetCount = U64; type NumberOfColumns = U128; - type DataColumnsPerSubnet = U2; type KzgCommitmentsInclusionProofDepth = U4; params_from_eth_spec!(MainnetEthSpec { @@ -582,7 +578,6 @@ impl EthSpec for GnosisEthSpec { type CustodyRequirement = U4; type DataColumnSidecarSubnetCount = U64; type NumberOfColumns = U128; - type DataColumnsPerSubnet = U2; type KzgCommitmentsInclusionProofDepth = U4; fn default_spec() -> ChainSpec { @@ -593,22 +588,3 @@ impl EthSpec for GnosisEthSpec { EthSpecId::Gnosis } } - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_peer_das_config_all_specs() { - test_peer_das_config::(); - test_peer_das_config::(); - test_peer_das_config::(); - } - - fn test_peer_das_config() { - assert_eq!( - E::data_columns_per_subnet(), - E::number_of_columns() / E::data_column_subnet_count() - ); - } -} From 71520c99b938e608e30a7cc65cd514afe380e3e3 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Thu, 16 May 2024 14:20:11 +0300 Subject: [PATCH 34/76] Remove penalty for duplicate columns (expected due to reconstruction) (#5798) --- .../src/network_beacon_processor/gossip_methods.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) 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 044258611d7..edd145a7168 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -1013,6 +1013,14 @@ impl NetworkBeaconProcessor { } } } + Err(BlockError::BlockIsAlreadyKnown(_)) => { + debug!( + self.log, + "Ignoring gossip column already imported"; + "block_root" => ?block_root, + "data_column_index" => data_column_index, + ); + } Err(err) => { debug!( self.log, @@ -1027,10 +1035,6 @@ impl NetworkBeaconProcessor { PeerAction::MidToleranceError, "bad_gossip_data_column_ssz", ); - trace!( - self.log, - "Invalid gossip data column ssz"; - ); } } } From c98bb523884b61bfd60fac19c39e64935d0b0841 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Thu, 16 May 2024 14:57:33 +0300 Subject: [PATCH 35/76] Revert DAS config for interop testing. Optimise get_custody_columns function. (#5799) --- consensus/types/src/data_column_subnet_id.rs | 6 +++--- consensus/types/src/eth_spec.rs | 12 ++++++------ testing/ef_tests/tests/tests.rs | 2 -- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/consensus/types/src/data_column_subnet_id.rs b/consensus/types/src/data_column_subnet_id.rs index 4813f20aa34..4e656a6f6fc 100644 --- a/consensus/types/src/data_column_subnet_id.rs +++ b/consensus/types/src/data_column_subnet_id.rs @@ -5,7 +5,7 @@ use ethereum_types::U256; use itertools::Itertools; use safe_arith::{ArithError, SafeArith}; use serde::{Deserialize, Serialize}; -use smallvec::SmallVec; +use std::collections::HashSet; use std::fmt::{self, Display}; use std::ops::{Deref, DerefMut}; @@ -67,7 +67,7 @@ impl DataColumnSubnetId { // TODO(das): we could perform check on `custody_subnet_count` here to ensure that it is a valid // value, but here we assume it is valid. - let mut subnets = SmallVec::<[u64; 32]>::new(); + let mut subnets: HashSet = HashSet::new(); let mut current_id = node_id; while (subnets.len() as u64) < custody_subnet_count { let mut node_id_bytes = [0u8; 32]; @@ -80,7 +80,7 @@ impl DataColumnSubnetId { let subnet = hash_prefix_u64 % (E::data_column_subnet_count() as u64); if !subnets.contains(&subnet) { - subnets.push(subnet); + subnets.insert(subnet); } if current_id == U256::MAX { diff --git a/consensus/types/src/eth_spec.rs b/consensus/types/src/eth_spec.rs index 0afdcdb9f34..32d695edd42 100644 --- a/consensus/types/src/eth_spec.rs +++ b/consensus/types/src/eth_spec.rs @@ -429,8 +429,8 @@ impl EthSpec for MainnetEthSpec { type BytesPerBlob = U131072; type BytesPerCell = U2048; type KzgCommitmentInclusionProofDepth = U17; - type CustodyRequirement = U4; - type DataColumnSidecarSubnetCount = U64; + type CustodyRequirement = U1; + type DataColumnSidecarSubnetCount = U32; type NumberOfColumns = U128; type KzgCommitmentsInclusionProofDepth = U4; // inclusion of the whole list of commitments type SyncSubcommitteeSize = U128; // 512 committee size / 4 sync committee subnet count @@ -482,8 +482,8 @@ impl EthSpec for MinimalEthSpec { type FieldElementsPerCell = U64; type FieldElementsPerExtBlob = U8192; type BytesPerCell = U2048; - type CustodyRequirement = U4; - type DataColumnSidecarSubnetCount = U64; + type CustodyRequirement = U1; + type DataColumnSidecarSubnetCount = U32; type NumberOfColumns = U128; type KzgCommitmentsInclusionProofDepth = U4; @@ -575,8 +575,8 @@ impl EthSpec for GnosisEthSpec { type FieldElementsPerCell = U64; type FieldElementsPerExtBlob = U8192; type BytesPerCell = U2048; - type CustodyRequirement = U4; - type DataColumnSidecarSubnetCount = U64; + type CustodyRequirement = U1; + type DataColumnSidecarSubnetCount = U32; type NumberOfColumns = U128; type KzgCommitmentsInclusionProofDepth = U4; diff --git a/testing/ef_tests/tests/tests.rs b/testing/ef_tests/tests/tests.rs index 8775c6d3bd2..11c504c75b0 100644 --- a/testing/ef_tests/tests/tests.rs +++ b/testing/ef_tests/tests/tests.rs @@ -775,9 +775,7 @@ fn rewards() { } } -// TODO(das): ignore until new spec test release with column subnet count = 64. #[test] -#[ignore] fn get_custody_columns() { GetCustodyColumnsHandler::::default() .run_for_feature(ForkName::Deneb, FeatureName::Eip7594); From a88ca3c207111a77482cc5b81b51724b404e3b8b Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Fri, 17 May 2024 12:14:26 +0300 Subject: [PATCH 36/76] Don't perform reconstruction for proposer node as it already has all the columns. (#5806) --- beacon_node/beacon_chain/src/beacon_chain.rs | 37 +++++++++---- .../beacon_chain/src/block_verification.rs | 7 +++ .../src/data_availability_checker.rs | 18 ++++--- .../overflow_lru_cache.rs | 12 +++-- beacon_node/http_api/src/publish_blocks.rs | 52 ++++++++++--------- .../gossip_methods.rs | 8 ++- 6 files changed, 87 insertions(+), 47 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 58872978842..145449c553a 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -3030,11 +3030,11 @@ impl BeaconChain { self.remove_notified(&block_root, r) } - /// Cache the data column in the processing cache, process it, then evict it from the cache if it was + /// Cache the data columns in the processing cache, process it, then evict it from the cache if it was /// imported or errors. - pub async fn process_gossip_data_column( + pub async fn process_gossip_data_columns( self: &Arc, - data_column: GossipVerifiedDataColumn, + data_columns: Vec>, ) -> Result< ( AvailabilityProcessingStatus, @@ -3042,7 +3042,16 @@ impl BeaconChain { ), BlockError, > { - let block_root = data_column.block_root(); + let Ok(block_root) = data_columns + .iter() + .map(|c| c.block_root()) + .unique() + .exactly_one() + else { + return Err(BlockError::InternalError( + "Columns should be from the same block".to_string(), + )); + }; // If this block has already been imported to forkchoice it must have been available, so // we don't need to process its samples again. @@ -3055,7 +3064,7 @@ impl BeaconChain { } let r = self - .check_gossip_data_column_availability_and_import(data_column) + .check_gossip_data_columns_availability_and_import(data_columns) .await; self.remove_notified_custody_columns(&block_root, r) } @@ -3402,9 +3411,9 @@ impl BeaconChain { /// Checks if the provided data column can make any cached blocks available, and imports immediately /// if so, otherwise caches the data column in the data availability checker. - async fn check_gossip_data_column_availability_and_import( + async fn check_gossip_data_columns_availability_and_import( self: &Arc, - data_column: GossipVerifiedDataColumn, + data_columns: Vec>, ) -> Result< ( AvailabilityProcessingStatus, @@ -3412,13 +3421,21 @@ impl BeaconChain { ), BlockError, > { - let slot = data_column.slot(); if let Some(slasher) = self.slasher.as_ref() { - slasher.accept_block_header(data_column.signed_block_header()); + for data_colum in &data_columns { + slasher.accept_block_header(data_colum.signed_block_header()); + } } + + let Ok(slot) = data_columns.iter().map(|c| c.slot()).unique().exactly_one() else { + return Err(BlockError::InternalError( + "Columns for the same block should have matching slot".to_string(), + )); + }; + let (availability, data_columns_to_publish) = self .data_availability_checker - .put_gossip_data_column(data_column)?; + .put_gossip_data_columns(data_columns)?; self.process_availability(slot, availability) .await diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 9dc3ee7029f..942930e183a 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -317,6 +317,13 @@ pub enum BlockError { /// /// This indicates the peer is sending an unexpected gossip blob and should be penalised. BlobNotRequired(Slot), + /// An internal error has occurred when processing the block or sidecars. + /// + /// ## Peer scoring + /// + /// We were unable to process this block due to an internal error. It's unclear if the block is + /// valid. + InternalError(String), } impl From for BlockError { diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index 7e11a163d5a..12b7502e917 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -233,22 +233,26 @@ impl DataAvailabilityChecker { /// /// This should only accept gossip verified data columns, so we should not have to worry about dupes. #[allow(clippy::type_complexity)] - pub fn put_gossip_data_column( + pub fn put_gossip_data_columns( &self, - gossip_data_column: GossipVerifiedDataColumn, + gossip_data_columns: Vec>, ) -> Result<(Availability, DataColumnsToPublish), AvailabilityCheckError> { let Some(kzg) = self.kzg.as_ref() else { return Err(AvailabilityCheckError::KzgNotInitialized); }; - let block_root = gossip_data_column.block_root(); + let block_root = gossip_data_columns + .first() + .ok_or(AvailabilityCheckError::MissingCustodyColumns)? + .block_root(); - // TODO(das): ensure that our custody requirements include this column - let custody_column = - KzgVerifiedCustodyDataColumn::from_asserted_custody(gossip_data_column.into_inner()); + let custody_columns = gossip_data_columns + .into_iter() + .map(|c| KzgVerifiedCustodyDataColumn::from_asserted_custody(c.into_inner())) + .collect::>(); self.availability_cache - .put_kzg_verified_data_columns(kzg, block_root, vec![custody_column]) + .put_kzg_verified_data_columns(kzg, block_root, custody_columns) } /// Check if we have all the blobs for a block. Returns `Availability` which has information diff --git a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs index d6fd796158d..71f55c61468 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs @@ -906,7 +906,7 @@ impl OverflowLRUCache { /// Potentially trigger reconstruction if: /// - Our custody requirement is all columns - /// - We >= 50% of columns + /// - We >= 50% of columns, but not all columns fn should_reconstruct( &self, block_import_requirement: &BlockImportRequirement, @@ -917,9 +917,13 @@ impl OverflowLRUCache { return false; }; - !pending_components.reconstruction_started - && *num_expected_columns == T::EthSpec::number_of_columns() - && pending_components.verified_data_columns.len() >= T::EthSpec::number_of_columns() / 2 + let num_of_columns = T::EthSpec::number_of_columns(); + let has_missing_columns = pending_components.verified_data_columns.len() < num_of_columns; + + has_missing_columns + && !pending_components.reconstruction_started + && *num_expected_columns == num_of_columns + && pending_components.verified_data_columns.len() >= num_of_columns / 2 } pub fn put_kzg_verified_blobs>>( diff --git a/beacon_node/http_api/src/publish_blocks.rs b/beacon_node/http_api/src/publish_blocks.rs index 787e14b6a00..87e79924d3c 100644 --- a/beacon_node/http_api/src/publish_blocks.rs +++ b/beacon_node/http_api/src/publish_blocks.rs @@ -273,31 +273,33 @@ pub async fn publish_block &msg - ); - Err(warp_utils::reject::custom_bad_request(msg)) - }; - } - } + let custody_columns_indices = + network_globals + .custody_columns(block.epoch()) + .map_err(|e| { + warp_utils::reject::broadcast_without_import(format!( + "Failed to compute custody column indices: {:?}", + e + )) + })?; + + let custody_columns = gossip_verified_data_columns + .into_iter() + .filter(|data_column| custody_columns_indices.contains(&data_column.index())) + .collect(); + + if let Err(e) = Box::pin(chain.process_gossip_data_columns(custody_columns)).await { + let msg = format!("Invalid data column: {e}"); + return if let BroadcastValidation::Gossip = validation_level { + Err(warp_utils::reject::broadcast_without_import(msg)) + } else { + error!( + log, + "Invalid blob provided to HTTP API"; + "reason" => &msg + ); + Err(warp_utils::reject::custom_bad_request(msg)) + }; } } 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 edd145a7168..f3a3f00a125 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -981,7 +981,7 @@ impl NetworkBeaconProcessor { match self .chain - .process_gossip_data_column(verified_data_column) + .process_gossip_data_columns(vec![verified_data_column]) .await { Ok((availability, data_columns_to_publish)) => { @@ -1266,6 +1266,12 @@ impl NetworkBeaconProcessor { ); return None; } + Err(e @ BlockError::InternalError(_)) => { + error!(self.log, "Internal block gossip validation error"; + "error" => %e + ); + return None; + } Err(e @ BlockError::BlobNotRequired(_)) => { // TODO(das): penalty not implemented yet as other clients may still send us blobs // during early stage of implementation. From 8059c3aa39ed989ac6a8d57bd2b7f46abaf024a2 Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Fri, 17 May 2024 12:18:50 +0300 Subject: [PATCH 37/76] Multithread compute_cells_and_proofs (#5805) * Multi-thread reconstruct data columns * Multi-thread path for block production --- Cargo.lock | 1 + beacon_node/beacon_chain/Cargo.toml | 5 ++ beacon_node/beacon_chain/benches/benches.rs | 66 +++++++++++++++++++++ consensus/types/src/data_column_sidecar.rs | 61 +++++++++++-------- crypto/kzg/src/lib.rs | 3 +- 5 files changed, 109 insertions(+), 27 deletions(-) create mode 100644 beacon_node/beacon_chain/benches/benches.rs diff --git a/Cargo.lock b/Cargo.lock index d041c005187..1ddcd9beb98 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -761,6 +761,7 @@ version = "0.2.0" dependencies = [ "bitvec 1.0.1", "bls", + "criterion", "derivative", "environment", "eth1", diff --git a/beacon_node/beacon_chain/Cargo.toml b/beacon_node/beacon_chain/Cargo.toml index 32a1056c10e..059245b9a81 100644 --- a/beacon_node/beacon_chain/Cargo.toml +++ b/beacon_node/beacon_chain/Cargo.toml @@ -5,6 +5,10 @@ authors = ["Paul Hauner ", "Age Manning ( + num_of_blobs: usize, + spec: &ChainSpec, +) -> (SignedBeaconBlock, BlobsList) { + let mut block = BeaconBlock::Deneb(BeaconBlockDeneb::empty(spec)); + let mut body = block.body_mut(); + let blob_kzg_commitments = body.blob_kzg_commitments_mut().unwrap(); + *blob_kzg_commitments = + KzgCommitments::::new(vec![KzgCommitment::empty_for_testing(); num_of_blobs]).unwrap(); + + let signed_block = SignedBeaconBlock::from_block(block, Signature::empty()); + + let blobs = (0..num_of_blobs) + .map(|_| Blob::::default()) + .collect::>() + .into(); + + (signed_block, blobs) +} + +fn all_benches(c: &mut Criterion) { + type E = MainnetEthSpec; + + let trusted_setup: TrustedSetup = serde_json::from_reader(TRUSTED_SETUP_BYTES) + .map_err(|e| format!("Unable to read trusted setup file: {}", e)) + .expect("should have trusted setup"); + let kzg = Arc::new(Kzg::new_from_trusted_setup(trusted_setup).expect("should create kzg")); + + for blob_count in [1, 2, 3, 6] { + let kzg = kzg.clone(); + let spec = E::default_spec(); + let (signed_block, blob_sidecars) = create_test_block_and_blobs::(blob_count, &spec); + + let column_sidecars = + DataColumnSidecar::build_sidecars(&blob_sidecars, &signed_block, &kzg.clone()).unwrap(); + + c.bench( + &format!("reconstruct_{}", blob_count), + Benchmark::new("kzg/reconstruct", move |b| { + b.iter(|| { + black_box(DataColumnSidecar::reconstruct( + &kzg, + &column_sidecars.iter().as_slice()[0..column_sidecars.len() / 2], + )) + }) + }), + ); + } +} + +criterion_group!(benches, all_benches,); +criterion_main!(benches); diff --git a/consensus/types/src/data_column_sidecar.rs b/consensus/types/src/data_column_sidecar.rs index 75687a43dfa..bcf7001904f 100644 --- a/consensus/types/src/data_column_sidecar.rs +++ b/consensus/types/src/data_column_sidecar.rs @@ -14,6 +14,7 @@ use kzg::{KzgCommitment, KzgProof}; use merkle_proof::verify_merkle_proof; #[cfg(test)] use mockall_double::double; +use rayon::prelude::*; use safe_arith::ArithError; use serde::{Deserialize, Serialize}; use ssz::Encode; @@ -120,10 +121,15 @@ impl DataColumnSidecar { vec![Vec::with_capacity(E::max_blobs_per_block()); E::number_of_columns()]; // NOTE: assumes blob sidecars are ordered by index - for blob in blobs { - let blob = KzgBlob::from_bytes(blob).map_err(KzgError::from)?; - let (blob_cells, blob_cell_proofs) = kzg.compute_cells_and_proofs(&blob)?; + let blob_cells_and_proofs_vec = blobs + .into_par_iter() + .map(|blob| { + let blob = KzgBlob::from_bytes(blob).map_err(KzgError::from)?; + kzg.compute_cells_and_proofs(&blob) + }) + .collect::, KzgError>>()?; + for (blob_cells, blob_cell_proofs) in blob_cells_and_proofs_vec { // we iterate over each column, and we construct the column from "top to bottom", // pushing on the cell and the corresponding proof at each column index. we do this for // each blob (i.e. the outer loop). @@ -197,31 +203,34 @@ impl DataColumnSidecar { ))?; let num_of_blobs = first_data_column.kzg_commitments.len(); - for row_index in 0..num_of_blobs { - let mut cells: Vec = vec![]; - let mut cell_ids: Vec = vec![]; - for data_column in data_columns { - let cell = - data_column - .column - .get(row_index) - .ok_or(KzgError::InconsistentArrayLength(format!( + let blob_cells_and_proofs_vec = (0..num_of_blobs) + .into_par_iter() + .map(|row_index| { + let mut cells: Vec = vec![]; + let mut cell_ids: Vec = vec![]; + for data_column in data_columns { + let cell = data_column.column.get(row_index).ok_or( + KzgError::InconsistentArrayLength(format!( "Missing data column at index {row_index}" - )))?; - - cells.push(ssz_cell_to_crypto_cell::(cell)?); - cell_ids.push(data_column.index); - } - // recover_all_cells does not expect sorted - let all_cells = kzg.recover_all_cells(&cell_ids, &cells)?; - let blob = kzg.cells_to_blob(&all_cells)?; - - // Note: This function computes all cells and proofs. According to Justin this is okay, - // computing a partial set may be more expensive and requires code paths that don't exist. - // Computing the blobs cells is technically unnecessary but very cheap. It's done here again - // for simplicity. - let (blob_cells, blob_cell_proofs) = kzg.compute_cells_and_proofs(&blob)?; + )), + )?; + + cells.push(ssz_cell_to_crypto_cell::(cell)?); + cell_ids.push(data_column.index); + } + // recover_all_cells does not expect sorted + let all_cells = kzg.recover_all_cells(&cell_ids, &cells)?; + let blob = kzg.cells_to_blob(&all_cells)?; + + // Note: This function computes all cells and proofs. According to Justin this is okay, + // computing a partial set may be more expensive and requires code paths that don't exist. + // Computing the blobs cells is technically unnecessary but very cheap. It's done here again + // for simplicity. + kzg.compute_cells_and_proofs(&blob) + }) + .collect::, KzgError>>()?; + for (blob_cells, blob_cell_proofs) in blob_cells_and_proofs_vec { // we iterate over each column, and we construct the column from "top to bottom", // pushing on the cell and the corresponding proof at each column index. we do this for // each blob (i.e. the outer loop). diff --git a/crypto/kzg/src/lib.rs b/crypto/kzg/src/lib.rs index 3d5461628e6..e41345e7046 100644 --- a/crypto/kzg/src/lib.rs +++ b/crypto/kzg/src/lib.rs @@ -46,7 +46,8 @@ impl Kzg { trusted_setup: KzgSettings::load_trusted_setup( &trusted_setup.g1_points(), &trusted_setup.g2_points(), - // Enable precomputed table for 8 bits + // Enable precomputed table for 8 bits, with 96MB of memory overhead per process + // Ref: https://notes.ethereum.org/@jtraglia/windowed_multiplications 8, )?, }) From bebcabed31a95cf2fdd383e86f4f67b58e5af488 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Fri, 17 May 2024 17:12:53 +0300 Subject: [PATCH 38/76] Fix CI errors. --- beacon_node/lighthouse_network/src/types/globals.rs | 2 +- beacon_node/network/src/sync/block_lookups/common.rs | 2 +- .../network/src/sync/block_lookups/single_block_lookup.rs | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/beacon_node/lighthouse_network/src/types/globals.rs b/beacon_node/lighthouse_network/src/types/globals.rs index 8aae2c5984a..7c6b1e74b09 100644 --- a/beacon_node/lighthouse_network/src/types/globals.rs +++ b/beacon_node/lighthouse_network/src/types/globals.rs @@ -151,7 +151,7 @@ mod test { E::number_of_columns() / E::data_column_subnet_count() * E::custody_requirement(); let globals = NetworkGlobals::::new_test_globals(vec![], &log); let any_epoch = Epoch::new(0); - let columns = globals.custody_columns(any_epoch).unwrap(); + let columns = globals.custody_columns(any_epoch); assert_eq!(columns.len(), default_custody_requirement_column_count); } } diff --git a/beacon_node/network/src/sync/block_lookups/common.rs b/beacon_node/network/src/sync/block_lookups/common.rs index 856d8999f92..aeaa9a0d8a8 100644 --- a/beacon_node/network/src/sync/block_lookups/common.rs +++ b/beacon_node/network/src/sync/block_lookups/common.rs @@ -183,7 +183,7 @@ impl RequestState for CustodyRequestState { cx: &mut SyncNetworkContext, ) -> Result { cx.custody_lookup_request(id, self.block_root, downloaded_block_expected_blobs) - .map_err(|e| LookupRequestError::SendFailedNetwork(e)) + .map_err(LookupRequestError::SendFailedNetwork) } fn send_for_processing( 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 3cac22167e4..9bd5682f5dc 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 @@ -3,7 +3,8 @@ use super::{BlockComponent, PeerId, SINGLE_BLOCK_LOOKUP_MAX_ATTEMPTS}; use crate::sync::block_lookups::common::RequestState; use crate::sync::block_lookups::Id; use crate::sync::network_context::{ - LookupRequestResult, PeerGroup, ReqId, RpcRequestSendError, SendErrorProcessor, SyncNetworkContext, + LookupRequestResult, PeerGroup, ReqId, RpcRequestSendError, SendErrorProcessor, + SyncNetworkContext, }; use beacon_chain::data_column_verification::CustodyDataColumn; use beacon_chain::BeaconChainTypes; From 6965498b7586c8fb8ec207a9c27e022a3dc3de18 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Thu, 23 May 2024 20:49:30 +1000 Subject: [PATCH 39/76] Move PeerDAS type-level config to configurable `ChainSpec` (#5828) * Move PeerDAS type level config to `ChainSpec`. * Fix tests --- .github/workflows/test-suite.yml | 3 +- beacon_node/beacon_chain/benches/benches.rs | 8 +- beacon_node/beacon_chain/src/beacon_chain.rs | 4 +- .../beacon_chain/src/block_verification.rs | 18 +- .../src/block_verification_types.rs | 14 +- beacon_node/beacon_chain/src/builder.rs | 5 +- .../src/data_availability_checker.rs | 38 ++- .../child_components.rs | 94 ------- .../overflow_lru_cache.rs | 121 +++++---- .../src/data_column_verification.rs | 19 +- .../beacon_chain/src/early_attester_cache.rs | 1 - beacon_node/beacon_chain/src/kzg_utils.rs | 5 +- .../src/observed_data_sidecars.rs | 29 +- beacon_node/beacon_chain/src/test_utils.rs | 3 +- beacon_node/beacon_chain/tests/store_tests.rs | 8 +- beacon_node/http_api/src/publish_blocks.rs | 14 +- .../lighthouse_network/src/discovery/enr.rs | 16 +- .../lighthouse_network/src/discovery/mod.rs | 14 +- .../src/discovery/subnet_predicate.rs | 12 +- .../src/peer_manager/peerdb.rs | 5 +- .../lighthouse_network/src/service/mod.rs | 13 +- .../lighthouse_network/src/service/utils.rs | 4 +- .../lighthouse_network/src/types/globals.rs | 22 +- .../gossip_methods.rs | 6 +- beacon_node/network/src/service.rs | 8 +- .../network/src/sync/block_lookups/tests.rs | 17 +- .../src/sync/block_sidecar_coupling.rs | 21 +- .../network/src/sync/network_context.rs | 15 +- beacon_node/network/src/sync/sampling.rs | 8 +- beacon_node/store/src/hot_cold_store.rs | 6 +- beacon_node/store/src/lib.rs | 1 - .../chiado/config.yaml | 4 + .../gnosis/config.yaml | 5 + .../holesky/config.yaml | 5 + .../mainnet/config.yaml | 5 + .../prater/config.yaml | 5 + .../sepolia/config.yaml | 5 + consensus/types/src/chain_spec.rs | 36 +++ consensus/types/src/data_column_sidecar.rs | 47 ++-- consensus/types/src/data_column_subnet_id.rs | 50 ++-- consensus/types/src/eth_spec.rs | 34 --- consensus/types/src/lib.rs | 4 +- consensus/types/src/runtime_var_list.rs | 253 ++++++++++++++---- lcli/src/generate_bootnode_enr.rs | 5 +- lcli/src/main.rs | 6 +- .../environment/tests/testnet_dir/config.yaml | 5 + .../ef_tests/src/cases/get_custody_columns.rs | 10 +- 47 files changed, 637 insertions(+), 394 deletions(-) delete mode 100644 beacon_node/beacon_chain/src/data_availability_checker/child_components.rs diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index 88caac75b51..17f21d13210 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -325,7 +325,8 @@ jobs: run: | make - name: Install lcli - if: env.SELF_HOSTED_RUNNERS == 'false' + # TODO(das): uncomment once merged to unstable +# if: env.SELF_HOSTED_RUNNERS == 'false' run: make install-lcli - name: Run the doppelganger protection failure test script run: | diff --git a/beacon_node/beacon_chain/benches/benches.rs b/beacon_node/beacon_chain/benches/benches.rs index f8f074b17c9..c4ca030e40c 100644 --- a/beacon_node/beacon_chain/benches/benches.rs +++ b/beacon_node/beacon_chain/benches/benches.rs @@ -34,6 +34,7 @@ fn create_test_block_and_blobs( fn all_benches(c: &mut Criterion) { type E = MainnetEthSpec; + let spec = Arc::new(E::default_spec()); let trusted_setup: TrustedSetup = serde_json::from_reader(TRUSTED_SETUP_BYTES) .map_err(|e| format!("Unable to read trusted setup file: {}", e)) @@ -42,11 +43,13 @@ fn all_benches(c: &mut Criterion) { for blob_count in [1, 2, 3, 6] { let kzg = kzg.clone(); - let spec = E::default_spec(); let (signed_block, blob_sidecars) = create_test_block_and_blobs::(blob_count, &spec); let column_sidecars = - DataColumnSidecar::build_sidecars(&blob_sidecars, &signed_block, &kzg.clone()).unwrap(); + DataColumnSidecar::build_sidecars(&blob_sidecars, &signed_block, &kzg.clone(), &spec) + .unwrap(); + + let spec = spec.clone(); c.bench( &format!("reconstruct_{}", blob_count), @@ -55,6 +58,7 @@ fn all_benches(c: &mut Criterion) { black_box(DataColumnSidecar::reconstruct( &kzg, &column_sidecars.iter().as_slice()[0..column_sidecars.len() / 2], + spec.as_ref(), )) }) }), diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 145449c553a..2eff33477ba 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -125,7 +125,7 @@ use task_executor::{ShutdownReason, TaskExecutor}; use tokio_stream::Stream; use tree_hash::TreeHash; use types::blob_sidecar::FixedBlobSidecarList; -use types::data_column_sidecar::{ColumnIndex, DataColumnIdentifier, DataColumnSidecarList}; +use types::data_column_sidecar::{ColumnIndex, DataColumnIdentifier}; use types::payload::BlockProductionVersion; use types::*; @@ -1324,7 +1324,7 @@ impl BeaconChain { ) -> Result, Error> { match self.store.get_data_columns(block_root)? { Some(data_columns) => Ok(data_columns), - None => Ok(DataColumnSidecarList::default()), + None => Ok(RuntimeVariableList::empty(self.spec.number_of_columns)), } } diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 942930e183a..33b8901e7c9 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -101,7 +101,7 @@ use types::data_column_sidecar::DataColumnSidecarError; use types::{ BeaconBlockRef, BeaconState, BeaconStateError, BlobsList, ChainSpec, DataColumnSidecar, DataColumnSubnetId, Epoch, EthSpec, ExecutionBlockHash, FullPayload, Hash256, InconsistentFork, - KzgProofs, PublicKey, PublicKeyBytes, RelativeEpoch, SignedBeaconBlock, + KzgProofs, PublicKey, PublicKeyBytes, RelativeEpoch, RuntimeVariableList, SignedBeaconBlock, SignedBeaconBlockHeader, Slot, }; use types::{BlobSidecar, ExecPayload}; @@ -801,18 +801,22 @@ fn build_gossip_verified_data_columns( ))?; let timer = metrics::start_timer(&metrics::DATA_COLUMN_SIDECAR_COMPUTATION); - let sidecars = DataColumnSidecar::build_sidecars(&blobs, block, kzg)?; + let sidecars = DataColumnSidecar::build_sidecars(&blobs, block, kzg, &chain.spec)?; drop(timer); let mut gossip_verified_data_columns = vec![]; for sidecar in sidecars { - let subnet = - DataColumnSubnetId::from_column_index::(sidecar.index as usize); + let subnet = DataColumnSubnetId::from_column_index::( + sidecar.index as usize, + &chain.spec, + ); let column = GossipVerifiedDataColumn::new(sidecar, subnet.into(), chain)?; gossip_verified_data_columns.push(column); } - let gossip_verified_data_columns = - GossipVerifiedDataColumnList::new(gossip_verified_data_columns) - .map_err(DataColumnSidecarError::SszError)?; + let gossip_verified_data_columns = RuntimeVariableList::new( + gossip_verified_data_columns, + chain.spec.number_of_columns, + ) + .map_err(DataColumnSidecarError::SszError)?; Ok::<_, BlockContentsError>(gossip_verified_data_columns) }) .transpose() diff --git a/beacon_node/beacon_chain/src/block_verification_types.rs b/beacon_node/beacon_chain/src/block_verification_types.rs index 022c4f97e71..3723b22730a 100644 --- a/beacon_node/beacon_chain/src/block_verification_types.rs +++ b/beacon_node/beacon_chain/src/block_verification_types.rs @@ -15,8 +15,8 @@ use std::sync::Arc; use types::blob_sidecar::{self, BlobIdentifier, FixedBlobSidecarList}; use types::data_column_sidecar::{self}; use types::{ - BeaconBlockRef, BeaconState, BlindedPayload, BlobSidecarList, Epoch, EthSpec, Hash256, - SignedBeaconBlock, SignedBeaconBlockHeader, Slot, + BeaconBlockRef, BeaconState, BlindedPayload, BlobSidecarList, ChainSpec, Epoch, EthSpec, + Hash256, RuntimeVariableList, SignedBeaconBlock, SignedBeaconBlockHeader, Slot, }; /// A block that has been received over RPC. It has 2 internal variants: @@ -158,6 +158,7 @@ impl RpcBlock { block_root: Option, block: Arc>, custody_columns: Vec>, + spec: &ChainSpec, ) -> Result { let block_root = block_root.unwrap_or_else(|| get_block_root(&block)); @@ -167,7 +168,10 @@ impl RpcBlock { } // Treat empty data column lists as if they are missing. let inner = if !custody_columns.is_empty() { - RpcBlockInner::BlockAndCustodyColumns(block, VariableList::new(custody_columns)?) + RpcBlockInner::BlockAndCustodyColumns( + block, + RuntimeVariableList::new(custody_columns, spec.number_of_columns)?, + ) } else { RpcBlockInner::Block(block) }; @@ -592,6 +596,7 @@ impl AsBlock for AvailableBlock { } fn into_rpc_block(self) -> RpcBlock { + let number_of_columns = self.spec.number_of_columns; let (block_root, block, blobs_opt, data_columns_opt) = self.deconstruct(); // Circumvent the constructor here, because an Available block will have already had // consistency checks performed. @@ -600,7 +605,7 @@ impl AsBlock for AvailableBlock { (Some(blobs), _) => RpcBlockInner::BlockAndBlobs(block, blobs), (_, Some(data_columns)) => RpcBlockInner::BlockAndCustodyColumns( block, - VariableList::new( + RuntimeVariableList::new( data_columns .into_iter() // TODO(das): This is an ugly hack that should be removed. After updating @@ -609,6 +614,7 @@ impl AsBlock for AvailableBlock { // columns. .map(|d| CustodyDataColumn::from_asserted_custody(d)) .collect(), + number_of_columns, ) .expect("data column list is within bounds"), ), diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index d5b408af526..59b21a462f3 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -11,6 +11,7 @@ use crate::graffiti_calculator::{GraffitiCalculator, GraffitiOrigin}; use crate::head_tracker::HeadTracker; use crate::light_client_server_cache::LightClientServerCache; use crate::migrate::{BackgroundMigrator, MigratorConfig}; +use crate::observed_data_sidecars::ObservedDataSidecars; use crate::persisted_beacon_chain::PersistedBeaconChain; use crate::shuffling_cache::{BlockShufflingIds, ShufflingCache}; use crate::timeout_rw_lock::TimeoutRwLock; @@ -936,8 +937,8 @@ where observed_sync_aggregators: <_>::default(), // TODO: allow for persisting and loading the pool from disk. observed_block_producers: <_>::default(), - observed_column_sidecars: <_>::default(), - observed_blob_sidecars: <_>::default(), + observed_column_sidecars: RwLock::new(ObservedDataSidecars::new(self.spec.clone())), + observed_blob_sidecars: RwLock::new(ObservedDataSidecars::new(self.spec.clone())), observed_slashable: <_>::default(), observed_voluntary_exits: <_>::default(), observed_proposer_slashings: <_>::default(), diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index 12b7502e917..39d2cefc626 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -7,7 +7,6 @@ use crate::{BeaconChain, BeaconChainTypes, BeaconStore}; use kzg::Kzg; use slog::{debug, error, o, Logger}; use slot_clock::SlotClock; -use ssz_types::VariableList; use std::fmt; use std::fmt::Debug; use std::num::NonZeroUsize; @@ -16,7 +15,8 @@ use std::time::Duration; use task_executor::TaskExecutor; use types::blob_sidecar::{BlobIdentifier, BlobSidecar, FixedBlobSidecarList}; use types::{ - BlobSidecarList, ChainSpec, DataColumnSidecar, Epoch, EthSpec, Hash256, SignedBeaconBlock, + BlobSidecarList, ChainSpec, DataColumnSidecar, DataColumnSidecarList, Epoch, EthSpec, Hash256, + RuntimeVariableList, SignedBeaconBlock, }; mod error; @@ -28,7 +28,7 @@ use crate::data_column_verification::{ KzgVerifiedCustodyDataColumn, }; pub use error::{Error as AvailabilityCheckError, ErrorCategory as AvailabilityCheckErrorCategory}; -use types::data_column_sidecar::{DataColumnIdentifier, DataColumnSidecarList}; +use types::data_column_sidecar::DataColumnIdentifier; use types::non_zero_usize::new_non_zero_usize; pub use self::overflow_lru_cache::DataColumnsToPublish; @@ -51,7 +51,7 @@ pub struct DataAvailabilityChecker { availability_cache: Arc>, slot_clock: T::SlotClock, kzg: Option>, - spec: ChainSpec, + spec: Arc, } /// This type is returned after adding a block / blob to the `DataAvailabilityChecker`. @@ -84,14 +84,15 @@ impl DataAvailabilityChecker { log: &Logger, spec: ChainSpec, ) -> Result { + let spec = Arc::new(spec); let custody_subnet_count = if import_all_data_columns { - T::EthSpec::data_column_subnet_count() + spec.data_column_sidecar_subnet_count as usize } else { - T::EthSpec::custody_requirement() + spec.custody_requirement as usize }; let custody_column_count = - custody_subnet_count.saturating_mul(T::EthSpec::data_columns_per_subnet()); + custody_subnet_count.saturating_mul(spec.data_columns_per_subnet()); let overflow_cache = OverflowLRUCache::new( OVERFLOW_LRU_CAPACITY, store, @@ -289,6 +290,7 @@ impl DataAvailabilityChecker { blobs, blobs_available_timestamp: None, data_columns: None, + spec: self.spec.clone(), })) } else { Ok(MaybeAvailableBlock::AvailabilityPending { block_root, block }) @@ -314,14 +316,16 @@ impl DataAvailabilityChecker { blobs_available_timestamp: None, // TODO(das): update store type to prevent this conversion data_columns: Some( - VariableList::new( + RuntimeVariableList::new( data_column_list .into_iter() .map(|d| d.clone_arc()) .collect(), + self.spec.number_of_columns, ) .expect("data column list is within bounds"), ), + spec: self.spec.clone(), })) } else { Ok(MaybeAvailableBlock::AvailabilityPending { block_root, block }) @@ -334,6 +338,7 @@ impl DataAvailabilityChecker { blobs: None, blobs_available_timestamp: None, data_columns: None, + spec: self.spec.clone(), })) } @@ -366,15 +371,16 @@ impl DataAvailabilityChecker { verify_kzg_for_blob_list(all_blobs.iter(), kzg)?; } - let all_data_columns: DataColumnSidecarList = blocks + let all_data_columns = blocks .iter() .filter(|block| self.data_columns_required_for_block(block.as_block())) // this clone is cheap as it's cloning an Arc .filter_map(|block| block.custody_columns().cloned()) .flatten() .map(CustodyDataColumn::into_inner) - .collect::>() - .into(); + .collect::>(); + let all_data_columns = + RuntimeVariableList::from_vec(all_data_columns, self.spec.number_of_columns); // verify kzg for all data columns at once if !all_data_columns.is_empty() { @@ -396,6 +402,7 @@ impl DataAvailabilityChecker { blobs, blobs_available_timestamp: None, data_columns: None, + spec: self.spec.clone(), }) } else { MaybeAvailableBlock::AvailabilityPending { block_root, block } @@ -409,11 +416,13 @@ impl DataAvailabilityChecker { blobs_available_timestamp: None, // TODO(das): update store type to prevent this conversion data_columns: data_columns.map(|data_columns| { - VariableList::new( + RuntimeVariableList::new( data_columns.into_iter().map(|d| d.into_inner()).collect(), + self.spec.number_of_columns, ) .expect("data column list is within bounds") }), + spec: self.spec.clone(), }) } else { MaybeAvailableBlock::AvailabilityPending { block_root, block } @@ -425,6 +434,7 @@ impl DataAvailabilityChecker { blobs: None, blobs_available_timestamp: None, data_columns: None, + spec: self.spec.clone(), }) }; @@ -601,6 +611,7 @@ pub struct AvailableBlock { /// Timestamp at which this block first became available (UNIX timestamp, time since 1970). blobs_available_timestamp: Option, data_columns: Option>, + pub spec: Arc, } impl AvailableBlock { @@ -609,6 +620,7 @@ impl AvailableBlock { block: Arc>, blobs: Option>, data_columns: Option>, + spec: Arc, ) -> Self { Self { block_root, @@ -616,6 +628,7 @@ impl AvailableBlock { blobs, blobs_available_timestamp: None, data_columns, + spec, } } @@ -654,6 +667,7 @@ impl AvailableBlock { blobs, blobs_available_timestamp: _, data_columns, + .. } = self; (block_root, block, blobs, data_columns) } diff --git a/beacon_node/beacon_chain/src/data_availability_checker/child_components.rs b/beacon_node/beacon_chain/src/data_availability_checker/child_components.rs deleted file mode 100644 index 2ae767c3f99..00000000000 --- a/beacon_node/beacon_chain/src/data_availability_checker/child_components.rs +++ /dev/null @@ -1,94 +0,0 @@ -use crate::block_verification_types::RpcBlock; -use bls::Hash256; -use std::sync::Arc; -use types::blob_sidecar::FixedBlobSidecarList; -use types::data_column_sidecar::FixedDataColumnSidecarList; -use types::{BlobSidecar, DataColumnSidecar, EthSpec, SignedBeaconBlock}; - -/// For requests triggered by an `UnknownBlockParent` or `UnknownBlobParent`, this struct -/// is used to cache components as they are sent to the network service. We can't use the -/// data availability cache currently because any blocks or blobs without parents -/// won't pass validation and therefore won't make it into the cache. -pub struct ChildComponents { - pub block_root: Hash256, - pub downloaded_block: Option>>, - pub downloaded_blobs: FixedBlobSidecarList, - pub downloaded_data_columns: FixedDataColumnSidecarList, -} - -impl From> for ChildComponents { - fn from(value: RpcBlock) -> Self { - let (block_root, block, blobs, data_columns) = value.deconstruct(); - let fixed_blobs = blobs.map(|blobs| { - FixedBlobSidecarList::from(blobs.into_iter().map(Some).collect::>()) - }); - let fixed_data_columns = data_columns.map(|data_columns| { - FixedDataColumnSidecarList::from(data_columns.into_iter().map(Some).collect::>()) - }); - Self::new(block_root, Some(block), fixed_blobs, fixed_data_columns) - } -} - -impl ChildComponents { - pub fn empty(block_root: Hash256) -> Self { - Self { - block_root, - downloaded_block: None, - downloaded_blobs: <_>::default(), - downloaded_data_columns: <_>::default(), - } - } - pub fn new( - block_root: Hash256, - block: Option>>, - blobs: Option>, - data_columns: Option>, - ) -> Self { - let mut cache = Self::empty(block_root); - if let Some(block) = block { - cache.merge_block(block); - } - if let Some(blobs) = blobs { - cache.merge_blobs(blobs); - } - if let Some(data_columns) = data_columns { - cache.merge_data_columns(data_columns); - } - cache - } - - pub fn merge_block(&mut self, block: Arc>) { - self.downloaded_block = Some(block); - } - - pub fn merge_blob(&mut self, blob: Arc>) { - if let Some(blob_ref) = self.downloaded_blobs.get_mut(blob.index as usize) { - *blob_ref = Some(blob); - } - } - - pub fn merge_blobs(&mut self, blobs: FixedBlobSidecarList) { - for blob in blobs.iter().flatten() { - self.merge_blob(blob.clone()); - } - } - - pub fn merge_data_column(&mut self, data_column: Arc>) { - if let Some(data_column_ref) = self - .downloaded_data_columns - .get_mut(data_column.index as usize) - { - *data_column_ref = Some(data_column); - } - } - - pub fn merge_data_columns(&mut self, data_columns: FixedDataColumnSidecarList) { - for data_column in data_columns.iter().flatten() { - self.merge_data_column(data_column.clone()); - } - } - - pub fn clear_blobs(&mut self) { - self.downloaded_blobs = FixedBlobSidecarList::default(); - } -} diff --git a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs index 71f55c61468..712d41c2b7f 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs @@ -43,13 +43,15 @@ use lru::LruCache; use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard}; use slog::{debug, trace, Logger}; use ssz::{Decode, Encode}; -use ssz_derive::{Decode, Encode}; use ssz_types::{FixedVector, VariableList}; use std::num::NonZeroUsize; use std::{collections::HashSet, sync::Arc}; use types::blob_sidecar::BlobIdentifier; use types::data_column_sidecar::DataColumnIdentifier; -use types::{BlobSidecar, ChainSpec, ColumnIndex, DataColumnSidecar, Epoch, EthSpec, Hash256}; +use types::{ + BlobSidecar, ChainSpec, ColumnIndex, DataColumnSidecar, Epoch, EthSpec, Hash256, + RuntimeVariableList, +}; pub type DataColumnsToPublish = Option>>>; @@ -59,11 +61,11 @@ pub type DataColumnsToPublish = Option>>>; /// The block has completed all verifications except the availability check. /// TODO(das): this struct can potentially be reafactored as blobs and data columns are mutually /// exclusive and this could simplify `is_importable`. -#[derive(Encode, Decode, Clone)] +#[derive(Clone)] pub struct PendingComponents { pub block_root: Hash256, pub verified_blobs: FixedVector>, E::MaxBlobsPerBlock>, - pub verified_data_columns: VariableList, E::NumberOfColumns>, + pub verified_data_columns: RuntimeVariableList>, pub executed_block: Option>, pub reconstruction_started: bool, } @@ -273,11 +275,11 @@ impl PendingComponents { } /// Returns an empty `PendingComponents` object with the given block root. - pub fn empty(block_root: Hash256) -> Self { + pub fn empty(block_root: Hash256, spec: &ChainSpec) -> Self { Self { block_root, verified_blobs: FixedVector::default(), - verified_data_columns: VariableList::default(), + verified_data_columns: RuntimeVariableList::empty(spec.number_of_columns), executed_block: None, reconstruction_started: false, } @@ -291,7 +293,7 @@ impl PendingComponents { /// reconstructed from disk. Ensure you are not holding any write locks while calling this. pub fn make_available( self, - spec: &ChainSpec, + spec: &Arc, recover: R, ) -> Result, AvailabilityCheckError> where @@ -349,14 +351,16 @@ impl PendingComponents { blobs_available_timestamp, // TODO(das): Update store types to prevent this conversion data_columns: Some( - VariableList::new( + RuntimeVariableList::new( verified_data_columns .into_iter() .map(|d| d.into_inner()) .collect(), + spec.number_of_columns, ) .expect("data column list is within bounds"), ), + spec: spec.clone(), }; Ok(Availability::Available(Box::new( AvailableExecutedBlock::new(available_block, import_data, payload_verification_outcome), @@ -420,10 +424,11 @@ impl OverflowKey { Ok(Self::Blob(blob_id.block_root, blob_id.index as u8)) } - pub fn from_data_column_id( + pub fn from_data_column_id( data_column_id: DataColumnIdentifier, + spec: &ChainSpec, ) -> Result { - if data_column_id.index >= E::number_of_columns() as u64 + if data_column_id.index >= spec.number_of_columns as u64 || data_column_id.index > u8::MAX as u64 { return Err(AvailabilityCheckError::DataColumnIndexInvalid( @@ -448,9 +453,16 @@ impl OverflowKey { /// A wrapper around BeaconStore that implements various /// methods used for saving and retrieving blocks / blobs /// from the store (for organization) -struct OverflowStore(BeaconStore); +struct OverflowStore { + store: BeaconStore, + spec: Arc, +} impl OverflowStore { + fn new(store: BeaconStore, spec: Arc) -> Self { + Self { store, spec } + } + /// Store pending components in the database pub fn persist_pending_components( &self, @@ -461,7 +473,7 @@ impl OverflowStore { if let Some(block) = pending_components.executed_block.take() { let key = OverflowKey::from_block_root(block_root); - self.0 + self.store .hot_db .put_bytes(col.as_str(), &key.as_ssz_bytes(), &block.as_ssz_bytes())? } @@ -475,18 +487,21 @@ impl OverflowStore { index: blob.blob_index(), })?; - self.0 + self.store .hot_db .put_bytes(col.as_str(), &key.as_ssz_bytes(), &blob.as_ssz_bytes())? } for data_column in pending_components.verified_data_columns.into_iter() { - let key = OverflowKey::from_data_column_id::(DataColumnIdentifier { - block_root, - index: data_column.index(), - })?; + let key = OverflowKey::from_data_column_id( + DataColumnIdentifier { + block_root, + index: data_column.index(), + }, + &self.spec, + )?; - self.0.hot_db.put_bytes( + self.store.hot_db.put_bytes( col.as_str(), &key.as_ssz_bytes(), &data_column.as_ssz_bytes(), @@ -504,7 +519,7 @@ impl OverflowStore { // read everything from disk and reconstruct let mut maybe_pending_components = None; for res in self - .0 + .store .hot_db .iter_raw_entries(DBColumn::OverflowLRUCache, block_root.as_bytes()) { @@ -512,7 +527,7 @@ impl OverflowStore { match OverflowKey::from_ssz_bytes(&key_bytes)? { OverflowKey::Block(_) => { maybe_pending_components - .get_or_insert_with(|| PendingComponents::empty(block_root)) + .get_or_insert_with(|| PendingComponents::empty(block_root, &self.spec)) .executed_block = Some(DietAvailabilityPendingExecutedBlock::from_ssz_bytes( value_bytes.as_slice(), @@ -520,7 +535,7 @@ impl OverflowStore { } OverflowKey::Blob(_, index) => { *maybe_pending_components - .get_or_insert_with(|| PendingComponents::empty(block_root)) + .get_or_insert_with(|| PendingComponents::empty(block_root, &self.spec)) .verified_blobs .get_mut(index as usize) .ok_or(AvailabilityCheckError::BlobIndexInvalid(index as u64))? = @@ -530,7 +545,7 @@ impl OverflowStore { let data_column = KzgVerifiedCustodyDataColumn::from_ssz_bytes(value_bytes.as_slice())?; maybe_pending_components - .get_or_insert_with(|| PendingComponents::empty(block_root)) + .get_or_insert_with(|| PendingComponents::empty(block_root, &self.spec)) .merge_data_columns(vec![data_column])?; } } @@ -542,7 +557,11 @@ impl OverflowStore { /// Returns the hashes of all the blocks we have any data for on disk pub fn read_keys_on_disk(&self) -> Result, AvailabilityCheckError> { let mut disk_keys = HashSet::new(); - for res in self.0.hot_db.iter_raw_keys(DBColumn::OverflowLRUCache, &[]) { + for res in self + .store + .hot_db + .iter_raw_keys(DBColumn::OverflowLRUCache, &[]) + { let key_bytes = res?; disk_keys.insert(*OverflowKey::from_ssz_bytes(&key_bytes)?.root()); } @@ -556,7 +575,7 @@ impl OverflowStore { ) -> Result>>, AvailabilityCheckError> { let key = OverflowKey::from_blob_id::(*blob_id)?; - self.0 + self.store .hot_db .get_bytes(DBColumn::OverflowLRUCache.as_str(), &key.as_ssz_bytes())? .map(|blob_bytes| Arc::>::from_ssz_bytes(blob_bytes.as_slice())) @@ -569,9 +588,9 @@ impl OverflowStore { &self, data_column_id: &DataColumnIdentifier, ) -> Result>>, AvailabilityCheckError> { - let key = OverflowKey::from_data_column_id::(*data_column_id)?; + let key = OverflowKey::from_data_column_id(*data_column_id, &self.spec)?; - self.0 + self.store .hot_db .get_bytes(DBColumn::OverflowLRUCache.as_str(), &key.as_ssz_bytes())? .map(|data_column_bytes| { @@ -584,7 +603,7 @@ impl OverflowStore { /// Delete a set of keys from the database pub fn delete_keys(&self, keys: &Vec) -> Result<(), AvailabilityCheckError> { for key in keys { - self.0 + self.store .hot_db .key_delete(DBColumn::OverflowLRUCache.as_str(), &key.as_ssz_bytes())?; } @@ -725,7 +744,7 @@ pub struct OverflowLRUCache { /// The number of data columns the node is custodying. custody_column_count: usize, log: Logger, - spec: ChainSpec, + spec: Arc, } impl OverflowLRUCache { @@ -734,15 +753,15 @@ impl OverflowLRUCache { beacon_store: BeaconStore, custody_column_count: usize, log: Logger, - spec: ChainSpec, + spec: Arc, ) -> Result { - let overflow_store = OverflowStore(beacon_store.clone()); + let overflow_store = OverflowStore::new(beacon_store.clone(), spec.clone()); let mut critical = Critical::new(capacity); critical.reload_store_keys(&overflow_store)?; Ok(Self { critical: RwLock::new(critical), overflow_store, - state_cache: StateLRUCache::new(beacon_store, spec.clone()), + state_cache: StateLRUCache::new(beacon_store, (*spec).clone()), maintenance_lock: Mutex::new(()), capacity, custody_column_count, @@ -833,7 +852,7 @@ impl OverflowLRUCache { // Grab existing entry or create a new entry. let mut pending_components = write_lock .pop_pending_components(block_root, &self.overflow_store)? - .unwrap_or_else(|| PendingComponents::empty(block_root)); + .unwrap_or_else(|| PendingComponents::empty(block_root, &self.spec)); // Merge in the data columns. pending_components.merge_data_columns(kzg_verified_data_columns)?; @@ -860,7 +879,8 @@ impl OverflowLRUCache { // - There are duplicates let all_data_columns = KzgVerifiedCustodyDataColumn::reconstruct_columns( kzg, - &pending_components.verified_data_columns, + pending_components.verified_data_columns.as_slice(), + &self.spec, )?; let data_columns_to_publish = all_data_columns @@ -869,7 +889,8 @@ impl OverflowLRUCache { .map(|d| d.clone_arc()) .collect::>(); - pending_components.verified_data_columns = all_data_columns.into(); + pending_components.verified_data_columns = + RuntimeVariableList::from_vec(all_data_columns, self.spec.number_of_columns); metrics::stop_timer(timer); metrics::inc_counter_by( @@ -917,7 +938,7 @@ impl OverflowLRUCache { return false; }; - let num_of_columns = T::EthSpec::number_of_columns(); + let num_of_columns = self.spec.number_of_columns; let has_missing_columns = pending_components.verified_data_columns.len() < num_of_columns; has_missing_columns @@ -944,7 +965,7 @@ impl OverflowLRUCache { // Grab existing entry or create a new entry. let mut pending_components = write_lock .pop_pending_components(block_root, &self.overflow_store)? - .unwrap_or_else(|| PendingComponents::empty(block_root)); + .unwrap_or_else(|| PendingComponents::empty(block_root, &self.spec)); // Merge in the blobs. pending_components.merge_blobs(fixed_blobs); @@ -983,7 +1004,7 @@ impl OverflowLRUCache { // Grab existing entry or create a new entry. let mut pending_components = write_lock .pop_pending_components(block_root, &self.overflow_store)? - .unwrap_or_else(|| PendingComponents::empty(block_root)); + .unwrap_or_else(|| PendingComponents::empty(block_root, &self.spec)); // Merge in the block. pending_components.merge_block(diet_executed_block); @@ -1133,7 +1154,7 @@ impl OverflowLRUCache { let mut current_block_data: Option = None; for res in self .overflow_store - .0 + .store .hot_db .iter_raw_entries(DBColumn::OverflowLRUCache, &[]) { @@ -1516,7 +1537,7 @@ mod test { let log = test_logger(); let chain_db_path = tempdir().expect("should get temp dir"); let harness = get_deneb_chain(log.clone(), &chain_db_path).await; - let spec = harness.spec.clone(); + let spec = Arc::new(harness.spec.clone()); let test_store = harness.chain.store.clone(); let capacity_non_zero = new_non_zero_usize(capacity); let cache = Arc::new( @@ -1525,7 +1546,7 @@ mod test { test_store, DEFAULT_TEST_CUSTODY_COLUMN_COUNT, harness.logger().clone(), - spec.clone(), + spec, ) .expect("should create cache"), ); @@ -2032,7 +2053,7 @@ mod test { harness.chain.store.clone(), DEFAULT_TEST_CUSTODY_COLUMN_COUNT, harness.logger().clone(), - harness.chain.spec.clone(), + Arc::new(harness.chain.spec.clone()), ) .expect("should recover cache"); // again, everything should be on disk @@ -2333,7 +2354,8 @@ mod pending_components_tests { 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); + let spec = E::default_spec(); + let mut cache = >::empty(block_root, &spec); cache.merge_block(block_commitments); cache.merge_blobs(random_blobs); cache.merge_blobs(blobs); @@ -2347,7 +2369,8 @@ mod pending_components_tests { 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); + let spec = E::default_spec(); + let mut cache = >::empty(block_root, &spec); cache.merge_blobs(random_blobs); cache.merge_block(block_commitments); cache.merge_blobs(blobs); @@ -2362,7 +2385,8 @@ mod pending_components_tests { setup_pending_components(block_commitments, blobs, random_blobs); let block_root = Hash256::zero(); - let mut cache = >::empty(block_root); + let spec = E::default_spec(); + let mut cache = >::empty(block_root, &spec); cache.merge_blobs(random_blobs); cache.merge_blobs(blobs); cache.merge_block(block_commitments); @@ -2377,7 +2401,8 @@ mod pending_components_tests { setup_pending_components(block_commitments, blobs, random_blobs); let block_root = Hash256::zero(); - let mut cache = >::empty(block_root); + let spec = E::default_spec(); + let mut cache = >::empty(block_root, &spec); cache.merge_block(block_commitments); cache.merge_blobs(blobs); cache.merge_blobs(random_blobs); @@ -2392,7 +2417,8 @@ mod pending_components_tests { setup_pending_components(block_commitments, blobs, random_blobs); let block_root = Hash256::zero(); - let mut cache = >::empty(block_root); + let spec = E::default_spec(); + let mut cache = >::empty(block_root, &spec); cache.merge_blobs(blobs); cache.merge_block(block_commitments); cache.merge_blobs(random_blobs); @@ -2407,7 +2433,8 @@ mod pending_components_tests { setup_pending_components(block_commitments, blobs, random_blobs); let block_root = Hash256::zero(); - let mut cache = >::empty(block_root); + let spec = E::default_spec(); + let mut cache = >::empty(block_root, &spec); cache.merge_blobs(blobs); cache.merge_blobs(random_blobs); cache.merge_block(block_commitments); diff --git a/beacon_node/beacon_chain/src/data_column_verification.rs b/beacon_node/beacon_chain/src/data_column_verification.rs index 1ae59da6ed5..5448e1a8be7 100644 --- a/beacon_node/beacon_chain/src/data_column_verification.rs +++ b/beacon_node/beacon_chain/src/data_column_verification.rs @@ -16,8 +16,8 @@ use std::iter; use std::sync::Arc; use types::data_column_sidecar::{ColumnIndex, DataColumnIdentifier}; use types::{ - BeaconStateError, DataColumnSidecar, DataColumnSubnetId, EthSpec, Hash256, - SignedBeaconBlockHeader, Slot, VariableList, + BeaconStateError, ChainSpec, DataColumnSidecar, DataColumnSubnetId, EthSpec, Hash256, + RuntimeVariableList, SignedBeaconBlockHeader, Slot, }; /// An error occurred while validating a gossip data column. @@ -147,10 +147,7 @@ impl From for GossipDataColumnError { } } -pub type GossipVerifiedDataColumnList = VariableList< - GossipVerifiedDataColumn, - <::EthSpec as EthSpec>::NumberOfColumns, ->; +pub type GossipVerifiedDataColumnList = RuntimeVariableList>; /// A wrapper around a `DataColumnSidecar` that indicates it has been approved for re-gossiping on /// the p2p network. @@ -242,8 +239,7 @@ impl KzgVerifiedDataColumn { } } -pub type CustodyDataColumnList = - VariableList, ::NumberOfColumns>; +pub type CustodyDataColumnList = RuntimeVariableList>; /// Data column that we must custody #[derive(Debug, Derivative, Clone, Encode, Decode)] @@ -303,6 +299,7 @@ impl KzgVerifiedCustodyDataColumn { pub fn reconstruct_columns( kzg: &Kzg, partial_set_of_columns: &[Self], + spec: &ChainSpec, ) -> Result, KzgError> { // Will only return an error if: // - < 50% of columns @@ -313,6 +310,7 @@ impl KzgVerifiedCustodyDataColumn { .iter() .map(|d| d.clone_arc()) .collect::>(), + spec, )?; Ok(all_data_columns @@ -376,7 +374,7 @@ pub fn validate_data_column_sidecar_for_gossip( ) -> Result, GossipDataColumnError> { let column_slot = data_column.slot(); - verify_index_matches_subnet(&data_column, subnet)?; + 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_first_sidecar(chain, &data_column)?; @@ -576,9 +574,10 @@ fn verify_proposer_and_signature( fn verify_index_matches_subnet( data_column: &DataColumnSidecar, subnet: u64, + spec: &ChainSpec, ) -> Result<(), GossipDataColumnError> { let expected_subnet: u64 = - DataColumnSubnetId::from_column_index::(data_column.index as usize).into(); + DataColumnSubnetId::from_column_index::(data_column.index as usize, spec).into(); if expected_subnet != subnet { return Err(GossipDataColumnError::InvalidSubnetId { received: subnet, diff --git a/beacon_node/beacon_chain/src/early_attester_cache.rs b/beacon_node/beacon_chain/src/early_attester_cache.rs index 28eb542f106..119f41122bf 100644 --- a/beacon_node/beacon_chain/src/early_attester_cache.rs +++ b/beacon_node/beacon_chain/src/early_attester_cache.rs @@ -6,7 +6,6 @@ use crate::{ use parking_lot::RwLock; use proto_array::Block as ProtoBlock; use std::sync::Arc; -use types::data_column_sidecar::DataColumnSidecarList; use types::*; pub struct CacheItem { diff --git a/beacon_node/beacon_chain/src/kzg_utils.rs b/beacon_node/beacon_chain/src/kzg_utils.rs index 3f5aa46203e..de788234a81 100644 --- a/beacon_node/beacon_chain/src/kzg_utils.rs +++ b/beacon_node/beacon_chain/src/kzg_utils.rs @@ -152,16 +152,17 @@ mod test { let kzg = Kzg::new_from_trusted_setup(trusted_setup).expect("should create kzg"); let column_sidecars = - DataColumnSidecar::build_sidecars(&blob_sidecars, &signed_block, &kzg).unwrap(); + DataColumnSidecar::build_sidecars(&blob_sidecars, &signed_block, &kzg, &spec).unwrap(); // Now reconstruct let reconstructed_columns = DataColumnSidecar::reconstruct( &kzg, &column_sidecars.iter().as_slice()[0..column_sidecars.len() / 2], + &spec, ) .unwrap(); - for i in 0..E::number_of_columns() { + for i in 0..spec.number_of_columns { assert_eq!(reconstructed_columns.get(i), column_sidecars.get(i), "{i}"); } } diff --git a/beacon_node/beacon_chain/src/observed_data_sidecars.rs b/beacon_node/beacon_chain/src/observed_data_sidecars.rs index 9485ef47692..59b343eaa7c 100644 --- a/beacon_node/beacon_chain/src/observed_data_sidecars.rs +++ b/beacon_node/beacon_chain/src/observed_data_sidecars.rs @@ -6,7 +6,7 @@ use crate::observed_block_producers::ProposalKey; use std::collections::{HashMap, HashSet}; use std::marker::PhantomData; -use types::{BlobSidecar, DataColumnSidecar, EthSpec, Slot}; +use types::{BlobSidecar, ChainSpec, DataColumnSidecar, EthSpec, Slot}; #[derive(Debug, PartialEq)] pub enum Error { @@ -23,7 +23,7 @@ pub trait ObservableDataSidecar { fn slot(&self) -> Slot; fn block_proposer_index(&self) -> u64; fn index(&self) -> u64; - fn max_num_of_items() -> usize; + fn max_num_of_items(spec: &ChainSpec) -> usize; } impl ObservableDataSidecar for BlobSidecar { @@ -39,7 +39,7 @@ impl ObservableDataSidecar for BlobSidecar { self.index } - fn max_num_of_items() -> usize { + fn max_num_of_items(_spec: &ChainSpec) -> usize { E::max_blobs_per_block() } } @@ -57,8 +57,8 @@ impl ObservableDataSidecar for DataColumnSidecar { self.index } - fn max_num_of_items() -> usize { - E::number_of_columns() + fn max_num_of_items(spec: &ChainSpec) -> usize { + spec.number_of_columns } } @@ -74,21 +74,21 @@ pub struct ObservedDataSidecars { finalized_slot: Slot, /// Stores all received data indices for a given `(ValidatorIndex, Slot)` tuple. items: HashMap>, + spec: ChainSpec, _phantom: PhantomData, } -impl Default for ObservedDataSidecars { +impl ObservedDataSidecars { /// Instantiates `Self` with `finalized_slot == 0`. - fn default() -> Self { + pub fn new(spec: ChainSpec) -> Self { Self { finalized_slot: Slot::new(0), items: HashMap::new(), + spec, _phantom: PhantomData, } } -} -impl ObservedDataSidecars { /// Observe the `data_sidecar` at (`data_sidecar.block_proposer_index, data_sidecar.slot`). /// This will update `self` so future calls to it indicate that this `data_sidecar` is known. /// @@ -102,7 +102,7 @@ impl ObservedDataSidecars { slot: data_sidecar.slot(), proposer: data_sidecar.block_proposer_index(), }) - .or_insert_with(|| HashSet::with_capacity(T::max_num_of_items())); + .or_insert_with(|| HashSet::with_capacity(T::max_num_of_items(&self.spec))); let did_not_exist = data_indices.insert(data_sidecar.index()); Ok(!did_not_exist) @@ -122,7 +122,7 @@ impl ObservedDataSidecars { } fn sanitize_data_sidecar(&self, data_sidecar: &T) -> Result<(), Error> { - if data_sidecar.index() >= T::max_num_of_items() as u64 { + if data_sidecar.index() >= T::max_num_of_items(&self.spec) as u64 { return Err(Error::InvalidDataIndex(data_sidecar.index())); } let finalized_slot = self.finalized_slot; @@ -150,6 +150,7 @@ impl ObservedDataSidecars { #[cfg(test)] mod tests { use super::*; + use crate::test_utils::test_spec; use bls::Hash256; use std::sync::Arc; use types::MainnetEthSpec; @@ -166,7 +167,8 @@ mod tests { #[test] fn pruning() { - let mut cache = ObservedDataSidecars::>::default(); + let spec = test_spec::(); + let mut cache = ObservedDataSidecars::>::new(spec); assert_eq!(cache.finalized_slot, 0, "finalized slot is zero"); assert_eq!(cache.items.len(), 0, "no slots should be present"); @@ -304,7 +306,8 @@ mod tests { #[test] fn simple_observations() { - let mut cache = ObservedDataSidecars::>::default(); + let spec = test_spec::(); + let mut cache = ObservedDataSidecars::>::new(spec); // Slot 0, index 0 let proposer_index_a = 420; diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index 6b18e095e34..1c3c8689d18 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -2585,13 +2585,14 @@ pub fn generate_rand_block_and_data_columns( fork_name: ForkName, num_blobs: NumBlobs, rng: &mut impl Rng, + spec: &ChainSpec, ) -> ( SignedBeaconBlock>, Vec>>, ) { let (block, blobs) = generate_rand_block_and_blobs(fork_name, num_blobs, rng); let blob: BlobsList = blobs.into_iter().map(|b| b.blob).collect::>().into(); - let data_columns = DataColumnSidecar::build_sidecars(&blob, &block, &KZG) + let data_columns = DataColumnSidecar::build_sidecars(&blob, &block, &KZG, spec) .unwrap() .into(); diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index 1c6849bc9f8..083e368d266 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -2533,7 +2533,13 @@ async fn weak_subjectivity_sync_test(slots: Vec, checkpoint_slot: Slot) { let (block_root, block, blobs, data_columns) = available_blocks[0].clone().deconstruct(); let mut corrupt_block = (*block).clone(); *corrupt_block.signature_mut() = Signature::empty(); - AvailableBlock::__new_for_testing(block_root, Arc::new(corrupt_block), blobs, data_columns) + AvailableBlock::__new_for_testing( + block_root, + Arc::new(corrupt_block), + blobs, + data_columns, + Arc::new(beacon_chain.spec.clone()), + ) }; // Importing the invalid batch should error. diff --git a/beacon_node/http_api/src/publish_blocks.rs b/beacon_node/http_api/src/publish_blocks.rs index 6b7e37f27ea..f2b68233168 100644 --- a/beacon_node/http_api/src/publish_blocks.rs +++ b/beacon_node/http_api/src/publish_blocks.rs @@ -19,11 +19,11 @@ use std::sync::Arc; use std::time::Duration; use tokio::sync::mpsc::UnboundedSender; use tree_hash::TreeHash; -use types::data_column_sidecar::DataColumnSidecarList; use types::{ - AbstractExecPayload, BeaconBlockRef, BlobSidecarList, BlockImportSource, DataColumnSubnetId, - EthSpec, ExecPayload, ExecutionBlockHash, ForkName, FullPayload, FullPayloadBellatrix, Hash256, - SignedBeaconBlock, SignedBlindedBeaconBlock, VariableList, + AbstractExecPayload, BeaconBlockRef, BlobSidecarList, BlockImportSource, DataColumnSidecarList, + DataColumnSubnetId, EthSpec, ExecPayload, ExecutionBlockHash, ForkName, FullPayload, + FullPayloadBellatrix, Hash256, RuntimeVariableList, SignedBeaconBlock, + SignedBlindedBeaconBlock, VariableList, }; use warp::http::StatusCode; use warp::{reply::Response, Rejection, Reply}; @@ -73,6 +73,7 @@ pub async fn publish_block block.slot()); let malicious_withhold_count = chain.config.malicious_withhold_count; + let chain_cloned = chain.clone(); /* actually publish a block */ let publish_block = move |block: Arc>, @@ -135,6 +136,7 @@ pub async fn publish_block( data_col.index as usize, + &chain_cloned.spec, ); pubsub_messages.push(PubsubMessage::DataColumnSidecar(Box::new(( subnet, data_col, @@ -206,7 +208,7 @@ pub async fn publish_block>(); - VariableList::from(data_columns) + RuntimeVariableList::from_vec(data_columns, chain.spec.number_of_columns) }); let block_root = block_root.unwrap_or(gossip_verified_block.block_root); @@ -273,7 +275,7 @@ pub async fn publish_block Result, &'static str>; /// The peerdas custody subnet count associated with the ENR. - fn custody_subnet_count(&self) -> u64; + fn custody_subnet_count(&self, spec: &ChainSpec) -> u64; fn eth2(&self) -> Result; } @@ -66,10 +66,10 @@ impl Eth2Enr for Enr { /// if the custody value is non-existent in the ENR, then we assume the minimum custody value /// defined in the spec. - fn custody_subnet_count(&self) -> u64 { + fn custody_subnet_count(&self, spec: &ChainSpec) -> u64 { self.get(PEERDAS_CUSTODY_SUBNET_COUNT_ENR_KEY) .and_then(|custody_bytes| u64::from_ssz_bytes(custody_bytes).ok()) - .unwrap_or(E::custody_requirement() as u64) + .unwrap_or(spec.custody_requirement) } fn eth2(&self) -> Result { @@ -139,12 +139,13 @@ pub fn build_or_load_enr( config: &NetworkConfig, enr_fork_id: &EnrForkId, log: &slog::Logger, + spec: &ChainSpec, ) -> Result { // Build the local ENR. // Note: Discovery should update the ENR record's IP to the external IP as seen by the // majority of our peers, if the CLI doesn't expressly forbid it. let enr_key = CombinedKey::from_libp2p(local_key)?; - let mut local_enr = build_enr::(&enr_key, config, enr_fork_id)?; + let mut local_enr = build_enr::(&enr_key, config, enr_fork_id, spec)?; use_or_load_enr(&enr_key, &mut local_enr, config, log)?; Ok(local_enr) @@ -155,6 +156,7 @@ pub fn build_enr( enr_key: &CombinedKey, config: &NetworkConfig, enr_fork_id: &EnrForkId, + spec: &ChainSpec, ) -> Result { let mut builder = discv5::enr::Enr::builder(); let (maybe_ipv4_address, maybe_ipv6_address) = &config.enr_address; @@ -236,9 +238,9 @@ pub fn build_enr( // set the "custody_subnet_count" field on our ENR let custody_subnet_count = if config.subscribe_all_data_column_subnets { - E::data_column_subnet_count() as u64 + spec.data_column_sidecar_subnet_count } else { - E::custody_requirement() as u64 + spec.custody_requirement }; builder.add_value( diff --git a/beacon_node/lighthouse_network/src/discovery/mod.rs b/beacon_node/lighthouse_network/src/discovery/mod.rs index 7f8f9c4da60..d260cdb2c90 100644 --- a/beacon_node/lighthouse_network/src/discovery/mod.rs +++ b/beacon_node/lighthouse_network/src/discovery/mod.rs @@ -43,7 +43,7 @@ use std::{ time::{Duration, Instant}, }; use tokio::sync::mpsc; -use types::{EnrForkId, EthSpec}; +use types::{ChainSpec, EnrForkId, EthSpec}; mod subnet_predicate; pub use subnet_predicate::subnet_predicate; @@ -192,6 +192,7 @@ pub struct Discovery { /// Logger for the discovery behaviour. log: slog::Logger, + spec: ChainSpec, } impl Discovery { @@ -201,6 +202,7 @@ impl Discovery { config: &NetworkConfig, network_globals: Arc>, log: &slog::Logger, + spec: &ChainSpec, ) -> error::Result { let log = log.clone(); @@ -325,6 +327,7 @@ impl Discovery { update_ports, log, enr_dir, + spec: spec.clone(), }) } @@ -755,7 +758,7 @@ impl Discovery { // Only start a discovery query if we have a subnet to look for. if !filtered_subnet_queries.is_empty() { // build the subnet predicate as a combination of the eth2_fork_predicate and the subnet predicate - let subnet_predicate = subnet_predicate::(filtered_subnets, &self.log); + let subnet_predicate = subnet_predicate::(filtered_subnets, &self.log, &self.spec); debug!( self.log, @@ -883,7 +886,7 @@ impl Discovery { // Check the specific subnet against the enr let subnet_predicate = - subnet_predicate::(vec![query.subnet], &self.log); + subnet_predicate::(vec![query.subnet], &self.log, &self.spec); r.clone() .into_iter() @@ -1197,11 +1200,12 @@ mod tests { } async fn build_discovery() -> Discovery { + let spec = ChainSpec::default(); let keypair = secp256k1::Keypair::generate(); let mut config = NetworkConfig::default(); config.set_listening_addr(crate::ListenAddress::unused_v4_ports()); let enr_key: CombinedKey = CombinedKey::from_secp256k1(&keypair); - let enr: Enr = build_enr::(&enr_key, &config, &EnrForkId::default()).unwrap(); + let enr: Enr = build_enr::(&enr_key, &config, &EnrForkId::default(), &spec).unwrap(); let log = build_log(slog::Level::Debug, false); let globals = NetworkGlobals::new( enr, @@ -1215,7 +1219,7 @@ mod tests { &log, ); let keypair = keypair.into(); - Discovery::new(keypair, &config, Arc::new(globals), &log) + Discovery::new(keypair, &config, Arc::new(globals), &log, &spec) .await .unwrap() } diff --git a/beacon_node/lighthouse_network/src/discovery/subnet_predicate.rs b/beacon_node/lighthouse_network/src/discovery/subnet_predicate.rs index 8f737eec6ec..f2c8b11174e 100644 --- a/beacon_node/lighthouse_network/src/discovery/subnet_predicate.rs +++ b/beacon_node/lighthouse_network/src/discovery/subnet_predicate.rs @@ -4,14 +4,19 @@ use crate::types::{EnrAttestationBitfield, EnrSyncCommitteeBitfield}; use itertools::Itertools; use slog::trace; use std::ops::Deref; -use types::DataColumnSubnetId; +use types::{ChainSpec, DataColumnSubnetId}; /// Returns the predicate for a given subnet. -pub fn subnet_predicate(subnets: Vec, log: &slog::Logger) -> impl Fn(&Enr) -> bool + Send +pub fn subnet_predicate( + subnets: Vec, + log: &slog::Logger, + spec: &ChainSpec, +) -> impl Fn(&Enr) -> bool + Send where E: EthSpec, { let log_clone = log.clone(); + let spec_clone = spec.clone(); move |enr: &Enr| { let attestation_bitfield: EnrAttestationBitfield = match enr.attestation_bitfield::() @@ -25,7 +30,7 @@ where let sync_committee_bitfield: Result, _> = enr.sync_committee_bitfield::(); - let custody_subnet_count = enr.custody_subnet_count::(); + let custody_subnet_count = enr.custody_subnet_count::(&spec_clone); let predicate = subnets.iter().any(|subnet| match subnet { Subnet::Attestation(s) => attestation_bitfield @@ -38,6 +43,7 @@ where let mut subnets = DataColumnSubnetId::compute_custody_subnets::( enr.node_id().raw().into(), custody_subnet_count, + &spec_clone, ); subnets.contains(s) } diff --git a/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs b/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs index 76dbd483b57..ec9b011d365 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs @@ -14,7 +14,7 @@ use std::{ fmt::Formatter, }; use sync_status::SyncStatus; -use types::EthSpec; +use types::{ChainSpec, EthSpec}; pub mod client; pub mod peer_info; @@ -679,6 +679,7 @@ impl PeerDB { &mut self, peer_id: &PeerId, supernode: bool, + spec: &ChainSpec, ) -> Option { let enr_key = CombinedKey::generate_secp256k1(); let mut enr = Enr::builder().build(&enr_key).unwrap(); @@ -686,7 +687,7 @@ impl PeerDB { if supernode { enr.insert( PEERDAS_CUSTODY_SUBNET_COUNT_ENR_KEY, - &(E::data_column_subnet_count() as u64).as_ssz_bytes(), + &spec.data_column_sidecar_subnet_count.as_ssz_bytes(), &enr_key, ) .expect("u64 can be encoded"); diff --git a/beacon_node/lighthouse_network/src/service/mod.rs b/beacon_node/lighthouse_network/src/service/mod.rs index ba2d7a25ce9..5b39d2751d7 100644 --- a/beacon_node/lighthouse_network/src/service/mod.rs +++ b/beacon_node/lighthouse_network/src/service/mod.rs @@ -39,10 +39,10 @@ use std::{ sync::Arc, task::{Context, Poll}, }; -use types::ForkName; use types::{ consts::altair::SYNC_COMMITTEE_SUBNET_COUNT, EnrForkId, EthSpec, ForkContext, Slot, SubnetId, }; +use types::{ChainSpec, ForkName}; use utils::{build_transport, strip_peer_id, Context as ServiceContext, MAX_CONNECTIONS_PER_PEER}; pub mod api_types; @@ -129,6 +129,7 @@ pub struct Network { pub local_peer_id: PeerId, /// Logger for behaviour actions. log: slog::Logger, + spec: ChainSpec, } /// Implements the combined behaviour for the libp2p service. @@ -161,6 +162,7 @@ impl Network { &config, &ctx.enr_fork_id, &log, + ctx.chain_spec, )?; // Construct the metadata let meta_data = utils::load_or_build_metadata(&config.network_dir, &log); @@ -240,8 +242,7 @@ impl Network { let max_topics = ctx.chain_spec.attestation_subnet_count as usize + SYNC_COMMITTEE_SUBNET_COUNT as usize + ctx.chain_spec.blob_sidecar_subnet_count as usize - // TODO: move to chainspec - + E::data_column_subnet_count() + + ctx.chain_spec.data_column_sidecar_subnet_count as usize + BASE_CORE_TOPICS.len() + ALTAIR_CORE_TOPICS.len() + CAPELLA_CORE_TOPICS.len() @@ -255,7 +256,7 @@ impl Network { ctx.chain_spec.attestation_subnet_count, SYNC_COMMITTEE_SUBNET_COUNT, ctx.chain_spec.blob_sidecar_subnet_count, - E::data_column_subnet_count() as u64, + ctx.chain_spec.data_column_sidecar_subnet_count, ), // during a fork we subscribe to both the old and new topics max_subscribed_topics: max_topics * 4, @@ -330,6 +331,7 @@ impl Network { &config, network_globals.clone(), &log, + ctx.chain_spec, ) .await?; // start searching for peers @@ -462,6 +464,7 @@ impl Network { gossip_cache, local_peer_id, log, + spec: ctx.chain_spec.clone(), }; network.start(&config).await?; @@ -1216,7 +1219,7 @@ impl Network { /// Dial cached Enrs in discovery service that are in the given `subnet_id` and aren't /// in Connected, Dialing or Banned state. fn dial_cached_enrs_in_subnet(&mut self, subnet: Subnet) { - let predicate = subnet_predicate::(vec![subnet], &self.log); + let predicate = subnet_predicate::(vec![subnet], &self.log, &self.spec); let peers_to_dial: Vec = self .discovery() .cached_enrs() diff --git a/beacon_node/lighthouse_network/src/service/utils.rs b/beacon_node/lighthouse_network/src/service/utils.rs index a0b08f7cdd5..cbf9a4a725b 100644 --- a/beacon_node/lighthouse_network/src/service/utils.rs +++ b/beacon_node/lighthouse_network/src/service/utils.rs @@ -232,7 +232,7 @@ pub(crate) fn create_whitelist_filter( attestation_subnet_count: u64, sync_committee_subnet_count: u64, blob_sidecar_subnet_count: u64, - data_column_subnet_count: u64, + data_column_sidecar_subnet_count: u64, ) -> gossipsub::WhitelistSubscriptionFilter { let mut possible_hashes = HashSet::new(); for fork_digest in possible_fork_digests { @@ -261,7 +261,7 @@ pub(crate) fn create_whitelist_filter( for id in 0..blob_sidecar_subnet_count { add(BlobSidecar(id)); } - for id in 0..data_column_subnet_count { + for id in 0..data_column_sidecar_subnet_count { add(DataColumnSidecar(DataColumnSubnetId::new(id))); } } diff --git a/beacon_node/lighthouse_network/src/types/globals.rs b/beacon_node/lighthouse_network/src/types/globals.rs index 7c6b1e74b09..b3d37e23104 100644 --- a/beacon_node/lighthouse_network/src/types/globals.rs +++ b/beacon_node/lighthouse_network/src/types/globals.rs @@ -8,7 +8,7 @@ use crate::{Enr, GossipTopic, Multiaddr, PeerId}; use parking_lot::RwLock; use std::collections::HashSet; use types::data_column_sidecar::ColumnIndex; -use types::{DataColumnSubnetId, Epoch, EthSpec}; +use types::{ChainSpec, DataColumnSubnetId, Epoch, EthSpec}; pub struct NetworkGlobals { /// The current local ENR. @@ -112,11 +112,12 @@ impl NetworkGlobals { } /// Compute custody data columns the node is assigned to custody. - pub fn custody_columns(&self, _epoch: Epoch) -> Vec { + pub fn custody_columns(&self, _epoch: Epoch, spec: &ChainSpec) -> Vec { let enr = self.local_enr(); let node_id = enr.node_id().raw().into(); - let custody_subnet_count = enr.custody_subnet_count::(); - DataColumnSubnetId::compute_custody_columns::(node_id, custody_subnet_count).collect() + let custody_subnet_count = enr.custody_subnet_count::(spec); + DataColumnSubnetId::compute_custody_columns::(node_id, custody_subnet_count, spec) + .collect() } /// TESTING ONLY. Build a dummy NetworkGlobals instance. @@ -146,12 +147,17 @@ mod test { #[test] fn test_custody_count_default() { + let spec = E::default_spec(); let log = logging::test_logger(); - let default_custody_requirement_column_count = - E::number_of_columns() / E::data_column_subnet_count() * E::custody_requirement(); + let default_custody_requirement_column_count = spec.number_of_columns as u64 + / spec.data_column_sidecar_subnet_count + * spec.custody_requirement; let globals = NetworkGlobals::::new_test_globals(vec![], &log); let any_epoch = Epoch::new(0); - let columns = globals.custody_columns(any_epoch); - assert_eq!(columns.len(), default_custody_requirement_column_count); + let columns = globals.custody_columns(any_epoch, &spec); + assert_eq!( + columns.len(), + default_custody_requirement_column_count as usize + ); } } 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 f3a3f00a125..70d652cc85b 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -179,8 +179,10 @@ impl NetworkBeaconProcessor { messages: data_columns_to_publish .iter() .map(|d| { - let subnet = - DataColumnSubnetId::from_column_index::(d.index as usize); + let subnet = DataColumnSubnetId::from_column_index::( + d.index as usize, + &self.chain.spec, + ); PubsubMessage::DataColumnSidecar(Box::new((subnet, d.clone()))) }) .collect(), diff --git a/beacon_node/network/src/service.rs b/beacon_node/network/src/service.rs index 5f922e25c9d..4a53b63917b 100644 --- a/beacon_node/network/src/service.rs +++ b/beacon_node/network/src/service.rs @@ -737,7 +737,8 @@ impl NetworkService { } if self.subscribe_all_data_column_subnets { - for column_subnet in 0..T::EthSpec::data_column_subnet_count() as u64 { + for column_subnet in 0..self.fork_context.spec.data_column_sidecar_subnet_count + { for fork_digest in self.required_gossip_fork_digests() { let gossip_kind = Subnet::DataColumn(DataColumnSubnetId::new(column_subnet)).into(); @@ -759,7 +760,10 @@ impl NetworkService { self.network_globals.local_enr().node_id().raw().into(), self.network_globals .local_enr() - .custody_subnet_count::<::EthSpec>(), + .custody_subnet_count::<::EthSpec>( + &self.fork_context.spec, + ), + &self.fork_context.spec, ) { for fork_digest in self.required_gossip_fork_digests() { let gossip_kind = Subnet::DataColumn(column_subnet).into(); diff --git a/beacon_node/network/src/sync/block_lookups/tests.rs b/beacon_node/network/src/sync/block_lookups/tests.rs index 00a36c79df8..b95a4fdbc77 100644 --- a/beacon_node/network/src/sync/block_lookups/tests.rs +++ b/beacon_node/network/src/sync/block_lookups/tests.rs @@ -245,7 +245,12 @@ impl TestRig { &mut self, ) -> (SignedBeaconBlock, Vec>>) { let num_blobs = NumBlobs::Number(1); - generate_rand_block_and_data_columns::(self.fork_name, num_blobs, &mut self.rng) + generate_rand_block_and_data_columns::( + self.fork_name, + num_blobs, + &mut self.rng, + &self.harness.spec, + ) } pub fn rand_block_and_parent( @@ -364,7 +369,7 @@ impl TestRig { self.network_globals .peers .write() - .__add_connected_peer_testing_only(&peer_id, false); + .__add_connected_peer_testing_only(&peer_id, false, &self.harness.spec); peer_id } @@ -373,7 +378,7 @@ impl TestRig { self.network_globals .peers .write() - .__add_connected_peer_testing_only(&peer_id, true); + .__add_connected_peer_testing_only(&peer_id, true, &self.harness.spec); peer_id } @@ -1845,6 +1850,7 @@ fn custody_lookup_happy_path() { let Some(mut r) = TestRig::test_setup_after_peerdas() else { return; }; + let spec = E::default_spec(); r.new_connected_peers_for_peerdas(); let (block, data_columns) = r.rand_block_and_data_columns(); let block_root = block.canonical_root(); @@ -1853,8 +1859,9 @@ fn custody_lookup_happy_path() { // Should not request blobs let id = r.expect_block_lookup_request(block.canonical_root()); r.complete_valid_block_request(id, block.into(), true); - let custody_column_count = E::custody_requirement() * E::data_columns_per_subnet(); - let custody_ids = r.expect_only_data_columns_by_root_requests(block_root, custody_column_count); + let custody_column_count = spec.custody_requirement * spec.data_columns_per_subnet() as u64; + let custody_ids = + r.expect_only_data_columns_by_root_requests(block_root, custody_column_count as usize); r.complete_valid_custody_request(custody_ids, data_columns, false); r.expect_no_active_lookups(); } diff --git a/beacon_node/network/src/sync/block_sidecar_coupling.rs b/beacon_node/network/src/sync/block_sidecar_coupling.rs index f2f22d1ecc1..56e0a51522f 100644 --- a/beacon_node/network/src/sync/block_sidecar_coupling.rs +++ b/beacon_node/network/src/sync/block_sidecar_coupling.rs @@ -6,7 +6,9 @@ use std::{ collections::{HashMap, VecDeque}, sync::Arc, }; -use types::{BlobSidecar, ColumnIndex, DataColumnSidecar, EthSpec, Hash256, SignedBeaconBlock}; +use types::{ + BlobSidecar, ChainSpec, ColumnIndex, DataColumnSidecar, EthSpec, Hash256, SignedBeaconBlock, +}; #[derive(Debug)] pub struct RangeBlockComponentsRequest { @@ -69,9 +71,9 @@ impl RangeBlockComponentsRequest { } } - pub fn into_responses(self) -> Result>, String> { + pub fn into_responses(self, spec: &ChainSpec) -> Result>, String> { if let Some(expects_custody_columns) = self.expects_custody_columns.clone() { - self.into_responses_with_custody_columns(expects_custody_columns) + self.into_responses_with_custody_columns(expects_custody_columns, spec) } else { self.into_responses_with_blobs() } @@ -123,6 +125,7 @@ impl RangeBlockComponentsRequest { fn into_responses_with_custody_columns( self, expects_custody_columns: Vec, + spec: &ChainSpec, ) -> Result>, String> { let RangeBlockComponentsRequest { blocks, @@ -185,7 +188,7 @@ impl RangeBlockComponentsRequest { )); } - RpcBlock::new_with_custody_columns(Some(block_root), block, custody_columns) + RpcBlock::new_with_custody_columns(Some(block_root), block, custody_columns, spec) .map_err(|e| format!("{e:?}"))? } else { RpcBlock::new_without_blobs(Some(block_root), block) @@ -221,7 +224,7 @@ impl RangeBlockComponentsRequest { mod tests { use super::RangeBlockComponentsRequest; use beacon_chain::test_utils::{ - generate_rand_block_and_blobs, generate_rand_block_and_data_columns, NumBlobs, + generate_rand_block_and_blobs, generate_rand_block_and_data_columns, test_spec, NumBlobs, }; use rand::SeedableRng; use types::{test_utils::XorShiftRng, ForkName, MinimalEthSpec as E}; @@ -242,7 +245,7 @@ mod tests { // Assert response is finished and RpcBlocks can be constructed assert!(info.is_finished()); - info.into_responses().unwrap(); + info.into_responses(&test_spec::()).unwrap(); } #[test] @@ -268,11 +271,12 @@ mod tests { // This makes sure we don't expect blobs here when they have expired. Checking this logic should // be hendled elsewhere. assert!(info.is_finished()); - info.into_responses().unwrap(); + info.into_responses(&test_spec::()).unwrap(); } #[test] fn rpc_block_with_custody_columns() { + let spec = test_spec::(); let expects_custody_columns = vec![1, 2, 3, 4]; let mut info = RangeBlockComponentsRequest::::new(false, Some(expects_custody_columns.clone())); @@ -283,6 +287,7 @@ mod tests { ForkName::Deneb, NumBlobs::Number(1), &mut rng, + &spec, ) }) .collect::>(); @@ -322,6 +327,6 @@ mod tests { } // All completed construct response - info.into_responses().unwrap(); + info.into_responses(&spec).unwrap(); } } diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index f5ffcf88c0e..c2f595a44d2 100644 --- a/beacon_node/network/src/sync/network_context.rs +++ b/beacon_node/network/src/sync/network_context.rs @@ -233,16 +233,17 @@ impl SyncNetworkContext { for (peer_id, peer_info) in self.network_globals().peers.read().connected_peers() { if let Some(enr) = peer_info.enr() { - let custody_subnet_count = enr.custody_subnet_count::(); + let custody_subnet_count = enr.custody_subnet_count::(&self.chain.spec); // TODO(das): consider caching a map of subnet -> Vec and invalidating // whenever a peer connected or disconnect event in received let mut subnets = DataColumnSubnetId::compute_custody_subnets::( enr.node_id().raw().into(), custody_subnet_count, + &self.chain.spec, ); if subnets.any(|subnet| { subnet - .columns::() + .columns::(&self.chain.spec) .any(|index| index == column_index) }) { peer_ids.push(*peer_id) @@ -345,7 +346,9 @@ impl SyncNetworkContext { let expects_custody_columns = if matches!(batch_type, ByRangeRequestType::BlocksAndColumns) { - let custody_indexes = self.network_globals().custody_columns(epoch); + let custody_indexes = self + .network_globals() + .custody_columns(epoch, &self.chain.spec); for column_index in &custody_indexes { let custody_peer_ids = self.get_custodial_peers(epoch, *column_index); @@ -432,7 +435,7 @@ impl SyncNetworkContext { let (expects_blobs, expects_custody_columns) = info.get_requirements(); Some(BlocksAndBlobsByRangeResponse { sender_id, - responses: info.into_responses(), + responses: info.into_responses(&self.chain.spec), expects_blobs, expects_custody_columns, }) @@ -668,7 +671,9 @@ impl SyncNetworkContext { // TODO(das): figure out how to pass block.slot if we end up doing rotation let block_epoch = Epoch::new(0); - let custody_indexes_duty = self.network_globals().custody_columns(block_epoch); + let custody_indexes_duty = self + .network_globals() + .custody_columns(block_epoch, &self.chain.spec); // Include only the blob indexes not yet imported (received through gossip) let custody_indexes_to_fetch = custody_indexes_duty diff --git a/beacon_node/network/src/sync/sampling.rs b/beacon_node/network/src/sync/sampling.rs index 0cb06980618..17359c47a7d 100644 --- a/beacon_node/network/src/sync/sampling.rs +++ b/beacon_node/network/src/sync/sampling.rs @@ -10,7 +10,7 @@ use std::{ collections::hash_map::Entry, collections::HashMap, marker::PhantomData, sync::Arc, time::Duration, }; -use types::{data_column_sidecar::ColumnIndex, DataColumnSidecar, EthSpec, Hash256, Slot}; +use types::{data_column_sidecar::ColumnIndex, ChainSpec, DataColumnSidecar, Hash256, Slot}; pub type SamplingResult = Result<(), SamplingError>; @@ -69,6 +69,7 @@ impl Sampling { id, &self.sampling_config, self.log.clone(), + &cx.chain.spec, )), Entry::Occupied(_) => { // Sampling is triggered from multiple sources, duplicate sampling requests are @@ -201,10 +202,11 @@ impl ActiveSamplingRequest { requester_id: SamplingRequester, sampling_config: &SamplingConfig, log: slog::Logger, + spec: &ChainSpec, ) -> Self { // Select ahead of time the full list of to-sample columns - let mut column_shuffle = (0..::number_of_columns() as ColumnIndex) - .collect::>(); + let mut column_shuffle = + (0..spec.number_of_columns as ColumnIndex).collect::>(); let mut rng = thread_rng(); column_shuffle.shuffle(&mut rng); diff --git a/beacon_node/store/src/hot_cold_store.rs b/beacon_node/store/src/hot_cold_store.rs index 9987db357f6..0009ab34481 100644 --- a/beacon_node/store/src/hot_cold_store.rs +++ b/beacon_node/store/src/hot_cold_store.rs @@ -41,7 +41,6 @@ use std::num::NonZeroUsize; use std::path::Path; use std::sync::Arc; use std::time::Duration; -use types::data_column_sidecar::DataColumnSidecarList; use types::*; /// On-disk database that stores finalized states efficiently. @@ -1670,7 +1669,10 @@ impl, Cold: ItemStore> HotColdDB .get_bytes(DBColumn::BeaconDataColumn.into(), block_root.as_bytes())? { Some(ref data_columns_bytes) => { - let data_columns = DataColumnSidecarList::from_ssz_bytes(data_columns_bytes)?; + let data_columns = RuntimeVariableList::from_ssz_bytes( + data_columns_bytes, + self.spec.number_of_columns, + )?; self.block_cache .lock() .put_data_columns(*block_root, data_columns.clone()); diff --git a/beacon_node/store/src/lib.rs b/beacon_node/store/src/lib.rs index 04fd983e52a..2b648d71dbc 100644 --- a/beacon_node/store/src/lib.rs +++ b/beacon_node/store/src/lib.rs @@ -45,7 +45,6 @@ pub use metrics::scrape_for_metrics; use parking_lot::MutexGuard; use std::sync::Arc; use strum::{EnumString, IntoStaticStr}; -use types::data_column_sidecar::DataColumnSidecarList; pub use types::*; pub type ColumnIter<'a, K> = Box), Error>> + 'a>; diff --git a/common/eth2_network_config/built_in_network_configs/chiado/config.yaml b/common/eth2_network_config/built_in_network_configs/chiado/config.yaml index c869d9cfc83..9332ddbf24b 100644 --- a/common/eth2_network_config/built_in_network_configs/chiado/config.yaml +++ b/common/eth2_network_config/built_in_network_configs/chiado/config.yaml @@ -137,3 +137,7 @@ MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS: 16384 # `6` BLOB_SIDECAR_SUBNET_COUNT: 6 +# DAS +CUSTODY_REQUIREMENT: 1 +DATA_COLUMN_SIDECAR_SUBNET_COUNT: 32 +NUMBER_OF_COLUMNS: 128 \ No newline at end of file diff --git a/common/eth2_network_config/built_in_network_configs/gnosis/config.yaml b/common/eth2_network_config/built_in_network_configs/gnosis/config.yaml index 50a5fcc3a50..23cf040b276 100644 --- a/common/eth2_network_config/built_in_network_configs/gnosis/config.yaml +++ b/common/eth2_network_config/built_in_network_configs/gnosis/config.yaml @@ -119,3 +119,8 @@ MAX_REQUEST_BLOB_SIDECARS: 768 MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS: 16384 # `6` BLOB_SIDECAR_SUBNET_COUNT: 6 + +# DAS +CUSTODY_REQUIREMENT: 1 +DATA_COLUMN_SIDECAR_SUBNET_COUNT: 32 +NUMBER_OF_COLUMNS: 128 \ No newline at end of file diff --git a/common/eth2_network_config/built_in_network_configs/holesky/config.yaml b/common/eth2_network_config/built_in_network_configs/holesky/config.yaml index 6a399b957d2..cec2b61f213 100644 --- a/common/eth2_network_config/built_in_network_configs/holesky/config.yaml +++ b/common/eth2_network_config/built_in_network_configs/holesky/config.yaml @@ -123,3 +123,8 @@ MAX_REQUEST_BLOB_SIDECARS: 768 MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS: 4096 # `6` BLOB_SIDECAR_SUBNET_COUNT: 6 + +# DAS +CUSTODY_REQUIREMENT: 1 +DATA_COLUMN_SIDECAR_SUBNET_COUNT: 32 +NUMBER_OF_COLUMNS: 128 \ No newline at end of file diff --git a/common/eth2_network_config/built_in_network_configs/mainnet/config.yaml b/common/eth2_network_config/built_in_network_configs/mainnet/config.yaml index b56793869ee..7e17773345c 100644 --- a/common/eth2_network_config/built_in_network_configs/mainnet/config.yaml +++ b/common/eth2_network_config/built_in_network_configs/mainnet/config.yaml @@ -145,3 +145,8 @@ MAX_REQUEST_BLOB_SIDECARS: 768 MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS: 4096 # `6` BLOB_SIDECAR_SUBNET_COUNT: 6 + +# DAS +CUSTODY_REQUIREMENT: 1 +DATA_COLUMN_SIDECAR_SUBNET_COUNT: 32 +NUMBER_OF_COLUMNS: 128 \ No newline at end of file diff --git a/common/eth2_network_config/built_in_network_configs/prater/config.yaml b/common/eth2_network_config/built_in_network_configs/prater/config.yaml index f474b172c51..5b63bfd79b4 100644 --- a/common/eth2_network_config/built_in_network_configs/prater/config.yaml +++ b/common/eth2_network_config/built_in_network_configs/prater/config.yaml @@ -132,3 +132,8 @@ MAX_REQUEST_BLOB_SIDECARS: 768 MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS: 4096 # `6` BLOB_SIDECAR_SUBNET_COUNT: 6 + +# DAS +CUSTODY_REQUIREMENT: 1 +DATA_COLUMN_SIDECAR_SUBNET_COUNT: 32 +NUMBER_OF_COLUMNS: 128 \ No newline at end of file diff --git a/common/eth2_network_config/built_in_network_configs/sepolia/config.yaml b/common/eth2_network_config/built_in_network_configs/sepolia/config.yaml index 72a48679118..2a1809d6ce9 100644 --- a/common/eth2_network_config/built_in_network_configs/sepolia/config.yaml +++ b/common/eth2_network_config/built_in_network_configs/sepolia/config.yaml @@ -119,3 +119,8 @@ MAX_REQUEST_BLOB_SIDECARS: 768 MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS: 4096 # `6` BLOB_SIDECAR_SUBNET_COUNT: 6 + +# DAS +CUSTODY_REQUIREMENT: 1 +DATA_COLUMN_SIDECAR_SUBNET_COUNT: 32 +NUMBER_OF_COLUMNS: 128 \ No newline at end of file diff --git a/consensus/types/src/chain_spec.rs b/consensus/types/src/chain_spec.rs index facca8faddb..8ef505e9eb0 100644 --- a/consensus/types/src/chain_spec.rs +++ b/consensus/types/src/chain_spec.rs @@ -195,6 +195,9 @@ pub struct ChainSpec { * DAS params */ pub eip7594_fork_epoch: Option, + pub custody_requirement: u64, + pub data_column_sidecar_subnet_count: u64, + pub number_of_columns: usize, /* * Networking @@ -576,6 +579,12 @@ impl ChainSpec { } } + pub fn data_columns_per_subnet(&self) -> usize { + self.number_of_columns + .safe_div(self.data_column_sidecar_subnet_count as usize) + .expect("Subnet count must be greater than 0") + } + /// Returns a `ChainSpec` compatible with the Ethereum Foundation specification. pub fn mainnet() -> Self { Self { @@ -770,6 +779,9 @@ impl ChainSpec { * DAS params */ eip7594_fork_epoch: None, + custody_requirement: 1, + data_column_sidecar_subnet_count: 32, + number_of_columns: 128, /* * Network specific @@ -1081,6 +1093,9 @@ impl ChainSpec { * DAS params */ eip7594_fork_epoch: None, + custody_requirement: 1, + data_column_sidecar_subnet_count: 32, + number_of_columns: 128, /* * Network specific */ @@ -1320,6 +1335,13 @@ pub struct Config { #[serde(default = "default_max_per_epoch_activation_exit_churn_limit")] #[serde(with = "serde_utils::quoted_u64")] max_per_epoch_activation_exit_churn_limit: u64, + + #[serde(with = "serde_utils::quoted_u64")] + custody_requirement: u64, + #[serde(with = "serde_utils::quoted_u64")] + data_column_sidecar_subnet_count: u64, + #[serde(with = "serde_utils::quoted_u64")] + number_of_columns: u64, } fn default_bellatrix_fork_version() -> [u8; 4] { @@ -1649,6 +1671,10 @@ impl Config { min_per_epoch_churn_limit_electra: spec.min_per_epoch_churn_limit_electra, max_per_epoch_activation_exit_churn_limit: spec .max_per_epoch_activation_exit_churn_limit, + + custody_requirement: spec.custody_requirement, + data_column_sidecar_subnet_count: spec.data_column_sidecar_subnet_count, + number_of_columns: spec.number_of_columns as u64, } } @@ -1721,6 +1747,9 @@ impl Config { min_per_epoch_churn_limit_electra, max_per_epoch_activation_exit_churn_limit, + custody_requirement, + data_column_sidecar_subnet_count, + number_of_columns, } = self; if preset_base != E::spec_name().to_string().as_str() { @@ -1797,6 +1826,10 @@ impl Config { max_request_data_column_sidecars, ), + custody_requirement, + data_column_sidecar_subnet_count, + number_of_columns: number_of_columns as usize, + ..chain_spec.clone() }) } @@ -2037,6 +2070,9 @@ mod yaml_tests { DEPOSIT_CHAIN_ID: 1 DEPOSIT_NETWORK_ID: 1 DEPOSIT_CONTRACT_ADDRESS: 0x00000000219ab540356cBB839Cbe05303d7705Fa + CUSTODY_REQUIREMENT: 1 + DATA_COLUMN_SIDECAR_SUBNET_COUNT: 32 + NUMBER_OF_COLUMNS: 128 "#; let chain_spec: Config = serde_yaml::from_str(spec).unwrap(); diff --git a/consensus/types/src/data_column_sidecar.rs b/consensus/types/src/data_column_sidecar.rs index bcf7001904f..74f9a9b4223 100644 --- a/consensus/types/src/data_column_sidecar.rs +++ b/consensus/types/src/data_column_sidecar.rs @@ -1,8 +1,8 @@ use crate::beacon_block_body::{KzgCommitments, BLOB_KZG_COMMITMENTS_INDEX}; use crate::test_utils::TestRandom; use crate::{ - BeaconBlockHeader, EthSpec, Hash256, KzgProofs, SignedBeaconBlock, SignedBeaconBlockHeader, - Slot, + BeaconBlockHeader, ChainSpec, EthSpec, Hash256, KzgProofs, RuntimeVariableList, + SignedBeaconBlock, SignedBeaconBlockHeader, Slot, }; use crate::{BeaconStateError, BlobsList}; use bls::Signature; @@ -22,6 +22,7 @@ use ssz_derive::{Decode, Encode}; use ssz_types::typenum::Unsigned; use ssz_types::Error as SszError; use ssz_types::{FixedVector, VariableList}; +use std::hash::Hash; use std::sync::Arc; use test_random_derive::TestRandom; use tree_hash::TreeHash; @@ -40,6 +41,8 @@ pub struct DataColumnIdentifier { pub index: ColumnIndex, } +pub type DataColumnSidecarList = RuntimeVariableList>>; + #[derive( Debug, Clone, @@ -102,9 +105,11 @@ impl DataColumnSidecar { blobs: &BlobsList, block: &SignedBeaconBlock, kzg: &Kzg, + spec: &ChainSpec, ) -> Result, DataColumnSidecarError> { + let number_of_columns = spec.number_of_columns; if blobs.is_empty() { - return Ok(DataColumnSidecarList::empty()); + return Ok(RuntimeVariableList::empty(number_of_columns)); } let kzg_commitments = block .message() @@ -115,10 +120,9 @@ impl DataColumnSidecar { block.message().body().kzg_commitments_merkle_proof()?; let signed_block_header = block.signed_block_header(); - let mut columns = - vec![Vec::with_capacity(E::max_blobs_per_block()); E::number_of_columns()]; + let mut columns = vec![Vec::with_capacity(E::max_blobs_per_block()); number_of_columns]; let mut column_kzg_proofs = - vec![Vec::with_capacity(E::max_blobs_per_block()); E::number_of_columns()]; + vec![Vec::with_capacity(E::max_blobs_per_block()); number_of_columns]; // NOTE: assumes blob sidecars are ordered by index let blob_cells_and_proofs_vec = blobs @@ -133,7 +137,7 @@ impl DataColumnSidecar { // we iterate over each column, and we construct the column from "top to bottom", // pushing on the cell and the corresponding proof at each column index. we do this for // each blob (i.e. the outer loop). - for col in 0..E::number_of_columns() { + for col in 0..number_of_columns { let cell = blob_cells .get(col) @@ -185,16 +189,20 @@ impl DataColumnSidecar { }) }) .collect(); - let sidecars = DataColumnSidecarList::from(sidecars); + let sidecars = RuntimeVariableList::from_vec(sidecars, number_of_columns); Ok(sidecars) } - pub fn reconstruct(kzg: &Kzg, data_columns: &[Arc]) -> Result>, KzgError> { - let mut columns = - vec![Vec::with_capacity(E::max_blobs_per_block()); E::number_of_columns()]; + pub fn reconstruct( + kzg: &Kzg, + data_columns: &[Arc], + spec: &ChainSpec, + ) -> Result>, KzgError> { + let number_of_columns = spec.number_of_columns; + let mut columns = vec![Vec::with_capacity(E::max_blobs_per_block()); number_of_columns]; let mut column_kzg_proofs = - vec![Vec::with_capacity(E::max_blobs_per_block()); E::number_of_columns()]; + vec![Vec::with_capacity(E::max_blobs_per_block()); number_of_columns]; let first_data_column = data_columns .first() @@ -234,7 +242,7 @@ impl DataColumnSidecar { // we iterate over each column, and we construct the column from "top to bottom", // pushing on the cell and the corresponding proof at each column index. we do this for // each blob (i.e. the outer loop). - for col in 0..E::number_of_columns() { + for col in 0..number_of_columns { let cell = blob_cells .get(col) .ok_or(KzgError::InconsistentArrayLength(format!( @@ -391,11 +399,6 @@ impl From for DataColumnSidecarError { } } -pub type DataColumnSidecarList = - VariableList>, ::NumberOfColumns>; -pub type FixedDataColumnSidecarList = - FixedVector>>, ::NumberOfColumns>; - /// Converts a cell ssz List object to an array to be used with the kzg /// crypto library. fn ssz_cell_to_crypto_cell(cell: &Cell) -> Result { @@ -425,7 +428,8 @@ mod test { let mock_kzg = Arc::new(Kzg::default()); let column_sidecars = - DataColumnSidecar::build_sidecars(&blob_sidecars, &signed_block, &mock_kzg).unwrap(); + DataColumnSidecar::build_sidecars(&blob_sidecars, &signed_block, &mock_kzg, &spec) + .unwrap(); assert!(column_sidecars.is_empty()); } @@ -443,7 +447,8 @@ mod test { .returning(kzg::mock::compute_cells_and_proofs); let column_sidecars = - DataColumnSidecar::build_sidecars(&blob_sidecars, &signed_block, &mock_kzg).unwrap(); + DataColumnSidecar::build_sidecars(&blob_sidecars, &signed_block, &mock_kzg, &spec) + .unwrap(); let block_kzg_commitments = signed_block .message() @@ -457,7 +462,7 @@ mod test { .kzg_commitments_merkle_proof() .unwrap(); - assert_eq!(column_sidecars.len(), E::number_of_columns()); + assert_eq!(column_sidecars.len(), spec.number_of_columns); for (idx, col_sidecar) in column_sidecars.iter().enumerate() { assert_eq!(col_sidecar.index, idx as u64); diff --git a/consensus/types/src/data_column_subnet_id.rs b/consensus/types/src/data_column_subnet_id.rs index 4e656a6f6fc..4b8b12e39ce 100644 --- a/consensus/types/src/data_column_subnet_id.rs +++ b/consensus/types/src/data_column_subnet_id.rs @@ -1,6 +1,6 @@ //! Identifies each data column subnet by an integer identifier. use crate::data_column_sidecar::ColumnIndex; -use crate::EthSpec; +use crate::{ChainSpec, EthSpec}; use ethereum_types::U256; use itertools::Itertools; use safe_arith::{ArithError, SafeArith}; @@ -41,20 +41,21 @@ impl DataColumnSubnetId { id.into() } - pub fn from_column_index(column_index: usize) -> Self { + pub fn from_column_index(column_index: usize, spec: &ChainSpec) -> Self { (column_index - .safe_rem(E::data_column_subnet_count()) - .expect("data_column_subnet_count should never be zero if this function is called") - as u64) + .safe_rem(spec.data_column_sidecar_subnet_count as usize) + .expect( + "data_column_sidecar_subnet_count should never be zero if this function is called", + ) as u64) .into() } #[allow(clippy::arithmetic_side_effects)] - pub fn columns(&self) -> impl Iterator { + pub fn columns(&self, spec: &ChainSpec) -> impl Iterator { let subnet = self.0; - let data_column_subnet_count = E::data_column_subnet_count() as u64; - let columns_per_subnet = E::data_columns_per_subnet() as u64; - (0..columns_per_subnet).map(move |i| data_column_subnet_count * i + subnet) + let data_column_sidecar_subnet = spec.data_column_sidecar_subnet_count; + let columns_per_subnet = spec.data_columns_per_subnet() as u64; + (0..columns_per_subnet).map(move |i| data_column_sidecar_subnet * i + subnet) } /// Compute required subnets to subscribe to given the node id. @@ -63,6 +64,7 @@ impl DataColumnSubnetId { pub fn compute_custody_subnets( node_id: U256, custody_subnet_count: u64, + spec: &ChainSpec, ) -> impl Iterator { // TODO(das): we could perform check on `custody_subnet_count` here to ensure that it is a valid // value, but here we assume it is valid. @@ -77,7 +79,7 @@ impl DataColumnSubnetId { hash[0], hash[1], hash[2], hash[3], hash[4], hash[5], hash[6], hash[7], ]; let hash_prefix_u64 = u64::from_le_bytes(hash_prefix); - let subnet = hash_prefix_u64 % (E::data_column_subnet_count() as u64); + let subnet = hash_prefix_u64 % spec.data_column_sidecar_subnet_count; if !subnets.contains(&subnet) { subnets.insert(subnet); @@ -94,9 +96,10 @@ impl DataColumnSubnetId { pub fn compute_custody_columns( node_id: U256, custody_subnet_count: u64, + spec: &ChainSpec, ) -> impl Iterator { - Self::compute_custody_subnets::(node_id, custody_subnet_count) - .flat_map(|subnet| subnet.columns::()) + Self::compute_custody_subnets::(node_id, custody_subnet_count, spec) + .flat_map(|subnet| subnet.columns::(spec)) .sorted() } } @@ -166,6 +169,7 @@ mod test { #[test] fn test_compute_subnets_for_data_column() { + let spec = E::default_spec(); let node_ids = [ "0", "88752428858350697756262172400162263450541348766581994718383409852729519486397", @@ -184,22 +188,25 @@ mod test { let custody_requirement = 4; for node_id in node_ids { - let computed_subnets = - DataColumnSubnetId::compute_custody_subnets::(node_id, custody_requirement); + let computed_subnets = DataColumnSubnetId::compute_custody_subnets::( + node_id, + custody_requirement, + &spec, + ); let computed_subnets: Vec<_> = computed_subnets.collect(); // the number of subnets is equal to the custody requirement assert_eq!(computed_subnets.len() as u64, custody_requirement); - let subnet_count = E::data_column_subnet_count(); + let subnet_count = spec.data_column_sidecar_subnet_count; for subnet in computed_subnets { - let columns: Vec<_> = subnet.columns::().collect(); + let columns: Vec<_> = subnet.columns::(&spec).collect(); // the number of columns is equal to the specified number of columns per subnet - assert_eq!(columns.len(), E::data_columns_per_subnet()); + assert_eq!(columns.len(), spec.data_columns_per_subnet()); for pair in columns.windows(2) { // each successive column index is offset by the number of subnets - assert_eq!(pair[1] - pair[0], subnet_count as u64); + assert_eq!(pair[1] - pair[0], subnet_count); } } } @@ -207,12 +214,13 @@ mod test { #[test] fn test_columns_subnet_conversion() { - for subnet in 0..E::data_column_subnet_count() as u64 { + let spec = E::default_spec(); + for subnet in 0..spec.data_column_sidecar_subnet_count { let subnet_id = DataColumnSubnetId::new(subnet); - for column_index in subnet_id.columns::() { + for column_index in subnet_id.columns::(&spec) { assert_eq!( subnet_id, - DataColumnSubnetId::from_column_index::(column_index as usize) + DataColumnSubnetId::from_column_index::(column_index as usize, &spec) ); } } diff --git a/consensus/types/src/eth_spec.rs b/consensus/types/src/eth_spec.rs index 32d695edd42..db1b51d3373 100644 --- a/consensus/types/src/eth_spec.rs +++ b/consensus/types/src/eth_spec.rs @@ -118,13 +118,6 @@ pub trait EthSpec: type FieldElementsPerCell: Unsigned + Clone + Sync + Send + Debug + PartialEq; type FieldElementsPerExtBlob: Unsigned + Clone + Sync + Send + Debug + PartialEq; type KzgCommitmentsInclusionProofDepth: Unsigned + Clone + Sync + Send + Debug + PartialEq; - /* - * Config values in PeerDAS - * TODO(das) move to `ChainSpec` - */ - type CustodyRequirement: Unsigned + Clone + Sync + Send + Debug + PartialEq; - type DataColumnSidecarSubnetCount: Unsigned + Clone + Sync + Send + Debug + PartialEq; - type NumberOfColumns: Unsigned + Clone + Sync + Send + Debug + PartialEq; /* * Derived values (set these CAREFULLY) */ @@ -360,24 +353,6 @@ pub trait EthSpec: Self::MaxWithdrawalRequestsPerPayload::to_usize() } - fn number_of_columns() -> usize { - Self::NumberOfColumns::to_usize() - } - - fn data_columns_per_subnet() -> usize { - Self::number_of_columns() - .safe_div(Self::data_column_subnet_count()) - .expect("Subnet count must be greater than 0") - } - - fn custody_requirement() -> usize { - Self::CustodyRequirement::to_usize() - } - - fn data_column_subnet_count() -> usize { - Self::DataColumnSidecarSubnetCount::to_usize() - } - fn kzg_commitments_inclusion_proof_depth() -> usize { Self::KzgCommitmentsInclusionProofDepth::to_usize() } @@ -429,9 +404,6 @@ impl EthSpec for MainnetEthSpec { type BytesPerBlob = U131072; type BytesPerCell = U2048; type KzgCommitmentInclusionProofDepth = U17; - type CustodyRequirement = U1; - type DataColumnSidecarSubnetCount = U32; - type NumberOfColumns = U128; type KzgCommitmentsInclusionProofDepth = U4; // inclusion of the whole list of commitments type SyncSubcommitteeSize = U128; // 512 committee size / 4 sync committee subnet count type MaxPendingAttestations = U4096; // 128 max attestations * 32 slots per epoch @@ -482,9 +454,6 @@ impl EthSpec for MinimalEthSpec { type FieldElementsPerCell = U64; type FieldElementsPerExtBlob = U8192; type BytesPerCell = U2048; - type CustodyRequirement = U1; - type DataColumnSidecarSubnetCount = U32; - type NumberOfColumns = U128; type KzgCommitmentsInclusionProofDepth = U4; params_from_eth_spec!(MainnetEthSpec { @@ -575,9 +544,6 @@ impl EthSpec for GnosisEthSpec { type FieldElementsPerCell = U64; type FieldElementsPerExtBlob = U8192; type BytesPerCell = U2048; - type CustodyRequirement = U1; - type DataColumnSidecarSubnetCount = U32; - type NumberOfColumns = U128; type KzgCommitmentsInclusionProofDepth = U4; fn default_spec() -> ChainSpec { diff --git a/consensus/types/src/lib.rs b/consensus/types/src/lib.rs index 400f8877a2c..79abe7af763 100644 --- a/consensus/types/src/lib.rs +++ b/consensus/types/src/lib.rs @@ -143,7 +143,9 @@ pub use crate::config_and_preset::{ }; pub use crate::consolidation::Consolidation; pub use crate::contribution_and_proof::ContributionAndProof; -pub use crate::data_column_sidecar::{ColumnIndex, DataColumnIdentifier, DataColumnSidecar}; +pub use crate::data_column_sidecar::{ + ColumnIndex, DataColumnIdentifier, DataColumnSidecar, DataColumnSidecarList, +}; pub use crate::data_column_subnet_id::DataColumnSubnetId; pub use crate::deposit::{Deposit, DEPOSIT_TREE_DEPTH}; pub use crate::deposit_data::DepositData; diff --git a/consensus/types/src/runtime_var_list.rs b/consensus/types/src/runtime_var_list.rs index 84ad5d074e7..e9cd7f7b9d5 100644 --- a/consensus/types/src/runtime_var_list.rs +++ b/consensus/types/src/runtime_var_list.rs @@ -1,20 +1,58 @@ -use ssz::{Decode, Encode}; -use ssz_derive::Encode; +use derivative::Derivative; +use serde::{Deserialize, Serialize}; +use ssz::Decode; +use ssz_types::Error; +use std::ops::{Deref, DerefMut, Index, IndexMut}; +use std::slice::SliceIndex; -#[derive(Debug, Clone, PartialEq, Encode)] -#[ssz(struct_behaviour = "transparent")] -pub struct RuntimeVariableList { +/// Emulates a SSZ `List`. +/// +/// An ordered, heap-allocated, variable-length, homogeneous collection of `T`, with no more than +/// `max_len` values. +/// +/// ## Example +/// +/// ``` +/// use ssz_types::{RuntimeVariableList}; +/// +/// let base: Vec = vec![1, 2, 3, 4]; +/// +/// // Create a `RuntimeVariableList` from a `Vec` that has the expected length. +/// let exact: RuntimeVariableList<_> = RuntimeVariableList::from_vec(base.clone(), 4); +/// assert_eq!(&exact[..], &[1, 2, 3, 4]); +/// +/// // Create a `RuntimeVariableList` from a `Vec` that is too long and the `Vec` is truncated. +/// let short: RuntimeVariableList<_> = RuntimeVariableList::from_vec(base.clone(), 3); +/// assert_eq!(&short[..], &[1, 2, 3]); +/// +/// // Create a `RuntimeVariableList` from a `Vec` that is shorter than the maximum. +/// let mut long: RuntimeVariableList<_> = RuntimeVariableList::from_vec(base, 5); +/// assert_eq!(&long[..], &[1, 2, 3, 4]); +/// +/// // Push a value to if it does not exceed the maximum +/// long.push(5).unwrap(); +/// assert_eq!(&long[..], &[1, 2, 3, 4, 5]); +/// +/// // Push a value to if it _does_ exceed the maximum. +/// assert!(long.push(6).is_err()); +/// ``` +#[derive(Debug, Clone, Serialize, Deserialize, Derivative)] +#[derivative(PartialEq, Eq, Hash(bound = "T: std::hash::Hash"))] +#[serde(transparent)] +pub struct RuntimeVariableList { vec: Vec, - #[ssz(skip_serializing, skip_deserializing)] + #[serde(skip)] max_len: usize, } -impl RuntimeVariableList { - pub fn new(vec: Vec, max_len: usize) -> Result { +impl RuntimeVariableList { + /// Returns `Some` if the given `vec` equals the fixed length of `Self`. Otherwise returns + /// `None`. + pub fn new(vec: Vec, max_len: usize) -> Result { if vec.len() <= max_len { Ok(Self { vec, max_len }) } else { - Err(ssz_types::Error::OutOfBounds { + Err(Error::OutOfBounds { i: vec.len(), len: max_len, }) @@ -27,22 +65,50 @@ impl RuntimeVariableList { Self { vec, max_len } } - pub fn to_vec(&self) -> Vec { - self.vec.clone() + /// Create an empty list. + pub fn empty(max_len: usize) -> Self { + Self { + vec: vec![], + max_len, + } } pub fn as_slice(&self) -> &[T] { self.vec.as_slice() } + /// Returns the number of values presently in `self`. pub fn len(&self) -> usize { self.vec.len() } + /// True if `self` does not contain any values. pub fn is_empty(&self) -> bool { - self.vec.is_empty() + self.len() == 0 + } + + /// Returns the type-level maximum length. + pub fn max_len(&self) -> usize { + self.max_len + } + + /// Appends `value` to the back of `self`. + /// + /// Returns `Err(())` when appending `value` would exceed the maximum length. + pub fn push(&mut self, value: T) -> Result<(), Error> { + if self.vec.len() < self.max_len { + self.vec.push(value); + Ok(()) + } else { + Err(Error::OutOfBounds { + i: self.vec.len().saturating_add(1), + len: self.max_len, + }) + } } +} +impl RuntimeVariableList { pub fn from_ssz_bytes(bytes: &[u8], max_len: usize) -> Result { let vec = if bytes.is_empty() { vec![] @@ -54,7 +120,7 @@ impl RuntimeVariableList { if num_items > max_len { return Err(ssz::DecodeError::BytesInvalid(format!( - "VariableList of {} items exceeds maximum of {}", + "RuntimeVariableList of {} items exceeds maximum of {}", num_items, max_len ))); } @@ -73,65 +139,162 @@ impl RuntimeVariableList { } } +impl From> for Vec { + fn from(list: RuntimeVariableList) -> Vec { + list.vec + } +} + +impl> Index for RuntimeVariableList { + type Output = I::Output; + + #[inline] + fn index(&self, index: I) -> &Self::Output { + Index::index(&self.vec, index) + } +} + +impl> IndexMut for RuntimeVariableList { + #[inline] + fn index_mut(&mut self, index: I) -> &mut Self::Output { + IndexMut::index_mut(&mut self.vec, index) + } +} + +impl Deref for RuntimeVariableList { + type Target = [T]; + + fn deref(&self) -> &[T] { + &self.vec[..] + } +} + +impl DerefMut for RuntimeVariableList { + fn deref_mut(&mut self) -> &mut [T] { + &mut self.vec[..] + } +} + +impl<'a, T> IntoIterator for &'a RuntimeVariableList { + type Item = &'a T; + type IntoIter = std::slice::Iter<'a, T>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +impl IntoIterator for RuntimeVariableList { + type Item = T; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.vec.into_iter() + } +} + +impl ssz::Encode for RuntimeVariableList +where + T: ssz::Encode, +{ + fn is_ssz_fixed_len() -> bool { + >::is_ssz_fixed_len() + } + + fn ssz_append(&self, buf: &mut Vec) { + self.vec.ssz_append(buf) + } + + fn ssz_fixed_len() -> usize { + >::ssz_fixed_len() + } + + fn ssz_bytes_len(&self) -> usize { + self.vec.ssz_bytes_len() + } +} + #[cfg(test)] mod test { - use ssz_types::{typenum::U4, VariableList}; - use super::*; + use ssz::*; + use std::fmt::Debug; #[test] fn new() { let vec = vec![42; 5]; - let runtime_var_list: Result, _> = - RuntimeVariableList::new(vec, 4); - assert!(runtime_var_list.is_err()); + let fixed: Result, _> = RuntimeVariableList::new(vec, 4); + assert!(fixed.is_err()); let vec = vec![42; 3]; - let runtime_var_list: Result, _> = - RuntimeVariableList::new(vec, 4); - assert!(runtime_var_list.is_ok()); + let fixed: Result, _> = RuntimeVariableList::new(vec, 4); + assert!(fixed.is_ok()); let vec = vec![42; 4]; - let runtime_var_list: Result, _> = - RuntimeVariableList::new(vec, 4); - assert!(runtime_var_list.is_ok()); + let fixed: Result, _> = RuntimeVariableList::new(vec, 4); + assert!(fixed.is_ok()); + } + + #[test] + fn indexing() { + let vec = vec![1, 2]; + + let mut fixed: RuntimeVariableList = RuntimeVariableList::from_vec(vec.clone(), 8192); + + assert_eq!(fixed[0], 1); + assert_eq!(&fixed[0..1], &vec[0..1]); + assert_eq!(fixed[..].len(), 2); + + fixed[1] = 3; + assert_eq!(fixed[1], 3); } #[test] fn length() { + let vec = vec![42; 5]; + let fixed: RuntimeVariableList = RuntimeVariableList::from_vec(vec.clone(), 4); + assert_eq!(&fixed[..], &vec[0..4]); + let vec = vec![42; 3]; - let runtime_var_list: RuntimeVariableList = - RuntimeVariableList::new(vec.clone(), 4).unwrap(); - let var_list: VariableList = VariableList::from(vec.clone()); - assert_eq!(&runtime_var_list.as_slice()[0..3], &vec[..]); - assert_eq!(runtime_var_list.as_slice(), &vec![42, 42, 42][..]); - assert_eq!(runtime_var_list.len(), var_list.len()); + let fixed: RuntimeVariableList = RuntimeVariableList::from_vec(vec.clone(), 4); + assert_eq!(&fixed[0..3], &vec[..]); + assert_eq!(&fixed[..], &vec![42, 42, 42][..]); let vec = vec![]; - let runtime_var_list: RuntimeVariableList = RuntimeVariableList::new(vec, 4).unwrap(); - assert_eq!(runtime_var_list.as_slice(), &[] as &[u64]); - assert!(runtime_var_list.is_empty()); + let fixed: RuntimeVariableList = RuntimeVariableList::from_vec(vec, 4); + assert_eq!(&fixed[..], &[] as &[u64]); } #[test] - fn encode() { - let runtime_var_list: RuntimeVariableList = - RuntimeVariableList::new(vec![0; 2], 2).unwrap(); + fn deref() { + let vec = vec![0, 2, 4, 6]; + let fixed: RuntimeVariableList = RuntimeVariableList::from_vec(vec, 4); - assert_eq!(runtime_var_list.as_ssz_bytes(), vec![0, 0, 0, 0]); - assert_eq!( as Encode>::ssz_fixed_len(), 4); + assert_eq!(fixed.first(), Some(&0)); + assert_eq!(fixed.get(3), Some(&6)); + assert_eq!(fixed.get(4), None); } #[test] - fn round_trip() { - let item = RuntimeVariableList::::new(vec![42; 8], 8).unwrap(); - let encoded = &item.as_ssz_bytes(); - assert_eq!(item.ssz_bytes_len(), encoded.len()); - assert_eq!(RuntimeVariableList::from_ssz_bytes(encoded, 8), Ok(item)); + fn encode() { + let vec: RuntimeVariableList = RuntimeVariableList::from_vec(vec![0; 2], 2); + assert_eq!(vec.as_ssz_bytes(), vec![0, 0, 0, 0]); + assert_eq!( as Encode>::ssz_fixed_len(), 4); + } - let item = RuntimeVariableList::::new(vec![0; 8], 8).unwrap(); + fn round_trip(item: RuntimeVariableList) { + let max_len = item.max_len(); let encoded = &item.as_ssz_bytes(); assert_eq!(item.ssz_bytes_len(), encoded.len()); - assert_eq!(RuntimeVariableList::from_ssz_bytes(encoded, 8), Ok(item)); + assert_eq!( + RuntimeVariableList::from_ssz_bytes(encoded, max_len), + Ok(item) + ); + } + + #[test] + fn u16_len_8() { + round_trip::(RuntimeVariableList::from_vec(vec![42; 8], 8)); + round_trip::(RuntimeVariableList::from_vec(vec![0; 8], 8)); } } diff --git a/lcli/src/generate_bootnode_enr.rs b/lcli/src/generate_bootnode_enr.rs index 52960b929d8..5dd7bc7bc24 100644 --- a/lcli/src/generate_bootnode_enr.rs +++ b/lcli/src/generate_bootnode_enr.rs @@ -10,7 +10,7 @@ use std::{fs, net::Ipv4Addr}; use std::{fs::File, num::NonZeroU16}; use types::{ChainSpec, EnrForkId, Epoch, EthSpec, Hash256}; -pub fn run(matches: &ArgMatches) -> Result<(), String> { +pub fn run(matches: &ArgMatches, spec: &ChainSpec) -> Result<(), String> { let ip: Ipv4Addr = clap_utils::parse_required(matches, "ip")?; let udp_port: NonZeroU16 = clap_utils::parse_required(matches, "udp-port")?; let tcp_port: NonZeroU16 = clap_utils::parse_required(matches, "tcp-port")?; @@ -37,7 +37,8 @@ pub fn run(matches: &ArgMatches) -> Result<(), String> { next_fork_version: genesis_fork_version, next_fork_epoch: Epoch::max_value(), // FAR_FUTURE_EPOCH }; - let enr = build_enr::(&enr_key, &config, &enr_fork_id) + + let enr = build_enr::(&enr_key, &config, &enr_fork_id, spec) .map_err(|e| format!("Unable to create ENR: {:?}", e))?; fs::create_dir_all(&output_dir).map_err(|e| format!("Unable to create output-dir: {:?}", e))?; diff --git a/lcli/src/main.rs b/lcli/src/main.rs index 7b5c1598c9e..3567626468e 100644 --- a/lcli/src/main.rs +++ b/lcli/src/main.rs @@ -1087,8 +1087,10 @@ fn run( } ("check-deposit-data", Some(matches)) => check_deposit_data::run(matches) .map_err(|e| format!("Failed to run check-deposit-data command: {}", e)), - ("generate-bootnode-enr", Some(matches)) => generate_bootnode_enr::run::(matches) - .map_err(|e| format!("Failed to run generate-bootnode-enr command: {}", e)), + ("generate-bootnode-enr", Some(matches)) => { + generate_bootnode_enr::run::(matches, &env.eth2_config.spec) + .map_err(|e| format!("Failed to run generate-bootnode-enr command: {}", e)) + } ("insecure-validators", Some(matches)) => insecure_validators::run(matches) .map_err(|e| format!("Failed to run insecure-validators command: {}", e)), ("mnemonic-validators", Some(matches)) => mnemonic_validators::run(matches) diff --git a/lighthouse/environment/tests/testnet_dir/config.yaml b/lighthouse/environment/tests/testnet_dir/config.yaml index c71feaa7dc5..4fc7bc2dcff 100644 --- a/lighthouse/environment/tests/testnet_dir/config.yaml +++ b/lighthouse/environment/tests/testnet_dir/config.yaml @@ -98,3 +98,8 @@ ATTESTATION_SUBNET_COUNT: 64 ATTESTATION_SUBNET_EXTRA_BITS: 0 ATTESTATION_SUBNET_PREFIX_BITS: 6 ATTESTATION_SUBNET_SHUFFLING_PREFIX_BITS: 3 + +# DAS +CUSTODY_REQUIREMENT: 1 +DATA_COLUMN_SIDECAR_SUBNET_COUNT: 32 +NUMBER_OF_COLUMNS: 128 \ No newline at end of file diff --git a/testing/ef_tests/src/cases/get_custody_columns.rs b/testing/ef_tests/src/cases/get_custody_columns.rs index 608362bbf0d..efe5b147e44 100644 --- a/testing/ef_tests/src/cases/get_custody_columns.rs +++ b/testing/ef_tests/src/cases/get_custody_columns.rs @@ -22,11 +22,15 @@ impl LoadCase for GetCustodyColumns { impl Case for GetCustodyColumns { fn result(&self, _case_index: usize, _fork_name: ForkName) -> Result<(), Error> { + let spec = E::default_spec(); let node_id = U256::from_dec_str(&self.node_id) .map_err(|e| Error::FailedToParseTest(format!("{e:?}")))?; - let computed = - DataColumnSubnetId::compute_custody_columns::(node_id, self.custody_subnet_count) - .collect::>(); + let computed = DataColumnSubnetId::compute_custody_columns::( + node_id, + self.custody_subnet_count, + &spec, + ) + .collect::>(); let expected = &self.result; if computed == *expected { Ok(()) From 656cd8db66949cfa70a1b3461a102a83b269509c Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Fri, 24 May 2024 16:31:13 +0200 Subject: [PATCH 40/76] Misc custody lookup improvements (#5821) * Improve custody requests * Type DataColumnsByRootRequestId * Prioritize peers and load balance * Update tests * Address PR review --- .../network/src/sync/block_lookups/tests.rs | 137 ++--- beacon_node/network/src/sync/manager.rs | 41 +- .../network/src/sync/network_context.rs | 109 ++-- .../src/sync/network_context/custody.rs | 491 ++++++++++++------ .../src/sync/network_context/requests.rs | 8 +- 5 files changed, 484 insertions(+), 302 deletions(-) diff --git a/beacon_node/network/src/sync/block_lookups/tests.rs b/beacon_node/network/src/sync/block_lookups/tests.rs index b95a4fdbc77..3defe08465e 100644 --- a/beacon_node/network/src/sync/block_lookups/tests.rs +++ b/beacon_node/network/src/sync/block_lookups/tests.rs @@ -1,9 +1,7 @@ use crate::network_beacon_processor::NetworkBeaconProcessor; - use crate::service::RequestId; use crate::sync::manager::{ - DataColumnsByRootRequestId, DataColumnsByRootRequester, RequestId as SyncRequestId, - SingleLookupReqId, SyncManager, + DataColumnsByRootRequester, RequestId as SyncRequestId, SingleLookupReqId, SyncManager, }; use crate::sync::sampling::{SamplingConfig, SamplingRequester}; use crate::sync::{SamplingId, SyncMessage}; @@ -94,7 +92,8 @@ const D: Duration = Duration::new(0, 0); const PARENT_FAIL_TOLERANCE: u8 = SINGLE_BLOCK_LOOKUP_MAX_ATTEMPTS; const SAMPLING_REQUIRED_SUCCESSES: usize = 2; -type SamplingIds = Vec<(DataColumnsByRootRequestId, ColumnIndex)>; +type DCByRootIds = Vec; +type DCByRootId = (SyncRequestId, Vec); struct TestRigConfig { peer_das_enabled: bool, @@ -614,18 +613,18 @@ impl TestRig { }) } - fn return_empty_sampling_requests(&mut self, sampling_ids: SamplingIds) { - for (id, column_index) in sampling_ids { - self.log(&format!("return empty data column for {column_index}")); + fn return_empty_sampling_requests(&mut self, ids: DCByRootIds) { + for id in ids { + self.log(&format!("return empty data column for {id:?}")); self.return_empty_sampling_request(id) } } - fn return_empty_sampling_request(&mut self, id: DataColumnsByRootRequestId) { + fn return_empty_sampling_request(&mut self, (request_id, _): DCByRootId) { let peer_id = PeerId::random(); // Send stream termination self.send_sync_message(SyncMessage::RpcDataColumn { - request_id: SyncRequestId::DataColumnsByRoot(id), + request_id, peer_id, data_column: None, seen_timestamp: timestamp_now(), @@ -660,26 +659,29 @@ impl TestRig { fn complete_valid_sampling_column_requests( &mut self, - sampling_ids: SamplingIds, + ids: DCByRootIds, data_columns: Vec>>, ) { - for (id, column_index) in sampling_ids { - self.log(&format!("return valid data column for {column_index}")); - self.complete_valid_sampling_column_request( - id, - data_columns[column_index as usize].clone(), - ); + for id in ids { + self.log(&format!("return valid data column for {id:?}")); + let indices = &id.1; + let columns_to_send = indices + .iter() + .map(|&i| data_columns[i as usize].clone()) + .collect::>(); + self.complete_valid_sampling_column_request(id, &columns_to_send); } } fn complete_valid_sampling_column_request( &mut self, - id: DataColumnsByRootRequestId, - data_column: Arc>, + id: DCByRootId, + data_columns: &[Arc>], ) { - let block_root = data_column.block_root(); - let column_index = data_column.index; - self.complete_data_columns_by_root_request(id, data_column); + let first_dc = data_columns.first().unwrap(); + let block_root = first_dc.block_root(); + let column_index = first_dc.index; + self.complete_data_columns_by_root_request(id, data_columns); // Expect work event // TODO(das): worth it to append sender id to the work event for stricter assertion? @@ -697,25 +699,29 @@ impl TestRig { fn complete_valid_custody_request( &mut self, - sampling_ids: SamplingIds, + ids: DCByRootIds, data_columns: Vec>>, missing_components: bool, ) { - let lookup_id = if let DataColumnsByRootRequester::Custody(id) = - sampling_ids.first().unwrap().0.requester - { - id.id.0.lookup_id - } else { - panic!("not a custody requester") - }; + let lookup_id = + if let SyncRequestId::DataColumnsByRoot(_, DataColumnsByRootRequester::Custody(id)) = + ids.first().unwrap().0 + { + id.requester.0.lookup_id + } else { + panic!("not a custody requester") + }; let first_column = data_columns.first().cloned().unwrap(); - for (id, column_index) in sampling_ids { - self.log(&format!("return valid data column for {column_index}")); - - let data_column = data_columns[column_index as usize].clone(); - self.complete_data_columns_by_root_request(id, data_column); + for id in ids { + self.log(&format!("return valid data column for {id:?}")); + let indices = &id.1; + let columns_to_send = indices + .iter() + .map(|&i| data_columns[i as usize].clone()) + .collect::>(); + self.complete_data_columns_by_root_request(id, &columns_to_send); } // Expect work event @@ -740,20 +746,22 @@ impl TestRig { fn complete_data_columns_by_root_request( &mut self, - id: DataColumnsByRootRequestId, - data_column: Arc>, + (request_id, _): DCByRootId, + data_columns: &[Arc>], ) { let peer_id = PeerId::random(); - // Send chunk - self.send_sync_message(SyncMessage::RpcDataColumn { - request_id: SyncRequestId::DataColumnsByRoot(id), - peer_id, - data_column: Some(data_column), - seen_timestamp: timestamp_now(), - }); + for data_column in data_columns { + // Send chunks + self.send_sync_message(SyncMessage::RpcDataColumn { + request_id, + peer_id, + data_column: Some(data_column.clone()), + seen_timestamp: timestamp_now(), + }); + } // Send stream termination self.send_sync_message(SyncMessage::RpcDataColumn { - request_id: SyncRequestId::DataColumnsByRoot(id), + request_id, peer_id, data_column: None, seen_timestamp: timestamp_now(), @@ -926,41 +934,54 @@ impl TestRig { .unwrap_or_else(|e| panic!("Expected blob parent request for {for_block:?}: {e}")) } + /// Retrieves an unknown number of requests for data columns of `block_root`. Because peer enrs + /// are random, and peer selection is random the total number of batches requests in unknown. fn expect_data_columns_by_root_requests( &mut self, - for_block: Hash256, + block_root: Hash256, count: usize, - ) -> SamplingIds { - (0..count) - .map(|i| { - self.pop_received_network_event(|ev| match ev { + ) -> DCByRootIds { + let mut requests: DCByRootIds = vec![]; + loop { + let req = self + .pop_received_network_event(|ev| match ev { NetworkMessage::SendRequest { peer_id: _, request: Request::DataColumnsByRoot(request), - request_id: RequestId::Sync(SyncRequestId::DataColumnsByRoot(id)), + request_id: RequestId::Sync(id @ SyncRequestId::DataColumnsByRoot { .. }), } if request .data_column_ids .to_vec() .iter() - .any(|r| r.block_root == for_block) => + .any(|r| r.block_root == block_root) => { - let index = request.data_column_ids.to_vec().first().unwrap().index; - Some((*id, index)) + let indices = request + .data_column_ids + .to_vec() + .iter() + .map(|cid| cid.index) + .collect::>(); + Some((*id, indices)) } _ => None, }) .unwrap_or_else(|e| { - panic!("Expected DataColumnsByRoot request {i}/{count} for {for_block:?}: {e}") - }) - }) - .collect() + panic!("Expected more DataColumnsByRoot requests for {block_root:?}: {e}") + }); + requests.push(req); + + // Should never infinite loop because sync does not send requests for 0 columns + if requests.iter().map(|r| r.1.len()).sum::() >= count { + return requests; + } + } } fn expect_only_data_columns_by_root_requests( &mut self, for_block: Hash256, count: usize, - ) -> SamplingIds { + ) -> DCByRootIds { let ids = self.expect_data_columns_by_root_requests(for_block, count); self.expect_empty_network(); ids diff --git a/beacon_node/network/src/sync/manager.rs b/beacon_node/network/src/sync/manager.rs index 3ef6039f403..5bd90613821 100644 --- a/beacon_node/network/src/sync/manager.rs +++ b/beacon_node/network/src/sync/manager.rs @@ -36,7 +36,8 @@ use super::backfill_sync::{BackFillSync, ProcessResult, SyncStart}; use super::block_lookups::BlockLookups; use super::network_context::{ - BlockOrBlob, CustodyId, RangeRequestId, RpcEvent, SyncNetworkContext, + BlockOrBlob, CustodyId, DataColumnsByRootRequestId, RangeRequestId, RpcEvent, + SyncNetworkContext, }; use super::peer_sync_info::{remote_sync_type, PeerSyncType}; use super::range_sync::{RangeSync, RangeSyncType, EPOCHS_PER_BATCH}; @@ -92,17 +93,11 @@ pub enum RequestId { /// Request searching for a set of blobs given a hash. SingleBlob { id: SingleLookupReqId }, /// Request searching for a set of data columns given a hash and list of column indices. - DataColumnsByRoot(DataColumnsByRootRequestId), + DataColumnsByRoot(DataColumnsByRootRequestId, DataColumnsByRootRequester), /// Range request that is composed by both a block range request and a blob range request. RangeBlockComponents(Id), } -#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] -pub struct DataColumnsByRootRequestId { - pub requester: DataColumnsByRootRequester, - pub req_id: Id, -} - #[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] pub enum DataColumnsByRootRequester { Sampling(SamplingId), @@ -398,9 +393,12 @@ impl SyncManager { RequestId::SingleBlob { id } => { self.on_single_blob_response(id, peer_id, RpcEvent::RPCError(error)) } - RequestId::DataColumnsByRoot(id) => { - self.on_single_data_column_response(id, peer_id, RpcEvent::RPCError(error)) - } + RequestId::DataColumnsByRoot(req_id, requester) => self.on_single_data_column_response( + req_id, + requester, + peer_id, + RpcEvent::RPCError(error), + ), RequestId::RangeBlockComponents(id) => { if let Some(sender_id) = self.network.range_request_failed(id) { match sender_id { @@ -975,9 +973,10 @@ impl SyncManager { RequestId::SingleBlock { .. } | RequestId::SingleBlob { .. } => { crit!(self.log, "bad request id for data_column"; "peer_id" => %peer_id ); } - RequestId::DataColumnsByRoot(id) => { + RequestId::DataColumnsByRoot(req_id, requester) => { self.on_single_data_column_response( - id, + req_id, + requester, peer_id, match data_column { Some(data_column) => RpcEvent::Response(data_column, seen_timestamp), @@ -1015,13 +1014,14 @@ impl SyncManager { fn on_single_data_column_response( &mut self, - id: DataColumnsByRootRequestId, + req_id: DataColumnsByRootRequestId, + requester: DataColumnsByRootRequester, peer_id: PeerId, data_column: RpcEvent>>, ) { - if let Some((requester, resp)) = + if let Some(resp) = self.network - .on_data_columns_by_root_response(id, peer_id, data_column) + .on_data_columns_by_root_response(req_id, peer_id, data_column) { match requester { DataColumnsByRootRequester::Sampling(id) => { @@ -1032,15 +1032,16 @@ impl SyncManager { self.on_sampling_result(requester, result) } } - DataColumnsByRootRequester::Custody(id) => { - if let Some((requester, custody_columns)) = - self.network.on_custody_by_root_response(id, peer_id, resp) + DataColumnsByRootRequester::Custody(custody_id) => { + if let Some(custody_columns) = self + .network + .on_custody_by_root_response(custody_id, req_id, peer_id, resp) { // TODO(das): get proper timestamp let seen_timestamp = timestamp_now(); self.block_lookups .on_download_response::>( - requester.0, + custody_id.requester.0, custody_columns.map(|(columns, peer_group)| { (columns, peer_group, seen_timestamp) }), diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index c2f595a44d2..acc498ccd9c 100644 --- a/beacon_node/network/src/sync/network_context.rs +++ b/beacon_node/network/src/sync/network_context.rs @@ -9,8 +9,7 @@ pub use self::requests::DataColumnsByRootSingleBlockRequest; use self::requests::{ActiveBlobsByRootRequest, ActiveBlocksByRootRequest}; use super::block_sidecar_coupling::RangeBlockComponentsRequest; use super::manager::{ - BlockProcessType, DataColumnsByRootRequestId, DataColumnsByRootRequester, Id, - RequestId as SyncRequestId, + BlockProcessType, DataColumnsByRootRequester, Id, RequestId as SyncRequestId, }; use super::range_sync::{BatchId, ByRangeRequestType, ChainId}; use crate::network_beacon_processor::NetworkBeaconProcessor; @@ -33,6 +32,7 @@ pub use requests::LookupVerifyError; use slog::{debug, error, warn}; use slot_clock::SlotClock; use std::collections::hash_map::Entry; +use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; use tokio::sync::mpsc; @@ -63,6 +63,11 @@ pub enum RangeRequestId { }, } +/// Request ID for data_columns_by_root requests. Block lookup do not issue this requests directly. +/// Wrapping this particular req_id, ensures not mixing this requests with a custody req_id. +#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] +pub struct DataColumnsByRootRequestId(Id); + #[derive(Debug)] pub enum RpcEvent { StreamTermination, @@ -116,30 +121,38 @@ impl From for RpcResponseError { } } +/// Represents a group of peers that served a block component. #[derive(Clone, Debug)] pub struct PeerGroup { - peers: Vec, + /// Peers group by which indexed section of the block component they served. For example: + /// - PeerA served = [blob index 0, blob index 2] + /// - PeerA served = [blob index 1] + peers: HashMap>, } impl PeerGroup { + /// Return a peer group where a single peer returned all parts of a block component. For + /// example, a block has a single component (the block = index 0/1). pub fn from_single(peer: PeerId) -> Self { - Self { peers: vec![peer] } + Self { + peers: HashMap::from_iter([(peer, vec![0])]), + } } - pub fn from_set(peers: Vec) -> Self { + pub fn from_set(peers: HashMap>) -> Self { Self { peers } } - pub fn all(&self) -> &[PeerId] { - &self.peers + pub fn all(&self) -> impl Iterator + '_ { + self.peers.keys() } } /// Sequential ID that uniquely identifies ReqResp outgoing requests pub type ReqId = u32; -pub enum LookupRequestResult { +pub enum LookupRequestResult { /// A request is sent. Sync MUST receive an event from the network in the future for either: /// completed response or failed request - RequestSent(ReqId), + RequestSent(R), /// No request is sent, and no further action is necessary to consider this request completed NoRequestNeeded, /// No request is sent, but the request is not completed. Sync MUST receive some future event @@ -162,10 +175,8 @@ pub struct SyncNetworkContext { /// A mapping of active BlobsByRoot requests, including both current slot and parent lookups. blobs_by_root_requests: FnvHashMap>, - data_columns_by_root_requests: FnvHashMap< - DataColumnsByRootRequestId, - ActiveDataColumnsByRootRequest, - >, + data_columns_by_root_requests: + FnvHashMap>, /// Mapping of active custody column requests for a block root custody_by_root_requests: FnvHashMap>, @@ -600,8 +611,8 @@ impl SyncNetworkContext { requester: DataColumnsByRootRequester, peer_id: PeerId, request: DataColumnsByRootSingleBlockRequest, - ) -> Result<(), &'static str> { - let req_id = self.next_id(); + ) -> Result, &'static str> { + let req_id = DataColumnsByRootRequestId(self.next_id()); debug!( self.log, "Sending DataColumnsByRoot Request"; @@ -610,27 +621,26 @@ impl SyncNetworkContext { "indices" => ?request.indices, "peer" => %peer_id, "requester" => ?requester, - "id" => req_id, + "req_id" => %req_id, ); - let id = DataColumnsByRootRequestId { requester, req_id }; self.send_network_msg(NetworkMessage::SendRequest { peer_id, request: Request::DataColumnsByRoot(request.clone().into_request(&self.chain.spec)), - request_id: RequestId::Sync(SyncRequestId::DataColumnsByRoot(id)), + request_id: RequestId::Sync(SyncRequestId::DataColumnsByRoot(req_id, requester)), })?; self.data_columns_by_root_requests - .insert(id, ActiveDataColumnsByRootRequest::new(request, requester)); + .insert(req_id, ActiveDataColumnsByRootRequest::new(request)); - Ok(()) + Ok(LookupRequestResult::RequestSent(req_id)) } pub fn custody_lookup_request( &mut self, lookup_id: SingleLookupId, block_root: Hash256, - downloaded_block_expected_data: Option, + downloaded_block_expected_blobs: Option, ) -> Result { // Check if we are into peerdas if !self @@ -648,18 +658,21 @@ impl SyncNetworkContext { return Ok(LookupRequestResult::NoRequestNeeded); } - let expects_data = downloaded_block_expected_data - .or_else(|| { - self.chain - .data_availability_checker - .num_expected_blobs(&block_root) - }) - .map(|n| n > 0) - // If we don't know about the block being requested, assume block has data - .unwrap_or(true); + let Some(expected_blobs) = downloaded_block_expected_blobs.or_else(|| { + self.chain + .data_availability_checker + .num_expected_blobs(&block_root) + }) else { + // Wait to download the block before downloading columns. Then we can be sure that the + // block has data, so there's no need to do "blind" requests for all possible columns and + // latter handle the case where if the peer sent no columns, penalize. + // - if `downloaded_block_expected_blobs` is Some = block is downloading or processing. + // - if `num_expected_blobs` returns Some = block is processed. + return Ok(LookupRequestResult::Pending); + }; // No data required for this block - if !expects_data { + if expected_blobs == 0 { return Ok(LookupRequestResult::NoRequestNeeded); } @@ -700,8 +713,9 @@ impl SyncNetworkContext { let requester = CustodyRequester(id); let mut request = ActiveCustodyRequest::new( block_root, - requester, - custody_indexes_to_fetch, + // TODO(das): req_id is duplicated here, also present in id + CustodyId { requester, req_id }, + &custody_indexes_to_fetch, self.log.clone(), ); @@ -947,16 +961,11 @@ impl SyncNetworkContext { id: DataColumnsByRootRequestId, peer_id: PeerId, item: RpcEvent>>, - ) -> Option<( - DataColumnsByRootRequester, - RpcResponseResult>>>, - )> { + ) -> Option>>>> { let Entry::Occupied(mut request) = self.data_columns_by_root_requests.entry(id) else { return None; }; - let requester = request.get().requester; - let resp = match item { RpcEvent::Response(item, _) => match request.get_mut().add_response(item) { // TODO: Track last chunk timestamp @@ -983,7 +992,7 @@ impl SyncNetworkContext { if let Err(ref e) = resp { self.on_lookup_failure(peer_id, e); } - Some((requester, resp)) + Some(resp) } /// Insert a downloaded column into an active custody request. Then make progress on the @@ -997,22 +1006,20 @@ impl SyncNetworkContext { pub fn on_custody_by_root_response( &mut self, id: CustodyId, + req_id: DataColumnsByRootRequestId, peer_id: PeerId, resp: RpcResponseResult>>>, - ) -> Option<( - CustodyRequester, - Result<(Vec>, PeerGroup), RpcResponseError>, - )> { + ) -> Option>, PeerGroup), RpcResponseError>> { // Note: need to remove the request to borrow self again below. Otherwise we can't // do nested requests - let Some(mut request) = self.custody_by_root_requests.remove(&id.id) else { + let Some(mut request) = self.custody_by_root_requests.remove(&id.requester) else { // TOOD(das): This log can happen if the request is error'ed early and dropped debug!(self.log, "Custody column downloaded event for unknown request"; "id" => ?id); return None; }; let result = request - .on_data_column_downloaded(peer_id, id.column_index, resp, self) + .on_data_column_downloaded(peer_id, req_id, resp, self) .map_err(RpcResponseError::CustodyRequestError) .transpose(); @@ -1028,9 +1035,9 @@ impl SyncNetworkContext { } } - Some((id.id, result)) + Some(result) } else { - self.custody_by_root_requests.insert(id.id, request); + self.custody_by_root_requests.insert(id.requester, request); None } } @@ -1144,3 +1151,9 @@ fn to_fixed_blob_sidecar_list( } Ok(fixed_list) } + +impl std::fmt::Display for DataColumnsByRootRequestId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} diff --git a/beacon_node/network/src/sync/network_context/custody.rs b/beacon_node/network/src/sync/network_context/custody.rs index 21acce8e6e5..e8e0491d707 100644 --- a/beacon_node/network/src/sync/network_context/custody.rs +++ b/beacon_node/network/src/sync/network_context/custody.rs @@ -1,22 +1,30 @@ -use crate::sync::manager::SingleLookupReqId; +use crate::sync::manager::{DataColumnsByRootRequester, SingleLookupReqId}; +use crate::sync::network_context::{ + DataColumnsByRootRequestId, DataColumnsByRootSingleBlockRequest, +}; -use self::request::ActiveColumnSampleRequest; use beacon_chain::data_column_verification::CustodyDataColumn; use beacon_chain::BeaconChainTypes; use fnv::FnvHashMap; use lighthouse_network::PeerId; +use lru_cache::LRUTimeCache; +use rand::Rng; use slog::{debug, warn}; -use std::{marker::PhantomData, sync::Arc}; +use std::time::Duration; +use std::{collections::HashMap, marker::PhantomData, sync::Arc}; +use types::EthSpec; use types::{data_column_sidecar::ColumnIndex, DataColumnSidecar, Epoch, Hash256}; -use super::{PeerGroup, RpcResponseResult, SyncNetworkContext}; +use super::{LookupRequestResult, PeerGroup, ReqId, RpcResponseResult, SyncNetworkContext}; #[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] pub struct CustodyId { - pub id: CustodyRequester, - pub column_index: ColumnIndex, + pub requester: CustodyRequester, + pub req_id: ReqId, } +const FAILED_PEERS_CACHE_EXPIRY_SECONDS: u64 = 5; + /// Downstream components that perform custody by root requests. /// Currently, it's only single block lookups, so not using an enum #[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] @@ -27,9 +35,15 @@ type DataColumnSidecarList = Vec>>; pub struct ActiveCustodyRequest { block_root: Hash256, block_epoch: Epoch, - requester_id: CustodyRequester, - column_requests: FnvHashMap, - columns: Vec>, + custody_id: CustodyId, + /// List of column indices this request needs to download to complete successfully + column_requests: FnvHashMap>, + /// Active requests for 1 or more columns each + active_batch_columns_requests: + FnvHashMap, + /// Peers that have recently failed to successfully respond to a columns by root request. + /// Having a LRUTimeCache allows this request to not have to track disconnecting peers. + failed_peers: LRUTimeCache, /// Logger for the `SyncNetworkContext`. pub log: slog::Logger, _phantom: PhantomData, @@ -41,6 +55,18 @@ pub enum Error { TooManyFailures, BadState(String), NoPeers(ColumnIndex), + /// Received a download result for a different request id than the in-flight request. + /// There should only exist a single request at a time. Having multiple requests is a bug and + /// can result in undefined state, so it's treated as a hard error and the lookup is dropped. + UnexpectedRequestId { + expected_req_id: DataColumnsByRootRequestId, + req_id: DataColumnsByRootRequestId, + }, +} + +struct ActiveBatchColumnsRequest { + peer_id: PeerId, + indices: Vec, } type CustodyRequestResult = Result>, PeerGroup)>, Error>; @@ -48,79 +74,136 @@ type CustodyRequestResult = Result>, PeerGro impl ActiveCustodyRequest { pub(crate) fn new( block_root: Hash256, - requester_id: CustodyRequester, - column_indexes: Vec, + custody_id: CustodyId, + column_indices: &[ColumnIndex], log: slog::Logger, ) -> Self { Self { block_root, // TODO(das): use actual epoch if there's rotation block_epoch: Epoch::new(0), - requester_id, - column_requests: column_indexes - .into_iter() - .map(|index| (index, ActiveColumnSampleRequest::new(index))) - .collect(), - columns: vec![], + custody_id, + column_requests: HashMap::from_iter( + column_indices + .iter() + .map(|index| (*index, ColumnRequest::new())), + ), + active_batch_columns_requests: <_>::default(), + failed_peers: LRUTimeCache::new(Duration::from_secs(FAILED_PEERS_CACHE_EXPIRY_SECONDS)), log, _phantom: PhantomData, } } - /// Insert a downloaded column into an active sampling request. Then make progress on the + /// Insert a downloaded column into an active custody request. Then make progress on the /// entire request. /// /// ### Returns /// - /// - `Err`: Sampling request has failed and will be dropped - /// - `Ok(Some)`: Sampling request has successfully completed and will be dropped - /// - `Ok(None)`: Sampling request still active + /// - `Err`: Custody request has failed and will be dropped + /// - `Ok(Some)`: Custody request has successfully completed and will be dropped + /// - `Ok(None)`: Custody request still active pub(crate) fn on_data_column_downloaded( &mut self, - _peer_id: PeerId, - column_index: ColumnIndex, + peer_id: PeerId, + req_id: DataColumnsByRootRequestId, resp: RpcResponseResult>, cx: &mut SyncNetworkContext, ) -> CustodyRequestResult { // TODO(das): Should downscore peers for verify errors here - let Some(request) = self.column_requests.get_mut(&column_index) else { - warn!( - self.log, - "Received sampling response for unrequested column index" + let Some(batch_request) = self.active_batch_columns_requests.get_mut(&req_id) else { + warn!(self.log, + "Received custody column response for unrequested index"; + "id" => ?self.custody_id, + "block_root" => ?self.block_root, + "req_id" => %req_id, ); return Ok(None); }; match resp { - Ok((mut data_columns, _seen_timestamp)) => { - debug!(self.log, "Sample download success"; "block_root" => %self.block_root, "column_index" => column_index, "count" => data_columns.len()); - - // No need to check data_columns has len > 1, as the SyncNetworkContext ensure that - // only requested is returned (or none); - if let Some(data_column) = data_columns.pop() { - request.on_download_success()?; - - // If on_download_success is successful, we are expecting a columna for this - // custody requirement. - self.columns - .push(CustodyDataColumn::from_asserted_custody(data_column)); - } else { - // Peer does not have the requested data. - // TODO(das) what to do? - // TODO(das): If the peer is in the lookup peer set it claims to have imported - // the block AND its custody columns. So in this case we can downscore - debug!(self.log, "Sampling peer claims to not have the data"; "block_root" => %self.block_root, "column_index" => column_index); - // TODO(das) tolerate this failure if you are not sure the block has data - request.on_download_success()?; + Ok((data_columns, _seen_timestamp)) => { + debug!(self.log, + "Custody column download success"; + "id" => ?self.custody_id, + "block_root" => ?self.block_root, + "req_id" => %req_id, + "peer" => %peer_id, + "count" => data_columns.len() + ); + + // Map columns by index as an optimization to not loop the returned list on each + // requested index. The worse case is 128 loops over a 128 item vec + mutation to + // drop the consumed columns. + let mut data_columns = HashMap::::from_iter( + data_columns.into_iter().map(|d| (d.index, d)), + ); + // Accumulate columns that the peer does not have to issue a single log per request + let mut missing_column_indexes = vec![]; + + for column_index in &batch_request.indices { + let column_request = self + .column_requests + .get_mut(column_index) + .ok_or(Error::BadState("unknown column_index".to_owned()))?; + + if let Some(data_column) = data_columns.remove(column_index) { + column_request.on_download_success( + req_id, + peer_id, + // Safe to cast, self.column_requests only contains indexes for columns we must custody + CustodyDataColumn::from_asserted_custody(data_column), + )?; + } else { + // Peer does not have the requested data. + // TODO(das) do not consider this case a success. We know for sure the block has + // data. However we allow the peer to return empty as we can't attribute fault. + // TODO(das): Should track which columns are missing and eventually give up + // TODO(das): If the peer is in the lookup peer set it claims to have imported + // the block AND its custody columns. So in this case we can downscore + column_request.on_download_error(req_id)?; + missing_column_indexes.push(column_index); + } + } + + // Note: no need to check data_columns is empty, SyncNetworkContext ensures that + // successful responses only contain requested data. + + if !missing_column_indexes.is_empty() { + // Note: Batch logging that columns are missing to not spam logger + debug!(self.log, + "Custody column peer claims to not have some data"; + "id" => ?self.custody_id, + "block_root" => ?self.block_root, + "req_id" => %req_id, + "peer" => %peer_id, + // TODO(das): this property can become very noisy, being the full range 0..128 + "missing_column_indexes" => ?missing_column_indexes + ); + + self.failed_peers.insert(peer_id); } } Err(err) => { - debug!(self.log, "Sample download error"; "block_root" => %self.block_root, "column_index" => column_index, "error" => ?err); + debug!(self.log, + "Custody column download error"; + "id" => ?self.custody_id, + "block_root" => ?self.block_root, + "req_id" => %req_id, + "peer" => %peer_id, + "error" => ?err + ); + + // TODO(das): Should mark peer as failed and try from another peer + for column_index in &batch_request.indices { + self.column_requests + .get_mut(column_index) + .ok_or(Error::BadState("unknown column_index".to_owned()))? + .on_download_error_and_mark_failure(req_id)?; + } - // Error downloading, maybe penalize peer and retry again. - // TODO(das) with different peer or different peer? - request.on_download_error()?; + self.failed_peers.insert(peer_id); } }; @@ -131,156 +214,222 @@ impl ActiveCustodyRequest { &mut self, cx: &mut SyncNetworkContext, ) -> CustodyRequestResult { - // First check if sampling is completed, by computing `required_successes` - let mut successes = 0; + if self.column_requests.values().all(|r| r.is_downloaded()) { + // All requests have completed successfully. + let mut peers = HashMap::>::new(); + let columns = std::mem::take(&mut self.column_requests) + .into_values() + .map(|request| { + let (peer, data_column) = request.complete()?; + peers + .entry(peer) + .or_default() + .push(data_column.as_data_column().index as usize); + Ok(data_column) + }) + .collect::, _>>()?; - for request in self.column_requests.values() { - if request.is_downloaded() { - successes += 1; - } + let peer_group = PeerGroup::from_set(peers); + return Ok(Some((columns, peer_group))); } - // All requests have completed successfully. We may not have all the expected columns if the - // serving peers claim that this block has no data. - if successes == self.column_requests.len() { - let columns = std::mem::take(&mut self.columns); + let mut columns_to_request_by_peer = HashMap::>::new(); - let peers = self - .column_requests - .values() - .filter_map(|r| r.peer()) - .collect::>(); - let peer_group = PeerGroup::from_set(peers); + // Need to: + // - track how many active requests a peer has for load balancing + // - which peers have failures to attempt others + // - which peer returned what to have PeerGroup attributability - return Ok(Some((columns, peer_group))); + for (column_index, request) in self.column_requests.iter_mut() { + if request.is_awaiting_download() { + if request.download_failures > MAX_CUSTODY_COLUMN_DOWNLOAD_ATTEMPTS { + return Err(Error::TooManyFailures); + } + + // TODO: When is a fork and only a subset of your peers know about a block, we should only + // query the peers on that fork. Should this case be handled? How to handle it? + let custodial_peers = cx.get_custodial_peers(self.block_epoch, *column_index); + + // TODO(das): cache this computation in a OneCell or similar to prevent having to + // run it every loop + let mut active_requests_by_peer = HashMap::::new(); + for batch_request in self.active_batch_columns_requests.values() { + *active_requests_by_peer + .entry(batch_request.peer_id) + .or_default() += 1; + } + + let mut priorized_peers = custodial_peers + .iter() + .map(|peer| { + ( + // De-prioritize peers that have failed to successfully respond to + // requests recently + self.failed_peers.contains(peer), + // Prefer peers with less requests to load balance across peers + active_requests_by_peer.get(peer).copied().unwrap_or(0), + // Final random factor to give all peers a shot in each retry + rand::thread_rng().gen::(), + *peer, + ) + }) + .collect::>(); + priorized_peers.sort_unstable(); + + let Some((_, _, _, peer_id)) = priorized_peers.first() else { + // Do not tolerate not having custody peers, hard error. + // TODO(das): we might implement some grace period. The request will pause for X + // seconds expecting the peer manager to find peers before failing the request. + return Err(Error::NoPeers(*column_index)); + }; + + columns_to_request_by_peer + .entry(*peer_id) + .or_default() + .push(*column_index); + } } - for (_, request) in self.column_requests.iter_mut() { - request.request(self.block_root, self.block_epoch, self.requester_id, cx)?; + for (peer_id, indices) in columns_to_request_by_peer.into_iter() { + let request_result = cx + .data_column_lookup_request( + DataColumnsByRootRequester::Custody(self.custody_id), + peer_id, + DataColumnsByRootSingleBlockRequest { + block_root: self.block_root, + indices: indices.clone(), + }, + ) + .map_err(Error::SendFailed)?; + + match request_result { + LookupRequestResult::RequestSent(req_id) => { + for column_index in &indices { + let column_request = self + .column_requests + .get_mut(column_index) + .ok_or(Error::BadState("unknown column_index".to_owned()))?; + + column_request.on_download_start(req_id)?; + } + + self.active_batch_columns_requests + .insert(req_id, ActiveBatchColumnsRequest { indices, peer_id }); + } + LookupRequestResult::NoRequestNeeded => unreachable!(), + LookupRequestResult::Pending => unreachable!(), + } } Ok(None) } } -mod request { - use super::{CustodyId, CustodyRequester, Error}; - use crate::sync::{ - manager::DataColumnsByRootRequester, - network_context::{DataColumnsByRootSingleBlockRequest, SyncNetworkContext}, - }; - use beacon_chain::BeaconChainTypes; - use lighthouse_network::PeerId; - use types::{data_column_sidecar::ColumnIndex, Epoch, Hash256}; - - /// TODO(das): this attempt count is nested into the existing lookup request count. - const MAX_CUSTODY_COLUMN_DOWNLOAD_ATTEMPTS: usize = 3; - - pub(crate) struct ActiveColumnSampleRequest { - column_index: ColumnIndex, - status: Status, - download_failures: usize, - } +/// TODO(das): this attempt count is nested into the existing lookup request count. +const MAX_CUSTODY_COLUMN_DOWNLOAD_ATTEMPTS: usize = 3; - #[derive(Debug, Clone)] - enum Status { - NotStarted, - Downloading(PeerId), - Downloaded(PeerId), - } +struct ColumnRequest { + status: Status, + download_failures: usize, +} - impl ActiveColumnSampleRequest { - pub(crate) fn new(column_index: ColumnIndex) -> Self { - Self { - column_index, - status: Status::NotStarted, - download_failures: 0, - } - } +#[derive(Debug, Clone)] +enum Status { + NotStarted, + Downloading(DataColumnsByRootRequestId), + Downloaded(PeerId, CustodyDataColumn), +} - pub(crate) fn is_downloaded(&self) -> bool { - match self.status { - Status::NotStarted | Status::Downloading(_) => false, - Status::Downloaded(_) => true, - } +impl ColumnRequest { + fn new() -> Self { + Self { + status: Status::NotStarted, + download_failures: 0, } + } - pub(crate) fn peer(&self) -> Option { - match self.status { - Status::NotStarted | Status::Downloading(_) => None, - Status::Downloaded(peer) => Some(peer), - } + fn is_awaiting_download(&self) -> bool { + match self.status { + Status::NotStarted => true, + Status::Downloading { .. } | Status::Downloaded { .. } => false, } + } - pub(crate) fn request( - &mut self, - block_root: Hash256, - block_epoch: Epoch, - requester: CustodyRequester, - cx: &mut SyncNetworkContext, - ) -> Result { - match &self.status { - Status::NotStarted => {} // Ok to continue - Status::Downloading(_) => return Ok(false), // Already downloading - Status::Downloaded(_) => return Ok(false), // Already completed - } + fn is_downloaded(&self) -> bool { + match self.status { + Status::NotStarted | Status::Downloading { .. } => false, + Status::Downloaded { .. } => true, + } + } - if self.download_failures > MAX_CUSTODY_COLUMN_DOWNLOAD_ATTEMPTS { - return Err(Error::TooManyFailures); + fn on_download_start(&mut self, req_id: DataColumnsByRootRequestId) -> Result<(), Error> { + match &self.status { + Status::NotStarted => { + self.status = Status::Downloading(req_id); + Ok(()) } - - // TODO: When is a fork and only a subset of your peers know about a block, sampling should only - // be queried on the peers on that fork. Should this case be handled? How to handle it? - let peer_ids = cx.get_custodial_peers(block_epoch, self.column_index); - - // TODO(das) randomize custodial peer and avoid failing peers - let Some(peer_id) = peer_ids.first().cloned() else { - // Do not tolerate not having custody peers, hard error. - // TODO(das): we might implement some grace period. The request will pause for X - // seconds expecting the peer manager to find peers before failing the request. - return Err(Error::NoPeers(self.column_index)); - }; - - cx.data_column_lookup_request( - DataColumnsByRootRequester::Custody(CustodyId { - id: requester, - column_index: self.column_index, - }), - peer_id, - DataColumnsByRootSingleBlockRequest { - block_root, - indices: vec![self.column_index], - }, - ) - .map_err(Error::SendFailed)?; - - self.status = Status::Downloading(peer_id); - Ok(true) + other => Err(Error::BadState(format!( + "bad state on_download_start expected NotStarted got {other:?}" + ))), } + } - pub(crate) fn on_download_error(&mut self) -> Result { - match self.status.clone() { - Status::Downloading(peer_id) => { - self.download_failures += 1; - self.status = Status::NotStarted; - Ok(peer_id) + fn on_download_error(&mut self, req_id: DataColumnsByRootRequestId) -> Result<(), Error> { + match &self.status { + Status::Downloading(expected_req_id) => { + if req_id != *expected_req_id { + return Err(Error::UnexpectedRequestId { + expected_req_id: *expected_req_id, + req_id, + }); } - other => Err(Error::BadState(format!( - "bad state on_sampling_error expected Sampling got {other:?}" - ))), + self.status = Status::NotStarted; + Ok(()) } + other => Err(Error::BadState(format!( + "bad state on_download_error expected Downloading got {other:?}" + ))), } + } + + fn on_download_error_and_mark_failure( + &mut self, + req_id: DataColumnsByRootRequestId, + ) -> Result<(), Error> { + // TODO(das): Should track which peers don't have data + self.download_failures += 1; + self.on_download_error(req_id) + } - pub(crate) fn on_download_success(&mut self) -> Result<(), Error> { - match &self.status { - Status::Downloading(peer) => { - self.status = Status::Downloaded(*peer); - Ok(()) + fn on_download_success( + &mut self, + req_id: DataColumnsByRootRequestId, + peer_id: PeerId, + data_column: CustodyDataColumn, + ) -> Result<(), Error> { + match &self.status { + Status::Downloading(expected_req_id) => { + if req_id != *expected_req_id { + return Err(Error::UnexpectedRequestId { + expected_req_id: *expected_req_id, + req_id, + }); } - other => Err(Error::BadState(format!( - "bad state on_sampling_success expected Sampling got {other:?}" - ))), + self.status = Status::Downloaded(peer_id, data_column); + Ok(()) } + other => Err(Error::BadState(format!( + "bad state on_download_success expected Downloading got {other:?}" + ))), + } + } + + fn complete(self) -> Result<(PeerId, CustodyDataColumn), Error> { + match self.status { + Status::Downloaded(peer_id, data_column) => Ok((peer_id, data_column)), + other => Err(Error::BadState(format!( + "bad state complete expected Downloaded got {other:?}" + ))), } } } diff --git a/beacon_node/network/src/sync/network_context/requests.rs b/beacon_node/network/src/sync/network_context/requests.rs index e8d7e200aa7..971d329adf4 100644 --- a/beacon_node/network/src/sync/network_context/requests.rs +++ b/beacon_node/network/src/sync/network_context/requests.rs @@ -183,17 +183,15 @@ impl DataColumnsByRootSingleBlockRequest { } } -pub struct ActiveDataColumnsByRootRequest { - pub requester: T, +pub struct ActiveDataColumnsByRootRequest { request: DataColumnsByRootSingleBlockRequest, items: Vec>>, resolved: bool, } -impl ActiveDataColumnsByRootRequest { - pub fn new(request: DataColumnsByRootSingleBlockRequest, requester: T) -> Self { +impl ActiveDataColumnsByRootRequest { + pub fn new(request: DataColumnsByRootSingleBlockRequest) -> Self { Self { - requester, request, items: vec![], resolved: false, From 4e063d5febf8c38204d99856d728cfdd808eee94 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Tue, 28 May 2024 16:57:54 +1000 Subject: [PATCH 41/76] Rename deploy_block in network config (`das` branch) (#5852) * Rename deploy_block.txt to deposit_contract_block.txt * fmt --------- Co-authored-by: Pawan Dhananjay --- common/eth2_config/src/lib.rs | 6 +++++- .../chiado/{deploy_block.txt => deposit_contract_block.txt} | 0 .../gnosis/{deploy_block.txt => deposit_contract_block.txt} | 0 .../{deploy_block.txt => deposit_contract_block.txt} | 0 .../{deploy_block.txt => deposit_contract_block.txt} | 0 .../{deploy_block.txt => deposit_contract_block.txt} | 0 common/eth2_network_config/src/lib.rs | 2 +- 7 files changed, 6 insertions(+), 2 deletions(-) rename common/eth2_network_config/built_in_network_configs/chiado/{deploy_block.txt => deposit_contract_block.txt} (100%) rename common/eth2_network_config/built_in_network_configs/gnosis/{deploy_block.txt => deposit_contract_block.txt} (100%) rename common/eth2_network_config/built_in_network_configs/holesky/{deploy_block.txt => deposit_contract_block.txt} (100%) rename common/eth2_network_config/built_in_network_configs/mainnet/{deploy_block.txt => deposit_contract_block.txt} (100%) rename common/eth2_network_config/built_in_network_configs/sepolia/{deploy_block.txt => deposit_contract_block.txt} (100%) diff --git a/common/eth2_config/src/lib.rs b/common/eth2_config/src/lib.rs index cda9d59a503..9104db8f67d 100644 --- a/common/eth2_config/src/lib.rs +++ b/common/eth2_config/src/lib.rs @@ -192,7 +192,11 @@ macro_rules! define_net { config_dir: ETH2_NET_DIR.config_dir, genesis_state_source: ETH2_NET_DIR.genesis_state_source, config: $this_crate::$include_file!($this_crate, "../", "config.yaml"), - deploy_block: $this_crate::$include_file!($this_crate, "../", "deploy_block.txt"), + deploy_block: $this_crate::$include_file!( + $this_crate, + "../", + "deposit_contract_block.txt" + ), boot_enr: $this_crate::$include_file!($this_crate, "../", "boot_enr.yaml"), genesis_state_bytes: $this_crate::$include_file!($this_crate, "../", "genesis.ssz"), } diff --git a/common/eth2_network_config/built_in_network_configs/chiado/deploy_block.txt b/common/eth2_network_config/built_in_network_configs/chiado/deposit_contract_block.txt similarity index 100% rename from common/eth2_network_config/built_in_network_configs/chiado/deploy_block.txt rename to common/eth2_network_config/built_in_network_configs/chiado/deposit_contract_block.txt diff --git a/common/eth2_network_config/built_in_network_configs/gnosis/deploy_block.txt b/common/eth2_network_config/built_in_network_configs/gnosis/deposit_contract_block.txt similarity index 100% rename from common/eth2_network_config/built_in_network_configs/gnosis/deploy_block.txt rename to common/eth2_network_config/built_in_network_configs/gnosis/deposit_contract_block.txt diff --git a/common/eth2_network_config/built_in_network_configs/holesky/deploy_block.txt b/common/eth2_network_config/built_in_network_configs/holesky/deposit_contract_block.txt similarity index 100% rename from common/eth2_network_config/built_in_network_configs/holesky/deploy_block.txt rename to common/eth2_network_config/built_in_network_configs/holesky/deposit_contract_block.txt diff --git a/common/eth2_network_config/built_in_network_configs/mainnet/deploy_block.txt b/common/eth2_network_config/built_in_network_configs/mainnet/deposit_contract_block.txt similarity index 100% rename from common/eth2_network_config/built_in_network_configs/mainnet/deploy_block.txt rename to common/eth2_network_config/built_in_network_configs/mainnet/deposit_contract_block.txt diff --git a/common/eth2_network_config/built_in_network_configs/sepolia/deploy_block.txt b/common/eth2_network_config/built_in_network_configs/sepolia/deposit_contract_block.txt similarity index 100% rename from common/eth2_network_config/built_in_network_configs/sepolia/deploy_block.txt rename to common/eth2_network_config/built_in_network_configs/sepolia/deposit_contract_block.txt diff --git a/common/eth2_network_config/src/lib.rs b/common/eth2_network_config/src/lib.rs index 9fd0682f54b..fb8c6938cdb 100644 --- a/common/eth2_network_config/src/lib.rs +++ b/common/eth2_network_config/src/lib.rs @@ -29,7 +29,7 @@ use url::Url; pub use eth2_config::GenesisStateSource; -pub const DEPLOY_BLOCK_FILE: &str = "deploy_block.txt"; +pub const DEPLOY_BLOCK_FILE: &str = "deposit_contract_block.txt"; pub const BOOT_ENR_FILE: &str = "boot_enr.yaml"; pub const GENESIS_STATE_FILE: &str = "genesis.ssz"; pub const BASE_CONFIG_FILE: &str = "config.yaml"; From b42249f77184d0ccbea18df395c9ccdccdcc5fc5 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Tue, 4 Jun 2024 14:07:26 +1000 Subject: [PATCH 42/76] Fix CI and merge issues. --- beacon_node/src/cli.rs | 3 ++- book/src/help_bn.md | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index 2446f635695..0c243bf2cc5 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -68,9 +68,10 @@ pub fn cli_app() -> Command { // TODO(das): remove this before release Arg::new("malicious-withhold-count") .long("malicious-withhold-count") - .action(ArgAction::SetTrue) + .action(ArgAction::Set) .help_heading(FLAG_HEADER) .help("TESTING ONLY do not use this") + .hide(true) .display_order(0) ) .arg( diff --git a/book/src/help_bn.md b/book/src/help_bn.md index b458842e083..f326be1b654 100644 --- a/book/src/help_bn.md +++ b/book/src/help_bn.md @@ -593,6 +593,10 @@ Flags: server on localhost:5052 and import deposit logs from the execution node. This is equivalent to `--http` on merge-ready networks, or `--http --eth1` pre-merge + --subscribe-all-data-column-subnets + Subscribe to all data column subnets and participate in data custody + for all columns. This will also advertise the beacon node as being + long-lived subscribed to all data column subnets. --subscribe-all-subnets Subscribe to all subnets regardless of validator count. This will also advertise the beacon node as being long-lived subscribed to all From dac580ba341fca155cbf49e2070153f6fb56cd5e Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Tue, 18 Jun 2024 09:09:57 +0200 Subject: [PATCH 43/76] Store data columns individually in store and caches (#5890) * Store data columns individually in store and caches * Implement data column pruning --- beacon_node/beacon_chain/src/beacon_chain.rs | 55 ++---- .../src/data_availability_checker.rs | 30 ++-- .../overflow_lru_cache.rs | 13 +- .../beacon_chain/src/early_attester_cache.rs | 4 +- beacon_node/beacon_chain/src/test_utils.rs | 4 +- beacon_node/http_api/src/publish_blocks.rs | 12 +- .../network_beacon_processor/rpc_methods.rs | 104 ++++++----- .../src/sync/network_context/custody.rs | 4 +- beacon_node/network/src/sync/sampling.rs | 6 +- beacon_node/store/src/hot_cold_store.rs | 163 ++++++++++++------ beacon_node/store/src/leveldb_store.rs | 4 + beacon_node/store/src/lib.rs | 32 +++- beacon_node/store/src/memory_store.rs | 13 ++ consensus/types/src/data_column_sidecar.rs | 11 +- consensus/types/src/lib.rs | 2 +- 15 files changed, 249 insertions(+), 208 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index feb90ff468b..5cf2d14dd3d 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -1184,42 +1184,23 @@ impl BeaconChain { .map_or_else(|| self.get_blobs(block_root), Ok) } - pub fn get_data_columns_checking_early_attester_cache( - &self, - block_root: &Hash256, - ) -> Result, Error> { - self.early_attester_cache - .get_data_columns(*block_root) - .map_or_else(|| self.get_data_columns(block_root), Ok) - } - - pub fn get_selected_data_columns_checking_all_caches( + pub fn get_data_column_checking_all_caches( &self, block_root: Hash256, - indices: &[ColumnIndex], - ) -> Result>>, Error> { - let columns_from_availability_cache = indices - .iter() - .copied() - .filter_map(|index| { - self.data_availability_checker - .get_data_column(&DataColumnIdentifier { block_root, index }) - .transpose() - }) - .collect::, _>>()?; - // Existence of a column in the data availability cache and downstream caches is exclusive. - // If there's a single match in the availability cache we can safely skip other sources. - if !columns_from_availability_cache.is_empty() { - return Ok(columns_from_availability_cache); + index: ColumnIndex, + ) -> Result>>, Error> { + if let Some(column) = self + .data_availability_checker + .get_data_column(&DataColumnIdentifier { block_root, index })? + { + return Ok(Some(column)); } - Ok(self - .early_attester_cache - .get_data_columns(block_root) - .map_or_else(|| self.get_data_columns(&block_root), Ok)? - .into_iter() - .filter(|dc| indices.contains(&dc.index)) - .collect()) + if let Some(columns) = self.early_attester_cache.get_data_columns(block_root) { + return Ok(columns.iter().find(|c| c.index == index).cloned()); + } + + self.get_data_column(&block_root, &index) } /// Returns the import status of block checking (in order) pre-import caches, fork-choice, db store @@ -1332,14 +1313,12 @@ impl BeaconChain { /// /// ## Errors /// May return a database error. - pub fn get_data_columns( + pub fn get_data_column( &self, block_root: &Hash256, - ) -> Result, Error> { - match self.store.get_data_columns(block_root)? { - Some(data_columns) => Ok(data_columns), - None => Ok(RuntimeVariableList::empty(self.spec.number_of_columns)), - } + column_index: &ColumnIndex, + ) -> Result>>, Error> { + Ok(self.store.get_data_column(block_root, column_index)?) } pub fn get_blinded_block( diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index fe2dfb9fbff..f4316506510 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -15,7 +15,7 @@ use std::time::Duration; use task_executor::TaskExecutor; use types::blob_sidecar::{BlobIdentifier, BlobSidecar, FixedBlobSidecarList}; use types::{ - BlobSidecarList, ChainSpec, DataColumnSidecar, DataColumnSidecarList, Epoch, EthSpec, Hash256, + BlobSidecarList, ChainSpec, DataColumnSidecar, DataColumnSidecarVec, Epoch, EthSpec, Hash256, RuntimeVariableList, SignedBeaconBlock, }; @@ -314,16 +314,11 @@ impl DataAvailabilityChecker { block, blobs: None, blobs_available_timestamp: None, - // TODO(das): update store type to prevent this conversion data_columns: Some( - RuntimeVariableList::new( - data_column_list - .into_iter() - .map(|d| d.clone_arc()) - .collect(), - self.spec.number_of_columns, - ) - .expect("data column list is within bounds"), + data_column_list + .into_iter() + .map(|d| d.clone_arc()) + .collect(), ), spec: self.spec.clone(), })) @@ -414,13 +409,8 @@ impl DataAvailabilityChecker { block, blobs: None, blobs_available_timestamp: None, - // TODO(das): update store type to prevent this conversion data_columns: data_columns.map(|data_columns| { - RuntimeVariableList::new( - data_columns.into_iter().map(|d| d.into_inner()).collect(), - self.spec.number_of_columns, - ) - .expect("data column list is within bounds") + data_columns.into_iter().map(|d| d.into_inner()).collect() }), spec: self.spec.clone(), }) @@ -610,7 +600,7 @@ pub struct AvailableBlock { blobs: Option>, /// Timestamp at which this block first became available (UNIX timestamp, time since 1970). blobs_available_timestamp: Option, - data_columns: Option>, + data_columns: Option>, pub spec: Arc, } @@ -619,7 +609,7 @@ impl AvailableBlock { block_root: Hash256, block: Arc>, blobs: Option>, - data_columns: Option>, + data_columns: Option>, spec: Arc, ) -> Self { Self { @@ -648,7 +638,7 @@ impl AvailableBlock { self.blobs_available_timestamp } - pub fn data_columns(&self) -> Option<&DataColumnSidecarList> { + pub fn data_columns(&self) -> Option<&DataColumnSidecarVec> { self.data_columns.as_ref() } @@ -659,7 +649,7 @@ impl AvailableBlock { Hash256, Arc>, Option>, - Option>, + Option>, ) { let AvailableBlock { block_root, diff --git a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs index 8fff39017ca..88d622bd629 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs @@ -349,16 +349,11 @@ impl PendingComponents { block, blobs, blobs_available_timestamp, - // TODO(das): Update store types to prevent this conversion data_columns: Some( - RuntimeVariableList::new( - verified_data_columns - .into_iter() - .map(|d| d.into_inner()) - .collect(), - spec.number_of_columns, - ) - .expect("data column list is within bounds"), + verified_data_columns + .into_iter() + .map(|d| d.into_inner()) + .collect(), ), spec: spec.clone(), }; diff --git a/beacon_node/beacon_chain/src/early_attester_cache.rs b/beacon_node/beacon_chain/src/early_attester_cache.rs index 119f41122bf..0bdd2daa0d5 100644 --- a/beacon_node/beacon_chain/src/early_attester_cache.rs +++ b/beacon_node/beacon_chain/src/early_attester_cache.rs @@ -22,7 +22,7 @@ pub struct CacheItem { */ block: Arc>, blobs: Option>, - data_columns: Option>, + data_columns: Option>, proto_block: ProtoBlock, } @@ -169,7 +169,7 @@ impl EarlyAttesterCache { } /// Returns the data columns, if `block_root` matches the cached item. - pub fn get_data_columns(&self, block_root: Hash256) -> Option> { + pub fn get_data_columns(&self, block_root: Hash256) -> Option> { self.item .read() .as_ref() diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index d5685286194..9424deb2a70 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -2593,9 +2593,7 @@ pub fn generate_rand_block_and_data_columns( ) { let (block, blobs) = generate_rand_block_and_blobs(fork_name, num_blobs, rng); let blob: BlobsList = blobs.into_iter().map(|b| b.blob).collect::>().into(); - let data_columns = DataColumnSidecar::build_sidecars(&blob, &block, &KZG, spec) - .unwrap() - .into(); + let data_columns = DataColumnSidecar::build_sidecars(&blob, &block, &KZG, spec).unwrap(); (block, data_columns) } diff --git a/beacon_node/http_api/src/publish_blocks.rs b/beacon_node/http_api/src/publish_blocks.rs index f2b68233168..3822b36b3d9 100644 --- a/beacon_node/http_api/src/publish_blocks.rs +++ b/beacon_node/http_api/src/publish_blocks.rs @@ -20,10 +20,9 @@ use std::time::Duration; use tokio::sync::mpsc::UnboundedSender; use tree_hash::TreeHash; use types::{ - AbstractExecPayload, BeaconBlockRef, BlobSidecarList, BlockImportSource, DataColumnSidecarList, + AbstractExecPayload, BeaconBlockRef, BlobSidecarList, BlockImportSource, DataColumnSidecarVec, DataColumnSubnetId, EthSpec, ExecPayload, ExecutionBlockHash, ForkName, FullPayload, - FullPayloadBellatrix, Hash256, RuntimeVariableList, SignedBeaconBlock, - SignedBlindedBeaconBlock, VariableList, + FullPayloadBellatrix, Hash256, SignedBeaconBlock, SignedBlindedBeaconBlock, VariableList, }; use warp::http::StatusCode; use warp::{reply::Response, Rejection, Reply}; @@ -78,7 +77,7 @@ pub async fn publish_block>, blobs_opt: Option>, - data_cols_opt: Option>, + data_cols_opt: Option>, sender, log, seen_timestamp| { @@ -204,11 +203,10 @@ pub async fn publish_block>(); - RuntimeVariableList::from_vec(data_columns, chain.spec.number_of_columns) + .collect::>() }); let block_root = block_root.unwrap_or(gossip_verified_block.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 fe69786b522..815a353bb15 100644 --- a/beacon_node/network/src/network_beacon_processor/rpc_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/rpc_methods.rs @@ -14,13 +14,12 @@ use lighthouse_network::rpc::*; use lighthouse_network::{PeerId, PeerRequestId, ReportSource, Response, SyncInfo}; use slog::{debug, error, warn}; use slot_clock::SlotClock; -use std::collections::HashSet; use std::collections::{hash_map::Entry, HashMap}; use std::sync::Arc; use tokio::sync::mpsc; use tokio_stream::StreamExt; use types::blob_sidecar::BlobIdentifier; -use types::{ColumnIndex, Epoch, EthSpec, ForkName, Hash256, Slot}; +use types::{Epoch, EthSpec, ForkName, Hash256, Slot}; impl NetworkBeaconProcessor { /* Auxiliary functions */ @@ -332,13 +331,13 @@ impl NetworkBeaconProcessor { let column_indexes_by_block = request.group_by_ordered_block_root(); let mut send_data_column_count = 0; - for (block_root, column_ids) in column_indexes_by_block.iter() { - match self - .chain - .get_selected_data_columns_checking_all_caches(*block_root, column_ids) - { - Ok(data_columns) => { - for data_column in data_columns { + for (block_root, column_indices) in column_indexes_by_block.iter() { + for index in column_indices { + match self + .chain + .get_data_column_checking_all_caches(*block_root, *index) + { + Ok(Some(data_column)) => { send_data_column_count += 1; self.send_response( peer_id, @@ -346,21 +345,22 @@ impl NetworkBeaconProcessor { request_id, ); } - } - Err(e) => { - self.send_error_response( - peer_id, - RPCResponseErrorCode::ServerError, - // TODO(das): leak error details to ease debugging - format!("{:?}", e).to_string(), - request_id, - ); - error!(self.log, "Error getting data column"; - "block_root" => ?block_root, - "peer" => %peer_id, - "error" => ?e - ); - return; + Ok(None) => {} // no-op + Err(e) => { + self.send_error_response( + peer_id, + RPCResponseErrorCode::ServerError, + // TODO(das): leak error details to ease debugging + format!("{:?}", e).to_string(), + request_id, + ); + error!(self.log, "Error getting data column"; + "block_root" => ?block_root, + "peer" => %peer_id, + "error" => ?e + ); + return; + } } } } @@ -1077,40 +1077,36 @@ impl NetworkBeaconProcessor { // remove all skip slots let block_roots = block_roots.into_iter().flatten(); - let mut data_columns_sent = 0; - let requested_column_indices = - HashSet::::from_iter(req.columns.iter().copied()); for root in block_roots { - match self.chain.get_data_columns(&root) { - Ok(data_column_sidecar_list) => { - for data_column_sidecar in data_column_sidecar_list.iter() { - if requested_column_indices.contains(&data_column_sidecar.index) { - data_columns_sent += 1; - self.send_network_message(NetworkMessage::SendResponse { - peer_id, - response: Response::DataColumnsByRange(Some( - data_column_sidecar.clone(), - )), - id: request_id, - }); - } + for index in &req.columns { + match self.chain.get_data_column(&root, index) { + Ok(Some(data_column_sidecar)) => { + data_columns_sent += 1; + self.send_network_message(NetworkMessage::SendResponse { + peer_id, + response: Response::DataColumnsByRange(Some( + data_column_sidecar.clone(), + )), + id: request_id, + }); + } + Ok(None) => {} // no-op + Err(e) => { + error!( + self.log, + "Error fetching data columns block root"; + "request" => ?req, + "peer" => %peer_id, + "block_root" => ?root, + "error" => ?e + ); + return Err(( + RPCResponseErrorCode::ServerError, + "No data columns and failed fetching corresponding block", + )); } - } - Err(e) => { - error!( - self.log, - "Error fetching data columns block root"; - "request" => ?req, - "peer" => %peer_id, - "block_root" => ?root, - "error" => ?e - ); - return Err(( - RPCResponseErrorCode::ServerError, - "No data columns and failed fetching corresponding block", - )); } } } diff --git a/beacon_node/network/src/sync/network_context/custody.rs b/beacon_node/network/src/sync/network_context/custody.rs index ef0927d6d4b..cc0fcec532a 100644 --- a/beacon_node/network/src/sync/network_context/custody.rs +++ b/beacon_node/network/src/sync/network_context/custody.rs @@ -30,7 +30,7 @@ const FAILED_PEERS_CACHE_EXPIRY_SECONDS: u64 = 5; #[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] pub struct CustodyRequester(pub SingleLookupReqId); -type DataColumnSidecarList = Vec>>; +type DataColumnSidecarVec = Vec>>; pub struct ActiveCustodyRequest { block_root: Hash256, @@ -107,7 +107,7 @@ impl ActiveCustodyRequest { &mut self, peer_id: PeerId, req_id: DataColumnsByRootRequestId, - resp: RpcResponseResult>, + resp: RpcResponseResult>, cx: &mut SyncNetworkContext, ) -> CustodyRequestResult { // TODO(das): Should downscore peers for verify errors here diff --git a/beacon_node/network/src/sync/sampling.rs b/beacon_node/network/src/sync/sampling.rs index 17359c47a7d..3fb21489d91 100644 --- a/beacon_node/network/src/sync/sampling.rs +++ b/beacon_node/network/src/sync/sampling.rs @@ -25,7 +25,7 @@ pub enum SamplingRequester { ImportedBlock(Hash256), } -type DataColumnSidecarList = Vec>>; +type DataColumnSidecarVec = Vec>>; pub struct Sampling { // TODO(das): stalled sampling request are never cleaned up @@ -102,7 +102,7 @@ impl Sampling { &mut self, id: SamplingId, peer_id: PeerId, - resp: Result<(DataColumnSidecarList, Duration), RpcResponseError>, + resp: Result<(DataColumnSidecarVec, Duration), RpcResponseError>, cx: &mut SyncNetworkContext, ) -> Option<(SamplingRequester, SamplingResult)> { let Some(request) = self.requests.get_mut(&id.id) else { @@ -237,7 +237,7 @@ impl ActiveSamplingRequest { &mut self, _peer_id: PeerId, column_index: ColumnIndex, - resp: Result<(DataColumnSidecarList, Duration), RpcResponseError>, + resp: Result<(DataColumnSidecarVec, Duration), RpcResponseError>, cx: &mut SyncNetworkContext, ) -> Result, SamplingError> { // Select columns to sample diff --git a/beacon_node/store/src/hot_cold_store.rs b/beacon_node/store/src/hot_cold_store.rs index 930a7b84632..fbcbbb52aa0 100644 --- a/beacon_node/store/src/hot_cold_store.rs +++ b/beacon_node/store/src/hot_cold_store.rs @@ -17,12 +17,12 @@ use crate::metadata::{ DATA_COLUMN_INFO_KEY, PRUNING_CHECKPOINT_KEY, SCHEMA_VERSION_KEY, SPLIT_KEY, STATE_UPPER_LIMIT_NO_RETAIN, }; -use crate::metrics; use crate::state_cache::{PutStateOutcome, StateCache}; use crate::{ - get_key_for_col, ChunkWriter, DBColumn, DatabaseBlock, Error, ItemStore, KeyValueStoreOp, - PartialBeaconState, StoreItem, StoreOp, + get_data_column_key, get_key_for_col, ChunkWriter, DBColumn, DatabaseBlock, Error, ItemStore, + KeyValueStoreOp, PartialBeaconState, StoreItem, StoreOp, }; +use crate::{metrics, parse_data_column_key}; use itertools::process_results; use leveldb::iterator::LevelDBIterator; use lru::LruCache; @@ -36,6 +36,7 @@ use state_processing::{ SlotProcessingError, }; use std::cmp::min; +use std::collections::HashMap; use std::marker::PhantomData; use std::num::NonZeroUsize; use std::path::Path; @@ -89,7 +90,7 @@ pub struct HotColdDB, Cold: ItemStore> { struct BlockCache { block_cache: LruCache>, blob_cache: LruCache>, - data_column_cache: LruCache>, + data_column_cache: LruCache>>>, } impl BlockCache { @@ -106,12 +107,10 @@ impl BlockCache { pub fn put_blobs(&mut self, block_root: Hash256, blobs: BlobSidecarList) { self.blob_cache.put(block_root, blobs); } - pub fn put_data_columns( - &mut self, - block_root: Hash256, - data_columns: DataColumnSidecarList, - ) { - self.data_column_cache.put(block_root, data_columns); + pub fn put_data_column(&mut self, block_root: Hash256, data_column: Arc>) { + self.data_column_cache + .get_or_insert_mut(block_root, Default::default) + .insert(data_column.index, data_column); } pub fn get_block<'a>(&'a mut self, block_root: &Hash256) -> Option<&'a SignedBeaconBlock> { self.block_cache.get(block_root) @@ -119,11 +118,14 @@ impl BlockCache { pub fn get_blobs<'a>(&'a mut self, block_root: &Hash256) -> Option<&'a BlobSidecarList> { self.blob_cache.get(block_root) } - pub fn get_data_columns<'a>( + pub fn get_data_column<'a>( &'a mut self, block_root: &Hash256, - ) -> Option<&'a DataColumnSidecarList> { - self.data_column_cache.get(block_root) + column_index: &ColumnIndex, + ) -> Option<&'a Arc>> { + self.data_column_cache + .get(block_root) + .and_then(|map| map.get(column_index)) } pub fn delete_block(&mut self, block_root: &Hash256) { let _ = self.block_cache.pop(block_root); @@ -672,15 +674,20 @@ impl, Cold: ItemStore> HotColdDB pub fn data_columns_as_kv_store_ops( &self, - key: &Hash256, - data_columns: DataColumnSidecarList, + block_root: &Hash256, + data_columns: DataColumnSidecarVec, ops: &mut Vec, ) { - let db_key = get_key_for_col(DBColumn::BeaconDataColumn.into(), key.as_bytes()); - ops.push(KeyValueStoreOp::PutKeyValue( - db_key, - data_columns.as_ssz_bytes(), - )); + for data_column in data_columns { + let db_key = get_key_for_col( + DBColumn::BeaconDataColumn.into(), + &get_data_column_key(block_root, &data_column.index), + ); + ops.push(KeyValueStoreOp::PutKeyValue( + db_key, + data_column.as_ssz_bytes(), + )); + } } pub fn put_state_summary( @@ -998,10 +1005,14 @@ impl, Cold: ItemStore> HotColdDB key_value_batch.push(KeyValueStoreOp::DeleteKey(key)); } - StoreOp::DeleteDataColumns(block_root) => { - let key = - get_key_for_col(DBColumn::BeaconDataColumn.into(), block_root.as_bytes()); - key_value_batch.push(KeyValueStoreOp::DeleteKey(key)); + StoreOp::DeleteDataColumns(block_root, column_indices) => { + for index in column_indices { + let key = get_key_for_col( + DBColumn::BeaconDataColumn.into(), + &get_data_column_key(&block_root, &index), + ); + key_value_batch.push(KeyValueStoreOp::DeleteKey(key)); + } } StoreOp::DeleteState(state_root, slot) => { @@ -1054,9 +1065,19 @@ impl, Cold: ItemStore> HotColdDB } true } - StoreOp::DeleteDataColumns(block_root) => { - match self.get_data_columns(block_root) { - Ok(Some(data_column_sidecar_list)) => { + StoreOp::DeleteDataColumns(block_root, indices) => { + match indices + .iter() + .map(|index| self.get_data_column(block_root, index)) + .collect::, _>>() + { + Ok(data_column_sidecar_list_opt) => { + let data_column_sidecar_list = data_column_sidecar_list_opt + .into_iter() + .flatten() + .collect::>(); + // Must push the same number of items as StoreOp::DeleteDataColumns items to + // prevent a `HotColdDBError::Rollback` error below in case of rollback data_columns_to_delete.push((*block_root, data_column_sidecar_list)); } Err(e) => { @@ -1066,7 +1087,6 @@ impl, Cold: ItemStore> HotColdDB "error" => ?e ); } - _ => (), } true } @@ -1101,14 +1121,15 @@ impl, Cold: ItemStore> HotColdDB for op in blob_cache_ops.iter_mut() { let reverse_op = match op { StoreOp::PutBlobs(block_root, _) => StoreOp::DeleteBlobs(*block_root), - StoreOp::PutDataColumns(block_root, _) => { - StoreOp::DeleteDataColumns(*block_root) + StoreOp::PutDataColumns(block_root, data_columns) => { + let indices = data_columns.iter().map(|c| c.index).collect(); + StoreOp::DeleteDataColumns(*block_root, indices) } StoreOp::DeleteBlobs(_) => match blobs_to_delete.pop() { Some((block_root, blobs)) => StoreOp::PutBlobs(block_root, blobs), None => return Err(HotColdDBError::Rollback.into()), }, - StoreOp::DeleteDataColumns(_) => match data_columns_to_delete.pop() { + StoreOp::DeleteDataColumns(_, _) => match data_columns_to_delete.pop() { Some((block_root, data_columns)) => { StoreOp::PutDataColumns(block_root, data_columns) } @@ -1152,7 +1173,7 @@ impl, Cold: ItemStore> HotColdDB StoreOp::DeleteBlobs(_) => (), - StoreOp::DeleteDataColumns(_) => (), + StoreOp::DeleteDataColumns(_, _) => (), StoreOp::DeleteExecutionPayload(_) => (), @@ -1653,30 +1674,40 @@ impl, Cold: ItemStore> HotColdDB } } - /// Fetch data_columns for a given block from the store. - pub fn get_data_columns( + /// Fetch all keys in the data_column column with prefix `block_root` + pub fn get_data_column_keys(&self, block_root: Hash256) -> Result, Error> { + self.blobs_db + .iter_raw_keys(DBColumn::BeaconDataColumn, block_root.as_bytes()) + .map(|key| key.and_then(|key| parse_data_column_key(key).map(|key| key.1))) + .collect() + } + + /// Fetch a single data_column for a given block from the store. + pub fn get_data_column( &self, block_root: &Hash256, - ) -> Result>, Error> { + column_index: &ColumnIndex, + ) -> Result>>, Error> { // Check the cache. - if let Some(data_columns) = self.block_cache.lock().get_data_columns(block_root) { + if let Some(data_column) = self + .block_cache + .lock() + .get_data_column(block_root, column_index) + { metrics::inc_counter(&metrics::BEACON_DATA_COLUMNS_CACHE_HIT_COUNT); - return Ok(Some(data_columns.clone())); + return Ok(Some(data_column.clone())); } - match self - .blobs_db - .get_bytes(DBColumn::BeaconDataColumn.into(), block_root.as_bytes())? - { - Some(ref data_columns_bytes) => { - let data_columns = RuntimeVariableList::from_ssz_bytes( - data_columns_bytes, - self.spec.number_of_columns, - )?; + match self.blobs_db.get_bytes( + DBColumn::BeaconDataColumn.into(), + &get_data_column_key(block_root, column_index), + )? { + Some(ref data_column_bytes) => { + let data_column = Arc::new(DataColumnSidecar::from_ssz_bytes(data_column_bytes)?); self.block_cache .lock() - .put_data_columns(*block_root, data_columns.clone()); - Ok(Some(data_columns)) + .put_data_column(*block_root, data_column.clone()); + Ok(Some(data_column)) } None => Ok(None), } @@ -2481,15 +2512,33 @@ impl, Cold: ItemStore> HotColdDB } }; - if Some(block_root) != last_pruned_block_root && self.blobs_exist(&block_root)? { - trace!( - self.log, - "Pruning blobs of block"; - "slot" => slot, - "block_root" => ?block_root, - ); - last_pruned_block_root = Some(block_root); - ops.push(StoreOp::DeleteBlobs(block_root)); + if Some(block_root) != last_pruned_block_root { + if self + .spec + .is_peer_das_enabled_for_epoch(slot.epoch(E::slots_per_epoch())) + { + // data columns + let indices = self.get_data_column_keys(block_root)?; + if !indices.is_empty() { + trace!( + self.log, + "Pruning data columns of block"; + "slot" => slot, + "block_root" => ?block_root, + ); + last_pruned_block_root = Some(block_root); + ops.push(StoreOp::DeleteDataColumns(block_root, indices)); + } + } else if self.blobs_exist(&block_root)? { + trace!( + self.log, + "Pruning blobs of block"; + "slot" => slot, + "block_root" => ?block_root, + ); + last_pruned_block_root = Some(block_root); + ops.push(StoreOp::DeleteBlobs(block_root)); + } } if slot >= end_slot { diff --git a/beacon_node/store/src/leveldb_store.rs b/beacon_node/store/src/leveldb_store.rs index ffd55c16a04..b224319ae4f 100644 --- a/beacon_node/store/src/leveldb_store.rs +++ b/beacon_node/store/src/leveldb_store.rs @@ -255,6 +255,10 @@ impl db_key::Key for BytesKey { } impl BytesKey { + pub fn starts_with(&self, prefix: &Self) -> bool { + self.key.starts_with(&prefix.key) + } + /// Return `true` iff this `BytesKey` was created with the given `column`. pub fn matches_column(&self, column: DBColumn) -> bool { self.key.starts_with(column.as_bytes()) diff --git a/beacon_node/store/src/lib.rs b/beacon_node/store/src/lib.rs index 2b648d71dbc..10b3d7a3a65 100644 --- a/beacon_node/store/src/lib.rs +++ b/beacon_node/store/src/lib.rs @@ -112,9 +112,7 @@ pub trait KeyValueStore: Sync + Send + Sized + 'static { Box::new(std::iter::empty()) } - fn iter_raw_keys(&self, _column: DBColumn, _prefix: &[u8]) -> RawKeyIter { - Box::new(std::iter::empty()) - } + fn iter_raw_keys(&self, column: DBColumn, prefix: &[u8]) -> RawKeyIter; /// Iterate through all keys in a particular column. fn iter_column_keys(&self, column: DBColumn) -> ColumnKeyIter; @@ -146,6 +144,28 @@ pub fn get_key_for_col(column: &str, key: &[u8]) -> Vec { result } +pub fn get_data_column_key(block_root: &Hash256, column_index: &ColumnIndex) -> Vec { + let mut result = block_root.as_bytes().to_vec(); + result.extend_from_slice(&column_index.to_le_bytes()); + result +} + +pub fn parse_data_column_key(data: Vec) -> Result<(Hash256, ColumnIndex), Error> { + if data.len() != 32 + 8 { + return Err(Error::InvalidKey); + } + // split_at panics if 32 < 40 which will never happen after the length check above + let (block_root_bytes, column_index_bytes) = data.split_at(32); + let block_root = Hash256::from_slice(block_root_bytes); + // column_index_bytes is asserted to be 8 bytes after the length check above + let column_index = ColumnIndex::from_le_bytes( + column_index_bytes + .try_into() + .expect("slice with incorrect length"), + ); + Ok((block_root, column_index)) +} + #[must_use] #[derive(Clone)] pub enum KeyValueStoreOp { @@ -206,13 +226,13 @@ pub enum StoreOp<'a, E: EthSpec> { PutBlock(Hash256, Arc>), PutState(Hash256, &'a BeaconState), PutBlobs(Hash256, BlobSidecarList), - PutDataColumns(Hash256, DataColumnSidecarList), + PutDataColumns(Hash256, DataColumnSidecarVec), PutStateSummary(Hash256, HotStateSummary), PutStateTemporaryFlag(Hash256), DeleteStateTemporaryFlag(Hash256), DeleteBlock(Hash256), DeleteBlobs(Hash256), - DeleteDataColumns(Hash256), + DeleteDataColumns(Hash256, Vec), DeleteState(Hash256, Option), DeleteExecutionPayload(Hash256), KeyValueOp(KeyValueStoreOp), @@ -301,7 +321,6 @@ impl DBColumn { | Self::BeaconBlock | Self::BeaconState | Self::BeaconBlob - | Self::BeaconDataColumn | Self::BeaconStateSummary | Self::BeaconStateTemporary | Self::ExecPayload @@ -318,6 +337,7 @@ impl DBColumn { | Self::BeaconHistoricalRoots | Self::BeaconHistoricalSummaries | Self::BeaconRandaoMixes => 8, + Self::BeaconDataColumn => 32 + 8, } } } diff --git a/beacon_node/store/src/memory_store.rs b/beacon_node/store/src/memory_store.rs index 302d2c2add2..a7c2bd2c575 100644 --- a/beacon_node/store/src/memory_store.rs +++ b/beacon_node/store/src/memory_store.rs @@ -1,3 +1,4 @@ +use crate::RawKeyIter; use crate::{ get_key_for_col, leveldb_store::BytesKey, ColumnIter, ColumnKeyIter, DBColumn, Error, ItemStore, Key, KeyValueStore, KeyValueStoreOp, @@ -100,6 +101,18 @@ impl KeyValueStore for MemoryStore { })) } + fn iter_raw_keys(&self, column: DBColumn, prefix: &[u8]) -> RawKeyIter { + let start_key = BytesKey::from_vec(get_key_for_col(column.as_str(), prefix)); + let keys = self + .db + .read() + .range(start_key.clone()..) + .take_while(|(k, _)| k.starts_with(&start_key)) + .filter_map(|(k, _)| k.remove_column_variable(column).map(|k| k.to_vec())) + .collect::>(); + Box::new(keys.into_iter().map(Ok)) + } + fn iter_column_keys(&self, column: DBColumn) -> ColumnKeyIter { Box::new(self.iter_column(column).map(|res| res.map(|(k, _)| k))) } diff --git a/consensus/types/src/data_column_sidecar.rs b/consensus/types/src/data_column_sidecar.rs index 74f9a9b4223..bbed920259c 100644 --- a/consensus/types/src/data_column_sidecar.rs +++ b/consensus/types/src/data_column_sidecar.rs @@ -1,8 +1,8 @@ use crate::beacon_block_body::{KzgCommitments, BLOB_KZG_COMMITMENTS_INDEX}; use crate::test_utils::TestRandom; use crate::{ - BeaconBlockHeader, ChainSpec, EthSpec, Hash256, KzgProofs, RuntimeVariableList, - SignedBeaconBlock, SignedBeaconBlockHeader, Slot, + BeaconBlockHeader, ChainSpec, EthSpec, Hash256, KzgProofs, SignedBeaconBlock, + SignedBeaconBlockHeader, Slot, }; use crate::{BeaconStateError, BlobsList}; use bls::Signature; @@ -41,7 +41,7 @@ pub struct DataColumnIdentifier { pub index: ColumnIndex, } -pub type DataColumnSidecarList = RuntimeVariableList>>; +pub type DataColumnSidecarVec = Vec>>; #[derive( Debug, @@ -106,10 +106,10 @@ impl DataColumnSidecar { block: &SignedBeaconBlock, kzg: &Kzg, spec: &ChainSpec, - ) -> Result, DataColumnSidecarError> { + ) -> Result, DataColumnSidecarError> { let number_of_columns = spec.number_of_columns; if blobs.is_empty() { - return Ok(RuntimeVariableList::empty(number_of_columns)); + return Ok(vec![]); } let kzg_commitments = block .message() @@ -189,7 +189,6 @@ impl DataColumnSidecar { }) }) .collect(); - let sidecars = RuntimeVariableList::from_vec(sidecars, number_of_columns); Ok(sidecars) } diff --git a/consensus/types/src/lib.rs b/consensus/types/src/lib.rs index 79abe7af763..bdddb7226ff 100644 --- a/consensus/types/src/lib.rs +++ b/consensus/types/src/lib.rs @@ -144,7 +144,7 @@ pub use crate::config_and_preset::{ pub use crate::consolidation::Consolidation; pub use crate::contribution_and_proof::ContributionAndProof; pub use crate::data_column_sidecar::{ - ColumnIndex, DataColumnIdentifier, DataColumnSidecar, DataColumnSidecarList, + ColumnIndex, DataColumnIdentifier, DataColumnSidecar, DataColumnSidecarVec, }; pub use crate::data_column_subnet_id::DataColumnSubnetId; pub use crate::deposit::{Deposit, DEPOSIT_TREE_DEPTH}; From c5a5c0e99634cc99718574b059538afb91d4ad1d Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Wed, 19 Jun 2024 02:26:47 +1000 Subject: [PATCH 44/76] Update reconstruction benches to newer criterion version. (#5949) --- beacon_node/beacon_chain/benches/benches.rs | 29 +++++++++------------ 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/beacon_node/beacon_chain/benches/benches.rs b/beacon_node/beacon_chain/benches/benches.rs index c4ca030e40c..4d61f513367 100644 --- a/beacon_node/beacon_chain/benches/benches.rs +++ b/beacon_node/beacon_chain/benches/benches.rs @@ -1,10 +1,8 @@ -#![allow(deprecated)] - use std::sync::Arc; +use criterion::{black_box, criterion_group, criterion_main, Criterion}; + use bls::Signature; -use criterion::Criterion; -use criterion::{black_box, criterion_group, criterion_main, Benchmark}; use eth2_network_config::TRUSTED_SETUP_BYTES; use kzg::{Kzg, KzgCommitment, TrustedSetup}; use types::{ @@ -51,20 +49,17 @@ fn all_benches(c: &mut Criterion) { let spec = spec.clone(); - c.bench( - &format!("reconstruct_{}", blob_count), - Benchmark::new("kzg/reconstruct", move |b| { - b.iter(|| { - black_box(DataColumnSidecar::reconstruct( - &kzg, - &column_sidecars.iter().as_slice()[0..column_sidecars.len() / 2], - spec.as_ref(), - )) - }) - }), - ); + c.bench_function(&format!("reconstruct_{}", blob_count), |b| { + b.iter(|| { + black_box(DataColumnSidecar::reconstruct( + &kzg, + &column_sidecars.iter().as_slice()[0..column_sidecars.len() / 2], + spec.as_ref(), + )) + }) + }); } } -criterion_group!(benches, all_benches,); +criterion_group!(benches, all_benches); criterion_main!(benches); From 6ff948008db0d1736129829bfa64c4b1a5f615b6 Mon Sep 17 00:00:00 2001 From: kevaundray Date: Wed, 19 Jun 2024 08:10:42 +0100 Subject: [PATCH 45/76] chore: add `recover_cells_and_compute_proofs` method (#5938) * chore: add recover_cells_and_compute_proofs method * Introduce type alias `CellsAndKzgProofs` to address type complexity. --------- Co-authored-by: Jimmy Chen --- consensus/types/src/data_column_sidecar.rs | 10 +---- crypto/kzg/src/lib.rs | 44 ++++++------------- .../cases/kzg_compute_cells_and_kzg_proofs.rs | 11 +---- 3 files changed, 17 insertions(+), 48 deletions(-) diff --git a/consensus/types/src/data_column_sidecar.rs b/consensus/types/src/data_column_sidecar.rs index bbed920259c..5f6439721f4 100644 --- a/consensus/types/src/data_column_sidecar.rs +++ b/consensus/types/src/data_column_sidecar.rs @@ -225,15 +225,7 @@ impl DataColumnSidecar { cells.push(ssz_cell_to_crypto_cell::(cell)?); cell_ids.push(data_column.index); } - // recover_all_cells does not expect sorted - let all_cells = kzg.recover_all_cells(&cell_ids, &cells)?; - let blob = kzg.cells_to_blob(&all_cells)?; - - // Note: This function computes all cells and proofs. According to Justin this is okay, - // computing a partial set may be more expensive and requires code paths that don't exist. - // Computing the blobs cells is technically unnecessary but very cheap. It's done here again - // for simplicity. - kzg.compute_cells_and_proofs(&blob) + kzg.recover_cells_and_compute_kzg_proofs(&cell_ids, &cells) }) .collect::, KzgError>>()?; diff --git a/crypto/kzg/src/lib.rs b/crypto/kzg/src/lib.rs index e41345e7046..9875126700c 100644 --- a/crypto/kzg/src/lib.rs +++ b/crypto/kzg/src/lib.rs @@ -16,6 +16,11 @@ pub use c_kzg::{ pub use c_kzg::{Cell, CELLS_PER_EXT_BLOB}; use mockall::automock; +pub type CellsAndKzgProofs = ( + Box<[Cell; CELLS_PER_EXT_BLOB]>, + Box<[KzgProof; CELLS_PER_EXT_BLOB]>, +); + #[derive(Debug)] pub enum Error { /// An error from the underlying kzg library. @@ -152,17 +157,7 @@ impl Kzg { } /// Computes the cells and associated proofs for a given `blob` at index `index`. - #[allow(clippy::type_complexity)] - pub fn compute_cells_and_proofs( - &self, - blob: &Blob, - ) -> Result< - ( - Box<[Cell; CELLS_PER_EXT_BLOB]>, - Box<[KzgProof; CELLS_PER_EXT_BLOB]>, - ), - Error, - > { + pub fn compute_cells_and_proofs(&self, blob: &Blob) -> Result { let (cells, proofs) = c_kzg::Cell::compute_cells_and_kzg_proofs(blob, &self.trusted_setup) .map_err(Into::::into)?; let proofs = Box::new(proofs.map(|proof| KzgProof::from(proof.to_bytes().into_inner()))); @@ -196,39 +191,28 @@ impl Kzg { } } - pub fn cells_to_blob(&self, cells: &[Cell; c_kzg::CELLS_PER_EXT_BLOB]) -> Result { + fn cells_to_blob(&self, cells: &[Cell; c_kzg::CELLS_PER_EXT_BLOB]) -> Result { Ok(Blob::cells_to_blob(cells)?) } - pub fn recover_all_cells( + pub fn recover_cells_and_compute_kzg_proofs( &self, cell_ids: &[u64], cells: &[Cell], - ) -> Result, Error> { - Ok(c_kzg::Cell::recover_all_cells( - cell_ids, - cells, - &self.trusted_setup, - )?) + ) -> Result { + let all_cells = c_kzg::Cell::recover_all_cells(cell_ids, cells, &self.trusted_setup)?; + let blob = self.cells_to_blob(&all_cells)?; + self.compute_cells_and_proofs(&blob) } } pub mod mock { - use crate::{Error, KzgProof}; + use crate::{CellsAndKzgProofs, Error, KzgProof}; use c_kzg::{Blob, Cell, CELLS_PER_EXT_BLOB}; pub const MOCK_KZG_BYTES_PER_CELL: usize = 2048; - #[allow(clippy::type_complexity)] - pub fn compute_cells_and_proofs( - _blob: &Blob, - ) -> Result< - ( - Box<[Cell; CELLS_PER_EXT_BLOB]>, - Box<[KzgProof; CELLS_PER_EXT_BLOB]>, - ), - Error, - > { + pub fn compute_cells_and_proofs(_blob: &Blob) -> Result { let empty_cell = Cell::new([0; MOCK_KZG_BYTES_PER_CELL]); Ok(( Box::new([empty_cell; CELLS_PER_EXT_BLOB]), diff --git a/testing/ef_tests/src/cases/kzg_compute_cells_and_kzg_proofs.rs b/testing/ef_tests/src/cases/kzg_compute_cells_and_kzg_proofs.rs index 63d528467bd..2effa68ff78 100644 --- a/testing/ef_tests/src/cases/kzg_compute_cells_and_kzg_proofs.rs +++ b/testing/ef_tests/src/cases/kzg_compute_cells_and_kzg_proofs.rs @@ -1,7 +1,6 @@ use super::*; use crate::case_result::compare_result; -use kzg::{Blob as KzgBlob, Cell}; -use kzg::{KzgProof, CELLS_PER_EXT_BLOB}; +use kzg::{Blob as KzgBlob, CellsAndKzgProofs}; use serde::Deserialize; use std::marker::PhantomData; @@ -62,12 +61,6 @@ impl Case for KZGComputeCellsAndKZGProofs { .ok() }); - compare_result::< - ( - Box<[Cell; CELLS_PER_EXT_BLOB]>, - Box<[KzgProof; CELLS_PER_EXT_BLOB]>, - ), - _, - >(&cells_and_proofs, &expected) + compare_result::(&cells_and_proofs, &expected) } } From 733b1dff01c53c3a93b50854db0b1b10b34fea70 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Wed, 26 Jun 2024 05:41:30 +1000 Subject: [PATCH 46/76] Update `csc` format in ENR and spec tests for devnet-1 (#5966) * Update `csc` format in ENR. * Add spec tests for `recover_cells_and_kzg_proofs`. * Add tests for ENR. * Fix failing tests. * Add protection against invalid csc value in ENR. * Fix lint --- beacon_node/lighthouse_network/src/config.rs | 2 +- .../lighthouse_network/src/discovery/enr.rs | 79 +++++++++++++-- .../src/peer_manager/peerdb.rs | 6 +- consensus/types/src/data_column_subnet_id.rs | 12 +-- testing/ef_tests/Makefile | 2 +- testing/ef_tests/check_all_files_accessed.py | 6 +- testing/ef_tests/src/cases.rs | 2 + .../cases/kzg_recover_cells_and_kzg_proofs.rs | 95 +++++++++++++++++++ testing/ef_tests/src/handler.rs | 20 ++++ testing/ef_tests/tests/tests.rs | 6 ++ 10 files changed, 211 insertions(+), 19 deletions(-) create mode 100644 testing/ef_tests/src/cases/kzg_recover_cells_and_kzg_proofs.rs diff --git a/beacon_node/lighthouse_network/src/config.rs b/beacon_node/lighthouse_network/src/config.rs index 30f35064586..7c95977140e 100644 --- a/beacon_node/lighthouse_network/src/config.rs +++ b/beacon_node/lighthouse_network/src/config.rs @@ -42,7 +42,7 @@ pub struct Config { pub network_dir: PathBuf, /// IP addresses to listen on. - listen_addresses: ListenAddress, + pub(crate) listen_addresses: ListenAddress, /// The address to broadcast to peers about which address we are listening on. None indicates /// that no discovery address has been set in the CLI args. diff --git a/beacon_node/lighthouse_network/src/discovery/enr.rs b/beacon_node/lighthouse_network/src/discovery/enr.rs index 67bfe0fd809..fc8d3809e2d 100644 --- a/beacon_node/lighthouse_network/src/discovery/enr.rs +++ b/beacon_node/lighthouse_network/src/discovery/enr.rs @@ -25,7 +25,7 @@ pub const ATTESTATION_BITFIELD_ENR_KEY: &str = "attnets"; /// The ENR field specifying the sync committee subnet bitfield. pub const SYNC_COMMITTEE_BITFIELD_ENR_KEY: &str = "syncnets"; /// The ENR field specifying the peerdas custody subnet count. -pub const PEERDAS_CUSTODY_SUBNET_COUNT_ENR_KEY: &str = "custody_subnet_count"; +pub const PEERDAS_CUSTODY_SUBNET_COUNT_ENR_KEY: &str = "csc"; /// Extension trait for ENR's within Eth2. pub trait Eth2Enr { @@ -68,7 +68,9 @@ impl Eth2Enr for Enr { /// defined in the spec. fn custody_subnet_count(&self, spec: &ChainSpec) -> u64 { self.get(PEERDAS_CUSTODY_SUBNET_COUNT_ENR_KEY) - .and_then(|custody_bytes| u64::from_ssz_bytes(custody_bytes).ok()) + .and_then(|custody_bytes| custody_bytes.try_into().map(u64::from_be_bytes).ok()) + // If value supplied in ENR is invalid, fallback to `custody_requirement` + .filter(|csc| csc <= &spec.data_column_sidecar_subnet_count) .unwrap_or(spec.custody_requirement) } @@ -243,10 +245,8 @@ pub fn build_enr( spec.custody_requirement }; - builder.add_value( - PEERDAS_CUSTODY_SUBNET_COUNT_ENR_KEY, - &custody_subnet_count.as_ssz_bytes(), - ); + let csc_bytes = custody_subnet_count.to_be_bytes(); + builder.add_value(PEERDAS_CUSTODY_SUBNET_COUNT_ENR_KEY, &csc_bytes.as_slice()); builder .build(enr_key) @@ -309,3 +309,70 @@ pub fn save_enr_to_disk(dir: &Path, enr: &Enr, log: &slog::Logger) { } } } + +#[cfg(test)] +mod test { + use super::*; + use crate::config::Config as NetworkConfig; + use types::MainnetEthSpec; + + type E = MainnetEthSpec; + + #[test] + fn custody_subnet_count_default() { + let config = NetworkConfig { + subscribe_all_data_column_subnets: false, + ..NetworkConfig::default() + }; + let spec = E::default_spec(); + let enr = build_enr_with_config(config, &spec).0; + + assert_eq!( + enr.custody_subnet_count::(&spec), + spec.custody_requirement, + ); + } + + #[test] + fn custody_subnet_count_all() { + let config = NetworkConfig { + subscribe_all_data_column_subnets: true, + ..NetworkConfig::default() + }; + let spec = E::default_spec(); + let enr = build_enr_with_config(config, &spec).0; + + assert_eq!( + enr.custody_subnet_count::(&spec), + spec.data_column_sidecar_subnet_count, + ); + } + + #[test] + fn custody_subnet_count_fallback_default() { + let config = NetworkConfig::default(); + let spec = E::default_spec(); + let (mut enr, enr_key) = build_enr_with_config(config, &spec); + let invalid_subnet_count = 999u64; + + enr.insert( + PEERDAS_CUSTODY_SUBNET_COUNT_ENR_KEY, + &invalid_subnet_count.to_be_bytes().as_slice(), + &enr_key, + ) + .unwrap(); + + assert_eq!( + enr.custody_subnet_count::(&spec), + spec.custody_requirement, + ); + } + + fn build_enr_with_config(config: NetworkConfig, spec: &ChainSpec) -> (Enr, CombinedKey) { + let keypair = libp2p::identity::secp256k1::Keypair::generate(); + let enr_key = CombinedKey::from_secp256k1(&keypair); + let enr_fork_id = EnrForkId::default(); + let enr = build_enr::(&enr_key, &config, &enr_fork_id, spec).unwrap(); + (enr, enr_key) + } +} diff --git a/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs b/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs index ec9b011d365..66ba0980392 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs @@ -5,7 +5,6 @@ use peer_info::{ConnectionDirection, PeerConnectionStatus, PeerInfo}; use rand::seq::SliceRandom; use score::{PeerAction, ReportSource, Score, ScoreState}; use slog::{crit, debug, error, trace, warn}; -use ssz::Encode; use std::net::IpAddr; use std::time::Instant; use std::{cmp::Ordering, fmt::Display}; @@ -687,7 +686,10 @@ impl PeerDB { if supernode { enr.insert( PEERDAS_CUSTODY_SUBNET_COUNT_ENR_KEY, - &spec.data_column_sidecar_subnet_count.as_ssz_bytes(), + &spec + .data_column_sidecar_subnet_count + .to_be_bytes() + .as_slice(), &enr_key, ) .expect("u64 can be encoded"); diff --git a/consensus/types/src/data_column_subnet_id.rs b/consensus/types/src/data_column_subnet_id.rs index 34a0302cc79..403216977df 100644 --- a/consensus/types/src/data_column_subnet_id.rs +++ b/consensus/types/src/data_column_subnet_id.rs @@ -107,15 +107,15 @@ impl From for DataColumnSubnetId { } } -impl Into for DataColumnSubnetId { - fn into(self) -> u64 { - self.0 +impl From for u64 { + fn from(val: DataColumnSubnetId) -> Self { + val.0 } } -impl Into for &DataColumnSubnetId { - fn into(self) -> u64 { - self.0 +impl From<&DataColumnSubnetId> for u64 { + fn from(val: &DataColumnSubnetId) -> Self { + val.0 } } diff --git a/testing/ef_tests/Makefile b/testing/ef_tests/Makefile index 5dc3d2a0404..59e08e5d259 100644 --- a/testing/ef_tests/Makefile +++ b/testing/ef_tests/Makefile @@ -1,4 +1,4 @@ -TESTS_TAG := v1.5.0-alpha.2 +TESTS_TAG := v1.5.0-alpha.3 TESTS = general minimal mainnet TARBALLS = $(patsubst %,%-$(TESTS_TAG).tar.gz,$(TESTS)) diff --git a/testing/ef_tests/check_all_files_accessed.py b/testing/ef_tests/check_all_files_accessed.py index 67cdbe9b969..1a279771979 100755 --- a/testing/ef_tests/check_all_files_accessed.py +++ b/testing/ef_tests/check_all_files_accessed.py @@ -35,12 +35,12 @@ "tests/.*/.*/ssz_static/LightClientStore", # LightClientSnapshot "tests/.*/.*/ssz_static/LightClientSnapshot", + # Unused container for das + "tests/.*/.*/ssz_static/MatrixEntry", # Unused kzg methods - "tests/.*/.*/kzg/compute_cells", - "tests/.*/.*/kzg/recover_all_cells", "tests/.*/.*/kzg/verify_cell_kzg_proof", # One of the EF researchers likes to pack the tarballs on a Mac - ".*\.DS_Store.*", + ".*/.DS_Store.*", # More Mac weirdness. "tests/mainnet/bellatrix/operations/deposit/pyspec_tests/deposit_with_previous_fork_version__valid_ineffective/._meta.yaml", "tests/mainnet/eip7594/networking/get_custody_columns/pyspec_tests/get_custody_columns__short_node_id/._meta.yaml", diff --git a/testing/ef_tests/src/cases.rs b/testing/ef_tests/src/cases.rs index 2137452d46d..3a9fc023624 100644 --- a/testing/ef_tests/src/cases.rs +++ b/testing/ef_tests/src/cases.rs @@ -23,6 +23,7 @@ mod kzg_blob_to_kzg_commitment; mod kzg_compute_blob_kzg_proof; mod kzg_compute_cells_and_kzg_proofs; mod kzg_compute_kzg_proof; +mod kzg_recover_cells_and_kzg_proofs; mod kzg_verify_blob_kzg_proof; mod kzg_verify_blob_kzg_proof_batch; mod kzg_verify_cell_kzg_proof_batch; @@ -56,6 +57,7 @@ pub use kzg_blob_to_kzg_commitment::*; pub use kzg_compute_blob_kzg_proof::*; pub use kzg_compute_cells_and_kzg_proofs::*; pub use kzg_compute_kzg_proof::*; +pub use kzg_recover_cells_and_kzg_proofs::*; pub use kzg_verify_blob_kzg_proof::*; pub use kzg_verify_blob_kzg_proof_batch::*; pub use kzg_verify_cell_kzg_proof_batch::*; diff --git a/testing/ef_tests/src/cases/kzg_recover_cells_and_kzg_proofs.rs b/testing/ef_tests/src/cases/kzg_recover_cells_and_kzg_proofs.rs new file mode 100644 index 00000000000..c177094d0b1 --- /dev/null +++ b/testing/ef_tests/src/cases/kzg_recover_cells_and_kzg_proofs.rs @@ -0,0 +1,95 @@ +use super::*; +use crate::case_result::compare_result; +use kzg::{CellsAndKzgProofs, KzgProof}; +use serde::Deserialize; +use std::convert::Infallible; +use std::marker::PhantomData; + +#[derive(Debug, Clone, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct KZGRecoverCellsAndKzgProofsInput { + pub cell_indices: Vec, + pub cells: Vec, + pub proofs: Vec, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(bound = "E: EthSpec", deny_unknown_fields)] +pub struct KZGRecoverCellsAndKZGProofs { + pub input: KZGRecoverCellsAndKzgProofsInput, + pub output: Option<(Vec, Vec)>, + #[serde(skip)] + _phantom: PhantomData, +} + +impl LoadCase for KZGRecoverCellsAndKZGProofs { + fn load_from_dir(path: &Path, _fork_name: ForkName) -> Result { + decode::yaml_decode_file(path.join("data.yaml").as_path()) + } +} + +impl Case for KZGRecoverCellsAndKZGProofs { + fn is_enabled_for_fork(fork_name: ForkName) -> bool { + fork_name == ForkName::Deneb + } + + fn result(&self, _case_index: usize, _fork_name: ForkName) -> Result<(), Error> { + let parse_input = |input: &KZGRecoverCellsAndKzgProofsInput| { + // Proofs are not used for `recover_cells_and_compute_kzg_proofs`, they are only checked + // to satisfy the spec tests. + if input.proofs.len() != input.cell_indices.len() { + return Err(Error::SkippedKnownFailure); + } + + let proofs = input + .proofs + .iter() + .map(|s| parse_proof(s)) + .collect::, Error>>()?; + + let cells = input + .cells + .iter() + .map(|s| parse_cell(s)) + .collect::, Error>>()?; + + Ok((proofs, cells, input.cell_indices.clone())) + }; + + let result = + parse_input(&self.input).and_then(|(input_proofs, input_cells, cell_indices)| { + let (cells, proofs) = KZG + .recover_cells_and_compute_kzg_proofs( + cell_indices.as_slice(), + input_cells.as_slice(), + ) + .map_err(|e| { + Error::InternalError(format!( + "Failed to recover cells and kzg proofs: {e:?}" + )) + })?; + + // Check recovered proofs matches inputs proofs. This is done only to satisfy the + // spec tests, as the ckzg library recomputes all proofs and does not require + // proofs to recover. + for (input_proof, cell_id) in input_proofs.iter().zip(cell_indices) { + if let Err(e) = compare_result::( + &Ok(*input_proof), + &proofs.get(cell_id as usize).cloned(), + ) { + return Err(e); + } + } + + Ok((cells, proofs)) + }); + + let expected = self + .output + .as_ref() + .and_then(|(cells, proofs)| parse_cells_and_proofs(cells, proofs).ok()) + .map(|(cells, proofs)| (cells.try_into().unwrap(), proofs.try_into().unwrap())); + + compare_result::(&result, &expected) + } +} diff --git a/testing/ef_tests/src/handler.rs b/testing/ef_tests/src/handler.rs index 39dbc6ed88b..a89edd36007 100644 --- a/testing/ef_tests/src/handler.rs +++ b/testing/ef_tests/src/handler.rs @@ -886,6 +886,26 @@ impl Handler for KZGVerifyCellKZGProofBatchHandler { } } +#[derive(Derivative)] +#[derivative(Default(bound = ""))] +pub struct KZGRecoverCellsAndKZGProofHandler(PhantomData); + +impl Handler for KZGRecoverCellsAndKZGProofHandler { + type Case = cases::KZGRecoverCellsAndKZGProofs; + + fn config_name() -> &'static str { + "general" + } + + fn runner_name() -> &'static str { + "kzg" + } + + fn handler_name(&self) -> String { + "recover_cells_and_kzg_proofs".into() + } +} + #[derive(Derivative)] #[derivative(Default(bound = ""))] pub struct MerkleProofValidityHandler(PhantomData); diff --git a/testing/ef_tests/tests/tests.rs b/testing/ef_tests/tests/tests.rs index 11c504c75b0..2c13284b3b6 100644 --- a/testing/ef_tests/tests/tests.rs +++ b/testing/ef_tests/tests/tests.rs @@ -755,6 +755,12 @@ fn kzg_verify_cell_proof_batch() { .run_for_feature(ForkName::Deneb, FeatureName::Eip7594); } +#[test] +fn kzg_recover_cells_and_proofs() { + KZGRecoverCellsAndKZGProofHandler::::default() + .run_for_feature(ForkName::Deneb, FeatureName::Eip7594); +} + #[test] fn merkle_proof_validity() { MerkleProofValidityHandler::::default().run(); From 5c0ccefab1a33f59fbc8ffa804ab641cdef6d3a6 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Thu, 27 Jun 2024 06:15:18 +1000 Subject: [PATCH 47/76] Fix csc encoding and decoding (#5997) --- beacon_node/lighthouse_network/src/discovery/enr.rs | 11 +++++------ .../lighthouse_network/src/peer_manager/peerdb.rs | 5 +---- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/beacon_node/lighthouse_network/src/discovery/enr.rs b/beacon_node/lighthouse_network/src/discovery/enr.rs index fc8d3809e2d..115c36b2073 100644 --- a/beacon_node/lighthouse_network/src/discovery/enr.rs +++ b/beacon_node/lighthouse_network/src/discovery/enr.rs @@ -67,8 +67,8 @@ impl Eth2Enr for Enr { /// if the custody value is non-existent in the ENR, then we assume the minimum custody value /// defined in the spec. fn custody_subnet_count(&self, spec: &ChainSpec) -> u64 { - self.get(PEERDAS_CUSTODY_SUBNET_COUNT_ENR_KEY) - .and_then(|custody_bytes| custody_bytes.try_into().map(u64::from_be_bytes).ok()) + self.get_decodable::(PEERDAS_CUSTODY_SUBNET_COUNT_ENR_KEY) + .and_then(|r| r.ok()) // If value supplied in ENR is invalid, fallback to `custody_requirement` .filter(|csc| csc <= &spec.data_column_sidecar_subnet_count) .unwrap_or(spec.custody_requirement) @@ -245,8 +245,7 @@ pub fn build_enr( spec.custody_requirement }; - let csc_bytes = custody_subnet_count.to_be_bytes(); - builder.add_value(PEERDAS_CUSTODY_SUBNET_COUNT_ENR_KEY, &csc_bytes.as_slice()); + builder.add_value(PEERDAS_CUSTODY_SUBNET_COUNT_ENR_KEY, &custody_subnet_count); builder .build(enr_key) @@ -353,11 +352,11 @@ mod test { let config = NetworkConfig::default(); let spec = E::default_spec(); let (mut enr, enr_key) = build_enr_with_config(config, &spec); - let invalid_subnet_count = 999u64; + let invalid_subnet_count = 99u64; enr.insert( PEERDAS_CUSTODY_SUBNET_COUNT_ENR_KEY, - &invalid_subnet_count.to_be_bytes().as_slice(), + &invalid_subnet_count, &enr_key, ) .unwrap(); diff --git a/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs b/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs index 66ba0980392..d0c6710e8a7 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs @@ -686,10 +686,7 @@ impl PeerDB { if supernode { enr.insert( PEERDAS_CUSTODY_SUBNET_COUNT_ENR_KEY, - &spec - .data_column_sidecar_subnet_count - .to_be_bytes() - .as_slice(), + &spec.data_column_sidecar_subnet_count, &enr_key, ) .expect("u64 can be encoded"); From 7206909fbc8ac82115ce7f7c3bcb1348e5dd0d2a Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Thu, 27 Jun 2024 06:15:56 +1000 Subject: [PATCH 48/76] Fix data column rpc request not being sent due to incorrect limits set. (#6000) --- .../src/rpc/codec/ssz_snappy.rs | 58 ++++++++++++++++++- .../lighthouse_network/src/rpc/methods.rs | 20 +++++++ .../lighthouse_network/src/rpc/protocol.rs | 4 +- 3 files changed, 79 insertions(+), 3 deletions(-) diff --git a/beacon_node/lighthouse_network/src/rpc/codec/ssz_snappy.rs b/beacon_node/lighthouse_network/src/rpc/codec/ssz_snappy.rs index 74751c604ba..3a4ddef0bc4 100644 --- a/beacon_node/lighthouse_network/src/rpc/codec/ssz_snappy.rs +++ b/beacon_node/lighthouse_network/src/rpc/codec/ssz_snappy.rs @@ -801,7 +801,8 @@ mod tests { use crate::types::{EnrAttestationBitfield, EnrSyncCommitteeBitfield}; use types::{ blob_sidecar::BlobIdentifier, BeaconBlock, BeaconBlockAltair, BeaconBlockBase, - BeaconBlockBellatrix, EmptyBlock, Epoch, FullPayload, Signature, Slot, + BeaconBlockBellatrix, DataColumnIdentifier, EmptyBlock, Epoch, FullPayload, Signature, + Slot, }; type Spec = types::MainnetEthSpec; @@ -848,6 +849,10 @@ mod tests { Arc::new(BlobSidecar::empty()) } + fn empty_data_column_sidecar() -> Arc> { + Arc::new(DataColumnSidecar::empty()) + } + /// Bellatrix block with length < max_rpc_size. fn bellatrix_block_small( fork_context: &ForkContext, @@ -909,6 +914,27 @@ mod tests { } } + fn dcbrange_request() -> DataColumnsByRangeRequest { + DataColumnsByRangeRequest { + start_slot: 0, + count: 10, + columns: vec![1, 2, 3], + } + } + + fn dcbroot_request(spec: &ChainSpec) -> DataColumnsByRootRequest { + DataColumnsByRootRequest { + data_column_ids: RuntimeVariableList::new( + vec![DataColumnIdentifier { + block_root: Hash256::zero(), + index: 0, + }], + spec.max_request_data_column_sidecars as usize, + ) + .unwrap(), + } + } + fn bbroot_request_v1(spec: &ChainSpec) -> BlocksByRootRequest { BlocksByRootRequest::new_v1(vec![Hash256::zero()], spec) } @@ -1198,6 +1224,34 @@ mod tests { ), Ok(Some(RPCResponse::BlobsByRoot(empty_blob_sidecar()))), ); + + assert_eq!( + encode_then_decode_response( + SupportedProtocol::DataColumnsByRangeV1, + RPCCodedResponse::Success(RPCResponse::DataColumnsByRange( + empty_data_column_sidecar() + )), + ForkName::Deneb, + &chain_spec + ), + Ok(Some(RPCResponse::DataColumnsByRange( + empty_data_column_sidecar() + ))), + ); + + assert_eq!( + encode_then_decode_response( + SupportedProtocol::DataColumnsByRootV1, + RPCCodedResponse::Success(RPCResponse::DataColumnsByRoot( + empty_data_column_sidecar() + )), + ForkName::Deneb, + &chain_spec + ), + Ok(Some(RPCResponse::DataColumnsByRoot( + empty_data_column_sidecar() + ))), + ); } // Test RPCResponse encoding/decoding for V1 messages @@ -1551,6 +1605,8 @@ mod tests { OutboundRequest::MetaData(MetadataRequest::new_v1()), OutboundRequest::BlobsByRange(blbrange_request()), OutboundRequest::BlobsByRoot(blbroot_request(&chain_spec)), + OutboundRequest::DataColumnsByRange(dcbrange_request()), + OutboundRequest::DataColumnsByRoot(dcbroot_request(&chain_spec)), OutboundRequest::MetaData(MetadataRequest::new_v2()), ]; diff --git a/beacon_node/lighthouse_network/src/rpc/methods.rs b/beacon_node/lighthouse_network/src/rpc/methods.rs index a892b7f07cc..f7a92ac29f7 100644 --- a/beacon_node/lighthouse_network/src/rpc/methods.rs +++ b/beacon_node/lighthouse_network/src/rpc/methods.rs @@ -312,6 +312,26 @@ impl DataColumnsByRangeRequest { .saturating_mul(E::max_blobs_per_block() as u64) .saturating_mul(self.columns.len() as u64) } + + pub fn ssz_min_len() -> usize { + DataColumnsByRangeRequest { + start_slot: 0, + count: 0, + columns: vec![0], + } + .as_ssz_bytes() + .len() + } + + pub fn ssz_max_len(spec: &ChainSpec) -> usize { + DataColumnsByRangeRequest { + start_slot: 0, + count: 0, + columns: vec![0; spec.number_of_columns], + } + .as_ssz_bytes() + .len() + } } /// Request a number of beacon block roots from a peer. diff --git a/beacon_node/lighthouse_network/src/rpc/protocol.rs b/beacon_node/lighthouse_network/src/rpc/protocol.rs index 6dcd1eabd84..65a0e3e88d4 100644 --- a/beacon_node/lighthouse_network/src/rpc/protocol.rs +++ b/beacon_node/lighthouse_network/src/rpc/protocol.rs @@ -496,8 +496,8 @@ impl ProtocolId { Protocol::BlobsByRoot => RpcLimits::new(0, spec.max_blobs_by_root_request), Protocol::DataColumnsByRoot => RpcLimits::new(0, spec.max_data_columns_by_root_request), Protocol::DataColumnsByRange => RpcLimits::new( - ::ssz_fixed_len(), - ::ssz_fixed_len(), + DataColumnsByRangeRequest::ssz_min_len(), + DataColumnsByRangeRequest::ssz_max_len(spec), ), Protocol::Ping => RpcLimits::new( ::ssz_fixed_len(), From 515382e98b4669010490487c2c7f14f5012862fe Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Tue, 2 Jul 2024 10:08:10 +1000 Subject: [PATCH 49/76] Fix incorrect inbound request count causing rate limiting. (#6025) --- beacon_node/lighthouse_network/src/rpc/methods.rs | 4 +--- beacon_node/lighthouse_network/src/rpc/mod.rs | 2 ++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/beacon_node/lighthouse_network/src/rpc/methods.rs b/beacon_node/lighthouse_network/src/rpc/methods.rs index f7a92ac29f7..4c394629e4c 100644 --- a/beacon_node/lighthouse_network/src/rpc/methods.rs +++ b/beacon_node/lighthouse_network/src/rpc/methods.rs @@ -308,9 +308,7 @@ pub struct DataColumnsByRangeRequest { impl DataColumnsByRangeRequest { pub fn max_requested(&self) -> u64 { - self.count - .saturating_mul(E::max_blobs_per_block() as u64) - .saturating_mul(self.columns.len() as u64) + self.count.saturating_mul(self.columns.len() as u64) } pub fn ssz_min_len() -> usize { diff --git a/beacon_node/lighthouse_network/src/rpc/mod.rs b/beacon_node/lighthouse_network/src/rpc/mod.rs index 666cbe6fbcc..c40f976e7a1 100644 --- a/beacon_node/lighthouse_network/src/rpc/mod.rs +++ b/beacon_node/lighthouse_network/src/rpc/mod.rs @@ -366,8 +366,10 @@ where protocol, Protocol::BlocksByRange | Protocol::BlobsByRange + | Protocol::DataColumnsByRange | Protocol::BlocksByRoot | Protocol::BlobsByRoot + | Protocol::DataColumnsByRoot ) { debug!(self.log, "Request too large to process"; "request" => %req, "protocol" => %protocol); } else { From 6a3f88f31fddc319d8284ee70a7f7feeccbc116b Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Mon, 8 Jul 2024 14:19:37 +1000 Subject: [PATCH 50/76] Add kurtosis config for DAS testing (#5968) * Add kurtosis config for DAS testing. * Fix invalid yaml file * Update network parameter files. --- scripts/local_testnet/network_params.yaml | 3 ++ .../network_params_das_devnet_1.yaml | 8 +++++ .../network_params_das_interop.yaml | 32 +++++++++++++++++++ .../network_params_das_local.yaml | 20 ++++++++++++ scripts/local_testnet/start_local_testnet.sh | 3 -- 5 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 scripts/local_testnet/network_params_das_devnet_1.yaml create mode 100644 scripts/local_testnet/network_params_das_interop.yaml create mode 100644 scripts/local_testnet/network_params_das_local.yaml diff --git a/scripts/local_testnet/network_params.yaml b/scripts/local_testnet/network_params.yaml index f54fce354a0..1769eaa2b47 100644 --- a/scripts/local_testnet/network_params.yaml +++ b/scripts/local_testnet/network_params.yaml @@ -12,3 +12,6 @@ network_params: seconds_per_slot: 3 global_log_level: debug snooper_enabled: false +additional_services: + - dora + - prometheus_grafana diff --git a/scripts/local_testnet/network_params_das_devnet_1.yaml b/scripts/local_testnet/network_params_das_devnet_1.yaml new file mode 100644 index 00000000000..fcd131a06ca --- /dev/null +++ b/scripts/local_testnet/network_params_das_devnet_1.yaml @@ -0,0 +1,8 @@ +participants: + - cl_type: lighthouse + cl_image: lighthouse:local +network_params: + network: peerdas-devnet-1 +global_log_level: debug +additional_services: + - prometheus_grafana \ No newline at end of file diff --git a/scripts/local_testnet/network_params_das_interop.yaml b/scripts/local_testnet/network_params_das_interop.yaml new file mode 100644 index 00000000000..e8369a9e85c --- /dev/null +++ b/scripts/local_testnet/network_params_das_interop.yaml @@ -0,0 +1,32 @@ +participants: + - cl_type: prysm + cl_image: ethpandaops/prysm-beacon-chain:peerDAS + + - cl_type: lighthouse + cl_extra_params: [ + --subscribe-all-data-column-subnets, + ] + cl_image: lighthouse:local + + - cl_type: lighthouse + cl_image: lighthouse:local + + - cl_type: teku + cl_image: ethpandaops/teku:nashatyrev-das + +# - cl_type: nimbus +# cl_image: ethpandaops/nimbus-eth2:kzgpeerdas +# +# - cl_type: grandine +# cl_image: ethpandaops/grandine:das +# +# - cl_type: lodestar +# cl_image: ethpandaops/lodestar:peerDAS +network_params: + eip7594_fork_epoch: 0 + eip7594_fork_version: "0x50000038" +snooper_enabled: false +global_log_level: debug +additional_services: + - dora + - goomy_blob diff --git a/scripts/local_testnet/network_params_das_local.yaml b/scripts/local_testnet/network_params_das_local.yaml new file mode 100644 index 00000000000..d1b646a34a3 --- /dev/null +++ b/scripts/local_testnet/network_params_das_local.yaml @@ -0,0 +1,20 @@ +participants: + - cl_type: lighthouse + cl_image: lighthouse:local + cl_extra_params: + - --subscribe-all-data-column-subnets + - --target-peers=2 + count: 2 + - cl_type: lighthouse + cl_image: lighthouse:local + cl_extra_params: + - --target-peers=2 + count: 1 +network_params: + eip7594_fork_epoch: 0 + seconds_per_slot: 6 +snooper_enabled: false +global_log_level: debug +additional_services: + - dora + - goomy_blob diff --git a/scripts/local_testnet/start_local_testnet.sh b/scripts/local_testnet/start_local_testnet.sh index 4b03b1e0102..3cafd439360 100755 --- a/scripts/local_testnet/start_local_testnet.sh +++ b/scripts/local_testnet/start_local_testnet.sh @@ -62,9 +62,6 @@ if [ "$CI" = true ]; then # TODO: run assertoor tests yq eval '.additional_services = []' -i $NETWORK_PARAMS_FILE echo "Running without additional services (CI mode)." -else - yq eval '.additional_services = ["dora", "prometheus_grafana"]' -i $NETWORK_PARAMS_FILE - echo "Additional services dora and prometheus_grafana added to network_params.yaml" fi if [ "$BUILD_IMAGE" = true ]; then From 094ee6036b935b2f7fd9e9b2e4250aef8ebd1110 Mon Sep 17 00:00:00 2001 From: kevaundray Date: Tue, 9 Jul 2024 15:21:21 +0100 Subject: [PATCH 51/76] chore: add rust PeerdasKZG crypto library for peerdas functionality and rollback c-kzg dependency to 4844 version (#5941) * chore: add recover_cells_and_compute_proofs method * chore: add rust peerdas crypto library * chore: integrate peerdaskzg rust library into kzg crate * chore(multi): - update `ssz_cell_to_crypto_cell` - update conversion from the crypto cell type to a Vec. Since the Rust library defines them as references to an array, the conversion is simply `to_vec` * chore(multi): - update rest of code to handle the new crypto `Cell` type - update test case code to no longer use the Box type * chore: cleanup of superfluous conversions * chore: revert c-kzg dependency back to v1 * chore: move dependency into correct order * chore: update rust dependency - This version includes a new method `PeerDasContext::with_num_threads` * chore: remove Default initialization of PeerDasContext and explicitly set the parameters in `new_from_trusted_setup` * chore: cleanup exports * chore: commit updated cargo.lock * Update Cargo.toml Co-authored-by: Jimmy Chen * chore: rename dependency * chore: update peerdas lib - sets the blst version to 0.3 so that it matches whatever lighthouse is using. Although 0.3.12 is latest, lighthouse is pinned to 0.3.3 * chore: fix clippy lifetime - Rust doesn't allow you to elide the lifetime on type aliases * chore: cargo clippy fix * chore: cargo fmt * chore: update lib to add redundant checks (these will be removed in consensus-specs PR 3819) * chore: update dependency to ignore proofs * chore: update peerdas lib to latest * update lib * chore: remove empty proof parameter --------- Co-authored-by: Jimmy Chen --- Cargo.lock | 189 +++++++++++++++++- Cargo.toml | 6 +- beacon_node/beacon_chain/src/kzg_utils.rs | 9 +- consensus/types/src/data_column_sidecar.rs | 23 +-- crypto/kzg/Cargo.toml | 1 + crypto/kzg/src/lib.rs | 124 ++++++++---- .../cases/kzg_recover_cells_and_kzg_proofs.rs | 3 +- .../src/cases/kzg_verify_blob_kzg_proof.rs | 5 +- .../cases/kzg_verify_cell_kzg_proof_batch.rs | 8 +- 9 files changed, 291 insertions(+), 77 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c6e2311803c..e4304549f44 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1051,6 +1051,22 @@ dependencies = [ "zeroize", ] +[[package]] +name = "blstrs" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a8a8ed6fefbeef4a8c7b460e4110e12c5e22a5b7cf32621aae6ad650c4dcf29" +dependencies = [ + "blst", + "byte-slice-cast", + "ff 0.13.0", + "group 0.13.0", + "pairing", + "rand_core", + "serde", + "subtle", +] + [[package]] name = "bollard-stubs" version = "1.42.0-rc.3" @@ -1160,14 +1176,14 @@ dependencies = [ [[package]] name = "c-kzg" version = "1.0.2" -source = "git+https://github.com/ethereum/c-kzg-4844?rev=114fa0382990e9b74b1f90f3b0dc5f97c2f8a7ad#114fa0382990e9b74b1f90f3b0dc5f97c2f8a7ad" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdf100c4cea8f207e883ff91ca886d621d8a166cb04971dfaa9bb8fd99ed95df" dependencies = [ "blst", "cc", "glob", "hex", "libc", - "serde", ] [[package]] @@ -1539,6 +1555,48 @@ dependencies = [ "libc", ] +[[package]] +name = "crate_crypto_internal_peerdas_bls12_381" +version = "0.3.0" +source = "git+https://github.com/crate-crypto/peerdas-kzg?rev=bb8295011cf27dc663699539c7f9a17fe273e896#bb8295011cf27dc663699539c7f9a17fe273e896" +dependencies = [ + "blst", + "blstrs", + "ff 0.13.0", + "group 0.13.0", + "pairing", + "rayon", +] + +[[package]] +name = "crate_crypto_internal_peerdas_erasure_codes" +version = "0.3.0" +source = "git+https://github.com/crate-crypto/peerdas-kzg?rev=bb8295011cf27dc663699539c7f9a17fe273e896#bb8295011cf27dc663699539c7f9a17fe273e896" +dependencies = [ + "crate_crypto_internal_peerdas_bls12_381", + "crate_crypto_internal_peerdas_polynomial", +] + +[[package]] +name = "crate_crypto_internal_peerdas_polynomial" +version = "0.3.0" +source = "git+https://github.com/crate-crypto/peerdas-kzg?rev=bb8295011cf27dc663699539c7f9a17fe273e896#bb8295011cf27dc663699539c7f9a17fe273e896" +dependencies = [ + "crate_crypto_internal_peerdas_bls12_381", +] + +[[package]] +name = "crate_crypto_kzg_multi_open_fk20" +version = "0.3.0" +source = "git+https://github.com/crate-crypto/peerdas-kzg?rev=bb8295011cf27dc663699539c7f9a17fe273e896#bb8295011cf27dc663699539c7f9a17fe273e896" +dependencies = [ + "crate_crypto_internal_peerdas_bls12_381", + "crate_crypto_internal_peerdas_polynomial", + "hex", + "rayon", + "sha2 0.10.8", +] + [[package]] name = "crc32fast" version = "1.4.2" @@ -2216,6 +2274,21 @@ dependencies = [ "types", ] +[[package]] +name = "eip7594" +version = "0.3.0" +source = "git+https://github.com/crate-crypto/peerdas-kzg?rev=bb8295011cf27dc663699539c7f9a17fe273e896#bb8295011cf27dc663699539c7f9a17fe273e896" +dependencies = [ + "crate_crypto_internal_peerdas_bls12_381", + "crate_crypto_internal_peerdas_erasure_codes", + "crate_crypto_kzg_multi_open_fk20", + "hex", + "rayon", + "rust-embed", + "serde", + "serde_json", +] + [[package]] name = "either" version = "1.12.0" @@ -2984,6 +3057,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" dependencies = [ + "bitvec 1.0.1", "rand_core", "subtle", ] @@ -3419,7 +3493,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ "ff 0.13.0", + "rand", "rand_core", + "rand_xorshift", "subtle", ] @@ -4060,6 +4136,40 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "include-flate" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2e11569346406931d20276cc460215ee2826e7cad43aa986999cb244dd7adb0" +dependencies = [ + "include-flate-codegen-exports", + "lazy_static", + "libflate 1.4.0", +] + +[[package]] +name = "include-flate-codegen" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a7d6e1419fa3129eb0802b4c99603c0d425c79fb5d76191d5a20d0ab0d664e8" +dependencies = [ + "libflate 1.4.0", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "include-flate-codegen-exports" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75657043ffe3d8280f1cb8aef0f505532b392ed7758e0baeac22edadcee31a03" +dependencies = [ + "include-flate-codegen", + "proc-macro-hack", +] + [[package]] name = "indenter" version = "0.3.3" @@ -4312,6 +4422,7 @@ dependencies = [ "arbitrary", "c-kzg", "derivative", + "eip7594", "ethereum_hashing", "ethereum_serde_utils", "ethereum_ssz", @@ -4401,6 +4512,17 @@ version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +[[package]] +name = "libflate" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ff4ae71b685bbad2f2f391fe74f6b7659a34871c08b210fdc039e43bee07d18" +dependencies = [ + "adler32", + "crc32fast", + "libflate_lz77 1.2.0", +] + [[package]] name = "libflate" version = "2.1.0" @@ -4411,7 +4533,16 @@ dependencies = [ "core2", "crc32fast", "dary_heap", - "libflate_lz77", + "libflate_lz77 2.1.0", +] + +[[package]] +name = "libflate_lz77" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a52d3a8bfc85f250440e4424db7d857e241a3aebbbe301f3eb606ab15c39acbf" +dependencies = [ + "rle-decode-fast", ] [[package]] @@ -5913,6 +6044,15 @@ dependencies = [ "sha2 0.10.8", ] +[[package]] +name = "pairing" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fec4625e73cf41ef4bb6846cafa6d44736525f442ba45e407c4a000a13996f" +dependencies = [ + "group 0.13.0", +] + [[package]] name = "parity-scale-codec" version = "2.3.1" @@ -6411,6 +6551,12 @@ dependencies = [ "toml_edit 0.21.1", ] +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + [[package]] name = "proc-macro2" version = "1.0.84" @@ -7058,6 +7204,41 @@ dependencies = [ "smallvec", ] +[[package]] +name = "rust-embed" +version = "8.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19549741604902eb99a7ed0ee177a0663ee1eda51a29f71401f166e47e77806a" +dependencies = [ + "include-flate", + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb9f96e283ec64401f30d3df8ee2aaeb2561f34c824381efa24a35f79bf40ee4" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn 2.0.66", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c74a686185620830701348de757fd36bef4aa9680fd23c49fc539ddcc1af32" +dependencies = [ + "sha2 0.10.8", + "walkdir", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -7870,7 +8051,7 @@ checksum = "75062c2738b82cd45ae633623caae3393f43eb00aada1dc2d3ebe88db6b0db9b" dependencies = [ "chrono", "libc", - "libflate", + "libflate 2.1.0", "once_cell", "regex", "serde", diff --git a/Cargo.toml b/Cargo.toml index c44001ec357..6e4a00014b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -92,16 +92,14 @@ bytes = "1" clap = { version = "4.5.4", features = ["cargo", "wrap_help"] } # Turn off c-kzg's default features which include `blst/portable`. We can turn on blst's portable # feature ourselves when desired. -# TODO(das): switch to c-kzg crate before merging back to unstable (and disable default-features) if possible -# Turn off c-kzg's default features which include `blst/portable`. We can turn on blst's portable -# feature ourselves when desired. -c-kzg = { git = "https://github.com/ethereum/c-kzg-4844", rev = "114fa0382990e9b74b1f90f3b0dc5f97c2f8a7ad" } +c-kzg = { version = "1", default-features = false } compare_fields_derive = { path = "common/compare_fields_derive" } criterion = "0.5" delay_map = "0.3" derivative = "2" dirs = "3" either = "1.9" +peerdas-kzg = { git = "https://github.com/crate-crypto/peerdas-kzg", rev = "bb8295011cf27dc663699539c7f9a17fe273e896", package = "eip7594" } discv5 = { version = "0.4.1", features = ["libp2p"] } env_logger = "0.9" error-chain = "0.12" diff --git a/beacon_node/beacon_chain/src/kzg_utils.rs b/beacon_node/beacon_chain/src/kzg_utils.rs index de788234a81..8015fd6ffa6 100644 --- a/beacon_node/beacon_chain/src/kzg_utils.rs +++ b/beacon_node/beacon_chain/src/kzg_utils.rs @@ -1,4 +1,4 @@ -use kzg::{Blob as KzgBlob, Bytes48, Cell as KzgCell, Error as KzgError, Kzg}; +use kzg::{Blob as KzgBlob, Bytes48, CellRef as KzgCellRef, Error as KzgError, Kzg}; use std::sync::Arc; use types::data_column_sidecar::Cell; use types::{Blob, DataColumnSidecar, EthSpec, Hash256, KzgCommitment, KzgProof}; @@ -11,8 +11,11 @@ fn ssz_blob_to_crypto_blob(blob: &Blob) -> Result(cell: &Cell) -> Result { - KzgCell::from_bytes(cell.as_ref()).map_err(Into::into) +fn ssz_cell_to_crypto_cell(cell: &Cell) -> Result { + let cell_bytes: &[u8] = cell.as_ref(); + Ok(cell_bytes + .try_into() + .expect("expected cell to have size {BYTES_PER_CELL}. This should be guaranteed by the `FixedVector type")) } /// Validate a single blob-commitment-proof triplet from a `BlobSidecar`. diff --git a/consensus/types/src/data_column_sidecar.rs b/consensus/types/src/data_column_sidecar.rs index 5f6439721f4..9b140ca0652 100644 --- a/consensus/types/src/data_column_sidecar.rs +++ b/consensus/types/src/data_column_sidecar.rs @@ -9,7 +9,7 @@ use bls::Signature; use derivative::Derivative; #[cfg_attr(test, double)] use kzg::Kzg; -use kzg::{Blob as KzgBlob, Cell as KzgCell, Error as KzgError}; +use kzg::{Blob as KzgBlob, CellRef as KzgCellRef, Error as KzgError}; use kzg::{KzgCommitment, KzgProof}; use merkle_proof::verify_merkle_proof; #[cfg(test)] @@ -144,11 +144,7 @@ impl DataColumnSidecar { .ok_or(DataColumnSidecarError::InconsistentArrayLength(format!( "Missing blob cell at index {col}" )))?; - let cell: Vec = cell - .into_inner() - .into_iter() - .flat_map(|data| (*data).into_iter()) - .collect(); + let cell: Vec = cell.to_vec(); let cell = Cell::::from(cell); let proof = blob_cell_proofs.get(col).ok_or( @@ -213,7 +209,7 @@ impl DataColumnSidecar { let blob_cells_and_proofs_vec = (0..num_of_blobs) .into_par_iter() .map(|row_index| { - let mut cells: Vec = vec![]; + let mut cells: Vec = vec![]; let mut cell_ids: Vec = vec![]; for data_column in data_columns { let cell = data_column.column.get(row_index).ok_or( @@ -239,11 +235,7 @@ impl DataColumnSidecar { .ok_or(KzgError::InconsistentArrayLength(format!( "Missing blob cell at index {col}" )))?; - let cell: Vec = cell - .into_inner() - .into_iter() - .flat_map(|data| (*data).into_iter()) - .collect(); + let cell: Vec = cell.to_vec(); let cell = Cell::::from(cell); let proof = blob_cell_proofs @@ -392,8 +384,11 @@ impl From for DataColumnSidecarError { /// Converts a cell ssz List object to an array to be used with the kzg /// crypto library. -fn ssz_cell_to_crypto_cell(cell: &Cell) -> Result { - KzgCell::from_bytes(cell.as_ref()).map_err(Into::into) +fn ssz_cell_to_crypto_cell(cell: &Cell) -> Result { + let cell_bytes: &[u8] = cell.as_ref(); + Ok(cell_bytes + .try_into() + .expect("expected cell to have size {BYTES_PER_CELL}. This should be guaranteed by the `FixedVector type")) } #[cfg(test)] diff --git a/crypto/kzg/Cargo.toml b/crypto/kzg/Cargo.toml index 2c3c894b49c..1f378c7dc9f 100644 --- a/crypto/kzg/Cargo.toml +++ b/crypto/kzg/Cargo.toml @@ -17,4 +17,5 @@ ethereum_serde_utils = { workspace = true } hex = { workspace = true } ethereum_hashing = { workspace = true } c-kzg = { workspace = true } +peerdas-kzg = { workspace = true } mockall = { workspace = true } diff --git a/crypto/kzg/src/lib.rs b/crypto/kzg/src/lib.rs index 9875126700c..9cfe42c8b97 100644 --- a/crypto/kzg/src/lib.rs +++ b/crypto/kzg/src/lib.rs @@ -13,18 +13,23 @@ pub use c_kzg::{ Blob, Bytes32, Bytes48, KzgSettings, BYTES_PER_BLOB, BYTES_PER_COMMITMENT, BYTES_PER_FIELD_ELEMENT, BYTES_PER_PROOF, FIELD_ELEMENTS_PER_BLOB, }; -pub use c_kzg::{Cell, CELLS_PER_EXT_BLOB}; use mockall::automock; -pub type CellsAndKzgProofs = ( - Box<[Cell; CELLS_PER_EXT_BLOB]>, - Box<[KzgProof; CELLS_PER_EXT_BLOB]>, -); +pub use peerdas_kzg::{ + constants::{BYTES_PER_CELL, CELLS_PER_EXT_BLOB}, + Cell, CellID, CellRef, TrustedSetup as PeerDASTrustedSetup, +}; +use peerdas_kzg::{prover::ProverError, verifier::VerifierError, PeerDASContext}; +pub type CellsAndKzgProofs = ([Cell; CELLS_PER_EXT_BLOB], [KzgProof; CELLS_PER_EXT_BLOB]); #[derive(Debug)] pub enum Error { /// An error from the underlying kzg library. Kzg(c_kzg::Error), + /// A prover error from the PeerdasKZG library + ProverKZG(ProverError), + /// A verifier error from the PeerdasKZG library + VerifierKZG(VerifierError), /// The kzg verification failed KzgVerificationFailed, /// Misc indexing error @@ -41,20 +46,31 @@ impl From for Error { #[derive(Debug)] pub struct Kzg { trusted_setup: KzgSettings, + context: PeerDASContext, } #[automock] impl Kzg { /// Load the kzg trusted setup parameters from a vec of G1 and G2 points. pub fn new_from_trusted_setup(trusted_setup: TrustedSetup) -> Result { + // Initialize the trusted setup using default parameters + // + // Note: One can also use `from_json` to initialize it from the consensus-specs + // json string. + let peerdas_trusted_setup = PeerDASTrustedSetup::default(); + // Set the number of threads to be used + // + // we set it to 1 to match the c-kzg performance + const NUM_THREADS: usize = 1; + + let context = PeerDASContext::with_threads(&peerdas_trusted_setup, NUM_THREADS); + Ok(Self { trusted_setup: KzgSettings::load_trusted_setup( &trusted_setup.g1_points(), &trusted_setup.g2_points(), - // Enable precomputed table for 8 bits, with 96MB of memory overhead per process - // Ref: https://notes.ethereum.org/@jtraglia/windowed_multiplications - 8, )?, + context, }) } @@ -158,10 +174,20 @@ impl Kzg { /// Computes the cells and associated proofs for a given `blob` at index `index`. pub fn compute_cells_and_proofs(&self, blob: &Blob) -> Result { - let (cells, proofs) = c_kzg::Cell::compute_cells_and_kzg_proofs(blob, &self.trusted_setup) - .map_err(Into::::into)?; - let proofs = Box::new(proofs.map(|proof| KzgProof::from(proof.to_bytes().into_inner()))); - Ok((cells, proofs)) + let blob_bytes: &[u8; BYTES_PER_BLOB] = blob + .as_ref() + .try_into() + .expect("Expected blob to have size {BYTES_PER_BLOB}"); + + let (cells, proofs) = self + .context + .prover_ctx() + .compute_cells_and_kzg_proofs(blob_bytes) + .map_err(Error::ProverKZG)?; + + // Convert the proof type to a c-kzg proof type + let c_kzg_proof = proofs.map(KzgProof); + Ok((cells, c_kzg_proof)) } /// Verifies a batch of cell-proof-commitment triplets. @@ -169,55 +195,67 @@ impl Kzg { /// Here, `coordinates` correspond to the (row, col) coordinate of the cell in the extended /// blob "matrix". In the 1D extension, row corresponds to the blob index, and col corresponds /// to the data column index. - pub fn verify_cell_proof_batch( + #[allow(clippy::needless_lifetimes)] + pub fn verify_cell_proof_batch<'a>( &self, - cells: &[Cell], + cells: &[CellRef<'a>], kzg_proofs: &[Bytes48], coordinates: &[(u64, u64)], kzg_commitments: &[Bytes48], ) -> Result<(), Error> { let (rows, columns): (Vec, Vec) = coordinates.iter().cloned().unzip(); - if !c_kzg::KzgProof::verify_cell_kzg_proof_batch( - kzg_commitments, - &rows, - &columns, - cells, - kzg_proofs, - &self.trusted_setup, - )? { - Err(Error::KzgVerificationFailed) - } else { - Ok(()) - } - } + // The result of this is either an Ok indicating the proof passed, or an Err indicating + // the proof failed or something else went wrong. + + let proofs: Vec<_> = kzg_proofs.iter().map(|proof| proof.as_ref()).collect(); + let commitments: Vec<_> = kzg_commitments + .iter() + .map(|commitment| commitment.as_ref()) + .collect(); + let verification_result = self.context.verifier_ctx().verify_cell_kzg_proof_batch( + commitments.to_vec(), + rows, + columns, + cells.to_vec(), + proofs.to_vec(), + ); - fn cells_to_blob(&self, cells: &[Cell; c_kzg::CELLS_PER_EXT_BLOB]) -> Result { - Ok(Blob::cells_to_blob(cells)?) + // Modify the result so it matches roughly what the previous method was doing. + match verification_result { + Ok(_) => Ok(()), + Err(VerifierError::InvalidProof) => Err(Error::KzgVerificationFailed), + Err(e) => Err(Error::VerifierKZG(e)), + } } - pub fn recover_cells_and_compute_kzg_proofs( + #[allow(clippy::needless_lifetimes)] + pub fn recover_cells_and_compute_kzg_proofs<'a>( &self, cell_ids: &[u64], - cells: &[Cell], + cells: &[CellRef<'a>], ) -> Result { - let all_cells = c_kzg::Cell::recover_all_cells(cell_ids, cells, &self.trusted_setup)?; - let blob = self.cells_to_blob(&all_cells)?; - self.compute_cells_and_proofs(&blob) + let (cells, proofs) = self + .context + .prover_ctx() + .recover_cells_and_proofs(cell_ids.to_vec(), cells.to_vec()) + .map_err(Error::ProverKZG)?; + + // Convert the proof type to a c-kzg proof type + let c_kzg_proof = proofs.map(KzgProof); + Ok((cells, c_kzg_proof)) } } pub mod mock { - use crate::{CellsAndKzgProofs, Error, KzgProof}; - use c_kzg::{Blob, Cell, CELLS_PER_EXT_BLOB}; - - pub const MOCK_KZG_BYTES_PER_CELL: usize = 2048; + use crate::{Blob, Cell, CellsAndKzgProofs, BYTES_PER_CELL, CELLS_PER_EXT_BLOB}; + use crate::{Error, KzgProof}; + #[allow(clippy::type_complexity)] pub fn compute_cells_and_proofs(_blob: &Blob) -> Result { - let empty_cell = Cell::new([0; MOCK_KZG_BYTES_PER_CELL]); - Ok(( - Box::new([empty_cell; CELLS_PER_EXT_BLOB]), - Box::new([KzgProof::empty(); CELLS_PER_EXT_BLOB]), - )) + let empty_cells = vec![Cell::new([0; BYTES_PER_CELL]); CELLS_PER_EXT_BLOB] + .try_into() + .expect("expected {CELLS_PER_EXT_BLOB} number of items"); + Ok((empty_cells, [KzgProof::empty(); CELLS_PER_EXT_BLOB])) } } diff --git a/testing/ef_tests/src/cases/kzg_recover_cells_and_kzg_proofs.rs b/testing/ef_tests/src/cases/kzg_recover_cells_and_kzg_proofs.rs index c177094d0b1..e759ccf4b00 100644 --- a/testing/ef_tests/src/cases/kzg_recover_cells_and_kzg_proofs.rs +++ b/testing/ef_tests/src/cases/kzg_recover_cells_and_kzg_proofs.rs @@ -58,10 +58,11 @@ impl Case for KZGRecoverCellsAndKZGProofs { let result = parse_input(&self.input).and_then(|(input_proofs, input_cells, cell_indices)| { + let input_cells_ref: Vec<_> = input_cells.iter().map(|cell| &**cell).collect(); let (cells, proofs) = KZG .recover_cells_and_compute_kzg_proofs( cell_indices.as_slice(), - input_cells.as_slice(), + input_cells_ref.as_slice(), ) .map_err(|e| { Error::InternalError(format!( diff --git a/testing/ef_tests/src/cases/kzg_verify_blob_kzg_proof.rs b/testing/ef_tests/src/cases/kzg_verify_blob_kzg_proof.rs index dca9af96161..d89d3a28790 100644 --- a/testing/ef_tests/src/cases/kzg_verify_blob_kzg_proof.rs +++ b/testing/ef_tests/src/cases/kzg_verify_blob_kzg_proof.rs @@ -40,8 +40,9 @@ pub fn parse_cell(cell: &str) -> Result { hex::decode(strip_0x(cell)?) .map_err(|e| Error::FailedToParseTest(format!("Failed to parse cell: {:?}", e))) .and_then(|bytes| { - Cell::from_bytes(bytes.as_ref()) - .map_err(|e| Error::FailedToParseTest(format!("Failed to parse proof: {:?}", e))) + bytes + .try_into() + .map_err(|e| Error::FailedToParseTest(format!("Failed to parse cell: {:?}", e))) }) } diff --git a/testing/ef_tests/src/cases/kzg_verify_cell_kzg_proof_batch.rs b/testing/ef_tests/src/cases/kzg_verify_cell_kzg_proof_batch.rs index 150cc47770f..f2f9a33067d 100644 --- a/testing/ef_tests/src/cases/kzg_verify_cell_kzg_proof_batch.rs +++ b/testing/ef_tests/src/cases/kzg_verify_cell_kzg_proof_batch.rs @@ -56,12 +56,8 @@ impl Case for KZGVerifyCellKZGProofBatch { parse_input(&self.input).and_then(|(cells, proofs, coordinates, commitments)| { let proofs: Vec = proofs.iter().map(|&proof| proof.into()).collect(); let commitments: Vec = commitments.iter().map(|&c| c.into()).collect(); - match KZG.verify_cell_proof_batch( - cells.as_slice(), - &proofs, - &coordinates, - &commitments, - ) { + let cells = cells.iter().map(|c| c.as_ref()).collect::>(); + match KZG.verify_cell_proof_batch(&cells, &proofs, &coordinates, &commitments) { Ok(_) => Ok(true), Err(KzgError::KzgVerificationFailed) => Ok(false), Err(e) => Err(Error::InternalError(format!( From bf300b335f35e86331a5ed72944f1426d746734c Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Thu, 11 Jul 2024 12:08:57 +1000 Subject: [PATCH 52/76] Update PeerDAS interop testnet config (#6069) * Update interop testnet config. * Fix typo and remove target peers --- scripts/local_testnet/network_params.yaml | 2 +- scripts/local_testnet/network_params_das_interop.yaml | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/scripts/local_testnet/network_params.yaml b/scripts/local_testnet/network_params.yaml index 1769eaa2b47..b53d88e52c5 100644 --- a/scripts/local_testnet/network_params.yaml +++ b/scripts/local_testnet/network_params.yaml @@ -1,4 +1,4 @@ -# Full configuration reference [here](https://github.com/kurtosis-tech/ethereum-package?tab=readme-ov-file#configuration). +# Full configuration reference [here](https://github.com/ethpandaops/ethereum-package?tab=readme-ov-file#configuration). participants: - el_type: geth el_image: ethereum/client-go:latest diff --git a/scripts/local_testnet/network_params_das_interop.yaml b/scripts/local_testnet/network_params_das_interop.yaml index e8369a9e85c..3e7fef557e6 100644 --- a/scripts/local_testnet/network_params_das_interop.yaml +++ b/scripts/local_testnet/network_params_das_interop.yaml @@ -1,3 +1,4 @@ +# Full configuration reference [here](https://github.com/ethpandaops/ethereum-package?tab=readme-ov-file#configuration). participants: - cl_type: prysm cl_image: ethpandaops/prysm-beacon-chain:peerDAS @@ -25,8 +26,13 @@ participants: network_params: eip7594_fork_epoch: 0 eip7594_fork_version: "0x50000038" + data_column_sidecar_subnet_count: 64 + samples_per_slot: 16 + custody_requirement: 4 snooper_enabled: false global_log_level: debug +ethereum_metrics_exporter_enabled: true additional_services: - dora - goomy_blob + - prometheus_grafana From 018f38257f791b47aa570db9ba7ce5245a7c58f3 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Tue, 16 Jul 2024 16:50:07 +1000 Subject: [PATCH 53/76] Avoid retrying same sampling peer that previously failed. (#6084) --- .../network/src/sync/block_lookups/tests.rs | 36 +++++++++++++++++++ beacon_node/network/src/sync/sampling.rs | 6 ++-- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/beacon_node/network/src/sync/block_lookups/tests.rs b/beacon_node/network/src/sync/block_lookups/tests.rs index 18d74625145..87a91c74092 100644 --- a/beacon_node/network/src/sync/block_lookups/tests.rs +++ b/beacon_node/network/src/sync/block_lookups/tests.rs @@ -651,6 +651,21 @@ impl TestRig { }); } + fn sampling_requests_failed( + &mut self, + sampling_ids: DCByRootIds, + peer_id: PeerId, + error: RPCError, + ) { + for (request_id, _) in sampling_ids { + self.send_sync_message(SyncMessage::RpcError { + peer_id, + request_id, + error: error.clone(), + }) + } + } + fn complete_valid_block_request( &mut self, id: SingleLookupReqId, @@ -1964,6 +1979,27 @@ fn sampling_with_retries() { r.expect_clean_finished_sampling(); } +#[test] +fn sampling_avoid_retrying_same_peer() { + let Some(mut r) = TestRig::test_setup_after_peerdas() else { + return; + }; + let peer_id_1 = r.new_connected_supernode_peer(); + let peer_id_2 = r.new_connected_supernode_peer(); + let block_root = Hash256::random(); + r.trigger_sample_block(block_root, Slot::new(0)); + // Retrieve all outgoing sample requests for random column indexes, and return empty responses + let sampling_ids = + r.expect_only_data_columns_by_root_requests(block_root, SAMPLING_REQUIRED_SUCCESSES); + r.sampling_requests_failed(sampling_ids, peer_id_1, RPCError::Disconnected); + // Should retry the other peer + let sampling_ids = + r.expect_only_data_columns_by_root_requests(block_root, SAMPLING_REQUIRED_SUCCESSES); + r.sampling_requests_failed(sampling_ids, peer_id_2, RPCError::Disconnected); + // Expect no more retries + r.expect_empty_network(); +} + #[test] fn custody_lookup_happy_path() { let Some(mut r) = TestRig::test_setup_after_peerdas() else { diff --git a/beacon_node/network/src/sync/sampling.rs b/beacon_node/network/src/sync/sampling.rs index 3fb21489d91..9cfd41dead7 100644 --- a/beacon_node/network/src/sync/sampling.rs +++ b/beacon_node/network/src/sync/sampling.rs @@ -433,7 +433,6 @@ mod request { column_index: ColumnIndex, status: Status, // TODO(das): Should downscore peers that claim to not have the sample? - #[allow(dead_code)] peers_dont_have: HashSet, } @@ -490,11 +489,13 @@ mod request { // TODO: When is a fork and only a subset of your peers know about a block, sampling should only // be queried on the peers on that fork. Should this case be handled? How to handle it? - let peer_ids = cx.get_custodial_peers( + let mut peer_ids = cx.get_custodial_peers( block_slot.epoch(::slots_per_epoch()), self.column_index, ); + peer_ids.retain(|peer_id| !self.peers_dont_have.contains(peer_id)); + // TODO(das) randomize custodial peer and avoid failing peers if let Some(peer_id) = peer_ids.first().cloned() { cx.data_column_lookup_request( @@ -521,6 +522,7 @@ mod request { pub(crate) fn on_sampling_error(&mut self) -> Result { match self.status.clone() { Status::Sampling(peer_id) => { + self.peers_dont_have.insert(peer_id); self.status = Status::NotStarted; Ok(peer_id) } From 55a3be72bb5923033ce39eeddb2c16497bbdbaea Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Tue, 16 Jul 2024 17:19:49 +1000 Subject: [PATCH 54/76] Various fixes to custody range sync (#6004) * Only start requesting batches when there are good peers across all custody columns to avoid spaming block requests. * Add custody peer check before mutating `BatchInfo` to avoid inconsistent state. * Add check to cover a case where batch is not processed while waiting for custody peers to become available. * Fix lint and logic error * Fix `good_peers_on_subnet` always returning false for `DataColumnSubnet`. * Add test for `get_custody_peers_for_column` * Revert epoch parameter refactor. * Fall back to default custody requiremnt if peer ENR is not present. * Add metrics and update code comment. * Add more debug logs. * Use subscribed peers on subnet before MetaDataV3 is implemented. Remove peer_id matching when injecting error because multiple peers are used for range requests. Use randomized custodial peer to avoid repeatedly sending requests to failing peers. Batch by range request where possible. * Remove unused code and update docs. * Add comment --- .../src/peer_manager/peerdb/peer_info.rs | 11 +- .../lighthouse_network/src/types/globals.rs | 32 +++++- beacon_node/network/src/metrics.rs | 5 + .../network/src/sync/backfill_sync/mod.rs | 8 +- .../network/src/sync/network_context.rs | 108 ++++++++++-------- .../network/src/sync/range_sync/batch.rs | 6 +- .../network/src/sync/range_sync/chain.rs | 63 +++++++++- beacon_node/network/src/sync/sampling.rs | 5 +- 8 files changed, 175 insertions(+), 63 deletions(-) diff --git a/beacon_node/lighthouse_network/src/peer_manager/peerdb/peer_info.rs b/beacon_node/lighthouse_network/src/peer_manager/peerdb/peer_info.rs index 59053b19292..0745cc26008 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/peerdb/peer_info.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/peerdb/peer_info.rs @@ -94,8 +94,15 @@ impl PeerInfo { .syncnets() .map_or(false, |s| s.get(**id as usize).unwrap_or(false)) } - // TODO(das) Add data column nets bitfield - Subnet::DataColumn(_) => return false, + Subnet::DataColumn(_) => { + // TODO(das): Pending spec PR https://github.com/ethereum/consensus-specs/pull/3821 + // We should use MetaDataV3 for peer selection rather than + // looking at subscribed peers (current behavior). Until MetaDataV3 is + // implemented, this is the perhaps the only viable option on the current devnet + // as the peer count is low and it's important to identify supernodes to get a + // good distribution of peers across subnets. + return true; + } } } false diff --git a/beacon_node/lighthouse_network/src/types/globals.rs b/beacon_node/lighthouse_network/src/types/globals.rs index b3d37e23104..0f99bfc5dae 100644 --- a/beacon_node/lighthouse_network/src/types/globals.rs +++ b/beacon_node/lighthouse_network/src/types/globals.rs @@ -2,9 +2,9 @@ use crate::peer_manager::peerdb::PeerDB; use crate::rpc::{MetaData, MetaDataV2}; use crate::types::{BackFillState, SyncState}; -use crate::EnrExt; use crate::{Client, Eth2Enr}; use crate::{Enr, GossipTopic, Multiaddr, PeerId}; +use crate::{EnrExt, Subnet}; use parking_lot::RwLock; use std::collections::HashSet; use types::data_column_sidecar::ColumnIndex; @@ -120,6 +120,34 @@ impl NetworkGlobals { .collect() } + /// Compute custody data column subnets the node is assigned to custody. + pub fn custody_subnets(&self, spec: &ChainSpec) -> impl Iterator { + let enr = self.local_enr(); + let node_id = enr.node_id().raw().into(); + let custody_subnet_count = enr.custody_subnet_count::(spec); + DataColumnSubnetId::compute_custody_subnets::(node_id, custody_subnet_count, spec) + } + + /// Returns a connected peer that: + /// 1. is connected + /// 2. assigned to custody the column based on it's `custody_subnet_count` from metadata (WIP) + /// 3. has a good score + /// 4. subscribed to the specified column - this condition can be removed later, so we can + /// identify and penalise peers that are supposed to custody the column. + pub fn custody_peers_for_column( + &self, + column_index: ColumnIndex, + spec: &ChainSpec, + ) -> Vec { + self.peers + .read() + .good_peers_on_subnet(Subnet::DataColumn( + DataColumnSubnetId::from_column_index::(column_index as usize, spec), + )) + .cloned() + .collect::>() + } + /// TESTING ONLY. Build a dummy NetworkGlobals instance. pub fn new_test_globals(trusted_peers: Vec, log: &slog::Logger) -> NetworkGlobals { use crate::CombinedKeyExt; @@ -142,7 +170,7 @@ impl NetworkGlobals { #[cfg(test)] mod test { - use crate::NetworkGlobals; + use super::*; use types::{Epoch, EthSpec, MainnetEthSpec as E}; #[test] diff --git a/beacon_node/network/src/metrics.rs b/beacon_node/network/src/metrics.rs index 56af9833f1a..dd5e359af37 100644 --- a/beacon_node/network/src/metrics.rs +++ b/beacon_node/network/src/metrics.rs @@ -239,6 +239,11 @@ lazy_static! { "Number of connected peers per sync status type", &["sync_status"] ); + pub static ref PEERS_PER_COLUMN_SUBNET: Result = try_create_int_gauge_vec( + "peers_per_column_subnet", + "Number of connected peers per column subnet", + &["subnet_id"] + ); pub static ref SYNCING_CHAINS_COUNT: Result = try_create_int_gauge_vec( "sync_range_chains", "Number of Syncing chains in range, per range type", diff --git a/beacon_node/network/src/sync/backfill_sync/mod.rs b/beacon_node/network/src/sync/backfill_sync/mod.rs index 74ccafda480..79acc93a3c1 100644 --- a/beacon_node/network/src/sync/backfill_sync/mod.rs +++ b/beacon_node/network/src/sync/backfill_sync/mod.rs @@ -371,7 +371,9 @@ impl BackFillSync { // A batch could be retried without the peer failing the request (disconnecting/ // sending an error /timeout) if the peer is removed from the chain for other // reasons. Check that this block belongs to the expected peer - if !batch.is_expecting_block(peer_id, &request_id) { + // TODO(das): removed peer_id matching as the node may request a different peer for data + // columns. + if !batch.is_expecting_block(&request_id) { return Ok(()); } debug!(self.log, "Batch failed"; "batch_epoch" => batch_id, "error" => "rpc_error"); @@ -419,7 +421,9 @@ impl BackFillSync { // sending an error /timeout) if the peer is removed from the chain for other // reasons. Check that this block belongs to the expected peer, and that the // request_id matches - if !batch.is_expecting_block(peer_id, &request_id) { + // TODO(das): removed peer_id matching as the node may request a different peer for data + // columns. + if !batch.is_expecting_block(&request_id) { return Ok(ProcessResult::Successful); } batch diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index 4250a4baa02..548e9a3bdc5 100644 --- a/beacon_node/network/src/sync/network_context.rs +++ b/beacon_node/network/src/sync/network_context.rs @@ -25,9 +25,9 @@ use beacon_chain::{BeaconChain, BeaconChainTypes, BlockProcessStatus, EngineStat use fnv::FnvHashMap; use lighthouse_network::rpc::methods::{BlobsByRangeRequest, DataColumnsByRangeRequest}; use lighthouse_network::rpc::{BlocksByRangeRequest, GoodbyeReason, RPCError}; -use lighthouse_network::{ - Client, Eth2Enr, NetworkGlobals, PeerAction, PeerId, ReportSource, Request, -}; +use lighthouse_network::{Client, NetworkGlobals, PeerAction, PeerId, ReportSource, Request}; +use rand::seq::SliceRandom; +use rand::thread_rng; pub use requests::LookupVerifyError; use slog::{debug, error, warn}; use slot_clock::SlotClock; @@ -38,8 +38,7 @@ use std::time::Duration; use tokio::sync::mpsc; use types::blob_sidecar::FixedBlobSidecarList; use types::{ - BlobSidecar, ColumnIndex, DataColumnSidecar, DataColumnSubnetId, Epoch, EthSpec, Hash256, - SignedBeaconBlock, Slot, + BlobSidecar, ColumnIndex, DataColumnSidecar, Epoch, EthSpec, Hash256, SignedBeaconBlock, Slot, }; pub mod custody; @@ -280,29 +279,18 @@ impl SyncNetworkContext { // TODO(das): epoch argument left here in case custody rotation is implemented pub fn get_custodial_peers(&self, _epoch: Epoch, column_index: ColumnIndex) -> Vec { - let mut peer_ids = vec![]; - - for (peer_id, peer_info) in self.network_globals().peers.read().connected_peers() { - if let Some(enr) = peer_info.enr() { - let custody_subnet_count = enr.custody_subnet_count::(&self.chain.spec); - // TODO(das): consider caching a map of subnet -> Vec and invalidating - // whenever a peer connected or disconnect event in received - let mut subnets = DataColumnSubnetId::compute_custody_subnets::( - enr.node_id().raw().into(), - custody_subnet_count, - &self.chain.spec, - ); - if subnets.any(|subnet| { - subnet - .columns::(&self.chain.spec) - .any(|index| index == column_index) - }) { - peer_ids.push(*peer_id) - } - } - } + self.network_globals() + .custody_peers_for_column(column_index, &self.chain.spec) + } - peer_ids + pub fn get_random_custodial_peer( + &self, + epoch: Epoch, + column_index: ColumnIndex, + ) -> Option { + self.get_custodial_peers(epoch, column_index) + .choose(&mut thread_rng()) + .cloned() } pub fn network_globals(&self) -> &NetworkGlobals { @@ -402,37 +390,24 @@ impl SyncNetworkContext { .network_globals() .custody_columns(epoch, &self.chain.spec); - for column_index in &custody_indexes { - let custody_peer_ids = self.get_custodial_peers(epoch, *column_index); - let Some(custody_peer) = custody_peer_ids.first().cloned() else { - // TODO(das): this will be pretty bad UX. To improve we should: - // - Attempt to fetch custody requests first, before requesting blocks - // - Handle the no peers case gracefully, maybe add some timeout and give a few - // minutes / seconds to the peer manager to locate peers on this subnet before - // abandoing progress on the chain completely. - return Err(RpcRequestSendError::NoCustodyPeers); - }; - - requested_peers.push(custody_peer); + for (peer_id, columns_by_range_request) in + self.make_columns_by_range_requests(epoch, request, &custody_indexes)? + { + requested_peers.push(peer_id); debug!( self.log, "Sending DataColumnsByRange requests"; "method" => "DataColumnsByRange", - "count" => request.count(), + "count" => columns_by_range_request.count, "epoch" => epoch, - "index" => column_index, - "peer" => %custody_peer, + "columns" => ?columns_by_range_request.columns, + "peer" => %peer_id, ); - // Create the blob request based on the blocks request. self.send_network_msg(NetworkMessage::SendRequest { - peer_id: custody_peer, - request: Request::DataColumnsByRange(DataColumnsByRangeRequest { - start_slot: *request.start_slot(), - count: *request.count(), - columns: vec![*column_index], - }), + peer_id, + request: Request::DataColumnsByRange(columns_by_range_request), request_id: RequestId::Sync(SyncRequestId::RangeBlockComponents(id)), }) .map_err(|_| RpcRequestSendError::NetworkSendError)?; @@ -453,6 +428,41 @@ impl SyncNetworkContext { Ok(id) } + fn make_columns_by_range_requests( + &self, + epoch: Epoch, + request: BlocksByRangeRequest, + custody_indexes: &Vec, + ) -> Result, RpcRequestSendError> { + let mut peer_id_to_request_map = HashMap::new(); + + for column_index in custody_indexes { + // TODO(das): The peer selection logic here needs to be improved - we should probably + // avoid retrying from failed peers, however `BatchState` currently only tracks the peer + // serving the blocks. + let Some(custody_peer) = self.get_random_custodial_peer(epoch, *column_index) else { + // TODO(das): this will be pretty bad UX. To improve we should: + // - Attempt to fetch custody requests first, before requesting blocks + // - Handle the no peers case gracefully, maybe add some timeout and give a few + // minutes / seconds to the peer manager to locate peers on this subnet before + // abandoing progress on the chain completely. + return Err(RpcRequestSendError::NoCustodyPeers); + }; + + let columns_by_range_request = peer_id_to_request_map + .entry(custody_peer) + .or_insert_with(|| DataColumnsByRangeRequest { + start_slot: *request.start_slot(), + count: *request.count(), + columns: vec![], + }); + + columns_by_range_request.columns.push(*column_index); + } + + Ok(peer_id_to_request_map) + } + pub fn range_request_failed(&mut self, request_id: Id) -> Option { let sender_id = self .range_block_components_requests diff --git a/beacon_node/network/src/sync/range_sync/batch.rs b/beacon_node/network/src/sync/range_sync/batch.rs index 138095aa343..30d31a8b506 100644 --- a/beacon_node/network/src/sync/range_sync/batch.rs +++ b/beacon_node/network/src/sync/range_sync/batch.rs @@ -199,9 +199,9 @@ impl BatchInfo { } /// Verifies if an incoming block belongs to this batch. - pub fn is_expecting_block(&self, peer_id: &PeerId, request_id: &Id) -> bool { - if let BatchState::Downloading(expected_peer, expected_id) = &self.state { - return peer_id == expected_peer && expected_id == request_id; + pub fn is_expecting_block(&self, request_id: &Id) -> bool { + if let BatchState::Downloading(_, expected_id) = &self.state { + return expected_id == request_id; } false } diff --git a/beacon_node/network/src/sync/range_sync/chain.rs b/beacon_node/network/src/sync/range_sync/chain.rs index b316e7d1e3f..6b724844291 100644 --- a/beacon_node/network/src/sync/range_sync/chain.rs +++ b/beacon_node/network/src/sync/range_sync/chain.rs @@ -1,4 +1,5 @@ use super::batch::{BatchInfo, BatchProcessingResult, BatchState}; +use crate::metrics::PEERS_PER_COLUMN_SUBNET; use crate::network_beacon_processor::ChainSegmentProcessId; use crate::sync::network_context::RangeRequestId; use crate::sync::{ @@ -7,7 +8,8 @@ use crate::sync::{ use beacon_chain::block_verification_types::RpcBlock; use beacon_chain::BeaconChainTypes; use fnv::FnvHashMap; -use lighthouse_network::{PeerAction, PeerId}; +use lighthouse_metrics::set_int_gauge; +use lighthouse_network::{PeerAction, PeerId, Subnet}; use rand::seq::SliceRandom; use slog::{crit, debug, o, warn}; use std::collections::{btree_map::Entry, BTreeMap, HashSet}; @@ -236,7 +238,9 @@ impl SyncingChain { // sending an error /timeout) if the peer is removed from the chain for other // reasons. Check that this block belongs to the expected peer, and that the // request_id matches - if !batch.is_expecting_block(peer_id, &request_id) { + // TODO(das): removed peer_id matching as the node may request a different peer for data + // columns. + if !batch.is_expecting_block(&request_id) { return Ok(KeepChain); } batch @@ -414,6 +418,11 @@ impl SyncingChain { self.request_batches(network)?; } } + } else if !self.good_peers_on_custody_subnets(self.processing_target, network) { + // This is to handle the case where no batch was sent for the current processing + // target when there is no custody peers available. This is a valid state and should not + // return an error. + return Ok(KeepChain); } else { return Err(RemoveChain::WrongChainState(format!( "Batch not found for current processing target {}", @@ -819,7 +828,9 @@ impl SyncingChain { // A batch could be retried without the peer failing the request (disconnecting/ // sending an error /timeout) if the peer is removed from the chain for other // reasons. Check that this block belongs to the expected peer - if !batch.is_expecting_block(peer_id, &request_id) { + // TODO(das): removed peer_id matching as the node may request a different peer for data + // columns. + if !batch.is_expecting_block(&request_id) { debug!( self.log, "Batch not expecting block"; @@ -1012,6 +1023,14 @@ impl SyncingChain { // check if we have the batch for our optimistic start. If not, request it first. // We wait for this batch before requesting any other batches. if let Some(epoch) = self.optimistic_start { + if !self.good_peers_on_custody_subnets(epoch, network) { + debug!( + self.log, + "Waiting for peers to be available on custody column subnets" + ); + return Ok(KeepChain); + } + if let Entry::Vacant(entry) = self.batches.entry(epoch) { if let Some(peer) = idle_peers.pop() { let batch_type = network.batch_type(epoch); @@ -1036,6 +1055,35 @@ impl SyncingChain { Ok(KeepChain) } + /// Checks all custody column subnets for peers. Returns `true` if there is at least one peer in + /// every custody column subnet. + fn good_peers_on_custody_subnets(&self, epoch: Epoch, network: &SyncNetworkContext) -> bool { + if network.chain.spec.is_peer_das_enabled_for_epoch(epoch) { + // Require peers on all custody column subnets before sending batches + let peers_on_all_custody_subnets = network + .network_globals() + .custody_subnets(&network.chain.spec) + .all(|subnet_id| { + let peer_count = network + .network_globals() + .peers + .read() + .good_peers_on_subnet(Subnet::DataColumn(subnet_id)) + .count(); + + set_int_gauge( + &PEERS_PER_COLUMN_SUBNET, + &[&subnet_id.to_string()], + peer_count as i64, + ); + peer_count > 0 + }); + peers_on_all_custody_subnets + } else { + true + } + } + /// Creates the next required batch from the chain. If there are no more batches required, /// `false` is returned. fn include_next_batch(&mut self, network: &mut SyncNetworkContext) -> Option { @@ -1066,6 +1114,15 @@ impl SyncingChain { return None; } + // don't send batch requests until we have peers on custody subnets + if !self.good_peers_on_custody_subnets(self.to_be_downloaded, network) { + debug!( + self.log, + "Waiting for peers to be available on custody column subnets" + ); + return None; + } + let batch_id = self.to_be_downloaded; // this batch could have been included already being an optimistic batch match self.batches.entry(batch_id) { diff --git a/beacon_node/network/src/sync/sampling.rs b/beacon_node/network/src/sync/sampling.rs index 9cfd41dead7..307ecc2fd93 100644 --- a/beacon_node/network/src/sync/sampling.rs +++ b/beacon_node/network/src/sync/sampling.rs @@ -426,6 +426,8 @@ mod request { }; use beacon_chain::BeaconChainTypes; use lighthouse_network::PeerId; + use rand::seq::SliceRandom; + use rand::thread_rng; use std::collections::HashSet; use types::{data_column_sidecar::ColumnIndex, EthSpec, Hash256, Slot}; @@ -496,8 +498,7 @@ mod request { peer_ids.retain(|peer_id| !self.peers_dont_have.contains(peer_id)); - // TODO(das) randomize custodial peer and avoid failing peers - if let Some(peer_id) = peer_ids.first().cloned() { + if let Some(peer_id) = peer_ids.choose(&mut thread_rng()).cloned() { cx.data_column_lookup_request( DataColumnsByRootRequester::Sampling(SamplingId { id: requester, From 04d9eef8a74ae23eee7f473cbafd48f64f0ffa01 Mon Sep 17 00:00:00 2001 From: kevaundray Date: Wed, 17 Jul 2024 07:27:08 +0100 Subject: [PATCH 55/76] chore: update peerdas-kzg library (#6118) * chore: update peerDAS lib * chore: update library * chore: update library to version that include "init context" benchmarks and optional validation checks * chore: (can remove) -- Add benchmarks for init context --- Cargo.lock | 13 ++++++++----- Cargo.toml | 2 +- crypto/kzg/Cargo.toml | 9 +++++++++ crypto/kzg/benches/benchmark.rs | 27 +++++++++++++++++++++++++++ crypto/kzg/src/lib.rs | 6 ++---- 5 files changed, 47 insertions(+), 10 deletions(-) create mode 100644 crypto/kzg/benches/benchmark.rs diff --git a/Cargo.lock b/Cargo.lock index e4304549f44..26782a2beba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1558,7 +1558,7 @@ dependencies = [ [[package]] name = "crate_crypto_internal_peerdas_bls12_381" version = "0.3.0" -source = "git+https://github.com/crate-crypto/peerdas-kzg?rev=bb8295011cf27dc663699539c7f9a17fe273e896#bb8295011cf27dc663699539c7f9a17fe273e896" +source = "git+https://github.com/crate-crypto/peerdas-kzg?rev=55ae9e9011d792a2998d242c2a71d822ba909fa9#55ae9e9011d792a2998d242c2a71d822ba909fa9" dependencies = [ "blst", "blstrs", @@ -1571,7 +1571,7 @@ dependencies = [ [[package]] name = "crate_crypto_internal_peerdas_erasure_codes" version = "0.3.0" -source = "git+https://github.com/crate-crypto/peerdas-kzg?rev=bb8295011cf27dc663699539c7f9a17fe273e896#bb8295011cf27dc663699539c7f9a17fe273e896" +source = "git+https://github.com/crate-crypto/peerdas-kzg?rev=55ae9e9011d792a2998d242c2a71d822ba909fa9#55ae9e9011d792a2998d242c2a71d822ba909fa9" dependencies = [ "crate_crypto_internal_peerdas_bls12_381", "crate_crypto_internal_peerdas_polynomial", @@ -1580,7 +1580,7 @@ dependencies = [ [[package]] name = "crate_crypto_internal_peerdas_polynomial" version = "0.3.0" -source = "git+https://github.com/crate-crypto/peerdas-kzg?rev=bb8295011cf27dc663699539c7f9a17fe273e896#bb8295011cf27dc663699539c7f9a17fe273e896" +source = "git+https://github.com/crate-crypto/peerdas-kzg?rev=55ae9e9011d792a2998d242c2a71d822ba909fa9#55ae9e9011d792a2998d242c2a71d822ba909fa9" dependencies = [ "crate_crypto_internal_peerdas_bls12_381", ] @@ -1588,7 +1588,7 @@ dependencies = [ [[package]] name = "crate_crypto_kzg_multi_open_fk20" version = "0.3.0" -source = "git+https://github.com/crate-crypto/peerdas-kzg?rev=bb8295011cf27dc663699539c7f9a17fe273e896#bb8295011cf27dc663699539c7f9a17fe273e896" +source = "git+https://github.com/crate-crypto/peerdas-kzg?rev=55ae9e9011d792a2998d242c2a71d822ba909fa9#55ae9e9011d792a2998d242c2a71d822ba909fa9" dependencies = [ "crate_crypto_internal_peerdas_bls12_381", "crate_crypto_internal_peerdas_polynomial", @@ -2277,7 +2277,7 @@ dependencies = [ [[package]] name = "eip7594" version = "0.3.0" -source = "git+https://github.com/crate-crypto/peerdas-kzg?rev=bb8295011cf27dc663699539c7f9a17fe273e896#bb8295011cf27dc663699539c7f9a17fe273e896" +source = "git+https://github.com/crate-crypto/peerdas-kzg?rev=55ae9e9011d792a2998d242c2a71d822ba909fa9#55ae9e9011d792a2998d242c2a71d822ba909fa9" dependencies = [ "crate_crypto_internal_peerdas_bls12_381", "crate_crypto_internal_peerdas_erasure_codes", @@ -4421,8 +4421,10 @@ version = "0.1.0" dependencies = [ "arbitrary", "c-kzg", + "criterion", "derivative", "eip7594", + "eth2_network_config", "ethereum_hashing", "ethereum_serde_utils", "ethereum_ssz", @@ -4430,6 +4432,7 @@ dependencies = [ "hex", "mockall", "serde", + "serde_json", "tree_hash", ] diff --git a/Cargo.toml b/Cargo.toml index 6e4a00014b4..6c38d02c151 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -99,7 +99,7 @@ delay_map = "0.3" derivative = "2" dirs = "3" either = "1.9" -peerdas-kzg = { git = "https://github.com/crate-crypto/peerdas-kzg", rev = "bb8295011cf27dc663699539c7f9a17fe273e896", package = "eip7594" } +peerdas-kzg = { git = "https://github.com/crate-crypto/peerdas-kzg", rev = "55ae9e9011d792a2998d242c2a71d822ba909fa9", package = "eip7594" } discv5 = { version = "0.4.1", features = ["libp2p"] } env_logger = "0.9" error-chain = "0.12" diff --git a/crypto/kzg/Cargo.toml b/crypto/kzg/Cargo.toml index 1f378c7dc9f..ccacb432b63 100644 --- a/crypto/kzg/Cargo.toml +++ b/crypto/kzg/Cargo.toml @@ -19,3 +19,12 @@ ethereum_hashing = { workspace = true } c-kzg = { workspace = true } peerdas-kzg = { workspace = true } mockall = { workspace = true } + +[dev-dependencies] +criterion = { workspace = true } +serde_json = { workspace = true } +eth2_network_config = { workspace = true } + +[[bench]] +name = "benchmark" +harness = false diff --git a/crypto/kzg/benches/benchmark.rs b/crypto/kzg/benches/benchmark.rs new file mode 100644 index 00000000000..be1704d5bd4 --- /dev/null +++ b/crypto/kzg/benches/benchmark.rs @@ -0,0 +1,27 @@ +use c_kzg::KzgSettings; +use criterion::{criterion_group, criterion_main, Criterion}; +use eth2_network_config::TRUSTED_SETUP_BYTES; +use kzg::TrustedSetup; +use peerdas_kzg::{PeerDASContext, TrustedSetup as PeerDASTrustedSetup}; + +pub fn bench_init_context(c: &mut Criterion) { + c.bench_function(&format!("Initialize context peerdas-kzg"), |b| { + b.iter(|| { + const NUM_THREADS: usize = 1; + let trusted_setup = PeerDASTrustedSetup::default(); + PeerDASContext::with_threads(&trusted_setup, NUM_THREADS) + }) + }); + c.bench_function(&format!("Initialize context c-kzg (4844)"), |b| { + b.iter(|| { + let trusted_setup: TrustedSetup = serde_json::from_reader(TRUSTED_SETUP_BYTES) + .map_err(|e| format!("Unable to read trusted setup file: {}", e)) + .expect("should have trusted setup"); + KzgSettings::load_trusted_setup(&trusted_setup.g1_points(), &trusted_setup.g2_points()) + .unwrap() + }) + }); +} + +criterion_group!(benches, bench_init_context); +criterion_main!(benches); diff --git a/crypto/kzg/src/lib.rs b/crypto/kzg/src/lib.rs index 9cfe42c8b97..b9f556d5a47 100644 --- a/crypto/kzg/src/lib.rs +++ b/crypto/kzg/src/lib.rs @@ -17,7 +17,7 @@ use mockall::automock; pub use peerdas_kzg::{ constants::{BYTES_PER_CELL, CELLS_PER_EXT_BLOB}, - Cell, CellID, CellRef, TrustedSetup as PeerDASTrustedSetup, + Cell, CellIndex as CellID, CellRef, TrustedSetup as PeerDASTrustedSetup, }; use peerdas_kzg::{prover::ProverError, verifier::VerifierError, PeerDASContext}; pub type CellsAndKzgProofs = ([Cell; CELLS_PER_EXT_BLOB], [KzgProof; CELLS_PER_EXT_BLOB]); @@ -181,7 +181,6 @@ impl Kzg { let (cells, proofs) = self .context - .prover_ctx() .compute_cells_and_kzg_proofs(blob_bytes) .map_err(Error::ProverKZG)?; @@ -212,7 +211,7 @@ impl Kzg { .iter() .map(|commitment| commitment.as_ref()) .collect(); - let verification_result = self.context.verifier_ctx().verify_cell_kzg_proof_batch( + let verification_result = self.context.verify_cell_kzg_proof_batch( commitments.to_vec(), rows, columns, @@ -236,7 +235,6 @@ impl Kzg { ) -> Result { let (cells, proofs) = self .context - .prover_ctx() .recover_cells_and_proofs(cell_ids.to_vec(), cells.to_vec()) .map_err(Error::ProverKZG)?; From e1f8909b67cb725a4ab4614cd020dd48be93e3e8 Mon Sep 17 00:00:00 2001 From: Age Manning Date: Thu, 25 Jul 2024 14:13:58 +1000 Subject: [PATCH 56/76] Prevent continuous searchers for low-peer networks (#6162) --- beacon_node/lighthouse_network/src/peer_manager/mod.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/beacon_node/lighthouse_network/src/peer_manager/mod.rs b/beacon_node/lighthouse_network/src/peer_manager/mod.rs index d3d7b34eba8..9277f1f22a8 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/mod.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/mod.rs @@ -885,6 +885,11 @@ impl PeerManager { self.max_peers().saturating_sub(dialing_peers) - peer_count } else if outbound_only_peer_count < self.min_outbound_only_peers() && peer_count < self.max_outbound_dialing_peers() + && self.target_peers > 10 + // This condition is to attempt to exclude testnets without + // an explicit CLI flag. For networks with low peer counts, we don't want to do + // repetitive searches for outbound peers, when we may be already connected to every + // peer on the testnet { self.max_outbound_dialing_peers() .saturating_sub(dialing_peers) From b148c4baefe825025e0dc39995008206f5edd516 Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Wed, 31 Jul 2024 12:42:53 +0200 Subject: [PATCH 57/76] Fix merge conflicts --- beacon_node/beacon_chain/src/beacon_chain.rs | 41 ++- .../beacon_chain/src/block_verification.rs | 7 +- .../src/block_verification_types.rs | 6 +- beacon_node/beacon_chain/src/builder.rs | 1 - .../src/data_availability_checker.rs | 12 +- .../overflow_lru_cache.rs | 260 ++++++------------ .../state_lru_cache.rs | 4 +- .../src/data_column_verification.rs | 8 +- beacon_node/beacon_chain/src/errors.rs | 2 +- beacon_node/beacon_chain/src/metrics.rs | 75 +++-- .../src/observed_data_sidecars.rs | 4 +- beacon_node/beacon_processor/src/metrics.rs | 32 ++- .../lighthouse_network/src/discovery/mod.rs | 10 +- .../src/discovery/subnet_predicate.rs | 2 +- .../lighthouse_network/src/rpc/protocol.rs | 8 +- .../src/service/api_types.rs | 37 ++- .../lighthouse_network/src/service/mod.rs | 2 - beacon_node/network/src/metrics.rs | 71 ++--- .../gossip_methods.rs | 10 +- beacon_node/network/src/router.rs | 24 +- .../network/src/sync/block_lookups/common.rs | 2 +- .../network/src/sync/block_lookups/tests.rs | 14 +- beacon_node/network/src/sync/manager.rs | 35 ++- beacon_node/network/src/sync/mod.rs | 2 +- .../network/src/sync/network_context.rs | 34 +-- .../src/sync/network_context/custody.rs | 15 +- .../network/src/sync/range_sync/chain.rs | 2 + beacon_node/network/src/sync/sampling.rs | 19 +- beacon_node/store/src/metrics.rs | 7 + crypto/kzg/src/lib.rs | 22 -- .../src/cases/kzg_blob_to_kzg_commitment.rs | 3 +- .../src/cases/kzg_compute_blob_kzg_proof.rs | 3 +- .../cases/kzg_compute_cells_and_kzg_proofs.rs | 3 +- .../src/cases/kzg_compute_kzg_proof.rs | 3 +- .../cases/kzg_recover_cells_and_kzg_proofs.rs | 3 +- .../src/cases/kzg_verify_blob_kzg_proof.rs | 18 +- .../cases/kzg_verify_blob_kzg_proof_batch.rs | 3 +- .../cases/kzg_verify_cell_kzg_proof_batch.rs | 3 +- .../src/cases/kzg_verify_kzg_proof.rs | 3 +- 39 files changed, 387 insertions(+), 423 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 32c706f8870..ac91cf18ba6 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -27,7 +27,6 @@ use crate::data_availability_checker::{ use crate::data_column_verification::{ CustodyDataColumn, GossipDataColumnError, GossipVerifiedDataColumn, }; -use crate::data_column_verification::{GossipDataColumnError, GossipVerifiedDataColumn}; use crate::early_attester_cache::EarlyAttesterCache; use crate::errors::{BeaconChainError as Error, BlockProductionError}; use crate::eth1_chain::{Eth1Chain, Eth1ChainBackend}; @@ -2173,11 +2172,11 @@ impl BeaconChain { self: &Arc, data_column_sidecar: Arc>, subnet_id: u64, - ) -> Result, GossipDataColumnError> { - metrics::inc_counter(&metrics::BLOBS_COLUMN_SIDECAR_PROCESSING_REQUESTS); + ) -> Result, GossipDataColumnError> { + metrics::inc_counter(&metrics::DATA_COLUMN_SIDECAR_PROCESSING_REQUESTS); let _timer = metrics::start_timer(&metrics::DATA_COLUMN_SIDECAR_GOSSIP_VERIFICATION_TIMES); GossipVerifiedDataColumn::new(data_column_sidecar, subnet_id, self).map(|v| { - metrics::inc_counter(&metrics::DATA_COLUMNS_SIDECAR_PROCESSING_SUCCESSES); + metrics::inc_counter(&metrics::DATA_COLUMN_SIDECAR_PROCESSING_SUCCESSES); v }) } @@ -3050,7 +3049,13 @@ impl BeaconChain { pub async fn process_gossip_data_columns( self: &Arc, data_columns: Vec>, - ) -> Result> { + ) -> Result< + ( + AvailabilityProcessingStatus, + DataColumnsToPublish, + ), + BlockError, + > { let Ok(block_root) = data_columns .iter() .map(|c| c.block_root()) @@ -3166,13 +3171,15 @@ impl BeaconChain { /// Remove any block components from the *processing cache* if we no longer require them. If the /// block was imported full or erred, we no longer require them. - fn remove_notified_custody_columns( + fn remove_notified_custody_columns

( &self, block_root: &Hash256, - r: Result>, - ) -> Result> { - let has_missing_components = - matches!(r, Ok(AvailabilityProcessingStatus::MissingComponents(_, _))); + r: Result<(AvailabilityProcessingStatus, P), BlockError>, + ) -> Result<(AvailabilityProcessingStatus, P), BlockError> { + let has_missing_components = matches!( + r, + Ok((AvailabilityProcessingStatus::MissingComponents(_, _), _)) + ); if !has_missing_components { self.reqresp_pre_import_cache.write().remove(block_root); } @@ -3428,7 +3435,13 @@ impl BeaconChain { async fn check_gossip_data_columns_availability_and_import( self: &Arc, data_columns: Vec>, - ) -> Result> { + ) -> Result< + ( + AvailabilityProcessingStatus, + DataColumnsToPublish, + ), + BlockError, + > { if let Some(slasher) = self.slasher.as_ref() { for data_colum in &data_columns { slasher.accept_block_header(data_colum.signed_block_header()); @@ -3441,11 +3454,13 @@ impl BeaconChain { )); }; - let availability = self + let (availability, data_columns_to_publish) = self .data_availability_checker .put_gossip_data_columns(data_columns)?; - self.process_availability(slot, availability).await + self.process_availability(slot, availability) + .await + .map(|result| (result, data_columns_to_publish)) } /// Checks if the provided blobs can make any cached blocks available, and imports immediately diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 18f041ecdd1..b2a0ac44a05 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -96,7 +96,6 @@ use std::io::Write; use std::sync::Arc; use store::{Error as DBError, HotStateSummary, KeyValueStore, StoreOp}; use task_executor::JoinHandle; -use tree_hash::TreeHash; use types::data_column_sidecar::DataColumnSidecarError; use types::{ BeaconBlockRef, BeaconState, BeaconStateError, BlobsList, ChainSpec, DataColumnSidecar, @@ -546,10 +545,10 @@ impl BlockSlashInfo> { } } -impl BlockSlashInfo> { +impl BlockSlashInfo { pub fn from_early_error_data_column( header: SignedBeaconBlockHeader, - e: GossipDataColumnError, + e: GossipDataColumnError, ) -> Self { match e { GossipDataColumnError::ProposalSignatureInvalid => BlockSlashInfo::SignatureInvalid(e), @@ -799,7 +798,7 @@ fn build_gossip_verified_data_columns( .kzg .as_ref() .ok_or(BlockContentsError::DataColumnError( - GossipDataColumnError::::KzgNotInitialized, + GossipDataColumnError::KzgNotInitialized, ))?; let timer = metrics::start_timer(&metrics::DATA_COLUMN_SIDECAR_COMPUTATION); diff --git a/beacon_node/beacon_chain/src/block_verification_types.rs b/beacon_node/beacon_chain/src/block_verification_types.rs index 3723b22730a..b271f0a2f98 100644 --- a/beacon_node/beacon_chain/src/block_verification_types.rs +++ b/beacon_node/beacon_chain/src/block_verification_types.rs @@ -401,7 +401,7 @@ pub enum BlockContentsError { BlockError(BlockError), BlobError(GossipBlobError), BlobSidecarError(blob_sidecar::BlobSidecarError), - DataColumnError(GossipDataColumnError), + DataColumnError(GossipDataColumnError), DataColumnSidecarError(data_column_sidecar::DataColumnSidecarError), } @@ -417,8 +417,8 @@ impl From> for BlockContentsError { } } -impl From> for BlockContentsError { - fn from(value: GossipDataColumnError) -> Self { +impl From for BlockContentsError { + fn from(value: GossipDataColumnError) -> Self { Self::DataColumnError(value) } } diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index cd377b4745f..84c6dea3680 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -988,7 +988,6 @@ where self.kzg.clone(), store, self.import_all_data_columns, - &log, self.spec, ) .map_err(|e| format!("Error initializing DataAvailabilityChecker: {:?}", e))?, diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index a53281f8869..b27387cfc46 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::{ use crate::data_availability_checker::overflow_lru_cache::DataAvailabilityCheckerInner; use crate::{BeaconChain, BeaconChainTypes, BeaconStore}; use kzg::Kzg; -use slog::{debug, error, o, Logger}; +use slog::{debug, error}; use slot_clock::SlotClock; use std::fmt; use std::fmt::Debug; @@ -101,7 +101,6 @@ impl DataAvailabilityChecker { kzg: Option>, store: BeaconStore, import_all_data_columns: bool, - log: &Logger, spec: ChainSpec, ) -> Result { let spec = Arc::new(spec); @@ -111,7 +110,6 @@ impl DataAvailabilityChecker { spec.custody_requirement as usize }; - let custody_subnet_count = spec.custody_requirement as usize; let custody_column_count = custody_subnet_count.saturating_mul(spec.data_columns_per_subnet()); @@ -247,14 +245,6 @@ impl DataAvailabilityChecker { ) } - pub fn put_gossip_data_columns( - &self, - _gossip_data_columns: Vec>, - ) -> Result, AvailabilityCheckError> { - // TODO(das) to be implemented - Err(AvailabilityCheckError::Unexpected) - } - /// 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/data_availability_checker/overflow_lru_cache.rs b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs index 7ba6ced7387..7f4f71da7c2 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs @@ -5,23 +5,21 @@ use crate::block_verification_types::{ AvailabilityPendingExecutedBlock, AvailableBlock, AvailableExecutedBlock, }; use crate::data_availability_checker::{Availability, AvailabilityCheckError}; -use crate::data_column_verification::{KzgVerifiedCustodyDataColumn, KzgVerifiedDataColumn}; +use crate::data_column_verification::KzgVerifiedCustodyDataColumn; use crate::metrics; -use crate::store::{DBColumn, KeyValueStore}; use crate::BeaconChainTypes; use kzg::Kzg; use lru::LruCache; -use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard}; -use slog::{debug, trace, Logger}; -use ssz::{Decode, Encode}; +use parking_lot::RwLock; use ssz_types::{FixedVector, VariableList}; +use std::collections::HashSet; use std::num::NonZeroUsize; use std::sync::Arc; use types::blob_sidecar::BlobIdentifier; use types::data_column_sidecar::DataColumnIdentifier; use types::{ BlobSidecar, ChainSpec, ColumnIndex, DataColumnSidecar, Epoch, EthSpec, Hash256, - RuntimeVariableList, SignedBeaconBlock, + SignedBeaconBlock, }; pub type DataColumnsToPublish = Option>>>; @@ -46,11 +44,6 @@ pub enum BlockImportRequirement { CustodyColumns(usize), } -pub enum BlockImportRequirement { - AllBlobs, - CustodyColumns(usize), -} - impl PendingComponents { /// Returns an immutable reference to the cached block. pub fn get_cached_block(&self) -> &Option> { @@ -99,15 +92,6 @@ impl PendingComponents { .unwrap_or(false) } - /// Checks if a data column of a given index exists in the cache. - /// - /// Returns: - /// - `true` if a data column for the given index exists. - /// - `false` otherwise. - fn data_column_exists(&self, data_column_index: u64) -> bool { - self.get_cached_data_column(data_column_index).is_some() - } - /// Returns the number of blobs that are expected to be present. Returns `None` if we don't have a /// block. /// @@ -123,9 +107,18 @@ impl PendingComponents { self.get_cached_blobs().iter().flatten().count() } + /// Checks if a data column of a given index exists in the cache. + /// + /// Returns: + /// - `true` if a data column for the given index exists. + /// - `false` otherwise. + fn data_column_exists(&self, data_column_index: u64) -> bool { + self.get_cached_data_column(data_column_index).is_some() + } + /// Returns the number of data columns that have been received and are stored in the cache. pub fn num_received_data_columns(&self) -> usize { - self.verified_data_columns.iter().count() + self.verified_data_columns.len() } /// Returns the indices of cached custody columns @@ -134,14 +127,6 @@ impl PendingComponents { .iter() .map(|d| d.index()) .collect() - - /// Checks if a data column of a given index exists in the cache. - /// - /// Returns: - /// - `true` if a data column for the given index exists. - /// - `false` otherwise. - fn data_column_exists(&self, data_column_index: u64) -> bool { - self.get_cached_data_column(data_column_index).is_some() } /// Inserts a block into the cache. @@ -199,7 +184,7 @@ impl PendingComponents { for data_column in kzg_verified_data_columns { // TODO(das): Add equivalent checks for data columns if necessary if !self.data_column_exists(data_column.index()) { - self.verified_data_columns.push(data_column)?; + self.verified_data_columns.push(data_column); } } Ok(()) @@ -239,7 +224,7 @@ impl PendingComponents { } /// Returns an empty `PendingComponents` object with the given block root. - pub fn empty(block_root: Hash256, spec: &ChainSpec) -> Self { + pub fn empty(block_root: Hash256) -> Self { Self { block_root, verified_blobs: FixedVector::default(), @@ -370,7 +355,7 @@ pub struct DataAvailabilityCheckerInner { state_cache: StateLRUCache, /// The number of data columns the node is custodying. custody_column_count: usize, - spec: ChainSpec, + spec: Arc, } impl DataAvailabilityCheckerInner { @@ -378,7 +363,7 @@ impl DataAvailabilityCheckerInner { capacity: NonZeroUsize, beacon_store: BeaconStore, custody_column_count: usize, - spec: ChainSpec, + spec: Arc, ) -> Result { Ok(Self { critical: RwLock::new(LruCache::new(capacity)), @@ -448,128 +433,17 @@ impl DataAvailabilityCheckerInner { &self, data_column_id: &DataColumnIdentifier, ) -> Result>>, AvailabilityCheckError> { - let read_lock = self.critical.read(); - if let Some(data_column) = read_lock.peek_data_column(data_column_id)? { - Ok(Some(data_column)) - } else if read_lock.store_keys.contains(&data_column_id.block_root) { - drop(read_lock); - self.overflow_store.load_data_column(data_column_id) + if let Some(pending_components) = self.critical.read().peek(&data_column_id.block_root) { + Ok(pending_components + .verified_data_columns + .iter() + .find(|data_column| data_column.as_data_column().index == data_column_id.index) + .map(|data_column| data_column.clone_arc())) } else { Ok(None) } } - fn block_import_requirement( - &self, - pending_components: &PendingComponents, - ) -> Result { - let epoch = pending_components - .epoch() - .ok_or(AvailabilityCheckError::UnableToDetermineImportRequirement)?; - - let peer_das_enabled = self.spec.is_peer_das_enabled_for_epoch(epoch); - if peer_das_enabled { - Ok(BlockImportRequirement::CustodyColumns( - self.custody_column_count, - )) - } else { - Ok(BlockImportRequirement::AllBlobs) - } - } - - #[allow(clippy::type_complexity)] - pub fn put_kzg_verified_data_columns< - I: IntoIterator>, - >( - &self, - kzg: &Kzg, - block_root: Hash256, - kzg_verified_data_columns: I, - ) -> Result<(Availability, DataColumnsToPublish), AvailabilityCheckError> - { - let mut write_lock = self.critical.write(); - - // Grab existing entry or create a new entry. - let mut pending_components = write_lock - .pop_pending_components(block_root, &self.overflow_store)? - .unwrap_or_else(|| PendingComponents::empty(block_root, &self.spec)); - - // Merge in the data columns. - pending_components.merge_data_columns(kzg_verified_data_columns)?; - - let block_import_requirement = self.block_import_requirement(&pending_components)?; - - // Potentially trigger reconstruction if: - // - Our custody requirement is all columns - // - We >= 50% of columns - let data_columns_to_publish = - if self.should_reconstruct(&block_import_requirement, &pending_components) { - pending_components.reconstruction_started(); - - let timer = metrics::start_timer(&metrics::DATA_AVAILABILITY_RECONSTRUCTION_TIME); - - let existing_column_indices = pending_components - .verified_data_columns - .iter() - .map(|d| d.index()) - .collect::>(); - - // Will only return an error if: - // - < 50% of columns - // - There are duplicates - let all_data_columns = KzgVerifiedCustodyDataColumn::reconstruct_columns( - kzg, - pending_components.verified_data_columns.as_slice(), - &self.spec, - )?; - - let data_columns_to_publish = all_data_columns - .iter() - .filter(|d| !existing_column_indices.contains(&d.index())) - .map(|d| d.clone_arc()) - .collect::>(); - - pending_components.verified_data_columns = - RuntimeVariableList::from_vec(all_data_columns, self.spec.number_of_columns); - - metrics::stop_timer(timer); - metrics::inc_counter_by( - &metrics::DATA_AVAILABILITY_RECONSTRUCTED_COLUMNS, - data_columns_to_publish.len() as u64, - ); - debug!(self.log, "Reconstructed columns"; "count" => data_columns_to_publish.len()); - - Some(data_columns_to_publish) - } else { - None - }; - - if pending_components.is_available(&block_import_requirement, &self.log) { - write_lock.put_pending_components( - block_root, - pending_components.clone(), - &self.overflow_store, - )?; - // No need to hold the write lock anymore - drop(write_lock); - pending_components - .make_available(&self.spec, |diet_block| { - self.state_cache.recover_pending_executed_block(diet_block) - }) - .map(|availability| (availability, data_columns_to_publish)) - } else { - write_lock.put_pending_components( - block_root, - pending_components, - &self.overflow_store, - )?; - Ok(( - Availability::MissingComponents(block_root), - data_columns_to_publish, - )) - } - } - /// Potentially trigger reconstruction if: /// - Our custody requirement is all columns /// - We >= 50% of columns, but not all columns @@ -633,14 +507,16 @@ impl DataAvailabilityCheckerInner { // TODO(das): gossip and rpc code paths to be implemented. #[allow(dead_code)] + #[allow(clippy::type_complexity)] pub fn put_kzg_verified_data_columns< I: IntoIterator>, >( &self, + kzg: &Kzg, block_root: Hash256, - epoch: Epoch, kzg_verified_data_columns: I, - ) -> Result, AvailabilityCheckError> { + ) -> Result<(Availability, DataColumnsToPublish), AvailabilityCheckError> + { let mut write_lock = self.critical.write(); // Grab existing entry or create a new entry. @@ -650,19 +526,71 @@ impl DataAvailabilityCheckerInner { .unwrap_or_else(|| PendingComponents::empty(block_root)); // Merge in the data columns. - pending_components.merge_data_columns(kzg_verified_data_columns); + pending_components.merge_data_columns(kzg_verified_data_columns)?; + let epoch = pending_components + .epoch() + .ok_or(AvailabilityCheckError::UnableToDetermineImportRequirement)?; let block_import_requirement = self.block_import_requirement(epoch)?; + + // Potentially trigger reconstruction if: + // - Our custody requirement is all columns + // - We >= 50% of columns + let data_columns_to_publish = + if self.should_reconstruct(&block_import_requirement, &pending_components) { + pending_components.reconstruction_started(); + + let timer = metrics::start_timer(&metrics::DATA_AVAILABILITY_RECONSTRUCTION_TIME); + + let existing_column_indices = pending_components + .verified_data_columns + .iter() + .map(|d| d.index()) + .collect::>(); + + // Will only return an error if: + // - < 50% of columns + // - There are duplicates + let all_data_columns = KzgVerifiedCustodyDataColumn::reconstruct_columns( + kzg, + pending_components.verified_data_columns.as_slice(), + &self.spec, + )?; + + let data_columns_to_publish = all_data_columns + .iter() + .filter(|d| !existing_column_indices.contains(&d.index())) + .map(|d| d.clone_arc()) + .collect::>(); + + pending_components.verified_data_columns = all_data_columns; + + metrics::stop_timer(timer); + metrics::inc_counter_by( + &metrics::DATA_AVAILABILITY_RECONSTRUCTED_COLUMNS, + data_columns_to_publish.len() as u64, + ); + + Some(data_columns_to_publish) + } else { + None + }; + if pending_components.is_available(&block_import_requirement) { write_lock.put(block_root, pending_components.clone()); // No need to hold the write lock anymore drop(write_lock); - pending_components.make_available(|diet_block| { - self.state_cache.recover_pending_executed_block(diet_block) - }) + pending_components + .make_available(&self.spec, |diet_block| { + self.state_cache.recover_pending_executed_block(diet_block) + }) + .map(|availability| (availability, data_columns_to_publish)) } else { write_lock.put(block_root, pending_components); - Ok(Availability::MissingComponents(block_root)) + Ok(( + Availability::MissingComponents(block_root), + data_columns_to_publish, + )) } } @@ -1342,8 +1270,7 @@ mod pending_components_tests { let (block_commitments, blobs, random_blobs) = setup_pending_components(block_commitments, blobs, random_blobs); let block_root = Hash256::zero(); - let spec = E::default_spec(); - let mut cache = >::empty(block_root, &spec); + let mut cache = >::empty(block_root); cache.merge_block(block_commitments); cache.merge_blobs(random_blobs); cache.merge_blobs(blobs); @@ -1357,8 +1284,7 @@ mod pending_components_tests { let (block_commitments, blobs, random_blobs) = setup_pending_components(block_commitments, blobs, random_blobs); let block_root = Hash256::zero(); - let spec = E::default_spec(); - let mut cache = >::empty(block_root, &spec); + let mut cache = >::empty(block_root); cache.merge_blobs(random_blobs); cache.merge_block(block_commitments); cache.merge_blobs(blobs); @@ -1373,8 +1299,7 @@ mod pending_components_tests { setup_pending_components(block_commitments, blobs, random_blobs); let block_root = Hash256::zero(); - let spec = E::default_spec(); - let mut cache = >::empty(block_root, &spec); + let mut cache = >::empty(block_root); cache.merge_blobs(random_blobs); cache.merge_blobs(blobs); cache.merge_block(block_commitments); @@ -1389,8 +1314,7 @@ mod pending_components_tests { setup_pending_components(block_commitments, blobs, random_blobs); let block_root = Hash256::zero(); - let spec = E::default_spec(); - let mut cache = >::empty(block_root, &spec); + let mut cache = >::empty(block_root); cache.merge_block(block_commitments); cache.merge_blobs(blobs); cache.merge_blobs(random_blobs); @@ -1405,8 +1329,7 @@ mod pending_components_tests { setup_pending_components(block_commitments, blobs, random_blobs); let block_root = Hash256::zero(); - let spec = E::default_spec(); - let mut cache = >::empty(block_root, &spec); + let mut cache = >::empty(block_root); cache.merge_blobs(blobs); cache.merge_block(block_commitments); cache.merge_blobs(random_blobs); @@ -1421,8 +1344,7 @@ mod pending_components_tests { setup_pending_components(block_commitments, blobs, random_blobs); let block_root = Hash256::zero(); - let spec = E::default_spec(); - let mut cache = >::empty(block_root, &spec); + let mut cache = >::empty(block_root); cache.merge_blobs(blobs); cache.merge_blobs(random_blobs); cache.merge_block(block_commitments); diff --git a/beacon_node/beacon_chain/src/data_availability_checker/state_lru_cache.rs b/beacon_node/beacon_chain/src/data_availability_checker/state_lru_cache.rs index 59d77999962..5b9b7c70233 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker/state_lru_cache.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker/state_lru_cache.rs @@ -75,11 +75,11 @@ impl DietAvailabilityPendingExecutedBlock { pub struct StateLRUCache { states: RwLock>>, store: BeaconStore, - spec: ChainSpec, + spec: Arc, } impl StateLRUCache { - pub fn new(store: BeaconStore, spec: ChainSpec) -> Self { + pub fn new(store: BeaconStore, spec: Arc) -> Self { Self { states: RwLock::new(LruCache::new(STATE_LRU_CAPACITY_NON_ZERO)), store, diff --git a/beacon_node/beacon_chain/src/data_column_verification.rs b/beacon_node/beacon_chain/src/data_column_verification.rs index e6d43a20a14..b60166c857d 100644 --- a/beacon_node/beacon_chain/src/data_column_verification.rs +++ b/beacon_node/beacon_chain/src/data_column_verification.rs @@ -371,7 +371,7 @@ pub fn validate_data_column_sidecar_for_gossip( data_column: Arc>, subnet: u64, chain: &BeaconChain, -) -> Result, GossipDataColumnError> { +) -> Result, GossipDataColumnError> { let column_slot = data_column.slot(); verify_index_matches_subnet(&data_column, subnet, &chain.spec)?; @@ -429,7 +429,7 @@ fn verify_is_first_sidecar( fn verify_column_inclusion_proof( data_column: &DataColumnSidecar, -) -> Result<(), GossipDataColumnError> { +) -> Result<(), GossipDataColumnError> { let _timer = metrics::start_timer(&metrics::DATA_COLUMN_SIDECAR_INCLUSION_PROOF_VERIFICATION); if !data_column.verify_inclusion_proof() { return Err(GossipDataColumnError::InvalidInclusionProof); @@ -438,10 +438,10 @@ fn verify_column_inclusion_proof( Ok(()) } -fn verify_slot_higher_than_parent( +fn verify_slot_higher_than_parent( parent_block: &Block, data_column_slot: Slot, -) -> Result<(), GossipDataColumnError> { +) -> Result<(), GossipDataColumnError> { if parent_block.slot >= data_column_slot { return Err(GossipDataColumnError::IsNotLaterThanParent { data_column_slot, diff --git a/beacon_node/beacon_chain/src/errors.rs b/beacon_node/beacon_chain/src/errors.rs index 62d800ae715..4db3f0ebb41 100644 --- a/beacon_node/beacon_chain/src/errors.rs +++ b/beacon_node/beacon_chain/src/errors.rs @@ -10,7 +10,7 @@ use crate::naive_aggregation_pool::Error as NaiveAggregationError; use crate::observed_aggregates::Error as ObservedAttestationsError; use crate::observed_attesters::Error as ObservedAttestersError; use crate::observed_block_producers::Error as ObservedBlockProducersError; -use crate::observed_data_sidecars::Error as ObservedBlobSidecarsError; +use crate::observed_data_sidecars::Error as ObservedDataSidecarsError; use execution_layer::PayloadStatus; use fork_choice::ExecutionStatus; use futures::channel::mpsc::TrySendError; diff --git a/beacon_node/beacon_chain/src/metrics.rs b/beacon_node/beacon_chain/src/metrics.rs index 04948bfad6f..a03232b45f2 100644 --- a/beacon_node/beacon_chain/src/metrics.rs +++ b/beacon_node/beacon_chain/src/metrics.rs @@ -1618,6 +1618,13 @@ pub static BLOBS_SIDECAR_PROCESSING_REQUESTS: LazyLock> = Laz "Count of all blob sidecars submitted for processing", ) }); +pub static BLOBS_COLUMN_SIDECAR_PROCESSING_REQUESTS: LazyLock> = + LazyLock::new(|| { + try_create_int_counter( + "beacon_blobs_column_sidecar_processing_requests_total", + "Count of all data column sidecars submitted for processing", + ) + }); pub static BLOBS_SIDECAR_PROCESSING_SUCCESSES: LazyLock> = LazyLock::new(|| { try_create_int_counter( "beacon_blobs_sidecar_processing_successes_total", @@ -1645,6 +1652,19 @@ pub static BLOB_SIDECAR_INCLUSION_PROOF_COMPUTATION: LazyLock> "Time taken to compute blob sidecar inclusion proof", ) }); +pub static DATA_COLUMN_SIDECAR_COMPUTATION: LazyLock> = LazyLock::new(|| { + try_create_histogram( + "data_column_sidecar_computation_seconds", + "Time taken to compute data column sidecar, including cells, proofs and inclusion proof", + ) +}); +pub static DATA_COLUMN_SIDECAR_INCLUSION_PROOF_VERIFICATION: LazyLock> = + LazyLock::new(|| { + try_create_histogram( + "data_column_sidecar_inclusion_proof_verification_seconds", + "Time taken to verify data_column sidecar inclusion proof", + ) + }); pub static DATA_COLUMN_SIDECAR_PROCESSING_REQUESTS: LazyLock> = LazyLock::new(|| { try_create_int_counter( @@ -1666,6 +1686,13 @@ pub static DATA_COLUMN_SIDECAR_GOSSIP_VERIFICATION_TIMES: LazyLock> = + LazyLock::new(|| { + try_create_int_counter( + "beacon_blobs_column_sidecar_processing_successes_total", + "Number of data column sidecars verified for gossip", + ) + }); /* * Light server message verification @@ -1785,10 +1812,20 @@ pub static KZG_VERIFICATION_BATCH_TIMES: LazyLock> = LazyLock: "Runtime of batched kzg verification", ) }); - pub static ref KZG_VERIFICATION_DATA_COLUMN_SINGLE_TIMES: LazyLock> = LazyLock::new(|| { - try_create_histogram("kzg_verification_data_column_single_seconds", "Runtime of single data column kzg verification") }); - pub static ref KZG_VERIFICATION_DATA_COLUMN_BATCH_TIMES: LazyLock> = LazyLock::new(|| { - try_create_histogram("kzg_verification_data_column_batch_seconds", "Runtime of batched data column kzg verification") }); +pub static KZG_VERIFICATION_DATA_COLUMN_SINGLE_TIMES: LazyLock> = + LazyLock::new(|| { + try_create_histogram( + "kzg_verification_data_column_single_seconds", + "Runtime of single data column kzg verification", + ) + }); +pub static KZG_VERIFICATION_DATA_COLUMN_BATCH_TIMES: LazyLock> = + LazyLock::new(|| { + try_create_histogram( + "kzg_verification_data_column_batch_seconds", + "Runtime of batched data column kzg verification", + ) + }); pub static BLOCK_PRODUCTION_BLOBS_VERIFICATION_TIMES: LazyLock> = LazyLock::new( || { @@ -1829,19 +1866,23 @@ pub static DATA_AVAILABILITY_OVERFLOW_STORE_CACHE_SIZE: LazyLock> = LazyLock::new(|| { try_create_histogram( - "data_availability_reconstruction_time_seconds", - "Time taken to reconstruct columns" - ); -}); - pub static DATA_AVAILABILITY_RECONSTRUCTED_COLUMNS: Result = LazyLock::new(|| { try_create_int_counter( - "data_availability_reconstructed_columns_total", - "Total count of reconstructed columns" - ); -}); + "Number of entries in the data availability overflow store cache.", + ) + }); +pub static DATA_AVAILABILITY_RECONSTRUCTION_TIME: LazyLock> = + LazyLock::new(|| { + try_create_histogram( + "data_availability_reconstruction_time_seconds", + "Time taken to reconstruct columns", + ) + }); +pub static DATA_AVAILABILITY_RECONSTRUCTED_COLUMNS: LazyLock> = + LazyLock::new(|| { + try_create_int_counter( + "data_availability_reconstructed_columns_total", + "Total count of reconstructed columns", + ) + }); /* * light_client server metrics diff --git a/beacon_node/beacon_chain/src/observed_data_sidecars.rs b/beacon_node/beacon_chain/src/observed_data_sidecars.rs index 59b343eaa7c..601241dd8ad 100644 --- a/beacon_node/beacon_chain/src/observed_data_sidecars.rs +++ b/beacon_node/beacon_chain/src/observed_data_sidecars.rs @@ -10,7 +10,7 @@ use types::{BlobSidecar, ChainSpec, DataColumnSidecar, EthSpec, Slot}; #[derive(Debug, PartialEq)] pub enum Error { - /// The slot of the provided `ObservableSidecar` is prior to finalization and should not have been provided + /// The slot of the provided `ObservableDataSidecar` is prior to finalization and should not have been provided /// to this function. This is an internal error. FinalizedDataSidecar { slot: Slot, finalized_slot: Slot }, /// The data sidecar contains an invalid index, the data sidecar is invalid. @@ -62,7 +62,7 @@ impl ObservableDataSidecar for DataColumnSidecar { } } -/// Maintains a cache of seen `ObservableSidecar`s that are received over gossip +/// Maintains a cache of seen `ObservableDataSidecar`s that are received over gossip /// and have been gossip verified. /// /// The cache supports pruning based upon the finalized epoch. It does not automatically prune, you diff --git a/beacon_node/beacon_processor/src/metrics.rs b/beacon_node/beacon_processor/src/metrics.rs index 6ff02403665..11edb53c73b 100644 --- a/beacon_node/beacon_processor/src/metrics.rs +++ b/beacon_node/beacon_processor/src/metrics.rs @@ -133,6 +133,30 @@ pub static BEACON_PROCESSOR_RPC_BLOB_QUEUE_TOTAL: LazyLock> = "Count of blobs from the rpc waiting to be verified.", ) }); +// Rpc custody data columns. +pub static BEACON_PROCESSOR_RPC_CUSTODY_COLUMN_QUEUE_TOTAL: LazyLock> = + LazyLock::new(|| { + try_create_int_gauge( + "beacon_processor_rpc_custody_column_queue_total", + "Count of custody columns from the rpc waiting to be imported.", + ) + }); +// Rpc verify data columns +pub static BEACON_PROCESSOR_RPC_VERIFY_DATA_COLUMN_QUEUE_TOTAL: LazyLock> = + LazyLock::new(|| { + try_create_int_gauge( + "beacon_processor_rpc_verify_data_column_queue_total", + "Count of data columns from the rpc waiting to be verified.", + ) + }); +// Sampling result +pub static BEACON_PROCESSOR_SAMPLING_RESULT_QUEUE_TOTAL: LazyLock> = + LazyLock::new(|| { + try_create_int_gauge( + "beacon_processor_sampling_result_queue_total", + "Count of sampling results waiting to be processed.", + ) + }); // Chain segments. pub static BEACON_PROCESSOR_CHAIN_SEGMENT_QUEUE_TOTAL: LazyLock> = LazyLock::new(|| { @@ -222,10 +246,12 @@ pub static BEACON_PROCESSOR_REPROCESSING_QUEUE_MATCHED_ATTESTATIONS: LazyLock> = LazyLock::new(|| { +pub static BEACON_PROCESSOR_REPROCESSING_QUEUE_MATCHED_SAMPLING_REQUESTS: LazyLock< + Result, +> = LazyLock::new(|| { try_create_int_counter( "beacon_processor_reprocessing_queue_matches_sampling_requests", - "Number of queued sampling requests where a matching block has been imported." + "Number of queued sampling requests where a matching block has been imported.", ) }); @@ -246,7 +272,7 @@ pub static BEACON_PROCESSOR_REPROCESSING_QUEUE_MATCHED_OPTIMISTIC_UPDATES: LazyL try_create_int_counter( "beacon_processor_reprocessing_queue_matched_optimistic_updates", "Number of queued light client optimistic updates where a matching block has been imported." - ); + ) }); /// Errors and Debugging Stats diff --git a/beacon_node/lighthouse_network/src/discovery/mod.rs b/beacon_node/lighthouse_network/src/discovery/mod.rs index 47f358f2530..300c190cdaf 100644 --- a/beacon_node/lighthouse_network/src/discovery/mod.rs +++ b/beacon_node/lighthouse_network/src/discovery/mod.rs @@ -758,7 +758,8 @@ impl Discovery { // Only start a discovery query if we have a subnet to look for. if !filtered_subnet_queries.is_empty() { // build the subnet predicate as a combination of the eth2_fork_predicate and the subnet predicate - let subnet_predicate = subnet_predicate::(filtered_subnets, &self.log, &self.spec); + let subnet_predicate = + subnet_predicate::(filtered_subnets, &self.log, self.spec.clone()); debug!( self.log, @@ -885,8 +886,11 @@ impl Discovery { self.add_subnet_query(query.subnet, query.min_ttl, query.retries + 1); // Check the specific subnet against the enr - let subnet_predicate = - subnet_predicate::(vec![query.subnet], &self.log, &self.spec); + let subnet_predicate = subnet_predicate::( + vec![query.subnet], + &self.log, + self.spec.clone(), + ); r.clone() .into_iter() diff --git a/beacon_node/lighthouse_network/src/discovery/subnet_predicate.rs b/beacon_node/lighthouse_network/src/discovery/subnet_predicate.rs index 3cf38f4cb57..8bc5e25fde9 100644 --- a/beacon_node/lighthouse_network/src/discovery/subnet_predicate.rs +++ b/beacon_node/lighthouse_network/src/discovery/subnet_predicate.rs @@ -10,7 +10,7 @@ use types::{ChainSpec, DataColumnSubnetId}; pub fn subnet_predicate( subnets: Vec, log: &slog::Logger, - spec: &ChainSpec, + spec: Arc, ) -> impl Fn(&Enr) -> bool + Send where E: EthSpec, diff --git a/beacon_node/lighthouse_network/src/rpc/protocol.rs b/beacon_node/lighthouse_network/src/rpc/protocol.rs index 25e18d16dae..40674ef4bf4 100644 --- a/beacon_node/lighthouse_network/src/rpc/protocol.rs +++ b/beacon_node/lighthouse_network/src/rpc/protocol.rs @@ -17,10 +17,10 @@ use tokio_util::{ compat::{Compat, FuturesAsyncReadCompatExt}, }; use types::{ - BeaconBlock, BeaconBlockAltair, BeaconBlockBase, BeaconBlockBellatrix, BeaconBlockCapella, - BeaconBlockElectra, BlobSidecar, ChainSpec, DataColumnSidecar, EmptyBlock, EthSpec, - ForkContext, ForkName, LightClientBootstrap, LightClientBootstrapAltair, - LightClientFinalityUpdate, LightClientFinalityUpdateAltair, LightClientOptimisticUpdate, + BeaconBlock, BeaconBlockAltair, BeaconBlockBase, BeaconBlockCapella, BeaconBlockElectra, + BlobSidecar, ChainSpec, DataColumnSidecar, EmptyBlock, EthSpec, ForkContext, ForkName, + LightClientBootstrap, LightClientBootstrapAltair, LightClientFinalityUpdate, + LightClientFinalityUpdateAltair, LightClientOptimisticUpdate, LightClientOptimisticUpdateAltair, MainnetEthSpec, Signature, SignedBeaconBlock, }; diff --git a/beacon_node/lighthouse_network/src/service/api_types.rs b/beacon_node/lighthouse_network/src/service/api_types.rs index 5eb67a3d59d..7263ea8a3ca 100644 --- a/beacon_node/lighthouse_network/src/service/api_types.rs +++ b/beacon_node/lighthouse_network/src/service/api_types.rs @@ -2,8 +2,8 @@ use std::sync::Arc; use libp2p::swarm::ConnectionId; use types::{ - BlobSidecar, DataColumnSidecar, EthSpec, LightClientBootstrap, LightClientFinalityUpdate, - LightClientOptimisticUpdate, SignedBeaconBlock, + BlobSidecar, ColumnIndex, DataColumnSidecar, EthSpec, Hash256, LightClientBootstrap, + LightClientFinalityUpdate, LightClientOptimisticUpdate, SignedBeaconBlock, }; use crate::rpc::methods::{ @@ -42,12 +42,45 @@ pub enum SyncRequestId { RangeBlockAndBlobs { id: Id }, } +/// Request ID for data_columns_by_root requests. Block lookup do not issue this requests directly. +/// Wrapping this particular req_id, ensures not mixing this requests with a custody req_id. +#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] +pub struct DataColumnsByRootRequestId(pub Id); + +impl std::fmt::Display for DataColumnsByRootRequestId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + #[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] pub enum DataColumnsByRootRequester { Sampling(SamplingId), Custody(CustodyId), } +#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] +pub struct SamplingId { + pub id: SamplingRequester, + pub column_index: ColumnIndex, +} + +#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] +pub enum SamplingRequester { + ImportedBlock(Hash256), +} + +#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] +pub struct CustodyId { + pub requester: CustodyRequester, + pub req_id: Id, +} + +/// Downstream components that perform custody by root requests. +/// Currently, it's only single block lookups, so not using an enum +#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] +pub struct CustodyRequester(pub SingleLookupReqId); + /// Application level requests sent to the network. #[derive(Debug, Clone, Copy)] pub enum AppRequestId { diff --git a/beacon_node/lighthouse_network/src/service/mod.rs b/beacon_node/lighthouse_network/src/service/mod.rs index e9a8359bc0e..cec9f0841cc 100644 --- a/beacon_node/lighthouse_network/src/service/mod.rs +++ b/beacon_node/lighthouse_network/src/service/mod.rs @@ -130,7 +130,6 @@ pub struct Network { pub local_peer_id: PeerId, /// Logger for behaviour actions. log: slog::Logger, - spec: ChainSpec, } /// Implements the combined behaviour for the libp2p service. @@ -458,7 +457,6 @@ impl Network { gossip_cache, local_peer_id, log, - spec: ctx.chain_spec.clone(), }; network.start(&config).await?; diff --git a/beacon_node/network/src/metrics.rs b/beacon_node/network/src/metrics.rs index 8f0ea9ea3e8..9e42aa8e924 100644 --- a/beacon_node/network/src/metrics.rs +++ b/beacon_node/network/src/metrics.rs @@ -340,13 +340,14 @@ pub static PEERS_PER_SYNC_TYPE: LazyLock> = LazyLock::new(|| try_create_int_gauge_vec( "sync_peers_per_status", "Number of connected peers per sync status type", - &["sync_status"] + &["sync_status"], ) }); - pub static ref PEERS_PER_COLUMN_SUBNET: LazyLock> = LazyLock::new(|| { try_create_int_gauge_vec( +pub static PEERS_PER_COLUMN_SUBNET: LazyLock> = LazyLock::new(|| { + try_create_int_gauge_vec( "peers_per_column_subnet", "Number of connected peers per column subnet", - &["subnet_id"] + &["subnet_id"], ) }); pub static SYNCING_CHAINS_COUNT: LazyLock> = LazyLock::new(|| { @@ -490,22 +491,19 @@ pub static BEACON_BLOB_DELAY_GOSSIP: LazyLock> = LazyLock::new( ) }); - pub static ref BEACON_DATA_COLUMN_GOSSIP_PROPAGATION_VERIFICATION_DELAY_TIME: Result = try_create_histogram_with_buckets( +pub static BEACON_DATA_COLUMN_GOSSIP_PROPAGATION_VERIFICATION_DELAY_TIME: LazyLock< + Result, +> = LazyLock::new(|| { + try_create_histogram_with_buckets( "beacon_data_column_gossip_propagation_verification_delay_time", "Duration between when the data column sidecar is received over gossip and when it is verified for propagation.", // [0.001, 0.002, 0.005, 0.01, 0.02, 0.05, 0.1, 0.2, 0.5] decimal_buckets(-3,-1) - ); - pub static ref BEACON_BLOB_GOSSIP_SLOT_START_DELAY_TIME: Result = try_create_histogram_with_buckets( - "beacon_blob_gossip_slot_start_delay_time", - "Duration between when the blob is received over gossip and the start of the slot it belongs to.", - // Create a custom bucket list for greater granularity in block delay - Ok(vec![0.1, 0.2, 0.3,0.4,0.5,0.75,1.0,1.25,1.5,1.75,2.0,2.5,3.0,3.5,4.0,5.0,6.0,7.0,8.0,9.0,10.0,15.0,20.0]) - // NOTE: Previous values, which we may want to switch back to. - // [0.1, 0.2, 0.5, 1, 2, 5, 10, 20, 50] - //decimal_buckets(-1,2) - ); - pub static ref BEACON_DATA_COLUMN_GOSSIP_SLOT_START_DELAY_TIME: Result = try_create_histogram_with_buckets( + ) +}); +pub static BEACON_DATA_COLUMN_GOSSIP_SLOT_START_DELAY_TIME: LazyLock> = + LazyLock::new(|| { + try_create_histogram_with_buckets( "beacon_data_column_gossip_slot_start_delay_time", "Duration between when the data column sidecar is received over gossip and the start of the slot it belongs to.", // Create a custom bucket list for greater granularity in block delay @@ -513,7 +511,8 @@ pub static BEACON_BLOB_DELAY_GOSSIP: LazyLock> = LazyLock::new( // NOTE: Previous values, which we may want to switch back to. // [0.1, 0.2, 0.5, 1, 2, 5, 10, 20, 50] //decimal_buckets(-1,2) - ); + ) + }); pub static BEACON_BLOB_DELAY_GOSSIP_VERIFICATION: LazyLock> = LazyLock::new( || { @@ -554,22 +553,6 @@ pub static BEACON_BLOB_GOSSIP_ARRIVED_LATE_TOTAL: LazyLock> = }, ); -pub static BEACON_DATA_COLUMN_DELAY_GOSSIP: LazyLock> = LazyLock::new(|| { - try_create_int_gauge( - "beacon_data_column_delay_gossip_last_delay", - "The first time we see this data column as a delay from the start of the slot", - ) -}); - -pub static BEACON_DATA_COLUMN_DELAY_GOSSIP_VERIFICATION: LazyLock> = LazyLock::new( - || { - try_create_int_gauge( - "beacon_data_column_delay_gossip_verification", - "Keeps track of the time delay from the start of the slot to the point we propagate the data column" - ) - }, -); - /* * Light client update reprocessing queue metrics. */ @@ -585,21 +568,27 @@ pub static BEACON_PROCESSOR_REPROCESSING_QUEUE_SENT_OPTIMISTIC_UPDATES: LazyLock /* * Sampling */ - pub static ref SAMPLE_DOWNLOAD_RESULT: LazyLock> = LazyLock::new(|| { try_create_int_counter_vec( +pub static SAMPLE_DOWNLOAD_RESULT: LazyLock> = LazyLock::new(|| { + try_create_int_counter_vec( "beacon_sampling_sample_verify_result_total", "Total count of individual sample download results", - &["result"] - ) }); - pub static ref SAMPLE_VERIFY_RESULT: LazyLock> = LazyLock::new(|| { try_create_int_counter_vec( + &["result"], + ) +}); +pub static SAMPLE_VERIFY_RESULT: LazyLock> = LazyLock::new(|| { + try_create_int_counter_vec( "beacon_sampling_sample_verify_result_total", "Total count of individual sample verify results", - &["result"] - )}); - pub static ref SAMPLING_REQUEST_RESULT: LazyLock> = LazyLock::new(|| { try_create_int_counter_vec( + &["result"], + ) +}); +pub static SAMPLING_REQUEST_RESULT: LazyLock> = LazyLock::new(|| { + try_create_int_counter_vec( "beacon_sampling_request_result_total", "Total count of sample request results", - &["result"] - ) }); + &["result"], + ) +}); pub fn register_finality_update_error(error: &LightClientFinalityUpdateError) { inc_counter_vec(&GOSSIP_FINALITY_UPDATE_ERRORS_PER_TYPE, &[error.as_ref()]); 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 fdcffd8b116..d5d83d540a0 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -644,13 +644,9 @@ impl NetworkBeaconProcessor { &metrics::BEACON_DATA_COLUMN_GOSSIP_SLOT_START_DELAY_TIME, delay, ); - metrics::set_gauge( - &metrics::BEACON_DATA_COLUMN_LAST_DELAY, - delay.as_millis() as i64, - ); match self .chain - .verify_data_column_sidecar_for_gossip(column_sidecar, *subnet_id) + .verify_data_column_sidecar_for_gossip(column_sidecar.clone(), *subnet_id) { Ok(gossip_verified_data_column) => { metrics::inc_counter( @@ -1020,7 +1016,9 @@ impl NetworkBeaconProcessor { .process_gossip_data_columns(vec![verified_data_column]) .await { - Ok(availability) => { + Ok((availability, data_columns_to_publish)) => { + self.handle_data_columns_to_publish(data_columns_to_publish); + match availability { AvailabilityProcessingStatus::Imported(block_root) => { // Note: Reusing block imported metric here diff --git a/beacon_node/network/src/router.rs b/beacon_node/network/src/router.rs index 70d7755260b..a5e27f582af 100644 --- a/beacon_node/network/src/router.rs +++ b/beacon_node/network/src/router.rs @@ -520,8 +520,8 @@ impl Router { beacon_block: Option>>, ) { let request_id = match request_id { - RequestId::Sync(sync_id) => match sync_id { - id @ SyncId::RangeBlockComponents { .. } => id, + AppRequestId::Sync(sync_id) => match sync_id { + id @ SyncRequestId::RangeBlockAndBlobs { .. } => id, other => { crit!(self.log, "BlocksByRange response on incorrect request"; "request" => ?other); return; @@ -582,8 +582,8 @@ impl Router { beacon_block: Option>>, ) { let request_id = match request_id { - RequestId::Sync(sync_id) => match sync_id { - id @ SyncId::SingleBlock { .. } => id, + AppRequestId::Sync(sync_id) => match sync_id { + id @ SyncRequestId::SingleBlock { .. } => id, other => { crit!(self.log, "BlocksByRoot response on incorrect request"; "request" => ?other); return; @@ -616,8 +616,8 @@ impl Router { blob_sidecar: Option>>, ) { let request_id = match request_id { - RequestId::Sync(sync_id) => match sync_id { - id @ SyncId::SingleBlob { .. } => id, + AppRequestId::Sync(sync_id) => match sync_id { + id @ SyncRequestId::SingleBlob { .. } => id, other => { crit!(self.log, "BlobsByRoot response on incorrect request"; "request" => ?other); return; @@ -646,18 +646,18 @@ impl Router { pub fn on_data_columns_by_root_response( &mut self, peer_id: PeerId, - request_id: RequestId, + request_id: AppRequestId, data_column: Option>>, ) { let request_id = match request_id { - RequestId::Sync(sync_id) => match sync_id { - id @ SyncId::DataColumnsByRoot { .. } => id, + AppRequestId::Sync(sync_id) => match sync_id { + id @ SyncRequestId::DataColumnsByRoot { .. } => id, other => { crit!(self.log, "DataColumnsByRoot response on incorrect request"; "request" => ?other); return; } }, - RequestId::Router => { + AppRequestId::Router => { crit!(self.log, "All DataColumnsByRoot requests belong to sync"; "peer_id" => %peer_id); return; } @@ -679,7 +679,7 @@ impl Router { pub fn on_data_columns_by_range_response( &mut self, peer_id: PeerId, - request_id: RequestId, + request_id: AppRequestId, data_column: Option>>, ) { trace!( @@ -688,7 +688,7 @@ impl Router { "peer" => %peer_id, ); - if let RequestId::Sync(id) = request_id { + if let AppRequestId::Sync(id) = request_id { self.send_to_sync(SyncMessage::RpcDataColumn { peer_id, request_id: id, diff --git a/beacon_node/network/src/sync/block_lookups/common.rs b/beacon_node/network/src/sync/block_lookups/common.rs index f6ed7d47d6f..2645372b7c4 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, PeerId}; -use crate::sync::manager::{BlockProcessType, Id}; +use crate::sync::manager::BlockProcessType; use crate::sync::network_context::{LookupRequestResult, SyncNetworkContext}; use beacon_chain::block_verification_types::RpcBlock; use beacon_chain::data_column_verification::CustodyDataColumn; diff --git a/beacon_node/network/src/sync/block_lookups/tests.rs b/beacon_node/network/src/sync/block_lookups/tests.rs index 05b17840676..6f4eb454d16 100644 --- a/beacon_node/network/src/sync/block_lookups/tests.rs +++ b/beacon_node/network/src/sync/block_lookups/tests.rs @@ -1,11 +1,6 @@ use crate::network_beacon_processor::NetworkBeaconProcessor; -use crate::service::RequestId; use crate::sync::manager::{BlockProcessType, SyncManager}; -use crate::sync::manager::{ - DataColumnsByRootRequester, RequestId as SyncRequestId, SingleLookupReqId, SyncManager, -}; -use crate::sync::sampling::{SamplingConfig, SamplingRequester}; -use crate::sync::SyncMessage; +use crate::sync::sampling::SamplingConfig; use crate::sync::{SamplingId, SyncMessage}; use crate::NetworkMessage; use std::sync::Arc; @@ -28,7 +23,10 @@ use beacon_chain::{ }; use beacon_processor::WorkEvent; use lighthouse_network::rpc::{RPCError, RPCResponseErrorCode}; -use lighthouse_network::service::api_types::{AppRequestId, Id, SingleLookupReqId, SyncRequestId}; +use lighthouse_network::service::api_types::{ + AppRequestId, DataColumnsByRootRequester, Id, SamplingRequester, SingleLookupReqId, + SyncRequestId, +}; use lighthouse_network::types::SyncState; use lighthouse_network::{NetworkGlobals, Request}; use slog::info; @@ -988,7 +986,7 @@ impl TestRig { NetworkMessage::SendRequest { peer_id: _, request: Request::DataColumnsByRoot(request), - request_id: RequestId::Sync(id @ SyncRequestId::DataColumnsByRoot { .. }), + request_id: AppRequestId::Sync(id @ SyncRequestId::DataColumnsByRoot { .. }), } if request .data_column_ids .to_vec() diff --git a/beacon_node/network/src/sync/manager.rs b/beacon_node/network/src/sync/manager.rs index 5fa7dd2d2d7..60e5699e1a0 100644 --- a/beacon_node/network/src/sync/manager.rs +++ b/beacon_node/network/src/sync/manager.rs @@ -35,13 +35,10 @@ use super::backfill_sync::{BackFillSync, ProcessResult, SyncStart}; use super::block_lookups::BlockLookups; -use super::network_context::{ - BlockOrBlob, CustodyId, DataColumnsByRootRequestId, RangeRequestId, RpcEvent, - SyncNetworkContext, -}; +use super::network_context::{BlockOrBlob, RangeRequestId, RpcEvent, SyncNetworkContext}; use super::peer_sync_info::{remote_sync_type, PeerSyncType}; use super::range_sync::{RangeSync, RangeSyncType, EPOCHS_PER_BATCH}; -use super::sampling::{Sampling, SamplingConfig, SamplingId, SamplingRequester, SamplingResult}; +use super::sampling::{Sampling, SamplingConfig, SamplingResult}; use crate::network_beacon_processor::{ChainSegmentProcessId, NetworkBeaconProcessor}; use crate::service::NetworkMessage; use crate::status::ToStatusMessage; @@ -58,7 +55,10 @@ use beacon_chain::{ }; use futures::StreamExt; use lighthouse_network::rpc::RPCError; -use lighthouse_network::service::api_types::{Id, SingleLookupReqId, SyncRequestId}; +use lighthouse_network::service::api_types::{ + DataColumnsByRootRequestId, DataColumnsByRootRequester, Id, SamplingId, SamplingRequester, + SingleLookupReqId, SyncRequestId, +}; use lighthouse_network::types::{NetworkGlobals, SyncState}; use lighthouse_network::SyncInfo; use lighthouse_network::{PeerAction, PeerId}; @@ -108,7 +108,7 @@ pub enum SyncMessage { /// A data columns has been received from the RPC RpcDataColumn { - request_id: RequestId, + request_id: SyncRequestId, peer_id: PeerId, data_column: Option>>, seen_timestamp: Duration, @@ -388,7 +388,7 @@ impl SyncManager { peer_id, RpcEvent::RPCError(error), ), - SyncRequestId::RangeBlockComponents(id) => { + SyncRequestId::RangeBlockAndBlobs { id } => { if let Some(sender_id) = self.network.range_request_failed(id) { match sender_id { RangeRequestId::RangeSync { chain_id, batch_id } => { @@ -724,9 +724,6 @@ impl SyncManager { }), ); } - SyncMessage::UnknownParentDataColumn(_peer_id, _data_column) => { - // TODO(das): data column parent lookup to be implemented - } 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)); @@ -935,7 +932,7 @@ impl SyncManager { None => RpcEvent::StreamTermination, }, ), - RequestId::RangeBlockComponents(id) => { + SyncRequestId::RangeBlockAndBlobs { id } => { self.range_block_and_blobs_response(id, peer_id, block.into()) } other => { @@ -970,7 +967,7 @@ impl SyncManager { seen_timestamp: Duration, ) { match request_id { - RequestId::SingleBlob { id } => self.on_single_blob_response( + SyncRequestId::SingleBlob { id } => self.on_single_blob_response( id, peer_id, match blob { @@ -978,7 +975,7 @@ impl SyncManager { None => RpcEvent::StreamTermination, }, ), - RequestId::RangeBlockComponents(id) => { + SyncRequestId::RangeBlockAndBlobs { id } => { self.range_block_and_blobs_response(id, peer_id, blob.into()) } other => { @@ -989,16 +986,16 @@ impl SyncManager { fn rpc_data_column_received( &mut self, - request_id: RequestId, + request_id: SyncRequestId, peer_id: PeerId, data_column: Option>>, seen_timestamp: Duration, ) { match request_id { - RequestId::SingleBlock { .. } | RequestId::SingleBlob { .. } => { + SyncRequestId::SingleBlock { .. } | SyncRequestId::SingleBlob { .. } => { crit!(self.log, "bad request id for data_column"; "peer_id" => %peer_id ); } - RequestId::DataColumnsByRoot(req_id, requester) => { + SyncRequestId::DataColumnsByRoot(req_id, requester) => { self.on_single_data_column_response( req_id, requester, @@ -1009,7 +1006,7 @@ impl SyncManager { }, ); } - RequestId::RangeBlockComponents(id) => { + SyncRequestId::RangeBlockAndBlobs { id } => { self.range_block_and_blobs_response( id, peer_id, @@ -1176,7 +1173,7 @@ impl SyncManager { "sender_id" => ?resp.sender_id, "error" => e.clone() ); - let id = RequestId::RangeBlockComponents(id); + let id = SyncRequestId::RangeBlockAndBlobs { id }; self.network.report_peer( peer_id, PeerAction::MidToleranceError, diff --git a/beacon_node/network/src/sync/mod.rs b/beacon_node/network/src/sync/mod.rs index 0fb01a73e07..6669add4453 100644 --- a/beacon_node/network/src/sync/mod.rs +++ b/beacon_node/network/src/sync/mod.rs @@ -10,6 +10,6 @@ mod peer_sync_info; mod range_sync; mod sampling; +pub use lighthouse_network::service::api_types::SamplingId; pub use manager::{BatchProcessResult, SyncMessage}; pub use range_sync::{BatchOperationOutcome, ChainId}; -pub use sampling::SamplingId; diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index 94b05229d85..5963401faa8 100644 --- a/beacon_node/network/src/sync/network_context.rs +++ b/beacon_node/network/src/sync/network_context.rs @@ -1,23 +1,19 @@ //! Provides network functionality for the Syncing thread. This fundamentally wraps a network //! channel and stores a global RPC ID to perform requests. -pub use self::custody::CustodyId; -use self::custody::{ActiveCustodyRequest, CustodyRequester, Error as CustodyRequestError}; +use self::custody::{ActiveCustodyRequest, Error as CustodyRequestError}; use self::requests::ActiveDataColumnsByRootRequest; pub use self::requests::BlocksByRootSingleRequest; pub use self::requests::DataColumnsByRootSingleBlockRequest; use self::requests::{ActiveBlobsByRootRequest, ActiveBlocksByRootRequest}; use super::block_sidecar_coupling::RangeBlockComponentsRequest; -use super::manager::{ - BlockProcessType, DataColumnsByRootRequester, Id, RequestId as SyncRequestId, -}; +use super::manager::BlockProcessType; use super::range_sync::{BatchId, ByRangeRequestType, ChainId}; use crate::metrics; use crate::network_beacon_processor::NetworkBeaconProcessor; use crate::service::NetworkMessage; use crate::status::ToStatusMessage; use crate::sync::block_lookups::SingleLookupId; -use crate::sync::manager::SingleLookupReqId; use crate::sync::network_context::requests::BlobsByRootSingleBlockRequest; use beacon_chain::block_verification_types::RpcBlock; use beacon_chain::data_column_verification::CustodyDataColumn; @@ -26,7 +22,10 @@ use beacon_chain::{BeaconChain, BeaconChainTypes, BlockProcessStatus, EngineStat use fnv::FnvHashMap; use lighthouse_network::rpc::methods::{BlobsByRangeRequest, DataColumnsByRangeRequest}; use lighthouse_network::rpc::{BlocksByRangeRequest, GoodbyeReason, RPCError}; -use lighthouse_network::service::api_types::{AppRequestId, Id, SingleLookupReqId, SyncRequestId}; +use lighthouse_network::service::api_types::{ + AppRequestId, CustodyId, CustodyRequester, DataColumnsByRootRequestId, + DataColumnsByRootRequester, Id, SingleLookupReqId, SyncRequestId, +}; use lighthouse_network::{Client, NetworkGlobals, PeerAction, PeerId, ReportSource, Request}; use rand::seq::SliceRandom; use rand::thread_rng; @@ -64,11 +63,6 @@ pub enum RangeRequestId { }, } -/// Request ID for data_columns_by_root requests. Block lookup do not issue this requests directly. -/// Wrapping this particular req_id, ensures not mixing this requests with a custody req_id. -#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] -pub struct DataColumnsByRootRequestId(Id); - #[derive(Debug)] pub enum RpcEvent { StreamTermination, @@ -246,7 +240,7 @@ impl SyncNetworkContext { .iter() .filter_map(|(id, request)| { if request.1.peer_ids.contains(peer_id) { - Some(SyncRequestId::RangeBlockComponents(*id)) + Some(SyncRequestId::RangeBlockAndBlobs { id: *id }) } else { None } @@ -410,7 +404,7 @@ impl SyncNetworkContext { self.send_network_msg(NetworkMessage::SendRequest { peer_id, request: Request::DataColumnsByRange(columns_by_range_request), - request_id: RequestId::Sync(SyncRequestId::RangeBlockComponents(id)), + request_id: AppRequestId::Sync(SyncRequestId::RangeBlockAndBlobs { id }), }) .map_err(|_| RpcRequestSendError::NetworkSendError)?; } @@ -491,7 +485,7 @@ impl SyncNetworkContext { request_id: Id, block_or_blob: BlockOrBlob, ) -> Option> { - let Entry::Occupied(mut entry) = self.range_blocks_and_blobs_requests.entry(request_id) + let Entry::Occupied(mut entry) = self.range_block_components_requests.entry(request_id) else { metrics::inc_counter_vec(&metrics::SYNC_UNKNOWN_NETWORK_REQUESTS, &["range_blocks"]); return None; @@ -702,7 +696,7 @@ impl SyncNetworkContext { self.send_network_msg(NetworkMessage::SendRequest { peer_id, request: Request::DataColumnsByRoot(request.clone().into_request(&self.chain.spec)), - request_id: RequestId::Sync(SyncRequestId::DataColumnsByRoot(req_id, requester)), + request_id: AppRequestId::Sync(SyncRequestId::DataColumnsByRoot(req_id, requester)), })?; self.data_columns_by_root_requests @@ -1235,7 +1229,7 @@ impl SyncNetworkContext { metrics::set_gauge_vec( &metrics::SYNC_ACTIVE_NETWORK_REQUESTS, &["range_blocks"], - self.range_blocks_and_blobs_requests.len() as i64, + self.range_block_components_requests.len() as i64, ); } } @@ -1252,9 +1246,3 @@ fn to_fixed_blob_sidecar_list( } Ok(fixed_list) } - -impl std::fmt::Display for DataColumnsByRootRequestId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} diff --git a/beacon_node/network/src/sync/network_context/custody.rs b/beacon_node/network/src/sync/network_context/custody.rs index cc0fcec532a..5fcc59acf93 100644 --- a/beacon_node/network/src/sync/network_context/custody.rs +++ b/beacon_node/network/src/sync/network_context/custody.rs @@ -1,4 +1,3 @@ -use crate::sync::manager::{DataColumnsByRootRequester, SingleLookupReqId}; use crate::sync::network_context::{ DataColumnsByRootRequestId, DataColumnsByRootSingleBlockRequest, }; @@ -6,6 +5,7 @@ use crate::sync::network_context::{ use beacon_chain::data_column_verification::CustodyDataColumn; use beacon_chain::BeaconChainTypes; use fnv::FnvHashMap; +use lighthouse_network::service::api_types::{CustodyId, DataColumnsByRootRequester}; use lighthouse_network::PeerId; use lru_cache::LRUTimeCache; use rand::Rng; @@ -15,21 +15,10 @@ use std::{collections::HashMap, marker::PhantomData, sync::Arc}; use types::EthSpec; use types::{data_column_sidecar::ColumnIndex, DataColumnSidecar, Epoch, Hash256}; -use super::{LookupRequestResult, PeerGroup, ReqId, RpcResponseResult, SyncNetworkContext}; - -#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] -pub struct CustodyId { - pub requester: CustodyRequester, - pub req_id: ReqId, -} +use super::{LookupRequestResult, PeerGroup, RpcResponseResult, SyncNetworkContext}; const FAILED_PEERS_CACHE_EXPIRY_SECONDS: u64 = 5; -/// Downstream components that perform custody by root requests. -/// Currently, it's only single block lookups, so not using an enum -#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] -pub struct CustodyRequester(pub SingleLookupReqId); - type DataColumnSidecarVec = Vec>>; pub struct ActiveCustodyRequest { diff --git a/beacon_node/network/src/sync/range_sync/chain.rs b/beacon_node/network/src/sync/range_sync/chain.rs index 708fe64eead..57d269c104c 100644 --- a/beacon_node/network/src/sync/range_sync/chain.rs +++ b/beacon_node/network/src/sync/range_sync/chain.rs @@ -9,8 +9,10 @@ use beacon_chain::block_verification_types::RpcBlock; use beacon_chain::BeaconChainTypes; use fnv::FnvHashMap; use lighthouse_metrics::set_int_gauge; +use lighthouse_network::service::api_types::Id; use lighthouse_network::{PeerAction, PeerId, Subnet}; use rand::seq::SliceRandom; +use rand::Rng; use slog::{crit, debug, o, warn}; use std::collections::{btree_map::Entry, BTreeMap, HashSet}; use std::hash::{Hash, Hasher}; diff --git a/beacon_node/network/src/sync/sampling.rs b/beacon_node/network/src/sync/sampling.rs index 307ecc2fd93..32425ef8c8d 100644 --- a/beacon_node/network/src/sync/sampling.rs +++ b/beacon_node/network/src/sync/sampling.rs @@ -3,6 +3,7 @@ use super::network_context::{RpcResponseError, SyncNetworkContext}; use crate::metrics; use beacon_chain::BeaconChainTypes; use fnv::FnvHashMap; +use lighthouse_network::service::api_types::{SamplingId, SamplingRequester}; use lighthouse_network::{PeerAction, PeerId}; use rand::{seq::SliceRandom, thread_rng}; use slog::{debug, error, warn}; @@ -14,17 +15,6 @@ use types::{data_column_sidecar::ColumnIndex, ChainSpec, DataColumnSidecar, Hash pub type SamplingResult = Result<(), SamplingError>; -#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] -pub struct SamplingId { - pub id: SamplingRequester, - pub column_index: ColumnIndex, -} - -#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] -pub enum SamplingRequester { - ImportedBlock(Hash256), -} - type DataColumnSidecarVec = Vec>>; pub struct Sampling { @@ -420,12 +410,9 @@ impl ActiveSamplingRequest { mod request { use super::{SamplingError, SamplingId, SamplingRequester}; - use crate::sync::{ - manager::DataColumnsByRootRequester, - network_context::{DataColumnsByRootSingleBlockRequest, SyncNetworkContext}, - }; + use crate::sync::network_context::{DataColumnsByRootSingleBlockRequest, SyncNetworkContext}; use beacon_chain::BeaconChainTypes; - use lighthouse_network::PeerId; + use lighthouse_network::{service::api_types::DataColumnsByRootRequester, PeerId}; use rand::seq::SliceRandom; use rand::thread_rng; use std::collections::HashSet; diff --git a/beacon_node/store/src/metrics.rs b/beacon_node/store/src/metrics.rs index af7b5e93e89..902c440be86 100644 --- a/beacon_node/store/src/metrics.rs +++ b/beacon_node/store/src/metrics.rs @@ -151,6 +151,13 @@ pub static BEACON_BLOBS_CACHE_HIT_COUNT: LazyLock> = LazyLock "Number of hits to the store's blob cache", ) }); +pub static BEACON_DATA_COLUMNS_CACHE_HIT_COUNT: LazyLock> = + LazyLock::new(|| { + try_create_int_counter( + "store_beacon_data_columns_cache_hit_total", + "Number of hits to the store's data column cache", + ) + }); /// Updates the global metrics registry with store-related information. pub fn scrape_for_metrics(db_path: &Path, freezer_db_path: &Path) { diff --git a/crypto/kzg/src/lib.rs b/crypto/kzg/src/lib.rs index e40191fa2b4..b9f556d5a47 100644 --- a/crypto/kzg/src/lib.rs +++ b/crypto/kzg/src/lib.rs @@ -42,28 +42,6 @@ impl From for Error { } } -pub const CELLS_PER_EXT_BLOB: usize = 128; - -// TODO(das): use proper crypto once ckzg merges das branch -#[allow(dead_code)] -#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] -pub struct Cell { - bytes: [u8; 2048usize], -} - -impl Cell { - pub fn from_bytes(b: &[u8]) -> Result { - Ok(Self { - bytes: b - .try_into() - .map_err(|_| Error::Kzg(c_kzg::Error::MismatchLength("".to_owned())))?, - }) - } - pub fn into_inner(self) -> [u8; 2048usize] { - self.bytes - } -} - /// A wrapper over a kzg library that holds the trusted setup parameters. #[derive(Debug)] pub struct Kzg { diff --git a/testing/ef_tests/src/cases/kzg_blob_to_kzg_commitment.rs b/testing/ef_tests/src/cases/kzg_blob_to_kzg_commitment.rs index 78889cc0271..5194c3336c8 100644 --- a/testing/ef_tests/src/cases/kzg_blob_to_kzg_commitment.rs +++ b/testing/ef_tests/src/cases/kzg_blob_to_kzg_commitment.rs @@ -36,8 +36,9 @@ impl Case for KZGBlobToKZGCommitment { } fn result(&self, _case_index: usize, _fork_name: ForkName) -> Result<(), Error> { + let kzg = get_kzg()?; let commitment = parse_blob::(&self.input.blob).and_then(|blob| { - blob_to_kzg_commitment::(&KZG, &blob).map_err(|e| { + blob_to_kzg_commitment::(&kzg, &blob).map_err(|e| { Error::InternalError(format!("Failed to compute kzg commitment: {:?}", e)) }) }); diff --git a/testing/ef_tests/src/cases/kzg_compute_blob_kzg_proof.rs b/testing/ef_tests/src/cases/kzg_compute_blob_kzg_proof.rs index 15bcc6f7d0b..61e7248deeb 100644 --- a/testing/ef_tests/src/cases/kzg_compute_blob_kzg_proof.rs +++ b/testing/ef_tests/src/cases/kzg_compute_blob_kzg_proof.rs @@ -43,8 +43,9 @@ impl Case for KZGComputeBlobKZGProof { Ok((blob, commitment)) }; + let kzg = get_kzg()?; let proof = parse_input(&self.input).and_then(|(blob, commitment)| { - compute_blob_kzg_proof::(&KZG, &blob, commitment) + compute_blob_kzg_proof::(&kzg, &blob, commitment) .map_err(|e| Error::InternalError(format!("Failed to compute kzg proof: {:?}", e))) }); diff --git a/testing/ef_tests/src/cases/kzg_compute_cells_and_kzg_proofs.rs b/testing/ef_tests/src/cases/kzg_compute_cells_and_kzg_proofs.rs index 2effa68ff78..de3eca46cb6 100644 --- a/testing/ef_tests/src/cases/kzg_compute_cells_and_kzg_proofs.rs +++ b/testing/ef_tests/src/cases/kzg_compute_cells_and_kzg_proofs.rs @@ -35,7 +35,8 @@ impl Case for KZGComputeCellsAndKZGProofs { let blob = KzgBlob::from_bytes(&blob).map_err(|e| { Error::InternalError(format!("Failed to convert blob to kzg blob: {e:?}")) })?; - KZG.compute_cells_and_proofs(&blob).map_err(|e| { + let kzg = get_kzg()?; + kzg.compute_cells_and_proofs(&blob).map_err(|e| { Error::InternalError(format!("Failed to compute cells and kzg proofs: {e:?}")) }) }); diff --git a/testing/ef_tests/src/cases/kzg_compute_kzg_proof.rs b/testing/ef_tests/src/cases/kzg_compute_kzg_proof.rs index 88e90dbf1f2..ca19882d501 100644 --- a/testing/ef_tests/src/cases/kzg_compute_kzg_proof.rs +++ b/testing/ef_tests/src/cases/kzg_compute_kzg_proof.rs @@ -50,8 +50,9 @@ impl Case for KZGComputeKZGProof { Ok((blob, z)) }; + let kzg = get_kzg()?; let proof = parse_input(&self.input).and_then(|(blob, z)| { - compute_kzg_proof::(&KZG, &blob, z) + compute_kzg_proof::(&kzg, &blob, z) .map_err(|e| Error::InternalError(format!("Failed to compute kzg proof: {:?}", e))) }); diff --git a/testing/ef_tests/src/cases/kzg_recover_cells_and_kzg_proofs.rs b/testing/ef_tests/src/cases/kzg_recover_cells_and_kzg_proofs.rs index e759ccf4b00..fc41f1f2a62 100644 --- a/testing/ef_tests/src/cases/kzg_recover_cells_and_kzg_proofs.rs +++ b/testing/ef_tests/src/cases/kzg_recover_cells_and_kzg_proofs.rs @@ -59,7 +59,8 @@ impl Case for KZGRecoverCellsAndKZGProofs { let result = parse_input(&self.input).and_then(|(input_proofs, input_cells, cell_indices)| { let input_cells_ref: Vec<_> = input_cells.iter().map(|cell| &**cell).collect(); - let (cells, proofs) = KZG + let kzg = get_kzg()?; + let (cells, proofs) = kzg .recover_cells_and_compute_kzg_proofs( cell_indices.as_slice(), input_cells_ref.as_slice(), diff --git a/testing/ef_tests/src/cases/kzg_verify_blob_kzg_proof.rs b/testing/ef_tests/src/cases/kzg_verify_blob_kzg_proof.rs index d89d3a28790..e85950e8750 100644 --- a/testing/ef_tests/src/cases/kzg_verify_blob_kzg_proof.rs +++ b/testing/ef_tests/src/cases/kzg_verify_blob_kzg_proof.rs @@ -3,20 +3,15 @@ use crate::case_result::compare_result; use beacon_chain::kzg_utils::validate_blob; use eth2_network_config::TRUSTED_SETUP_BYTES; use kzg::{Cell, Error as KzgError, Kzg, KzgCommitment, KzgProof, TrustedSetup}; -use lazy_static::lazy_static; use serde::Deserialize; use std::marker::PhantomData; -use std::sync::Arc; use types::Blob; -lazy_static! { - pub static ref KZG: Arc = { - let trusted_setup: TrustedSetup = serde_json::from_reader(TRUSTED_SETUP_BYTES) - .map_err(|e| format!("Unable to read trusted setup file: {}", e)) - .expect("should have trusted setup"); - let kzg = Kzg::new_from_trusted_setup(trusted_setup).expect("should create kzg"); - Arc::new(kzg) - }; +pub fn get_kzg() -> Result { + let trusted_setup: TrustedSetup = serde_json::from_reader(TRUSTED_SETUP_BYTES) + .map_err(|e| Error::InternalError(format!("Failed to initialize kzg: {:?}", e)))?; + Kzg::new_from_trusted_setup(trusted_setup) + .map_err(|e| Error::InternalError(format!("Failed to initialize kzg: {:?}", e))) } pub fn parse_cells_and_proofs( @@ -124,8 +119,9 @@ impl Case for KZGVerifyBlobKZGProof { Ok((blob, commitment, proof)) }; + let kzg = get_kzg()?; let result = parse_input(&self.input).and_then(|(blob, commitment, proof)| { - match validate_blob::(&KZG, &blob, commitment, proof) { + match validate_blob::(&kzg, &blob, commitment, proof) { Ok(_) => Ok(true), Err(KzgError::KzgVerificationFailed) => Ok(false), Err(e) => Err(Error::InternalError(format!( diff --git a/testing/ef_tests/src/cases/kzg_verify_blob_kzg_proof_batch.rs b/testing/ef_tests/src/cases/kzg_verify_blob_kzg_proof_batch.rs index d4cb581c634..cfe15d5c05a 100644 --- a/testing/ef_tests/src/cases/kzg_verify_blob_kzg_proof_batch.rs +++ b/testing/ef_tests/src/cases/kzg_verify_blob_kzg_proof_batch.rs @@ -57,10 +57,11 @@ impl Case for KZGVerifyBlobKZGProofBatch { Ok((commitments, blobs, proofs)) }; + let kzg = get_kzg()?; let result = parse_input(&self.input).and_then( |(commitments, blobs, proofs)| match validate_blobs::( - &KZG, + &kzg, &commitments, blobs.iter().collect(), &proofs, diff --git a/testing/ef_tests/src/cases/kzg_verify_cell_kzg_proof_batch.rs b/testing/ef_tests/src/cases/kzg_verify_cell_kzg_proof_batch.rs index f2f9a33067d..2eb6d295a9f 100644 --- a/testing/ef_tests/src/cases/kzg_verify_cell_kzg_proof_batch.rs +++ b/testing/ef_tests/src/cases/kzg_verify_cell_kzg_proof_batch.rs @@ -57,7 +57,8 @@ impl Case for KZGVerifyCellKZGProofBatch { let proofs: Vec = proofs.iter().map(|&proof| proof.into()).collect(); let commitments: Vec = commitments.iter().map(|&c| c.into()).collect(); let cells = cells.iter().map(|c| c.as_ref()).collect::>(); - match KZG.verify_cell_proof_batch(&cells, &proofs, &coordinates, &commitments) { + let kzg = get_kzg()?; + match kzg.verify_cell_proof_batch(&cells, &proofs, &coordinates, &commitments) { Ok(_) => Ok(true), Err(KzgError::KzgVerificationFailed) => Ok(false), Err(e) => Err(Error::InternalError(format!( diff --git a/testing/ef_tests/src/cases/kzg_verify_kzg_proof.rs b/testing/ef_tests/src/cases/kzg_verify_kzg_proof.rs index 1839333fa53..4468176c277 100644 --- a/testing/ef_tests/src/cases/kzg_verify_kzg_proof.rs +++ b/testing/ef_tests/src/cases/kzg_verify_kzg_proof.rs @@ -46,8 +46,9 @@ impl Case for KZGVerifyKZGProof { Ok((commitment, z, y, proof)) }; + let kzg = get_kzg()?; let result = parse_input(&self.input).and_then(|(commitment, z, y, proof)| { - verify_kzg_proof::(&KZG, commitment, proof, z, y) + verify_kzg_proof::(&kzg, commitment, proof, z, y) .map_err(|e| Error::InternalError(format!("Failed to validate proof: {:?}", e))) }); From 37dd0eaba759cf0e5755edb440b441e9b7dd596d Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Wed, 31 Jul 2024 21:57:34 +1000 Subject: [PATCH 58/76] Add cli flag to enable sampling and disable by default. (#6209) --- beacon_node/beacon_chain/src/beacon_chain.rs | 6 ++++-- beacon_node/beacon_chain/src/chain_config.rs | 3 +++ beacon_node/src/cli.rs | 9 +++++++++ beacon_node/src/config.rs | 4 ++++ lighthouse/tests/beacon_node.rs | 13 +++++++++++++ 5 files changed, 33 insertions(+), 2 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index ac91cf18ba6..910f7f52270 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -6960,8 +6960,10 @@ impl BeaconChain { /// Returns true if we should issue a sampling request for this block /// TODO(das): check if the block is still within the da_window pub fn should_sample_slot(&self, slot: Slot) -> bool { - self.spec - .is_peer_das_enabled_for_epoch(slot.epoch(T::EthSpec::slots_per_epoch())) + self.config.enable_sampling + && self + .spec + .is_peer_das_enabled_for_epoch(slot.epoch(T::EthSpec::slots_per_epoch())) } pub fn logger(&self) -> &Logger { diff --git a/beacon_node/beacon_chain/src/chain_config.rs b/beacon_node/beacon_chain/src/chain_config.rs index e533924db7d..66749bbfa15 100644 --- a/beacon_node/beacon_chain/src/chain_config.rs +++ b/beacon_node/beacon_chain/src/chain_config.rs @@ -86,6 +86,8 @@ pub struct ChainConfig { pub enable_light_client_server: bool, /// Enable malicious PeerDAS mode where node withholds data columns when publishing a block pub malicious_withhold_count: usize, + /// Enable peer sampling on blocks. + pub enable_sampling: bool, } impl Default for ChainConfig { @@ -118,6 +120,7 @@ impl Default for ChainConfig { epochs_per_migration: crate::migrate::DEFAULT_EPOCHS_PER_MIGRATION, enable_light_client_server: false, malicious_withhold_count: 0, + enable_sampling: false, } } } diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index 57dfbe31b8b..199488f978b 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -74,6 +74,15 @@ pub fn cli_app() -> Command { .hide(true) .display_order(0) ) + .arg( + Arg::new("enable-sampling") + .long("enable-sampling") + .action(ArgAction::SetTrue) + .help_heading(FLAG_HEADER) + .help("Enable peer sampling on data columns. Disabled by default.") + .hide(true) + .display_order(0) + ) .arg( Arg::new("subscribe-all-subnets") .long("subscribe-all-subnets") diff --git a/beacon_node/src/config.rs b/beacon_node/src/config.rs index 02957ef54ea..10e21a7f8b4 100644 --- a/beacon_node/src/config.rs +++ b/beacon_node/src/config.rs @@ -181,6 +181,10 @@ pub fn get_config( client_config.chain.shuffling_cache_size = cache_size; } + if cli_args.get_flag("enable-sampling") { + client_config.chain.enable_sampling = true; + } + /* * Prometheus metrics HTTP server */ diff --git a/lighthouse/tests/beacon_node.rs b/lighthouse/tests/beacon_node.rs index 59ee35a16b5..f3832a1a1e5 100644 --- a/lighthouse/tests/beacon_node.rs +++ b/lighthouse/tests/beacon_node.rs @@ -832,6 +832,19 @@ fn network_subscribe_all_data_column_subnets_flag() { .with_config(|config| assert!(config.network.subscribe_all_data_column_subnets)); } #[test] +fn network_enable_sampling_flag() { + CommandLineTest::new() + .flag("enable-sampling", None) + .run_with_zero_port() + .with_config(|config| assert!(config.chain.enable_sampling)); +} +#[test] +fn network_enable_sampling_flag_default() { + CommandLineTest::new() + .run_with_zero_port() + .with_config(|config| assert!(!config.chain.enable_sampling)); +} +#[test] fn network_subscribe_all_subnets_flag() { CommandLineTest::new() .flag("subscribe-all-subnets", None) From 8c780109966bd9364286c01e3785e485d70ea0c5 Mon Sep 17 00:00:00 2001 From: kevaundray Date: Thu, 1 Aug 2024 17:33:39 +1000 Subject: [PATCH 59/76] chore: Use reference to an array representing a blob instead of an owned KzgBlob (#6179) * add KzgBlobRef type * modify code to use KzgBlobRef * clippy * Remove Deneb blob related changes to maintain compatibility with `c-kzg-4844`. --------- Co-authored-by: Jimmy Chen --- consensus/types/src/data_column_sidecar.rs | 9 ++++++--- crypto/kzg/src/lib.rs | 20 ++++++++++--------- .../cases/kzg_compute_cells_and_kzg_proofs.rs | 6 +++--- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/consensus/types/src/data_column_sidecar.rs b/consensus/types/src/data_column_sidecar.rs index 9b140ca0652..aa00e7d0213 100644 --- a/consensus/types/src/data_column_sidecar.rs +++ b/consensus/types/src/data_column_sidecar.rs @@ -9,7 +9,7 @@ use bls::Signature; use derivative::Derivative; #[cfg_attr(test, double)] use kzg::Kzg; -use kzg::{Blob as KzgBlob, CellRef as KzgCellRef, Error as KzgError}; +use kzg::{CellRef as KzgCellRef, Error as KzgError}; use kzg::{KzgCommitment, KzgProof}; use merkle_proof::verify_merkle_proof; #[cfg(test)] @@ -128,8 +128,11 @@ impl DataColumnSidecar { let blob_cells_and_proofs_vec = blobs .into_par_iter() .map(|blob| { - let blob = KzgBlob::from_bytes(blob).map_err(KzgError::from)?; - kzg.compute_cells_and_proofs(&blob) + let blob = blob + .as_ref() + .try_into() + .expect("blob should have a guaranteed size due to FixedVector"); + kzg.compute_cells_and_proofs(blob) }) .collect::, KzgError>>()?; diff --git a/crypto/kzg/src/lib.rs b/crypto/kzg/src/lib.rs index b9f556d5a47..c45fb8dfdd0 100644 --- a/crypto/kzg/src/lib.rs +++ b/crypto/kzg/src/lib.rs @@ -20,8 +20,11 @@ pub use peerdas_kzg::{ Cell, CellIndex as CellID, CellRef, TrustedSetup as PeerDASTrustedSetup, }; use peerdas_kzg::{prover::ProverError, verifier::VerifierError, PeerDASContext}; + pub type CellsAndKzgProofs = ([Cell; CELLS_PER_EXT_BLOB], [KzgProof; CELLS_PER_EXT_BLOB]); +pub type KzgBlobRef<'a> = &'a [u8; BYTES_PER_BLOB]; + #[derive(Debug)] pub enum Error { /// An error from the underlying kzg library. @@ -173,15 +176,14 @@ impl Kzg { } /// Computes the cells and associated proofs for a given `blob` at index `index`. - pub fn compute_cells_and_proofs(&self, blob: &Blob) -> Result { - let blob_bytes: &[u8; BYTES_PER_BLOB] = blob - .as_ref() - .try_into() - .expect("Expected blob to have size {BYTES_PER_BLOB}"); - + #[allow(clippy::needless_lifetimes)] + pub fn compute_cells_and_proofs<'a>( + &self, + blob: KzgBlobRef<'a>, + ) -> Result { let (cells, proofs) = self .context - .compute_cells_and_kzg_proofs(blob_bytes) + .compute_cells_and_kzg_proofs(blob) .map_err(Error::ProverKZG)?; // Convert the proof type to a c-kzg proof type @@ -245,11 +247,11 @@ impl Kzg { } pub mod mock { - use crate::{Blob, Cell, CellsAndKzgProofs, BYTES_PER_CELL, CELLS_PER_EXT_BLOB}; + use crate::{Cell, CellsAndKzgProofs, KzgBlobRef, BYTES_PER_CELL, CELLS_PER_EXT_BLOB}; use crate::{Error, KzgProof}; #[allow(clippy::type_complexity)] - pub fn compute_cells_and_proofs(_blob: &Blob) -> Result { + pub fn compute_cells_and_proofs(_blob: KzgBlobRef) -> Result { let empty_cells = vec![Cell::new([0; BYTES_PER_CELL]); CELLS_PER_EXT_BLOB] .try_into() .expect("expected {CELLS_PER_EXT_BLOB} number of items"); diff --git a/testing/ef_tests/src/cases/kzg_compute_cells_and_kzg_proofs.rs b/testing/ef_tests/src/cases/kzg_compute_cells_and_kzg_proofs.rs index de3eca46cb6..74a44fdddfc 100644 --- a/testing/ef_tests/src/cases/kzg_compute_cells_and_kzg_proofs.rs +++ b/testing/ef_tests/src/cases/kzg_compute_cells_and_kzg_proofs.rs @@ -1,6 +1,6 @@ use super::*; use crate::case_result::compare_result; -use kzg::{Blob as KzgBlob, CellsAndKzgProofs}; +use kzg::CellsAndKzgProofs; use serde::Deserialize; use std::marker::PhantomData; @@ -32,11 +32,11 @@ impl Case for KZGComputeCellsAndKZGProofs { fn result(&self, _case_index: usize, _fork_name: ForkName) -> Result<(), Error> { let cells_and_proofs = parse_blob::(&self.input.blob).and_then(|blob| { - let blob = KzgBlob::from_bytes(&blob).map_err(|e| { + let blob = blob.as_ref().try_into().map_err(|e| { Error::InternalError(format!("Failed to convert blob to kzg blob: {e:?}")) })?; let kzg = get_kzg()?; - kzg.compute_cells_and_proofs(&blob).map_err(|e| { + kzg.compute_cells_and_proofs(blob).map_err(|e| { Error::InternalError(format!("Failed to compute cells and kzg proofs: {e:?}")) }) }); From 90700fe12a28d2795d81a9cbb23cd2b19308d96a Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Thu, 8 Aug 2024 17:15:05 +1000 Subject: [PATCH 60/76] Store computed custody subnets in PeerDB and fix custody lookup test (#6218) * Fix failing custody lookup tests. * Store custody subnets in PeerDB, fix custody lookup test and refactor some methods. --- beacon_node/http_api/src/publish_blocks.rs | 2 +- beacon_node/http_api/src/test_utils.rs | 1 + .../lighthouse_network/src/discovery/mod.rs | 1 + .../src/peer_manager/mod.rs | 6 +- .../src/peer_manager/peerdb.rs | 66 +++++++++++++++---- .../src/peer_manager/peerdb/peer_info.rs | 17 ++++- .../lighthouse_network/src/service/mod.rs | 1 + .../lighthouse_network/src/types/globals.rs | 48 ++++++++------ .../src/network_beacon_processor/tests.rs | 11 +++- .../network/src/sync/block_lookups/tests.rs | 18 ++--- .../network/src/sync/network_context.rs | 10 +-- .../network/src/sync/range_sync/chain.rs | 39 +++++------ .../network/src/sync/range_sync/range.rs | 6 +- 13 files changed, 154 insertions(+), 72 deletions(-) diff --git a/beacon_node/http_api/src/publish_blocks.rs b/beacon_node/http_api/src/publish_blocks.rs index 3822b36b3d9..9f76390cefb 100644 --- a/beacon_node/http_api/src/publish_blocks.rs +++ b/beacon_node/http_api/src/publish_blocks.rs @@ -273,7 +273,7 @@ pub async fn publish_block( vec![], false, &log, + chain.spec.clone(), )); // Only a peer manager can add peers, so we create a dummy manager. diff --git a/beacon_node/lighthouse_network/src/discovery/mod.rs b/beacon_node/lighthouse_network/src/discovery/mod.rs index 300c190cdaf..7b297d243bd 100644 --- a/beacon_node/lighthouse_network/src/discovery/mod.rs +++ b/beacon_node/lighthouse_network/src/discovery/mod.rs @@ -1232,6 +1232,7 @@ mod tests { vec![], false, &log, + spec.clone(), ); let keypair = keypair.into(); Discovery::new(keypair, &config, Arc::new(globals), &log, &spec) diff --git a/beacon_node/lighthouse_network/src/peer_manager/mod.rs b/beacon_node/lighthouse_network/src/peer_manager/mod.rs index 3e0ead01ce5..8d6ec78e437 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/mod.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/mod.rs @@ -1383,7 +1383,8 @@ mod tests { ..Default::default() }; let log = build_log(slog::Level::Debug, false); - let globals = NetworkGlobals::new_test_globals(vec![], &log); + let spec = E::default_spec(); + let globals = NetworkGlobals::new_test_globals(vec![], &log, spec); PeerManager::new(config, Arc::new(globals), &log).unwrap() } @@ -1397,7 +1398,8 @@ mod tests { ..Default::default() }; let log = build_log(slog::Level::Debug, false); - let globals = NetworkGlobals::new_test_globals(trusted_peers, &log); + let spec = E::default_spec(); + let globals = NetworkGlobals::new_test_globals(trusted_peers, &log, spec); PeerManager::new(config, Arc::new(globals), &log).unwrap() } diff --git a/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs b/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs index d0c6710e8a7..fdde57b4a57 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs @@ -1,6 +1,8 @@ use crate::discovery::enr::PEERDAS_CUSTODY_SUBNET_COUNT_ENR_KEY; use crate::discovery::CombinedKey; -use crate::{metrics, multiaddr::Multiaddr, types::Subnet, Enr, Gossipsub, PeerId}; +use crate::{ + metrics, multiaddr::Multiaddr, types::Subnet, Enr, EnrExt, Eth2Enr, Gossipsub, PeerId, +}; use peer_info::{ConnectionDirection, PeerConnectionStatus, PeerInfo}; use rand::seq::SliceRandom; use score::{PeerAction, ReportSource, Score, ScoreState}; @@ -13,7 +15,7 @@ use std::{ fmt::Formatter, }; use sync_status::SyncStatus; -use types::{ChainSpec, EthSpec}; +use types::{ChainSpec, DataColumnSubnetId, EthSpec}; pub mod client; pub mod peer_info; @@ -45,10 +47,16 @@ pub struct PeerDB { disable_peer_scoring: bool, /// PeerDB's logger log: slog::Logger, + spec: ChainSpec, } impl PeerDB { - pub fn new(trusted_peers: Vec, disable_peer_scoring: bool, log: &slog::Logger) -> Self { + pub fn new( + trusted_peers: Vec, + disable_peer_scoring: bool, + log: &slog::Logger, + spec: ChainSpec, + ) -> Self { // Initialize the peers hashmap with trusted peers let peers = trusted_peers .into_iter() @@ -60,6 +68,7 @@ impl PeerDB { banned_peers_count: BannedPeersCount::default(), disable_peer_scoring, peers, + spec, } } @@ -247,6 +256,27 @@ impl PeerDB { .map(|(peer_id, _)| peer_id) } + pub fn good_custody_subnet_peer( + &self, + subnet: DataColumnSubnetId, + ) -> impl Iterator { + self.peers + .iter() + .filter(move |(_, info)| { + // TODO(das): we currently consider peer to be a subnet peer if the peer is *either* + // subscribed to the subnet or assigned to the subnet. + // The first condition is currently required as we don't have custody count in + // metadata implemented yet, and therefore unable to reliably determine custody + // subnet count (ENR is not always available). + // This condition can be removed later so that we can identify peers that are not + // serving custody columns and penalise accordingly. + let is_custody_subnet_peer = info.on_subnet_gossipsub(&Subnet::DataColumn(subnet)) + || info.is_assigned_to_custody_subnet(&subnet); + info.is_connected() && info.is_good_gossipsub_peer() && is_custody_subnet_peer + }) + .map(|(peer_id, _)| peer_id) + } + /// Gives the ids of all known disconnected peers. pub fn disconnected_peers(&self) -> impl Iterator { self.peers @@ -676,12 +706,12 @@ impl PeerDB { /// Updates the connection state. MUST ONLY BE USED IN TESTS. pub fn __add_connected_peer_testing_only( &mut self, - peer_id: &PeerId, supernode: bool, spec: &ChainSpec, - ) -> Option { + ) -> PeerId { let enr_key = CombinedKey::generate_secp256k1(); let mut enr = Enr::builder().build(&enr_key).unwrap(); + let peer_id = enr.peer_id(); if supernode { enr.insert( @@ -693,13 +723,15 @@ impl PeerDB { } self.update_connection_state( - peer_id, + &peer_id, NewConnectionState::Connected { enr: Some(enr), seen_address: Multiaddr::empty(), direction: ConnectionDirection::Outgoing, }, - ) + ); + + peer_id } /// The connection state of the peer has been changed. Modify the peer in the db to ensure all @@ -762,8 +794,17 @@ impl PeerDB { seen_address, }, ) => { - // Update the ENR if one exists + // Update the ENR if one exists, and compute the custody subnets if let Some(enr) = enr { + let node_id = enr.node_id().raw().into(); + let custody_subnet_count = enr.custody_subnet_count::(&self.spec); + let custody_subnets = DataColumnSubnetId::compute_custody_subnets::( + node_id, + custody_subnet_count, + &self.spec, + ) + .collect::>(); + info.set_custody_subnets(custody_subnets); info.set_enr(enr); } @@ -1314,7 +1355,8 @@ mod tests { fn get_db() -> PeerDB { let log = build_log(slog::Level::Debug, false); - PeerDB::new(vec![], false, &log) + let spec = M::default_spec(); + PeerDB::new(vec![], false, &log, spec) } #[test] @@ -2013,7 +2055,8 @@ mod tests { fn test_trusted_peers_score() { let trusted_peer = PeerId::random(); let log = build_log(slog::Level::Debug, false); - let mut pdb: PeerDB = PeerDB::new(vec![trusted_peer], false, &log); + let spec = M::default_spec(); + let mut pdb: PeerDB = PeerDB::new(vec![trusted_peer], false, &log, spec); pdb.connect_ingoing(&trusted_peer, "/ip4/0.0.0.0".parse().unwrap(), None); @@ -2037,7 +2080,8 @@ mod tests { fn test_disable_peer_scoring() { let peer = PeerId::random(); let log = build_log(slog::Level::Debug, false); - let mut pdb: PeerDB = PeerDB::new(vec![], true, &log); + let spec = M::default_spec(); + let mut pdb: PeerDB = PeerDB::new(vec![], true, &log, spec); pdb.connect_ingoing(&peer, "/ip4/0.0.0.0".parse().unwrap(), None); diff --git a/beacon_node/lighthouse_network/src/peer_manager/peerdb/peer_info.rs b/beacon_node/lighthouse_network/src/peer_manager/peerdb/peer_info.rs index 0745cc26008..8a04d450ba4 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/peerdb/peer_info.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/peerdb/peer_info.rs @@ -13,7 +13,7 @@ use std::collections::HashSet; use std::net::IpAddr; use std::time::Instant; use strum::AsRefStr; -use types::EthSpec; +use types::{DataColumnSubnetId, EthSpec}; use PeerConnectionStatus::*; /// Information about a given connected peer. @@ -40,6 +40,11 @@ pub struct PeerInfo { meta_data: Option>, /// Subnets the peer is connected to. subnets: HashSet, + /// This is computed from either metadata or the ENR, and contains the subnets that the peer + /// is *assigned* to custody, rather than *connected* to (different to `self.subnets`). + /// Note: Another reason to keep this separate to `self.subnets` is an upcoming change to + /// decouple custody requirements from the actual subnets, i.e. changing this to `custody_groups`. + custody_subnets: HashSet, /// The time we would like to retain this peer. After this time, the peer is no longer /// necessary. #[serde(skip)] @@ -62,6 +67,7 @@ impl Default for PeerInfo { listening_addresses: Vec::new(), seen_multiaddrs: HashSet::new(), subnets: HashSet::new(), + custody_subnets: HashSet::new(), sync_status: SyncStatus::Unknown, meta_data: None, min_ttl: None, @@ -210,6 +216,11 @@ impl PeerInfo { self.subnets.contains(subnet) } + /// Returns if the peer is assigned to a given `DataColumnSubnetId`. + pub fn is_assigned_to_custody_subnet(&self, subnet: &DataColumnSubnetId) -> bool { + self.custody_subnets.contains(subnet) + } + /// Returns true if the peer is connected to a long-lived subnet. pub fn has_long_lived_subnet(&self) -> bool { // Check the meta_data @@ -362,6 +373,10 @@ impl PeerInfo { self.connection_status = connection_status } + pub(super) fn set_custody_subnets(&mut self, custody_subnets: HashSet) { + self.custody_subnets = custody_subnets + } + /// Sets the ENR of the peer if one is known. pub(super) fn set_enr(&mut self, enr: Enr) { self.enr = Some(enr) diff --git a/beacon_node/lighthouse_network/src/service/mod.rs b/beacon_node/lighthouse_network/src/service/mod.rs index cec9f0841cc..d7af32420a7 100644 --- a/beacon_node/lighthouse_network/src/service/mod.rs +++ b/beacon_node/lighthouse_network/src/service/mod.rs @@ -172,6 +172,7 @@ impl Network { trusted_peers, config.disable_peer_scoring, &log, + ctx.chain_spec.clone(), ); Arc::new(globals) }; diff --git a/beacon_node/lighthouse_network/src/types/globals.rs b/beacon_node/lighthouse_network/src/types/globals.rs index 0f99bfc5dae..12c114e8d58 100644 --- a/beacon_node/lighthouse_network/src/types/globals.rs +++ b/beacon_node/lighthouse_network/src/types/globals.rs @@ -2,9 +2,9 @@ use crate::peer_manager::peerdb::PeerDB; use crate::rpc::{MetaData, MetaDataV2}; use crate::types::{BackFillState, SyncState}; +use crate::EnrExt; use crate::{Client, Eth2Enr}; use crate::{Enr, GossipTopic, Multiaddr, PeerId}; -use crate::{EnrExt, Subnet}; use parking_lot::RwLock; use std::collections::HashSet; use types::data_column_sidecar::ColumnIndex; @@ -27,6 +27,7 @@ pub struct NetworkGlobals { pub sync_state: RwLock, /// The current state of the backfill sync. pub backfill_state: RwLock, + spec: ChainSpec, } impl NetworkGlobals { @@ -36,16 +37,23 @@ impl NetworkGlobals { trusted_peers: Vec, disable_peer_scoring: bool, log: &slog::Logger, + spec: ChainSpec, ) -> Self { NetworkGlobals { local_enr: RwLock::new(enr.clone()), peer_id: RwLock::new(enr.peer_id()), listen_multiaddrs: RwLock::new(Vec::new()), local_metadata: RwLock::new(local_metadata), - peers: RwLock::new(PeerDB::new(trusted_peers, disable_peer_scoring, log)), + peers: RwLock::new(PeerDB::new( + trusted_peers, + disable_peer_scoring, + log, + spec.clone(), + )), gossipsub_subscriptions: RwLock::new(HashSet::new()), sync_state: RwLock::new(SyncState::Stalled), backfill_state: RwLock::new(BackFillState::NotRequired), + spec, } } @@ -112,44 +120,45 @@ impl NetworkGlobals { } /// Compute custody data columns the node is assigned to custody. - pub fn custody_columns(&self, _epoch: Epoch, spec: &ChainSpec) -> Vec { + pub fn custody_columns(&self, _epoch: Epoch) -> Vec { let enr = self.local_enr(); let node_id = enr.node_id().raw().into(); - let custody_subnet_count = enr.custody_subnet_count::(spec); - DataColumnSubnetId::compute_custody_columns::(node_id, custody_subnet_count, spec) + let custody_subnet_count = enr.custody_subnet_count::(&self.spec); + DataColumnSubnetId::compute_custody_columns::(node_id, custody_subnet_count, &self.spec) .collect() } /// Compute custody data column subnets the node is assigned to custody. - pub fn custody_subnets(&self, spec: &ChainSpec) -> impl Iterator { + pub fn custody_subnets(&self) -> impl Iterator { let enr = self.local_enr(); let node_id = enr.node_id().raw().into(); - let custody_subnet_count = enr.custody_subnet_count::(spec); - DataColumnSubnetId::compute_custody_subnets::(node_id, custody_subnet_count, spec) + let custody_subnet_count = enr.custody_subnet_count::(&self.spec); + DataColumnSubnetId::compute_custody_subnets::(node_id, custody_subnet_count, &self.spec) } /// Returns a connected peer that: /// 1. is connected - /// 2. assigned to custody the column based on it's `custody_subnet_count` from metadata (WIP) + /// 2. assigned to custody the column based on it's `custody_subnet_count` from ENR or metadata (WIP) /// 3. has a good score /// 4. subscribed to the specified column - this condition can be removed later, so we can /// identify and penalise peers that are supposed to custody the column. - pub fn custody_peers_for_column( - &self, - column_index: ColumnIndex, - spec: &ChainSpec, - ) -> Vec { + pub fn custody_peers_for_column(&self, column_index: ColumnIndex) -> Vec { self.peers .read() - .good_peers_on_subnet(Subnet::DataColumn( - DataColumnSubnetId::from_column_index::(column_index as usize, spec), + .good_custody_subnet_peer(DataColumnSubnetId::from_column_index::( + column_index as usize, + &self.spec, )) .cloned() .collect::>() } /// TESTING ONLY. Build a dummy NetworkGlobals instance. - pub fn new_test_globals(trusted_peers: Vec, log: &slog::Logger) -> NetworkGlobals { + pub fn new_test_globals( + trusted_peers: Vec, + log: &slog::Logger, + spec: ChainSpec, + ) -> NetworkGlobals { use crate::CombinedKeyExt; let keypair = libp2p::identity::secp256k1::Keypair::generate(); let enr_key: discv5::enr::CombinedKey = discv5::enr::CombinedKey::from_secp256k1(&keypair); @@ -164,6 +173,7 @@ impl NetworkGlobals { trusted_peers, false, log, + spec, ) } } @@ -180,9 +190,9 @@ mod test { let default_custody_requirement_column_count = spec.number_of_columns as u64 / spec.data_column_sidecar_subnet_count * spec.custody_requirement; - let globals = NetworkGlobals::::new_test_globals(vec![], &log); + let globals = NetworkGlobals::::new_test_globals(vec![], &log, spec.clone()); let any_epoch = Epoch::new(0); - let columns = globals.custody_columns(any_epoch, &spec); + let columns = globals.custody_columns(any_epoch); assert_eq!( columns.len(), default_custody_requirement_column_count as usize diff --git a/beacon_node/network/src/network_beacon_processor/tests.rs b/beacon_node/network/src/network_beacon_processor/tests.rs index a9b9f64a79d..40c69a0baa5 100644 --- a/beacon_node/network/src/network_beacon_processor/tests.rs +++ b/beacon_node/network/src/network_beacon_processor/tests.rs @@ -93,7 +93,7 @@ impl TestRig { spec.shard_committee_period = 2; let harness = BeaconChainHarness::builder(MainnetEthSpec) - .spec(spec) + .spec(spec.clone()) .deterministic_keypairs(VALIDATOR_COUNT) .fresh_ephemeral_store() .mock_execution_layer() @@ -204,7 +204,14 @@ impl TestRig { }); let enr_key = CombinedKey::generate_secp256k1(); let enr = enr::Enr::builder().build(&enr_key).unwrap(); - let network_globals = Arc::new(NetworkGlobals::new(enr, meta_data, vec![], false, &log)); + let network_globals = Arc::new(NetworkGlobals::new( + enr, + meta_data, + vec![], + false, + &log, + spec, + )); let executor = harness.runtime.task_executor.clone(); diff --git a/beacon_node/network/src/sync/block_lookups/tests.rs b/beacon_node/network/src/sync/block_lookups/tests.rs index 6f4eb454d16..cd363cfaee3 100644 --- a/beacon_node/network/src/sync/block_lookups/tests.rs +++ b/beacon_node/network/src/sync/block_lookups/tests.rs @@ -132,7 +132,11 @@ impl TestRig { let (network_tx, network_rx) = mpsc::unbounded_channel(); // TODO(das): make the generation of the ENR use the deterministic rng to have consistent // column assignments - let globals = Arc::new(NetworkGlobals::new_test_globals(Vec::new(), &log)); + let globals = Arc::new(NetworkGlobals::new_test_globals( + Vec::new(), + &log, + chain.spec.clone(), + )); let (beacon_processor, beacon_processor_rx) = NetworkBeaconProcessor::null_for_testing( globals, chain.clone(), @@ -385,21 +389,17 @@ impl TestRig { } fn new_connected_peer(&mut self) -> PeerId { - let peer_id = PeerId::random(); self.network_globals .peers .write() - .__add_connected_peer_testing_only(&peer_id, false, &self.harness.spec); - peer_id + .__add_connected_peer_testing_only(false, &self.harness.spec) } fn new_connected_supernode_peer(&mut self) -> PeerId { - let peer_id = PeerId::random(); self.network_globals .peers .write() - .__add_connected_peer_testing_only(&peer_id, true, &self.harness.spec); - peer_id + .__add_connected_peer_testing_only(true, &self.harness.spec) } fn new_connected_peers_for_peerdas(&mut self) { @@ -972,8 +972,8 @@ impl TestRig { .unwrap_or_else(|e| panic!("Expected blob parent request for {for_block:?}: {e}")) } - /// Retrieves an unknown number of requests for data columns of `block_root`. Because peer enrs - /// are random, and peer selection is random the total number of batches requests in unknown. + /// Retrieves an unknown number of requests for data columns of `block_root`. Because peer ENRs + /// are random, and peer selection is random, the total number of batched requests is unknown. fn expect_data_columns_by_root_requests( &mut self, block_root: Hash256, diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index 5963401faa8..f35ee145b19 100644 --- a/beacon_node/network/src/sync/network_context.rs +++ b/beacon_node/network/src/sync/network_context.rs @@ -276,7 +276,7 @@ impl SyncNetworkContext { // TODO(das): epoch argument left here in case custody rotation is implemented pub fn get_custodial_peers(&self, _epoch: Epoch, column_index: ColumnIndex) -> Vec { self.network_globals() - .custody_peers_for_column(column_index, &self.chain.spec) + .custody_peers_for_column(column_index) } pub fn get_random_custodial_peer( @@ -382,9 +382,7 @@ impl SyncNetworkContext { let expects_custody_columns = if matches!(batch_type, ByRangeRequestType::BlocksAndColumns) { - let custody_indexes = self - .network_globals() - .custody_columns(epoch, &self.chain.spec); + let custody_indexes = self.network_globals().custody_columns(epoch); for (peer_id, columns_by_range_request) in self.make_columns_by_range_requests(epoch, request, &custody_indexes)? @@ -755,9 +753,7 @@ impl SyncNetworkContext { // TODO(das): figure out how to pass block.slot if we end up doing rotation let block_epoch = Epoch::new(0); - let custody_indexes_duty = self - .network_globals() - .custody_columns(block_epoch, &self.chain.spec); + let custody_indexes_duty = self.network_globals().custody_columns(block_epoch); // Include only the blob indexes not yet imported (received through gossip) let custody_indexes_to_fetch = custody_indexes_duty diff --git a/beacon_node/network/src/sync/range_sync/chain.rs b/beacon_node/network/src/sync/range_sync/chain.rs index 57d269c104c..aece5737cc9 100644 --- a/beacon_node/network/src/sync/range_sync/chain.rs +++ b/beacon_node/network/src/sync/range_sync/chain.rs @@ -10,7 +10,7 @@ use beacon_chain::BeaconChainTypes; use fnv::FnvHashMap; use lighthouse_metrics::set_int_gauge; use lighthouse_network::service::api_types::Id; -use lighthouse_network::{PeerAction, PeerId, Subnet}; +use lighthouse_network::{PeerAction, PeerId}; use rand::seq::SliceRandom; use rand::Rng; use slog::{crit, debug, o, warn}; @@ -1115,24 +1115,25 @@ impl SyncingChain { fn good_peers_on_custody_subnets(&self, epoch: Epoch, network: &SyncNetworkContext) -> bool { if network.chain.spec.is_peer_das_enabled_for_epoch(epoch) { // Require peers on all custody column subnets before sending batches - let peers_on_all_custody_subnets = network - .network_globals() - .custody_subnets(&network.chain.spec) - .all(|subnet_id| { - let peer_count = network - .network_globals() - .peers - .read() - .good_peers_on_subnet(Subnet::DataColumn(subnet_id)) - .count(); - - set_int_gauge( - &PEERS_PER_COLUMN_SUBNET, - &[&subnet_id.to_string()], - peer_count as i64, - ); - peer_count > 0 - }); + let peers_on_all_custody_subnets = + network + .network_globals() + .custody_subnets() + .all(|subnet_id| { + let peer_count = network + .network_globals() + .peers + .read() + .good_custody_subnet_peer(subnet_id) + .count(); + + set_int_gauge( + &PEERS_PER_COLUMN_SUBNET, + &[&subnet_id.to_string()], + peer_count as i64, + ); + peer_count > 0 + }); peers_on_all_custody_subnets } else { true diff --git a/beacon_node/network/src/sync/range_sync/range.rs b/beacon_node/network/src/sync/range_sync/range.rs index 334c58090e2..c8bb9b3b09a 100644 --- a/beacon_node/network/src/sync/range_sync/range.rs +++ b/beacon_node/network/src/sync/range_sync/range.rs @@ -689,7 +689,11 @@ mod tests { log.new(o!("component" => "range")), ); let (network_tx, network_rx) = mpsc::unbounded_channel(); - let globals = Arc::new(NetworkGlobals::new_test_globals(Vec::new(), &log)); + let globals = Arc::new(NetworkGlobals::new_test_globals( + Vec::new(), + &log, + chain.spec.clone(), + )); let (network_beacon_processor, beacon_processor_rx) = NetworkBeaconProcessor::null_for_testing( globals.clone(), From b638019e322d724be963d4b695a3bbf2262bd955 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Tue, 13 Aug 2024 00:43:24 +1000 Subject: [PATCH 61/76] Fix CI failures after merge. --- beacon_node/beacon_chain/tests/store_tests.rs | 8 +++++++- book/src/help_bn.md | 4 ---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index 7049bf14fde..862078b947e 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -2796,7 +2796,13 @@ async fn weak_subjectivity_sync_test(slots: Vec, checkpoint_slot: Slot) { let (block_root, block, blobs, data_columns) = available_blocks[0].clone().deconstruct(); let mut corrupt_block = (*block).clone(); *corrupt_block.signature_mut() = Signature::empty(); - AvailableBlock::__new_for_testing(block_root, Arc::new(corrupt_block), blobs, data_columns) + AvailableBlock::__new_for_testing( + block_root, + Arc::new(corrupt_block), + blobs, + data_columns, + Arc::new(spec), + ) }; // Importing the invalid batch should error. diff --git a/book/src/help_bn.md b/book/src/help_bn.md index 9b5e405ba8f..f9180b65832 100644 --- a/book/src/help_bn.md +++ b/book/src/help_bn.md @@ -585,10 +585,6 @@ Flags: server on localhost:5052 and import deposit logs from the execution node. This is equivalent to `--http` on merge-ready networks, or `--http --eth1` pre-merge - --subscribe-all-data-column-subnets - Subscribe to all data column subnets and participate in data custody - for all columns. This will also advertise the beacon node as being - long-lived subscribed to all data column subnets. --subscribe-all-subnets Subscribe to all subnets regardless of validator count. This will also advertise the beacon node as being long-lived subscribed to all From 7ee37806821fd752a32d29928feaee89172a91d8 Mon Sep 17 00:00:00 2001 From: Akihito Nakano Date: Mon, 19 Aug 2024 15:49:21 +0900 Subject: [PATCH 62/76] Batch sampling requests by peer (#6256) * Batch sampling requests by peer * Fix clippy errors * Fix tests * Add column_index to error message for ease of tracing * Remove outdated comment --- .../src/service/api_types.rs | 8 +- .../network/src/sync/block_lookups/tests.rs | 10 +- beacon_node/network/src/sync/sampling.rs | 272 ++++++++++++------ 3 files changed, 201 insertions(+), 89 deletions(-) diff --git a/beacon_node/lighthouse_network/src/service/api_types.rs b/beacon_node/lighthouse_network/src/service/api_types.rs index 7263ea8a3ca..61688780907 100644 --- a/beacon_node/lighthouse_network/src/service/api_types.rs +++ b/beacon_node/lighthouse_network/src/service/api_types.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use libp2p::swarm::ConnectionId; use types::{ - BlobSidecar, ColumnIndex, DataColumnSidecar, EthSpec, Hash256, LightClientBootstrap, + BlobSidecar, DataColumnSidecar, EthSpec, Hash256, LightClientBootstrap, LightClientFinalityUpdate, LightClientOptimisticUpdate, SignedBeaconBlock, }; @@ -62,7 +62,7 @@ pub enum DataColumnsByRootRequester { #[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] pub struct SamplingId { pub id: SamplingRequester, - pub column_index: ColumnIndex, + pub sampling_request_id: SamplingRequestId, } #[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] @@ -70,6 +70,10 @@ pub enum SamplingRequester { ImportedBlock(Hash256), } +/// Identifier of sampling requests. +#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] +pub struct SamplingRequestId(pub usize); + #[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] pub struct CustodyId { pub requester: CustodyRequester, diff --git a/beacon_node/network/src/sync/block_lookups/tests.rs b/beacon_node/network/src/sync/block_lookups/tests.rs index cd363cfaee3..9572bf7f444 100644 --- a/beacon_node/network/src/sync/block_lookups/tests.rs +++ b/beacon_node/network/src/sync/block_lookups/tests.rs @@ -716,7 +716,13 @@ impl TestRig { ) { let first_dc = data_columns.first().unwrap(); let block_root = first_dc.block_root(); - let column_index = first_dc.index; + let sampling_request_id = match id.0 { + SyncRequestId::DataColumnsByRoot( + _, + _requester @ DataColumnsByRootRequester::Sampling(sampling_id), + ) => sampling_id.sampling_request_id, + _ => unreachable!(), + }; self.complete_data_columns_by_root_request(id, data_columns); // Expect work event @@ -727,7 +733,7 @@ impl TestRig { self.send_sync_message(SyncMessage::SampleVerified { id: SamplingId { id: SamplingRequester::ImportedBlock(block_root), - column_index, + sampling_request_id, }, result: Ok(()), }) diff --git a/beacon_node/network/src/sync/sampling.rs b/beacon_node/network/src/sync/sampling.rs index 32425ef8c8d..bae8547b1a7 100644 --- a/beacon_node/network/src/sync/sampling.rs +++ b/beacon_node/network/src/sync/sampling.rs @@ -1,9 +1,13 @@ use self::request::ActiveColumnSampleRequest; -use super::network_context::{RpcResponseError, SyncNetworkContext}; +use super::network_context::{ + DataColumnsByRootSingleBlockRequest, RpcResponseError, SyncNetworkContext, +}; use crate::metrics; use beacon_chain::BeaconChainTypes; use fnv::FnvHashMap; -use lighthouse_network::service::api_types::{SamplingId, SamplingRequester}; +use lighthouse_network::service::api_types::{ + DataColumnsByRootRequester, SamplingId, SamplingRequestId, SamplingRequester, +}; use lighthouse_network::{PeerAction, PeerId}; use rand::{seq::SliceRandom, thread_rng}; use slog::{debug, error, warn}; @@ -101,7 +105,7 @@ impl Sampling { return None; }; - let result = request.on_sample_downloaded(peer_id, id.column_index, resp, cx); + let result = request.on_sample_downloaded(peer_id, id.sampling_request_id, resp, cx); self.handle_sampling_result(result, &id.id) } @@ -124,7 +128,7 @@ impl Sampling { return None; }; - let result = request.on_sample_verified(id.column_index, result, cx); + let result = request.on_sample_verified(id.sampling_request_id, result, cx); self.handle_sampling_result(result, &id.id) } @@ -156,6 +160,10 @@ pub struct ActiveSamplingRequest { block_slot: Slot, requester_id: SamplingRequester, column_requests: FnvHashMap, + /// Mapping of column indexes for a sampling request. + column_indexes_by_sampling_request: FnvHashMap>, + /// Sequential ID for sampling requests. + current_sampling_request_id: SamplingRequestId, column_shuffle: Vec, required_successes: Vec, /// Logger for the `SyncNetworkContext`. @@ -205,6 +213,8 @@ impl ActiveSamplingRequest { block_slot, requester_id, column_requests: <_>::default(), + column_indexes_by_sampling_request: <_>::default(), + current_sampling_request_id: SamplingRequestId(0), column_shuffle, required_successes: match sampling_config { SamplingConfig::Default => REQUIRED_SUCCESSES.to_vec(), @@ -226,7 +236,7 @@ impl ActiveSamplingRequest { pub(crate) fn on_sample_downloaded( &mut self, _peer_id: PeerId, - column_index: ColumnIndex, + sampling_request_id: SamplingRequestId, resp: Result<(DataColumnSidecarVec, Duration), RpcResponseError>, cx: &mut SyncNetworkContext, ) -> Result, SamplingError> { @@ -235,57 +245,103 @@ impl ActiveSamplingRequest { // Progress requests // If request fails retry or expand search // If all good return - let Some(request) = self.column_requests.get_mut(&column_index) else { - warn!( - self.log, - "Received sampling response for unrequested column index" - ); + let Some(column_indexes) = self + .column_indexes_by_sampling_request + .get(&sampling_request_id) + else { + error!(self.log, "Column indexes for the sampling request ID not found"; "sampling_request_id" => ?sampling_request_id); return Ok(None); }; match resp { - Ok((mut data_columns, seen_timestamp)) => { - debug!(self.log, "Sample download success"; "block_root" => %self.block_root, "column_index" => column_index, "count" => data_columns.len()); + Ok((mut resp_data_columns, seen_timestamp)) => { + debug!(self.log, "Sample download success"; "block_root" => %self.block_root, "column_indexes" => ?column_indexes, "count" => resp_data_columns.len()); metrics::inc_counter_vec(&metrics::SAMPLE_DOWNLOAD_RESULT, &[metrics::SUCCESS]); - // No need to check data_columns has len > 1, as the SyncNetworkContext ensure that - // only requested is returned (or none); - if let Some(data_column) = data_columns.pop() { + // Filter the data received in the response using the requested column indexes. + let mut data_columns = vec![]; + for column_index in column_indexes { + let Some(request) = self.column_requests.get_mut(column_index) else { + warn!( + self.log, + "Active column sample request not found"; "block_root" => %self.block_root, "column_index" => column_index + ); + continue; + }; + + let Some(data_pos) = resp_data_columns + .iter() + .position(|data| &data.index == column_index) + else { + // Peer does not have the requested data. + // TODO(das) what to do? + debug!(self.log, "Sampling peer claims to not have the data"; "block_root" => %self.block_root, "column_index" => column_index); + request.on_sampling_error()?; + continue; + }; + + data_columns.push(resp_data_columns.swap_remove(data_pos)); + } + + if !resp_data_columns.is_empty() { + let resp_column_indexes = resp_data_columns + .iter() + .map(|d| d.index) + .collect::>(); + debug!( + self.log, + "Received data that was not requested"; "block_root" => %self.block_root, "column_indexes" => ?resp_column_indexes + ); + } + + // Handle the downloaded data columns. + if data_columns.is_empty() { + debug!(self.log,"Received empty response"; "block_root" => %self.block_root); + self.column_indexes_by_sampling_request + .remove(&sampling_request_id); + } else { + // Overwrite `column_indexes` with the column indexes received in the response. + let column_indexes = data_columns.iter().map(|d| d.index).collect::>(); + self.column_indexes_by_sampling_request + .insert(sampling_request_id, column_indexes.clone()); // Peer has data column, send to verify let Some(beacon_processor) = cx.beacon_processor_if_enabled() else { // If processor is not available, error the entire sampling debug!(self.log, "Dropping sampling"; "block" => %self.block_root, "reason" => "beacon processor unavailable"); return Err(SamplingError::ProcessorUnavailable); }; - - debug!(self.log, "Sending data_column for verification"; "block" => ?self.block_root, "column_index" => column_index); + debug!(self.log, "Sending data_column for verification"; "block" => ?self.block_root, "column_indexes" => ?column_indexes); if let Err(e) = beacon_processor.send_rpc_validate_data_columns( self.block_root, - vec![data_column], + data_columns, seen_timestamp, SamplingId { id: self.requester_id, - column_index, + sampling_request_id, }, ) { // TODO(das): Beacon processor is overloaded, what should we do? error!(self.log, "Dropping sampling"; "block" => %self.block_root, "reason" => e.to_string()); return Err(SamplingError::SendFailed("beacon processor send failure")); } - } else { - // Peer does not have the requested data. - // TODO(das) what to do? - debug!(self.log, "Sampling peer claims to not have the data"; "block_root" => %self.block_root, "column_index" => column_index); - request.on_sampling_error()?; } } Err(err) => { - debug!(self.log, "Sample download error"; "block_root" => %self.block_root, "column_index" => column_index, "error" => ?err); + debug!(self.log, "Sample download error"; "block_root" => %self.block_root, "column_indexes" => ?column_indexes, "error" => ?err); metrics::inc_counter_vec(&metrics::SAMPLE_DOWNLOAD_RESULT, &[metrics::FAILURE]); // Error downloading, maybe penalize peer and retry again. // TODO(das) with different peer or different peer? - request.on_sampling_error()?; + for column_index in column_indexes { + let Some(request) = self.column_requests.get_mut(column_index) else { + warn!( + self.log, + "Active column sample request not found"; "block_root" => %self.block_root, "column_index" => column_index + ); + continue; + }; + request.on_sampling_error()?; + } } }; @@ -302,43 +358,56 @@ impl ActiveSamplingRequest { /// - `Ok(None)`: Sampling request still active pub(crate) fn on_sample_verified( &mut self, - column_index: ColumnIndex, + sampling_request_id: SamplingRequestId, result: Result<(), String>, cx: &mut SyncNetworkContext, ) -> Result, SamplingError> { - // Select columns to sample - // Create individual request per column - // Progress requests - // If request fails retry or expand search - // If all good return - let Some(request) = self.column_requests.get_mut(&column_index) else { - warn!( - self.log, - "Received sampling response for unrequested column index" - ); + let Some(column_indexes) = self + .column_indexes_by_sampling_request + .get(&sampling_request_id) + else { + error!(self.log, "Column indexes for the sampling request ID not found"; "sampling_request_id" => ?sampling_request_id); return Ok(None); }; match result { Ok(_) => { - debug!(self.log, "Sample verification success"; "block_root" => %self.block_root, "column_index" => column_index); + debug!(self.log, "Sample verification success"; "block_root" => %self.block_root, "column_indexes" => ?column_indexes); metrics::inc_counter_vec(&metrics::SAMPLE_VERIFY_RESULT, &[metrics::SUCCESS]); // Valid, continue_sampling will maybe consider sampling succees - request.on_sampling_success()?; + for column_index in column_indexes { + let Some(request) = self.column_requests.get_mut(column_index) else { + warn!( + self.log, + "Active column sample request not found"; "block_root" => %self.block_root, "column_index" => column_index + ); + continue; + }; + request.on_sampling_success()?; + } } Err(err) => { - debug!(self.log, "Sample verification failure"; "block_root" => %self.block_root, "column_index" => column_index, "reason" => ?err); + debug!(self.log, "Sample verification failure"; "block_root" => %self.block_root, "column_indexes" => ?column_indexes, "reason" => ?err); metrics::inc_counter_vec(&metrics::SAMPLE_VERIFY_RESULT, &[metrics::FAILURE]); // TODO(das): Peer sent invalid data, penalize and try again from different peer // TODO(das): Count individual failures - let peer_id = request.on_sampling_error()?; - cx.report_peer( - peer_id, - PeerAction::LowToleranceError, - "invalid data column", - ); + for column_index in column_indexes { + let Some(request) = self.column_requests.get_mut(column_index) else { + warn!( + self.log, + "Active column sample request not found"; "block_root" => %self.block_root, "column_index" => column_index + ); + continue; + }; + let peer_id = request.on_sampling_error()?; + cx.report_peer( + peer_id, + PeerAction::LowToleranceError, + "invalid data column", + ); + } } } @@ -376,10 +445,11 @@ impl ActiveSamplingRequest { return Ok(Some(())); } - let mut sent_requests = 0; - // First, attempt to progress sampling by requesting more columns, so that request failures // are accounted for below. + + // Group the requested column indexes by the destination peer to batch sampling requests. + let mut column_indexes_to_request = FnvHashMap::default(); for idx in 0..*required_successes { // Re-request columns. Note: out of bounds error should never happen, inputs are hardcoded let column_index = *self @@ -391,8 +461,40 @@ impl ActiveSamplingRequest { .entry(column_index) .or_insert(ActiveColumnSampleRequest::new(column_index)); - if request.request(self.block_root, self.block_slot, self.requester_id, cx)? { - sent_requests += 1 + if request.is_ready_to_request() { + if let Some(peer_id) = request.choose_peer(self.block_slot, cx) { + let indexes = column_indexes_to_request.entry(peer_id).or_insert(vec![]); + indexes.push(column_index); + } + } + } + + // Send requests. + let mut sent_request = false; + for (peer_id, column_indexes) in column_indexes_to_request { + cx.data_column_lookup_request( + DataColumnsByRootRequester::Sampling(SamplingId { + id: self.requester_id, + sampling_request_id: self.current_sampling_request_id, + }), + peer_id, + DataColumnsByRootSingleBlockRequest { + block_root: self.block_root, + indices: column_indexes.clone(), + }, + ) + .map_err(SamplingError::SendFailed)?; + self.column_indexes_by_sampling_request + .insert(self.current_sampling_request_id, column_indexes.clone()); + self.current_sampling_request_id.0 += 1; + sent_request = true; + + // Update request status. + for column_index in column_indexes { + let Some(request) = self.column_requests.get_mut(&column_index) else { + continue; + }; + request.on_start_sampling(peer_id)?; } } @@ -400,7 +502,7 @@ impl ActiveSamplingRequest { // receive a new event of some type. If there are no ongoing requests, and no new // request was sent, loop to increase the required_successes until the sampling fails if // there are no peers. - if ongoings == 0 && sent_requests == 0 { + if ongoings == 0 && !sent_request { debug!(self.log, "Sampling request stalled"; "block_root" => %self.block_root); } @@ -409,14 +511,14 @@ impl ActiveSamplingRequest { } mod request { - use super::{SamplingError, SamplingId, SamplingRequester}; - use crate::sync::network_context::{DataColumnsByRootSingleBlockRequest, SyncNetworkContext}; + use super::SamplingError; + use crate::sync::network_context::SyncNetworkContext; use beacon_chain::BeaconChainTypes; - use lighthouse_network::{service::api_types::DataColumnsByRootRequester, PeerId}; + use lighthouse_network::PeerId; use rand::seq::SliceRandom; use rand::thread_rng; use std::collections::HashSet; - use types::{data_column_sidecar::ColumnIndex, EthSpec, Hash256, Slot}; + use types::{data_column_sidecar::ColumnIndex, EthSpec, Slot}; pub(crate) struct ActiveColumnSampleRequest { column_index: ColumnIndex, @@ -463,19 +565,18 @@ mod request { } } - pub(crate) fn request( - &mut self, - block_root: Hash256, - block_slot: Slot, - requester: SamplingRequester, - cx: &mut SyncNetworkContext, - ) -> Result { - match &self.status { - Status::NoPeers | Status::NotStarted => {} // Ok to continue - Status::Sampling(_) => return Ok(false), // Already downloading - Status::Verified => return Ok(false), // Already completed + pub(crate) fn is_ready_to_request(&self) -> bool { + match self.status { + Status::NoPeers | Status::NotStarted => true, + Status::Sampling(_) | Status::Verified => false, } + } + pub(crate) fn choose_peer( + &mut self, + block_slot: Slot, + cx: &SyncNetworkContext, + ) -> Option { // TODO: When is a fork and only a subset of your peers know about a block, sampling should only // be queried on the peers on that fork. Should this case be handled? How to handle it? let mut peer_ids = cx.get_custodial_peers( @@ -485,25 +586,24 @@ mod request { peer_ids.retain(|peer_id| !self.peers_dont_have.contains(peer_id)); - if let Some(peer_id) = peer_ids.choose(&mut thread_rng()).cloned() { - cx.data_column_lookup_request( - DataColumnsByRootRequester::Sampling(SamplingId { - id: requester, - column_index: self.column_index, - }), - peer_id, - DataColumnsByRootSingleBlockRequest { - block_root, - indices: vec![self.column_index], - }, - ) - .map_err(SamplingError::SendFailed)?; - - self.status = Status::Sampling(peer_id); - Ok(true) + if let Some(peer_id) = peer_ids.choose(&mut thread_rng()) { + Some(*peer_id) } else { self.status = Status::NoPeers; - Ok(false) + None + } + } + + pub(crate) fn on_start_sampling(&mut self, peer_id: PeerId) -> Result<(), SamplingError> { + match self.status.clone() { + Status::NoPeers | Status::NotStarted => { + self.status = Status::Sampling(peer_id); + Ok(()) + } + other => Err(SamplingError::BadState(format!( + "bad state on_start_sampling expected NoPeers|NotStarted got {other:?}. column_index:{}", + self.column_index + ))), } } @@ -515,7 +615,8 @@ mod request { Ok(peer_id) } other => Err(SamplingError::BadState(format!( - "bad state on_sampling_error expected Sampling got {other:?}" + "bad state on_sampling_error expected Sampling got {other:?}. column_index:{}", + self.column_index ))), } } @@ -527,7 +628,8 @@ mod request { Ok(()) } other => Err(SamplingError::BadState(format!( - "bad state on_sampling_success expected Sampling got {other:?}" + "bad state on_sampling_success expected Sampling got {other:?}. column_index:{}", + self.column_index ))), } } From 06e34c20d34d42652bb1b47cd33cfe743fcea04e Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Mon, 19 Aug 2024 18:03:26 +1000 Subject: [PATCH 63/76] Fix range sync never evaluating request as finished, causing it to get stuck. (#6276) --- .../src/sync/block_sidecar_coupling.rs | 71 +++++++++++++++++-- beacon_node/network/src/sync/manager.rs | 1 + .../network/src/sync/network_context.rs | 64 +++++++++-------- 3 files changed, 102 insertions(+), 34 deletions(-) diff --git a/beacon_node/network/src/sync/block_sidecar_coupling.rs b/beacon_node/network/src/sync/block_sidecar_coupling.rs index 0666a267173..966ce55fabe 100644 --- a/beacon_node/network/src/sync/block_sidecar_coupling.rs +++ b/beacon_node/network/src/sync/block_sidecar_coupling.rs @@ -26,6 +26,9 @@ pub struct RangeBlockComponentsRequest { /// Used to determine if this accumulator should wait for a sidecars stream termination expects_blobs: bool, expects_custody_columns: Option>, + /// Used to determine if the number of data columns stream termination this accumulator should + /// wait for. This may be less than the number of `expects_custody_columns` due to request batching. + num_custody_column_requests: Option, /// The peers the request was made to. pub(crate) peer_ids: Vec, } @@ -34,6 +37,7 @@ impl RangeBlockComponentsRequest { pub fn new( expects_blobs: bool, expects_custody_columns: Option>, + num_custody_column_requests: Option, peer_ids: Vec, ) -> Self { Self { @@ -45,6 +49,7 @@ impl RangeBlockComponentsRequest { custody_columns_streams_terminated: 0, expects_blobs, expects_custody_columns, + num_custody_column_requests, peer_ids, } } @@ -219,8 +224,8 @@ impl RangeBlockComponentsRequest { if self.expects_blobs && !self.is_sidecars_stream_terminated { return false; } - if let Some(expects_custody_columns) = &self.expects_custody_columns { - if self.custody_columns_streams_terminated < expects_custody_columns.len() { + if let Some(expects_custody_column_responses) = self.num_custody_column_requests { + if self.custody_columns_streams_terminated < expects_custody_column_responses { return false; } } @@ -241,7 +246,7 @@ mod tests { #[test] fn no_blobs_into_responses() { let peer_id = PeerId::random(); - let mut info = RangeBlockComponentsRequest::::new(false, None, vec![peer_id]); + let mut info = RangeBlockComponentsRequest::::new(false, None, None, vec![peer_id]); 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) @@ -261,7 +266,7 @@ mod tests { #[test] fn empty_blobs_into_responses() { let peer_id = PeerId::random(); - let mut info = RangeBlockComponentsRequest::::new(true, None, vec![peer_id]); + let mut info = RangeBlockComponentsRequest::::new(true, None, None, vec![peer_id]); let mut rng = XorShiftRng::from_seed([42; 16]); let blocks = (0..4) .map(|_| { @@ -292,6 +297,7 @@ mod tests { let mut info = RangeBlockComponentsRequest::::new( false, Some(expects_custody_columns.clone()), + Some(expects_custody_columns.len()), vec![PeerId::random()], ); let mut rng = XorShiftRng::from_seed([42; 16]); @@ -343,4 +349,61 @@ mod tests { // All completed construct response info.into_responses(&spec).unwrap(); } + + #[test] + fn rpc_block_with_custody_columns_batched() { + let spec = test_spec::(); + let expects_custody_columns = vec![1, 2, 3, 4]; + let num_of_data_column_requests = 2; + let mut info = RangeBlockComponentsRequest::::new( + false, + Some(expects_custody_columns.clone()), + Some(num_of_data_column_requests), + vec![PeerId::random()], + ); + let mut rng = XorShiftRng::from_seed([42; 16]); + let blocks = (0..4) + .map(|_| { + generate_rand_block_and_data_columns::( + ForkName::Deneb, + NumBlobs::Number(1), + &mut rng, + &spec, + ) + }) + .collect::>(); + + // Send blocks and complete terminate response + for block in &blocks { + info.add_block_response(Some(block.0.clone().into())); + } + info.add_block_response(None); + // Assert response is not finished + assert!(!info.is_finished()); + + // Send data columns interleaved + for block in &blocks { + for column in &block.1 { + if expects_custody_columns.contains(&column.index) { + info.add_data_column(Some(column.clone())); + } + } + } + + // Terminate the requests + for i in 0..num_of_data_column_requests { + info.add_data_column(None); + if i < num_of_data_column_requests - 1 { + assert!( + !info.is_finished(), + "requested should not be finished at loop {i}" + ); + } else { + assert!(info.is_finished(), "request should be finished at loop {i}"); + } + } + + // All completed construct response + info.into_responses(&spec).unwrap(); + } } diff --git a/beacon_node/network/src/sync/manager.rs b/beacon_node/network/src/sync/manager.rs index 60e5699e1a0..d078ec52a9e 100644 --- a/beacon_node/network/src/sync/manager.rs +++ b/beacon_node/network/src/sync/manager.rs @@ -1161,6 +1161,7 @@ impl SyncManager { RangeBlockComponentsRequest::new( resp.expects_blobs, resp.expects_custody_columns, + None, vec![], ), ); diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index f35ee145b19..8a75f7be448 100644 --- a/beacon_node/network/src/sync/network_context.rs +++ b/beacon_node/network/src/sync/network_context.rs @@ -380,41 +380,45 @@ impl SyncNetworkContext { false }; - let expects_custody_columns = if matches!(batch_type, ByRangeRequestType::BlocksAndColumns) - { - let custody_indexes = self.network_globals().custody_columns(epoch); - - for (peer_id, columns_by_range_request) in - self.make_columns_by_range_requests(epoch, request, &custody_indexes)? - { - requested_peers.push(peer_id); - - debug!( - self.log, - "Sending DataColumnsByRange requests"; - "method" => "DataColumnsByRange", - "count" => columns_by_range_request.count, - "epoch" => epoch, - "columns" => ?columns_by_range_request.columns, - "peer" => %peer_id, - ); - - self.send_network_msg(NetworkMessage::SendRequest { - peer_id, - request: Request::DataColumnsByRange(columns_by_range_request), - request_id: AppRequestId::Sync(SyncRequestId::RangeBlockAndBlobs { id }), - }) - .map_err(|_| RpcRequestSendError::NetworkSendError)?; - } + let (expects_custody_columns, num_of_custody_column_req) = + if matches!(batch_type, ByRangeRequestType::BlocksAndColumns) { + let custody_indexes = self.network_globals().custody_columns(epoch); + let mut num_of_custody_column_req = 0; + + for (peer_id, columns_by_range_request) in + self.make_columns_by_range_requests(epoch, request, &custody_indexes)? + { + requested_peers.push(peer_id); + + debug!( + self.log, + "Sending DataColumnsByRange requests"; + "method" => "DataColumnsByRange", + "count" => columns_by_range_request.count, + "epoch" => epoch, + "columns" => ?columns_by_range_request.columns, + "peer" => %peer_id, + ); + + self.send_network_msg(NetworkMessage::SendRequest { + peer_id, + request: Request::DataColumnsByRange(columns_by_range_request), + request_id: AppRequestId::Sync(SyncRequestId::RangeBlockAndBlobs { id }), + }) + .map_err(|_| RpcRequestSendError::NetworkSendError)?; + + num_of_custody_column_req += 1; + } - Some(custody_indexes) - } else { - None - }; + (Some(custody_indexes), Some(num_of_custody_column_req)) + } else { + (None, None) + }; let info = RangeBlockComponentsRequest::new( expected_blobs, expects_custody_columns, + num_of_custody_column_req, requested_peers, ); self.range_block_components_requests From 4e19984bad92eaedf56b4fa131b69b9370ea7c10 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Wed, 21 Aug 2024 20:52:46 +1000 Subject: [PATCH 64/76] Fix custody tests and load PeerDAS KZG instead. --- beacon_node/beacon_chain/src/test_utils.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index c8bcf566fe3..b28d221da7e 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -83,6 +83,14 @@ pub static KZG: LazyLock> = LazyLock::new(|| { Arc::new(kzg) }); +pub static KZG_PEERDAS: LazyLock> = LazyLock::new(|| { + let trusted_setup: TrustedSetup = serde_json::from_reader(TRUSTED_SETUP_BYTES) + .map_err(|e| format!("Unable to read trusted setup file: {}", e)) + .expect("should have trusted setup"); + let kzg = Kzg::new_from_trusted_setup_das_enabled(trusted_setup).expect("should create kzg"); + Arc::new(kzg) +}); + pub type BaseHarnessType = Witness, E, THotStore, TColdStore>; @@ -2704,7 +2712,7 @@ pub fn generate_rand_block_and_data_columns( ) { let (block, blobs) = generate_rand_block_and_blobs(fork_name, num_blobs, rng); let blob: BlobsList = blobs.into_iter().map(|b| b.blob).collect::>().into(); - let data_columns = blobs_to_data_column_sidecars(&blob, &block, &KZG, spec).unwrap(); + let data_columns = blobs_to_data_column_sidecars(&blob, &block, &KZG_PEERDAS, spec).unwrap(); (block, data_columns) } From 15d0f1566e3fd15e1743bc8f9641c89f5dede1ee Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Wed, 21 Aug 2024 21:38:02 +1000 Subject: [PATCH 65/76] Fix ef tests and bench compilation. --- beacon_node/beacon_chain/benches/benches.rs | 7 ++++--- testing/ef_tests/src/lib.rs | 5 +++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/beacon_node/beacon_chain/benches/benches.rs b/beacon_node/beacon_chain/benches/benches.rs index 4d61f513367..4a29be90251 100644 --- a/beacon_node/beacon_chain/benches/benches.rs +++ b/beacon_node/beacon_chain/benches/benches.rs @@ -1,5 +1,6 @@ use std::sync::Arc; +use beacon_chain::kzg_utils::{blobs_to_data_column_sidecars, reconstruct_data_columns}; use criterion::{black_box, criterion_group, criterion_main, Criterion}; use bls::Signature; @@ -7,7 +8,7 @@ use eth2_network_config::TRUSTED_SETUP_BYTES; use kzg::{Kzg, KzgCommitment, TrustedSetup}; use types::{ beacon_block_body::KzgCommitments, BeaconBlock, BeaconBlockDeneb, Blob, BlobsList, ChainSpec, - DataColumnSidecar, EmptyBlock, EthSpec, MainnetEthSpec, SignedBeaconBlock, + EmptyBlock, EthSpec, MainnetEthSpec, SignedBeaconBlock, }; fn create_test_block_and_blobs( @@ -44,14 +45,14 @@ fn all_benches(c: &mut Criterion) { let (signed_block, blob_sidecars) = create_test_block_and_blobs::(blob_count, &spec); let column_sidecars = - DataColumnSidecar::build_sidecars(&blob_sidecars, &signed_block, &kzg.clone(), &spec) + blobs_to_data_column_sidecars(&blob_sidecars, &signed_block, &kzg.clone(), &spec) .unwrap(); let spec = spec.clone(); c.bench_function(&format!("reconstruct_{}", blob_count), |b| { b.iter(|| { - black_box(DataColumnSidecar::reconstruct( + black_box(reconstruct_data_columns( &kzg, &column_sidecars.iter().as_slice()[0..column_sidecars.len() / 2], spec.as_ref(), diff --git a/testing/ef_tests/src/lib.rs b/testing/ef_tests/src/lib.rs index 430607c16c0..e7367719d72 100644 --- a/testing/ef_tests/src/lib.rs +++ b/testing/ef_tests/src/lib.rs @@ -3,8 +3,9 @@ pub use cases::WithdrawalsPayload; pub use cases::{ Case, EffectiveBalanceUpdates, Eth1DataReset, FeatureName, HistoricalRootsUpdate, HistoricalSummariesUpdate, InactivityUpdates, JustificationAndFinalization, - ParticipationFlagUpdates, ParticipationRecordUpdates, RandaoMixesReset, RegistryUpdates, - RewardsAndPenalties, Slashings, SlashingsReset, SyncCommitteeUpdates, + ParticipationFlagUpdates, ParticipationRecordUpdates, PendingBalanceDeposits, + PendingConsolidations, RandaoMixesReset, RegistryUpdates, RewardsAndPenalties, Slashings, + SlashingsReset, SyncCommitteeUpdates, }; pub use decode::log_file_access; pub use error::Error; From 481ebc10043f2a9a5962c33d41180175b6478e4f Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Wed, 21 Aug 2024 21:49:32 +1000 Subject: [PATCH 66/76] Fix failing sampling test. --- beacon_node/network/src/sync/network_context.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index 09171a24e3e..69c81f5ab9f 100644 --- a/beacon_node/network/src/sync/network_context.rs +++ b/beacon_node/network/src/sync/network_context.rs @@ -1048,7 +1048,7 @@ impl SyncNetworkContext { pub fn on_data_columns_by_root_response( &mut self, id: DataColumnsByRootRequestId, - peer_id: PeerId, + _peer_id: PeerId, rpc_event: RpcEvent>>, ) -> Option>>>> { let Entry::Occupied(mut request) = self.data_columns_by_root_requests.entry(id) else { @@ -1080,8 +1080,10 @@ impl SyncNetworkContext { // catch if a peer is returning more columns than requested or if the excess blobs are // invalid. Err((e, resolved)) => { - if let RpcResponseError::VerifyError(e) = &e { - self.report_peer(peer_id, PeerAction::LowToleranceError, e.into()); + if let RpcResponseError::VerifyError(_e) = &e { + // TODO(das): this is a bug, we should not penalise peer in this case. + // confirm this can be removed. + // self.report_peer(peer_id, PeerAction::LowToleranceError, e.into()); } if resolved { None From ea331a97081b473d2db2828059b1df2450b48b13 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Thu, 22 Aug 2024 14:30:23 +1000 Subject: [PATCH 67/76] Remove get_block_import_status --- beacon_node/beacon_chain/src/beacon_chain.rs | 37 -------------------- beacon_node/beacon_chain/src/lib.rs | 1 - 2 files changed, 38 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index a92c850e5ab..9ed2b72cda6 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -550,12 +550,6 @@ pub struct BeaconBlockResponse> { pub consensus_block_value: u64, } -pub enum BlockImportStatus { - PendingImport(Arc>), - Imported, - Unknown, -} - impl FinalizationAndCanonicity { pub fn is_finalized(self) -> bool { self.slot_is_finalized && self.canonical @@ -1184,37 +1178,6 @@ impl BeaconChain { self.get_data_column(&block_root, &index) } - /// Returns the import status of block checking (in order) pre-import caches, fork-choice, db store - pub fn get_block_import_status(&self, block_root: &Hash256) -> BlockImportStatus { - if let Some(block) = self - .reqresp_pre_import_cache - .read() - .get(block_root) - .map(|block| { - metrics::inc_counter(&metrics::BEACON_REQRESP_PRE_IMPORT_CACHE_HITS); - block.clone() - }) - { - return BlockImportStatus::PendingImport(block); - } - // Check fork-choice before early_attester_cache as the latter is pruned lazily - if self - .canonical_head - .fork_choice_read_lock() - .contains_block(block_root) - { - return BlockImportStatus::Imported; - } - if let Some(block) = self.early_attester_cache.get_block(*block_root) { - return BlockImportStatus::PendingImport(block); - } - if let Ok(true) = self.store.block_exists(block_root) { - BlockImportStatus::Imported - } else { - BlockImportStatus::Unknown - } - } - /// Returns the block at the given root, if any. /// /// ## Errors diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index 3a0eea220fe..7bfb5b08beb 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -73,7 +73,6 @@ pub use self::chain_config::ChainConfig; pub use self::errors::{BeaconChainError, BlockProductionError}; pub use self::historical_blocks::HistoricalBlockError; pub use attestation_verification::Error as AttestationError; -pub use beacon_chain::BlockImportStatus; pub use beacon_fork_choice_store::{BeaconForkChoiceStore, Error as ForkChoiceStoreError}; pub use block_verification::{ get_block_root, BlockError, ExecutionPayloadError, ExecutionPendingBlock, GossipVerifiedBlock, From 488a083317ab40ea52d0f5e38b0242dd66d124d5 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Thu, 22 Aug 2024 15:35:27 +1000 Subject: [PATCH 68/76] Re-enable Windows release tests. --- .github/workflows/test-suite.yml | 66 ++++++++++++++++---------------- 1 file changed, 32 insertions(+), 34 deletions(-) diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index 0c8baaa4278..769b889de4d 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -88,38 +88,37 @@ jobs: - name: Show cache stats if: env.SELF_HOSTED_RUNNERS == 'true' run: sccache --show-stats -# FIXME(das): disabled for now as the c-kzg-4844 `das` branch doesn't build on windows. -# release-tests-windows: -# name: release-tests-windows -# needs: [check-labels] -# if: needs.check-labels.outputs.skip_ci != 'true' -# runs-on: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "windows", "CI"]') || 'windows-2019' }} -# steps: -# - uses: actions/checkout@v4 -# - name: Get latest version of stable Rust -# if: env.SELF_HOSTED_RUNNERS == 'false' -# uses: moonrepo/setup-rust@v1 -# with: -# channel: stable -# cache-target: release -# bins: cargo-nextest -# env: -# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -# - name: Install Foundry (anvil) -# if: env.SELF_HOSTED_RUNNERS == 'false' -# uses: foundry-rs/foundry-toolchain@v1 -# with: -# version: nightly-ca67d15f4abd46394b324c50e21e66f306a1162d -# - name: Install make -# if: env.SELF_HOSTED_RUNNERS == 'false' -# run: choco install -y make -# - name: Set LIBCLANG_PATH -# run: echo "LIBCLANG_PATH=$((gcm clang).source -replace "clang.exe")" >> $env:GITHUB_ENV -# - name: Run tests in release -# run: make nextest-release -# - name: Show cache stats -# if: env.SELF_HOSTED_RUNNERS == 'true' -# run: sccache --show-stats + release-tests-windows: + name: release-tests-windows + needs: [check-labels] + if: needs.check-labels.outputs.skip_ci != 'true' + runs-on: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "windows", "CI"]') || 'windows-2019' }} + steps: + - uses: actions/checkout@v4 + - name: Get latest version of stable Rust + if: env.SELF_HOSTED_RUNNERS == 'false' + uses: moonrepo/setup-rust@v1 + with: + channel: stable + cache-target: release + bins: cargo-nextest + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Install Foundry (anvil) + if: env.SELF_HOSTED_RUNNERS == 'false' + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly-ca67d15f4abd46394b324c50e21e66f306a1162d + - name: Install make + if: env.SELF_HOSTED_RUNNERS == 'false' + run: choco install -y make + - name: Set LIBCLANG_PATH + run: echo "LIBCLANG_PATH=$((gcm clang).source -replace "clang.exe")" >> $env:GITHUB_ENV + - name: Run tests in release + run: make nextest-release + - name: Show cache stats + if: env.SELF_HOSTED_RUNNERS == 'true' + run: sccache --show-stats beacon-chain-tests: name: beacon-chain-tests needs: [check-labels] @@ -407,8 +406,7 @@ jobs: 'check-labels', 'target-branch-check', 'release-tests-ubuntu', - # FIXME(das): disabled for now as the c-kzg-4844 `das` branch doesn't build on windows. - # 'release-tests-windows', + 'release-tests-windows', 'beacon-chain-tests', 'op-pool-tests', 'network-tests', From d588661c7e5b10fbfdc359875e7ab40beaa850d0 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Thu, 22 Aug 2024 15:46:16 +1000 Subject: [PATCH 69/76] Address some review comments. --- beacon_node/beacon_chain/src/chain_config.rs | 2 +- beacon_node/src/cli.rs | 2 +- consensus/types/src/data_column_subnet_id.rs | 6 +++--- testing/simulator/src/basic_sim.rs | 2 +- testing/simulator/src/fallback_sim.rs | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/beacon_node/beacon_chain/src/chain_config.rs b/beacon_node/beacon_chain/src/chain_config.rs index 66749bbfa15..20edfbf31a4 100644 --- a/beacon_node/beacon_chain/src/chain_config.rs +++ b/beacon_node/beacon_chain/src/chain_config.rs @@ -84,7 +84,7 @@ pub struct ChainConfig { pub epochs_per_migration: u64, /// When set to true Light client server computes and caches state proofs for serving updates pub enable_light_client_server: bool, - /// Enable malicious PeerDAS mode where node withholds data columns when publishing a block + /// The number of data columns to withhold / exclude from publishing when proposing a block. pub malicious_withhold_count: usize, /// Enable peer sampling on blocks. pub enable_sampling: bool, diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index b244cd9f768..67bc9d7d407 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -67,7 +67,7 @@ pub fn cli_app() -> Command { .hide(true) ) .arg( - // TODO(das): remove this before release + // TODO(das): remove this before PeerDAS release Arg::new("malicious-withhold-count") .long("malicious-withhold-count") .action(ArgAction::Set) diff --git a/consensus/types/src/data_column_subnet_id.rs b/consensus/types/src/data_column_subnet_id.rs index cf5b1dd549d..dd58c6c36b4 100644 --- a/consensus/types/src/data_column_subnet_id.rs +++ b/consensus/types/src/data_column_subnet_id.rs @@ -51,9 +51,9 @@ impl DataColumnSubnetId { let mut node_id_bytes = [0u8; 32]; current_id.to_little_endian(&mut node_id_bytes); let hash = ethereum_hashing::hash_fixed(&node_id_bytes); - let hash_prefix = [ - hash[0], hash[1], hash[2], hash[3], hash[4], hash[5], hash[6], hash[7], - ]; + let hash_prefix: [u8; 8] = hash[0..8] + .try_into() + .expect("hash_fixed produces a 32 byte array"); let hash_prefix_u64 = u64::from_le_bytes(hash_prefix); let subnet = hash_prefix_u64 % spec.data_column_sidecar_subnet_count; diff --git a/testing/simulator/src/basic_sim.rs b/testing/simulator/src/basic_sim.rs index 422abd850e6..46196ba2b10 100644 --- a/testing/simulator/src/basic_sim.rs +++ b/testing/simulator/src/basic_sim.rs @@ -16,7 +16,7 @@ use tokio::time::sleep; use types::{Epoch, EthSpec, MinimalEthSpec}; const END_EPOCH: u64 = 16; -const GENESIS_DELAY: u64 = 60; +const GENESIS_DELAY: u64 = 32; const ALTAIR_FORK_EPOCH: u64 = 0; const BELLATRIX_FORK_EPOCH: u64 = 0; const CAPELLA_FORK_EPOCH: u64 = 1; diff --git a/testing/simulator/src/fallback_sim.rs b/testing/simulator/src/fallback_sim.rs index 35c2edbe237..73984aadad7 100644 --- a/testing/simulator/src/fallback_sim.rs +++ b/testing/simulator/src/fallback_sim.rs @@ -15,7 +15,7 @@ use tokio::time::sleep; use types::{Epoch, EthSpec, MinimalEthSpec}; const END_EPOCH: u64 = 16; -const GENESIS_DELAY: u64 = 60; +const GENESIS_DELAY: u64 = 32; const ALTAIR_FORK_EPOCH: u64 = 0; const BELLATRIX_FORK_EPOCH: u64 = 0; const CAPELLA_FORK_EPOCH: u64 = 1; From 93329c0dc0ee83249d4dcb10c71c915c51f04c77 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Thu, 22 Aug 2024 16:41:49 +1000 Subject: [PATCH 70/76] Address more review comments and cleanups. --- .../src/peer_manager/mod.rs | 5 --- .../lighthouse_network/src/rpc/protocol.rs | 3 -- .../lighthouse_network/src/service/mod.rs | 2 +- .../lighthouse_network/src/types/subnet.rs | 2 +- .../sync/block_lookups/single_block_lookup.rs | 2 +- .../network/src/sync/network_context.rs | 43 +++---------------- .../src/sync/network_context/custody.rs | 7 +-- .../network/src/sync/range_sync/chain.rs | 3 ++ beacon_node/network/src/sync/sampling.rs | 10 ++--- 9 files changed, 17 insertions(+), 60 deletions(-) diff --git a/beacon_node/lighthouse_network/src/peer_manager/mod.rs b/beacon_node/lighthouse_network/src/peer_manager/mod.rs index 068cfd19f39..7247425f500 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/mod.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/mod.rs @@ -895,11 +895,6 @@ impl PeerManager { self.max_peers().saturating_sub(dialing_peers) - peer_count } else if outbound_only_peer_count < self.min_outbound_only_peers() && peer_count < self.max_outbound_dialing_peers() - && self.target_peers > 10 - // This condition is to attempt to exclude testnets without - // an explicit CLI flag. For networks with low peer counts, we don't want to do - // repetitive searches for outbound peers, when we may be already connected to every - // peer on the testnet { self.max_outbound_dialing_peers() .saturating_sub(dialing_peers) diff --git a/beacon_node/lighthouse_network/src/rpc/protocol.rs b/beacon_node/lighthouse_network/src/rpc/protocol.rs index f3d5cb06f3f..6f7f0348345 100644 --- a/beacon_node/lighthouse_network/src/rpc/protocol.rs +++ b/beacon_node/lighthouse_network/src/rpc/protocol.rs @@ -399,9 +399,6 @@ impl SupportedProtocol { supported.extend_from_slice(&[ ProtocolId::new(SupportedProtocol::BlobsByRootV1, Encoding::SSZSnappy), ProtocolId::new(SupportedProtocol::BlobsByRangeV1, Encoding::SSZSnappy), - // TODO(das): add to PeerDAS fork - ProtocolId::new(SupportedProtocol::DataColumnsByRootV1, Encoding::SSZSnappy), - ProtocolId::new(SupportedProtocol::DataColumnsByRangeV1, Encoding::SSZSnappy), ]); } if fork_context.spec.is_peer_das_scheduled() { diff --git a/beacon_node/lighthouse_network/src/service/mod.rs b/beacon_node/lighthouse_network/src/service/mod.rs index d7af32420a7..50bce0217af 100644 --- a/beacon_node/lighthouse_network/src/service/mod.rs +++ b/beacon_node/lighthouse_network/src/service/mod.rs @@ -261,7 +261,7 @@ impl Network { ), // during a fork we subscribe to both the old and new topics max_subscribed_topics: max_topics * 4, - // 209 in theory = (64 attestation + 4 sync committee + 7 core topics + 6 blob topics + 64 column topics) * 2 + // 418 in theory = (64 attestation + 4 sync committee + 7 core topics + 6 blob topics + 128 column topics) * 2 max_subscriptions_per_request: max_topics * 2, }; diff --git a/beacon_node/lighthouse_network/src/types/subnet.rs b/beacon_node/lighthouse_network/src/types/subnet.rs index e814feefc70..1892dcc83af 100644 --- a/beacon_node/lighthouse_network/src/types/subnet.rs +++ b/beacon_node/lighthouse_network/src/types/subnet.rs @@ -12,7 +12,7 @@ pub enum Subnet { Attestation(SubnetId), /// Represents a gossipsub sync committee subnet and the metadata `syncnets` field. SyncCommittee(SyncSubnetId), - /// Represents a gossipsub data column subnet and the metadata `blbcolnets` field. + /// Represents a gossipsub data column subnet. DataColumn(DataColumnSubnetId), } 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 403b3e9b78f..b17bcedc5f5 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 @@ -362,7 +362,7 @@ pub enum State { AwaitingProcess(DownloadResult), /// Request is processing, sent by lookup sync Processing(DownloadResult), - /// Request is processed: + /// Request is processed Processed, } diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index 69c81f5ab9f..0b02a986f73 100644 --- a/beacon_node/network/src/sync/network_context.rs +++ b/beacon_node/network/src/sync/network_context.rs @@ -36,7 +36,7 @@ use std::time::Duration; use tokio::sync::mpsc; use types::blob_sidecar::FixedBlobSidecarList; use types::{ - BlobSidecar, ColumnIndex, DataColumnSidecar, DataColumnSidecarList, Epoch, EthSpec, Hash256, + BlobSidecar, ColumnIndex, DataColumnSidecar, DataColumnSidecarList, EthSpec, Hash256, SignedBeaconBlock, Slot, }; @@ -285,18 +285,13 @@ impl SyncNetworkContext { .collect() } - // TODO(das): epoch argument left here in case custody rotation is implemented - pub fn get_custodial_peers(&self, _epoch: Epoch, column_index: ColumnIndex) -> Vec { + pub fn get_custodial_peers(&self, column_index: ColumnIndex) -> Vec { self.network_globals() .custody_peers_for_column(column_index) } - pub fn get_random_custodial_peer( - &self, - epoch: Epoch, - column_index: ColumnIndex, - ) -> Option { - self.get_custodial_peers(epoch, column_index) + pub fn get_random_custodial_peer(&self, column_index: ColumnIndex) -> Option { + self.get_custodial_peers(column_index) .choose(&mut thread_rng()) .cloned() } @@ -398,7 +393,7 @@ impl SyncNetworkContext { let mut num_of_custody_column_req = 0; for (peer_id, columns_by_range_request) in - self.make_columns_by_range_requests(epoch, request, &custody_indexes)? + self.make_columns_by_range_requests(request, &custody_indexes)? { requested_peers.push(peer_id); @@ -440,7 +435,6 @@ impl SyncNetworkContext { fn make_columns_by_range_requests( &self, - epoch: Epoch, request: BlocksByRangeRequest, custody_indexes: &Vec, ) -> Result, RpcRequestSendError> { @@ -450,7 +444,7 @@ impl SyncNetworkContext { // TODO(das): The peer selection logic here needs to be improved - we should probably // avoid retrying from failed peers, however `BatchState` currently only tracks the peer // serving the blocks. - let Some(custody_peer) = self.get_random_custodial_peer(epoch, *column_index) else { + let Some(custody_peer) = self.get_random_custodial_peer(*column_index) else { // TODO(das): this will be pretty bad UX. To improve we should: // - Attempt to fetch custody requests first, before requesting blocks // - Handle the no peers case gracefully, maybe add some timeout and give a few @@ -859,31 +853,6 @@ impl SyncNetworkContext { }); } - pub fn report_peer_on_rpc_error(&self, peer_id: &PeerId, error: &RPCError) { - // Note: logging the report event here with the full error display. The log inside - // `report_peer` only includes a smaller string, like "invalid_data" - debug!(self.log, "reporting peer for sync lookup error"; "error" => %error); - if let Some(action) = match error { - // Protocol errors are heavily penalized - RPCError::SSZDecodeError(..) - | RPCError::IoError(..) - | RPCError::ErrorResponse(..) - | RPCError::InvalidData(..) - | RPCError::HandlerRejected => Some(PeerAction::LowToleranceError), - // Timing / network errors are less penalized - // TODO: Is IoError a protocol error or network error? - RPCError::StreamTimeout | RPCError::IncompleteStream | RPCError::NegotiationTimeout => { - Some(PeerAction::MidToleranceError) - } - // Not supporting a specific protocol is tolerated. TODO: Are you sure? - RPCError::UnsupportedProtocol => None, - // Our fault, don't penalize peer - RPCError::InternalError(..) | RPCError::Disconnected => None, - } { - self.report_peer(*peer_id, action, error.into()); - } - } - /// Subscribes to core topics. pub fn subscribe_core_topics(&self) { self.network_send diff --git a/beacon_node/network/src/sync/network_context/custody.rs b/beacon_node/network/src/sync/network_context/custody.rs index c19a40d6160..b1038c74703 100644 --- a/beacon_node/network/src/sync/network_context/custody.rs +++ b/beacon_node/network/src/sync/network_context/custody.rs @@ -12,7 +12,7 @@ use slog::{debug, warn}; use std::time::Duration; use std::{collections::HashMap, marker::PhantomData, sync::Arc}; use types::EthSpec; -use types::{data_column_sidecar::ColumnIndex, DataColumnSidecar, Epoch, Hash256}; +use types::{data_column_sidecar::ColumnIndex, DataColumnSidecar, Hash256}; use super::{LookupRequestResult, PeerGroup, RpcResponseResult, SyncNetworkContext}; @@ -22,7 +22,6 @@ type DataColumnSidecarList = Vec>>; pub struct ActiveCustodyRequest { block_root: Hash256, - block_epoch: Epoch, custody_id: CustodyId, /// List of column indices this request needs to download to complete successfully column_requests: FnvHashMap>, @@ -68,8 +67,6 @@ impl ActiveCustodyRequest { ) -> Self { Self { block_root, - // TODO(das): use actual epoch if there's rotation - block_epoch: Epoch::new(0), custody_id, column_requests: HashMap::from_iter( column_indices @@ -231,7 +228,7 @@ impl ActiveCustodyRequest { // TODO: When is a fork and only a subset of your peers know about a block, we should only // query the peers on that fork. Should this case be handled? How to handle it? - let custodial_peers = cx.get_custodial_peers(self.block_epoch, *column_index); + let custodial_peers = cx.get_custodial_peers(*column_index); // TODO(das): cache this computation in a OneCell or similar to prevent having to // run it every loop diff --git a/beacon_node/network/src/sync/range_sync/chain.rs b/beacon_node/network/src/sync/range_sync/chain.rs index 0810b34353e..1756fb513da 100644 --- a/beacon_node/network/src/sync/range_sync/chain.rs +++ b/beacon_node/network/src/sync/range_sync/chain.rs @@ -1168,6 +1168,9 @@ impl SyncingChain { } // don't send batch requests until we have peers on custody subnets + // TODO(das): this is a workaround to avoid sending out excessive block requests because + // block and data column requests are currently coupled. This can be removed once we find a + // way to decouple the requests and do retries individually, see issue #6258. if !self.good_peers_on_custody_subnets(self.to_be_downloaded, network) { debug!( self.log, diff --git a/beacon_node/network/src/sync/sampling.rs b/beacon_node/network/src/sync/sampling.rs index 2b14fe175bf..1b63c1b6c9e 100644 --- a/beacon_node/network/src/sync/sampling.rs +++ b/beacon_node/network/src/sync/sampling.rs @@ -462,7 +462,7 @@ impl ActiveSamplingRequest { .or_insert(ActiveColumnSampleRequest::new(column_index)); if request.is_ready_to_request() { - if let Some(peer_id) = request.choose_peer(self.block_slot, cx) { + if let Some(peer_id) = request.choose_peer(cx) { let indexes = column_indexes_to_request.entry(peer_id).or_insert(vec![]); indexes.push(column_index); } @@ -518,7 +518,7 @@ mod request { use rand::seq::SliceRandom; use rand::thread_rng; use std::collections::HashSet; - use types::{data_column_sidecar::ColumnIndex, EthSpec, Slot}; + use types::data_column_sidecar::ColumnIndex; pub(crate) struct ActiveColumnSampleRequest { column_index: ColumnIndex, @@ -574,15 +574,11 @@ mod request { pub(crate) fn choose_peer( &mut self, - block_slot: Slot, cx: &SyncNetworkContext, ) -> Option { // TODO: When is a fork and only a subset of your peers know about a block, sampling should only // be queried on the peers on that fork. Should this case be handled? How to handle it? - let mut peer_ids = cx.get_custodial_peers( - block_slot.epoch(::slots_per_epoch()), - self.column_index, - ); + let mut peer_ids = cx.get_custodial_peers(self.column_index); peer_ids.retain(|peer_id| !self.peers_dont_have.contains(peer_id)); From e049294d170cb5d06c05a7d16e60183967837b2e Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Thu, 22 Aug 2024 16:41:52 +1000 Subject: [PATCH 71/76] Comment out peer DAS KZG EF tests for now --- testing/ef_tests/src/cases/kzg_verify_blob_kzg_proof.rs | 1 + testing/ef_tests/tests/tests.rs | 2 ++ 2 files changed, 3 insertions(+) diff --git a/testing/ef_tests/src/cases/kzg_verify_blob_kzg_proof.rs b/testing/ef_tests/src/cases/kzg_verify_blob_kzg_proof.rs index e85950e8750..4e56b2b44c3 100644 --- a/testing/ef_tests/src/cases/kzg_verify_blob_kzg_proof.rs +++ b/testing/ef_tests/src/cases/kzg_verify_blob_kzg_proof.rs @@ -10,6 +10,7 @@ use types::Blob; pub fn get_kzg() -> Result { let trusted_setup: TrustedSetup = serde_json::from_reader(TRUSTED_SETUP_BYTES) .map_err(|e| Error::InternalError(format!("Failed to initialize kzg: {:?}", e)))?; + // TODO(das): need to enable these tests when rayon issues in rust_eth_kzg are fixed Kzg::new_from_trusted_setup(trusted_setup) .map_err(|e| Error::InternalError(format!("Failed to initialize kzg: {:?}", e))) } diff --git a/testing/ef_tests/tests/tests.rs b/testing/ef_tests/tests/tests.rs index a677736d519..2c62edb62cc 100644 --- a/testing/ef_tests/tests/tests.rs +++ b/testing/ef_tests/tests/tests.rs @@ -901,6 +901,7 @@ fn kzg_verify_kzg_proof() { KZGVerifyKZGProofHandler::::default().run(); } +/* TODO(das): enable these tests #[test] fn kzg_compute_cells_and_proofs() { KZGComputeCellsAndKZGProofHandler::::default() @@ -918,6 +919,7 @@ fn kzg_recover_cells_and_proofs() { KZGRecoverCellsAndKZGProofHandler::::default() .run_for_feature(ForkName::Deneb, FeatureName::Eip7594); } +*/ #[test] fn merkle_proof_validity() { From c44bc0a967cc876b2eb91cf30bab52f570ce9450 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Thu, 22 Aug 2024 16:47:31 +1000 Subject: [PATCH 72/76] Address more review comments and fix build. --- beacon_node/beacon_chain/src/data_availability_checker.rs | 1 - .../src/data_availability_checker/overflow_lru_cache.rs | 6 +++--- beacon_node/network/src/sync/manager.rs | 8 ++++---- beacon_node/network/src/sync/sampling.rs | 7 +------ 4 files changed, 8 insertions(+), 14 deletions(-) diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index 6cdc4965a44..470cee713fa 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -662,7 +662,6 @@ impl AvailableBlock { self.blobs.as_ref() } - #[allow(clippy::type_complexity)] pub fn blobs_available_timestamp(&self) -> Option { self.blobs_available_timestamp } diff --git a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs index e58236b3260..4863982b552 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs @@ -17,11 +17,11 @@ use std::num::NonZeroUsize; use std::sync::Arc; use types::blob_sidecar::BlobIdentifier; use types::{ - BlobSidecar, ChainSpec, ColumnIndex, DataColumnIdentifier, DataColumnSidecar, Epoch, EthSpec, - Hash256, SignedBeaconBlock, + BlobSidecar, ChainSpec, ColumnIndex, DataColumnIdentifier, DataColumnSidecar, + DataColumnSidecarList, Epoch, EthSpec, Hash256, SignedBeaconBlock, }; -pub type DataColumnsToPublish = Option>>>; +pub type DataColumnsToPublish = Option>; /// This represents the components of a partially available block /// diff --git a/beacon_node/network/src/sync/manager.rs b/beacon_node/network/src/sync/manager.rs index 7ed8f23e76d..d6ce14adb16 100644 --- a/beacon_node/network/src/sync/manager.rs +++ b/beacon_node/network/src/sync/manager.rs @@ -732,10 +732,10 @@ impl SyncManager { } } SyncMessage::SampleBlock(block_root, block_slot) => { - debug!(self.log, "Received SampleBlock message"; "block_root" => %block_root); - if let Some((requester, result)) = - self.sampling - .on_new_sample_request(block_root, block_slot, &mut self.network) + debug!(self.log, "Received SampleBlock message"; "block_root" => %block_root, "slot" => block_slot); + if let Some((requester, result)) = self + .sampling + .on_new_sample_request(block_root, &mut self.network) { self.on_sampling_result(requester, result) } diff --git a/beacon_node/network/src/sync/sampling.rs b/beacon_node/network/src/sync/sampling.rs index 1b63c1b6c9e..524fe86bee9 100644 --- a/beacon_node/network/src/sync/sampling.rs +++ b/beacon_node/network/src/sync/sampling.rs @@ -15,7 +15,7 @@ use std::{ collections::hash_map::Entry, collections::HashMap, marker::PhantomData, sync::Arc, time::Duration, }; -use types::{data_column_sidecar::ColumnIndex, ChainSpec, DataColumnSidecar, Hash256, Slot}; +use types::{data_column_sidecar::ColumnIndex, ChainSpec, DataColumnSidecar, Hash256}; pub type SamplingResult = Result<(), SamplingError>; @@ -51,7 +51,6 @@ impl Sampling { pub fn on_new_sample_request( &mut self, block_root: Hash256, - block_slot: Slot, cx: &mut SyncNetworkContext, ) -> Option<(SamplingRequester, SamplingResult)> { let id = SamplingRequester::ImportedBlock(block_root); @@ -59,7 +58,6 @@ impl Sampling { let request = match self.requests.entry(id) { Entry::Vacant(e) => e.insert(ActiveSamplingRequest::new( block_root, - block_slot, id, &self.sampling_config, self.log.clone(), @@ -157,7 +155,6 @@ impl Sampling { pub struct ActiveSamplingRequest { block_root: Hash256, - block_slot: Slot, requester_id: SamplingRequester, column_requests: FnvHashMap, /// Mapping of column indexes for a sampling request. @@ -196,7 +193,6 @@ pub enum SamplingConfig { impl ActiveSamplingRequest { fn new( block_root: Hash256, - block_slot: Slot, requester_id: SamplingRequester, sampling_config: &SamplingConfig, log: slog::Logger, @@ -210,7 +206,6 @@ impl ActiveSamplingRequest { Self { block_root, - block_slot, requester_id, column_requests: <_>::default(), column_indexes_by_sampling_request: <_>::default(), From 45b3203e5d94a54465ad80e8f14b7b7ed87db9a0 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Fri, 23 Aug 2024 10:41:04 +1000 Subject: [PATCH 73/76] Unignore Electra tests --- testing/ef_tests/check_all_files_accessed.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/testing/ef_tests/check_all_files_accessed.py b/testing/ef_tests/check_all_files_accessed.py index d4d9802befc..ae8574c5c6b 100755 --- a/testing/ef_tests/check_all_files_accessed.py +++ b/testing/ef_tests/check_all_files_accessed.py @@ -20,8 +20,6 @@ # following regular expressions, we will assume they are to be ignored (i.e., we are purposefully # *not* running the spec tests). excluded_paths = [ - # TODO(das): remove once electra tests are on unstable - "tests/.*/electra/", # TODO(das): ignore until new spec test release with column subnet count = 64. "tests/.*/.*/.*/get_custody_columns/", # Eth1Block and PowBlock From 63a39b71bf9b1304248d215b9a18f5b2b715c79b Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Fri, 23 Aug 2024 10:57:15 +1000 Subject: [PATCH 74/76] Fix metric name --- beacon_node/beacon_processor/src/metrics.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beacon_node/beacon_processor/src/metrics.rs b/beacon_node/beacon_processor/src/metrics.rs index 11edb53c73b..8bc03cee6c7 100644 --- a/beacon_node/beacon_processor/src/metrics.rs +++ b/beacon_node/beacon_processor/src/metrics.rs @@ -250,7 +250,7 @@ pub static BEACON_PROCESSOR_REPROCESSING_QUEUE_MATCHED_SAMPLING_REQUESTS: LazyLo Result, > = LazyLock::new(|| { try_create_int_counter( - "beacon_processor_reprocessing_queue_matches_sampling_requests", + "beacon_processor_reprocessing_queue_matched_sampling_requests", "Number of queued sampling requests where a matching block has been imported.", ) }); From 77caa971310387e1b6ff106aaf3e168e30c0c726 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Fri, 23 Aug 2024 12:49:42 +1000 Subject: [PATCH 75/76] Address some of Pawan's review comments --- beacon_node/beacon_chain/src/metrics.rs | 7 ------- consensus/types/src/runtime_var_list.rs | 4 ++-- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/beacon_node/beacon_chain/src/metrics.rs b/beacon_node/beacon_chain/src/metrics.rs index 32f58ed4ff1..3394946255f 100644 --- a/beacon_node/beacon_chain/src/metrics.rs +++ b/beacon_node/beacon_chain/src/metrics.rs @@ -1619,13 +1619,6 @@ pub static BLOBS_SIDECAR_PROCESSING_REQUESTS: LazyLock> = Laz "Count of all blob sidecars submitted for processing", ) }); -pub static BLOBS_COLUMN_SIDECAR_PROCESSING_REQUESTS: LazyLock> = - LazyLock::new(|| { - try_create_int_counter( - "beacon_blobs_column_sidecar_processing_requests_total", - "Count of all data column sidecars submitted for processing", - ) - }); pub static BLOBS_SIDECAR_PROCESSING_SUCCESSES: LazyLock> = LazyLock::new(|| { try_create_int_counter( "beacon_blobs_sidecar_processing_successes_total", diff --git a/consensus/types/src/runtime_var_list.rs b/consensus/types/src/runtime_var_list.rs index e9cd7f7b9d5..af4ee87c158 100644 --- a/consensus/types/src/runtime_var_list.rs +++ b/consensus/types/src/runtime_var_list.rs @@ -46,8 +46,8 @@ pub struct RuntimeVariableList { } impl RuntimeVariableList { - /// Returns `Some` if the given `vec` equals the fixed length of `Self`. Otherwise returns - /// `None`. + /// Returns `Ok` if the given `vec` equals the fixed length of `Self`. Otherwise returns + /// `Err(OutOfBounds { .. })`. pub fn new(vec: Vec, max_len: usize) -> Result { if vec.len() <= max_len { Ok(Self { vec, max_len }) From 2c16052300558352c1df9f6f3f0c89dcbcb8240e Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Mon, 26 Aug 2024 20:22:51 -0700 Subject: [PATCH 76/76] Update PeerDAS network parameters for peerdas-devnet-2 (#6290) * update subnet count & custody req * das network params * update ef tests --------- Co-authored-by: Jimmy Chen --- beacon_node/lighthouse_network/src/discovery/enr.rs | 2 +- .../built_in_network_configs/chiado/config.yaml | 4 ++-- .../built_in_network_configs/gnosis/config.yaml | 4 ++-- .../built_in_network_configs/holesky/config.yaml | 4 ++-- .../built_in_network_configs/mainnet/config.yaml | 4 ++-- .../built_in_network_configs/sepolia/config.yaml | 4 ++-- consensus/types/src/chain_spec.rs | 10 +++++----- lighthouse/environment/tests/testnet_dir/config.yaml | 4 ++-- scripts/local_testnet/network_params_das_interop.yaml | 2 +- 9 files changed, 19 insertions(+), 19 deletions(-) diff --git a/beacon_node/lighthouse_network/src/discovery/enr.rs b/beacon_node/lighthouse_network/src/discovery/enr.rs index 04ae9971502..7415fdaf590 100644 --- a/beacon_node/lighthouse_network/src/discovery/enr.rs +++ b/beacon_node/lighthouse_network/src/discovery/enr.rs @@ -360,7 +360,7 @@ mod test { let config = NetworkConfig::default(); let spec = make_eip7594_spec(); let (mut enr, enr_key) = build_enr_with_config(config, &spec); - let invalid_subnet_count = 99u64; + let invalid_subnet_count = 999u64; enr.insert( PEERDAS_CUSTODY_SUBNET_COUNT_ENR_KEY, diff --git a/common/eth2_network_config/built_in_network_configs/chiado/config.yaml b/common/eth2_network_config/built_in_network_configs/chiado/config.yaml index 066b27795cd..74fca4c5010 100644 --- a/common/eth2_network_config/built_in_network_configs/chiado/config.yaml +++ b/common/eth2_network_config/built_in_network_configs/chiado/config.yaml @@ -138,6 +138,6 @@ MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS: 16384 BLOB_SIDECAR_SUBNET_COUNT: 6 # DAS -CUSTODY_REQUIREMENT: 1 -DATA_COLUMN_SIDECAR_SUBNET_COUNT: 32 +CUSTODY_REQUIREMENT: 4 +DATA_COLUMN_SIDECAR_SUBNET_COUNT: 128 NUMBER_OF_COLUMNS: 128 \ No newline at end of file diff --git a/common/eth2_network_config/built_in_network_configs/gnosis/config.yaml b/common/eth2_network_config/built_in_network_configs/gnosis/config.yaml index 23cf040b276..07bd21b35c2 100644 --- a/common/eth2_network_config/built_in_network_configs/gnosis/config.yaml +++ b/common/eth2_network_config/built_in_network_configs/gnosis/config.yaml @@ -121,6 +121,6 @@ MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS: 16384 BLOB_SIDECAR_SUBNET_COUNT: 6 # DAS -CUSTODY_REQUIREMENT: 1 -DATA_COLUMN_SIDECAR_SUBNET_COUNT: 32 +CUSTODY_REQUIREMENT: 4 +DATA_COLUMN_SIDECAR_SUBNET_COUNT: 128 NUMBER_OF_COLUMNS: 128 \ No newline at end of file diff --git a/common/eth2_network_config/built_in_network_configs/holesky/config.yaml b/common/eth2_network_config/built_in_network_configs/holesky/config.yaml index cec2b61f213..67f1e5b6831 100644 --- a/common/eth2_network_config/built_in_network_configs/holesky/config.yaml +++ b/common/eth2_network_config/built_in_network_configs/holesky/config.yaml @@ -125,6 +125,6 @@ MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS: 4096 BLOB_SIDECAR_SUBNET_COUNT: 6 # DAS -CUSTODY_REQUIREMENT: 1 -DATA_COLUMN_SIDECAR_SUBNET_COUNT: 32 +CUSTODY_REQUIREMENT: 4 +DATA_COLUMN_SIDECAR_SUBNET_COUNT: 128 NUMBER_OF_COLUMNS: 128 \ No newline at end of file diff --git a/common/eth2_network_config/built_in_network_configs/mainnet/config.yaml b/common/eth2_network_config/built_in_network_configs/mainnet/config.yaml index 500b9e60a5c..acf4d83f323 100644 --- a/common/eth2_network_config/built_in_network_configs/mainnet/config.yaml +++ b/common/eth2_network_config/built_in_network_configs/mainnet/config.yaml @@ -147,6 +147,6 @@ MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS: 4096 BLOB_SIDECAR_SUBNET_COUNT: 6 # DAS -CUSTODY_REQUIREMENT: 1 -DATA_COLUMN_SIDECAR_SUBNET_COUNT: 32 +CUSTODY_REQUIREMENT: 4 +DATA_COLUMN_SIDECAR_SUBNET_COUNT: 128 NUMBER_OF_COLUMNS: 128 \ No newline at end of file diff --git a/common/eth2_network_config/built_in_network_configs/sepolia/config.yaml b/common/eth2_network_config/built_in_network_configs/sepolia/config.yaml index 2a1809d6ce9..8b84d870103 100644 --- a/common/eth2_network_config/built_in_network_configs/sepolia/config.yaml +++ b/common/eth2_network_config/built_in_network_configs/sepolia/config.yaml @@ -121,6 +121,6 @@ MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS: 4096 BLOB_SIDECAR_SUBNET_COUNT: 6 # DAS -CUSTODY_REQUIREMENT: 1 -DATA_COLUMN_SIDECAR_SUBNET_COUNT: 32 +CUSTODY_REQUIREMENT: 4 +DATA_COLUMN_SIDECAR_SUBNET_COUNT: 128 NUMBER_OF_COLUMNS: 128 \ No newline at end of file diff --git a/consensus/types/src/chain_spec.rs b/consensus/types/src/chain_spec.rs index 2c64d21130f..10b00d5ba1d 100644 --- a/consensus/types/src/chain_spec.rs +++ b/consensus/types/src/chain_spec.rs @@ -807,8 +807,8 @@ impl ChainSpec { * DAS params */ eip7594_fork_epoch: None, - custody_requirement: 1, - data_column_sidecar_subnet_count: 32, + custody_requirement: 4, + data_column_sidecar_subnet_count: 128, number_of_columns: 128, /* @@ -1129,8 +1129,8 @@ impl ChainSpec { * DAS params */ eip7594_fork_epoch: None, - custody_requirement: 1, - data_column_sidecar_subnet_count: 32, + custody_requirement: 4, + data_column_sidecar_subnet_count: 128, number_of_columns: 128, /* * Network specific @@ -2122,7 +2122,7 @@ mod yaml_tests { DEPOSIT_NETWORK_ID: 1 DEPOSIT_CONTRACT_ADDRESS: 0x00000000219ab540356cBB839Cbe05303d7705Fa CUSTODY_REQUIREMENT: 1 - DATA_COLUMN_SIDECAR_SUBNET_COUNT: 32 + DATA_COLUMN_SIDECAR_SUBNET_COUNT: 128 NUMBER_OF_COLUMNS: 128 "#; diff --git a/lighthouse/environment/tests/testnet_dir/config.yaml b/lighthouse/environment/tests/testnet_dir/config.yaml index 4fc7bc2dcff..84e8274f06e 100644 --- a/lighthouse/environment/tests/testnet_dir/config.yaml +++ b/lighthouse/environment/tests/testnet_dir/config.yaml @@ -100,6 +100,6 @@ ATTESTATION_SUBNET_PREFIX_BITS: 6 ATTESTATION_SUBNET_SHUFFLING_PREFIX_BITS: 3 # DAS -CUSTODY_REQUIREMENT: 1 -DATA_COLUMN_SIDECAR_SUBNET_COUNT: 32 +CUSTODY_REQUIREMENT: 4 +DATA_COLUMN_SIDECAR_SUBNET_COUNT: 128 NUMBER_OF_COLUMNS: 128 \ No newline at end of file diff --git a/scripts/local_testnet/network_params_das_interop.yaml b/scripts/local_testnet/network_params_das_interop.yaml index 3e7fef557e6..0c8f9d7f49d 100644 --- a/scripts/local_testnet/network_params_das_interop.yaml +++ b/scripts/local_testnet/network_params_das_interop.yaml @@ -26,7 +26,7 @@ participants: network_params: eip7594_fork_epoch: 0 eip7594_fork_version: "0x50000038" - data_column_sidecar_subnet_count: 64 + data_column_sidecar_subnet_count: 128 samples_per_slot: 16 custody_requirement: 4 snooper_enabled: false