From 2e0eb6d1b8705bbda2ba56eb195d9cc7c6575e95 Mon Sep 17 00:00:00 2001 From: Age Manning Date: Thu, 29 Aug 2024 14:44:34 +1000 Subject: [PATCH 001/254] Add retropgf funding (#6324) --- FUNDING.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/FUNDING.json b/FUNDING.json index 5001999927..b2fe1aed41 100644 --- a/FUNDING.json +++ b/FUNDING.json @@ -3,5 +3,8 @@ "ethereum": { "ownedBy": "0x25c4a76E7d118705e7Ea2e9b7d8C59930d8aCD3b" } + }, + "opRetro": { + "projectId": "0x04b1cd5a7c59117474ce414b309fa48e985bdaab4b0dab72045f74d04ebd8cff" } -} \ No newline at end of file +} From 6a8d13e8a9ad9ef95dcde81e7633527767d6c3af Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Tue, 5 Nov 2024 12:00:07 +1100 Subject: [PATCH 002/254] Send `IDONTWANT` on publish to avoid downloading data we already have (#6513) * Send `IDONTWANT` on publish to avoid downloading data we already have. * Merge branch 'unstable' into send-idontwant-on-publish * Move broadcast of `IDONTWANT` to after publishing. --- .../lighthouse_network/gossipsub/src/behaviour.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/beacon_node/lighthouse_network/gossipsub/src/behaviour.rs b/beacon_node/lighthouse_network/gossipsub/src/behaviour.rs index 60f3d48d06..88fe48c441 100644 --- a/beacon_node/lighthouse_network/gossipsub/src/behaviour.rs +++ b/beacon_node/lighthouse_network/gossipsub/src/behaviour.rs @@ -776,6 +776,11 @@ where return Err(PublishError::AllQueuesFull(recipient_peers.len())); } + // Broadcast IDONTWANT messages + if raw_message.raw_protobuf_len() > self.config.idontwant_message_size_threshold() { + self.send_idontwant(&raw_message, &msg_id, raw_message.source.as_ref()); + } + tracing::debug!(message=%msg_id, "Published message"); if let Some(metrics) = self.metrics.as_mut() { @@ -1830,7 +1835,7 @@ where // Broadcast IDONTWANT messages if raw_message.raw_protobuf_len() > self.config.idontwant_message_size_threshold() { - self.send_idontwant(&raw_message, &msg_id, propagation_source); + self.send_idontwant(&raw_message, &msg_id, Some(propagation_source)); } tracing::debug!( @@ -2702,7 +2707,7 @@ where &mut self, message: &RawMessage, msg_id: &MessageId, - propagation_source: &PeerId, + propagation_source: Option<&PeerId>, ) { let Some(mesh_peers) = self.mesh.get(&message.topic) else { return; @@ -2713,8 +2718,8 @@ where let recipient_peers = mesh_peers .iter() .chain(iwant_peers.iter()) - .filter(|peer_id| { - *peer_id != propagation_source && Some(*peer_id) != message.source.as_ref() + .filter(|&peer_id| { + Some(peer_id) != propagation_source && Some(peer_id) != message.source.as_ref() }); for peer_id in recipient_peers { From 38388979db0bd672103984b017468b76087ce122 Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Tue, 5 Nov 2024 03:00:10 +0200 Subject: [PATCH 003/254] Strict match of errors in backfill sync (#6520) * Strict match of errors in backfill sync * Fix tests --- beacon_node/beacon_chain/src/beacon_chain.rs | 23 +-- beacon_node/beacon_chain/src/errors.rs | 8 +- .../beacon_chain/src/historical_blocks.rs | 27 ++- beacon_node/beacon_chain/tests/store_tests.rs | 6 +- .../network_beacon_processor/rpc_methods.rs | 32 ++-- .../network_beacon_processor/sync_methods.rs | 160 ++++++++---------- 6 files changed, 113 insertions(+), 143 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index f8dfbc5515..90a203f722 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -34,7 +34,6 @@ use crate::execution_payload::{get_execution_payload, NotifyExecutionLayer, Prep use crate::fork_choice_signal::{ForkChoiceSignalRx, ForkChoiceSignalTx, ForkChoiceWaitResult}; use crate::graffiti_calculator::GraffitiCalculator; use crate::head_tracker::{HeadTracker, HeadTrackerReader, SszHeadTracker}; -use crate::historical_blocks::HistoricalBlockError; use crate::light_client_finality_update_verification::{ Error as LightClientFinalityUpdateError, VerifiedLightClientFinalityUpdate, }; @@ -755,12 +754,10 @@ impl BeaconChain { ) -> Result> + '_, Error> { let oldest_block_slot = self.store.get_oldest_block_slot(); if start_slot < oldest_block_slot { - return Err(Error::HistoricalBlockError( - HistoricalBlockError::BlockOutOfRange { - slot: start_slot, - oldest_block_slot, - }, - )); + return Err(Error::HistoricalBlockOutOfRange { + slot: start_slot, + oldest_block_slot, + }); } let local_head = self.head_snapshot(); @@ -785,12 +782,10 @@ impl BeaconChain { ) -> Result> + '_, Error> { let oldest_block_slot = self.store.get_oldest_block_slot(); if start_slot < oldest_block_slot { - return Err(Error::HistoricalBlockError( - HistoricalBlockError::BlockOutOfRange { - slot: start_slot, - oldest_block_slot, - }, - )); + return Err(Error::HistoricalBlockOutOfRange { + slot: start_slot, + oldest_block_slot, + }); } self.with_head(move |head| { @@ -991,7 +986,7 @@ impl BeaconChain { WhenSlotSkipped::Prev => self.block_root_at_slot_skips_prev(request_slot), } .or_else(|e| match e { - Error::HistoricalBlockError(_) => Ok(None), + Error::HistoricalBlockOutOfRange { .. } => Ok(None), e => Err(e), }) } diff --git a/beacon_node/beacon_chain/src/errors.rs b/beacon_node/beacon_chain/src/errors.rs index a26d755316..2a8fd4cd01 100644 --- a/beacon_node/beacon_chain/src/errors.rs +++ b/beacon_node/beacon_chain/src/errors.rs @@ -4,7 +4,6 @@ use crate::beacon_chain::ForkChoiceError; use crate::beacon_fork_choice_store::Error as ForkChoiceStoreError; use crate::data_availability_checker::AvailabilityCheckError; use crate::eth1_chain::Error as Eth1ChainError; -use crate::historical_blocks::HistoricalBlockError; use crate::migrate::PruningError; use crate::naive_aggregation_pool::Error as NaiveAggregationError; use crate::observed_aggregates::Error as ObservedAttestationsError; @@ -123,7 +122,11 @@ pub enum BeaconChainError { block_slot: Slot, state_slot: Slot, }, - HistoricalBlockError(HistoricalBlockError), + /// Block is not available (only returned when fetching historic blocks). + HistoricalBlockOutOfRange { + slot: Slot, + oldest_block_slot: Slot, + }, InvalidStateForShuffling { state_epoch: Epoch, shuffling_epoch: Epoch, @@ -245,7 +248,6 @@ easy_from_to!(BlockSignatureVerifierError, BeaconChainError); easy_from_to!(PruningError, BeaconChainError); easy_from_to!(ArithError, BeaconChainError); easy_from_to!(ForkChoiceStoreError, BeaconChainError); -easy_from_to!(HistoricalBlockError, BeaconChainError); easy_from_to!(StateAdvanceError, BeaconChainError); easy_from_to!(BlockReplayError, BeaconChainError); easy_from_to!(InconsistentFork, BeaconChainError); diff --git a/beacon_node/beacon_chain/src/historical_blocks.rs b/beacon_node/beacon_chain/src/historical_blocks.rs index a23b6ddc1e..813eb906b9 100644 --- a/beacon_node/beacon_chain/src/historical_blocks.rs +++ b/beacon_node/beacon_chain/src/historical_blocks.rs @@ -1,5 +1,5 @@ use crate::data_availability_checker::AvailableBlock; -use crate::{errors::BeaconChainError as Error, metrics, BeaconChain, BeaconChainTypes}; +use crate::{metrics, BeaconChain, BeaconChainTypes}; use itertools::Itertools; use slog::debug; use state_processing::{ @@ -10,7 +10,11 @@ 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 store::{ + chunked_vector::BlockRoots, AnchorInfo, BlobInfo, ChunkWriter, Error as StoreError, + KeyValueStore, +}; +use strum::IntoStaticStr; use types::{FixedBytesExtended, Hash256, Slot}; /// Use a longer timeout on the pubkey cache. @@ -18,10 +22,8 @@ use types::{FixedBytesExtended, Hash256, Slot}; /// It's ok if historical sync is stalled due to writes from forwards block processing. const PUBKEY_CACHE_LOCK_TIMEOUT: Duration = Duration::from_secs(30); -#[derive(Debug)] +#[derive(Debug, IntoStaticStr)] pub enum HistoricalBlockError { - /// Block is not available (only returned when fetching historic blocks). - BlockOutOfRange { slot: Slot, oldest_block_slot: Slot }, /// Block root mismatch, caller should retry with different blocks. MismatchedBlockRoot { block_root: Hash256, @@ -37,6 +39,14 @@ pub enum HistoricalBlockError { NoAnchorInfo, /// Logic error: should never occur. IndexOutOfBounds, + /// Internal store error + StoreError(StoreError), +} + +impl From for HistoricalBlockError { + fn from(e: StoreError) -> Self { + Self::StoreError(e) + } } impl BeaconChain { @@ -61,7 +71,7 @@ impl BeaconChain { pub fn import_historical_block_batch( &self, mut blocks: Vec>, - ) -> Result { + ) -> Result { let anchor_info = self .store .get_anchor_info() @@ -127,8 +137,7 @@ impl BeaconChain { return Err(HistoricalBlockError::MismatchedBlockRoot { block_root, expected_block_root, - } - .into()); + }); } let blinded_block = block.clone_as_blinded(); @@ -212,7 +221,7 @@ impl BeaconChain { let verify_timer = metrics::start_timer(&metrics::BACKFILL_SIGNATURE_VERIFY_TIMES); if !signature_set.verify() { - return Err(HistoricalBlockError::InvalidSignature.into()); + return Err(HistoricalBlockError::InvalidSignature); } drop(verify_timer); drop(sig_timer); diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index 119722b693..a241d752fc 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -2669,9 +2669,7 @@ async fn weak_subjectivity_sync_test(slots: Vec, checkpoint_slot: Slot) { // Forwards iterator from 0 should fail as we lack blocks. assert!(matches!( beacon_chain.forwards_iter_block_roots(Slot::new(0)), - Err(BeaconChainError::HistoricalBlockError( - HistoricalBlockError::BlockOutOfRange { .. } - )) + Err(BeaconChainError::HistoricalBlockOutOfRange { .. }) )); // Simulate processing of a `StatusMessage` with an older finalized epoch by calling @@ -2739,7 +2737,7 @@ async fn weak_subjectivity_sync_test(slots: Vec, checkpoint_slot: Slot) { beacon_chain .import_historical_block_batch(batch_with_invalid_first_block) .unwrap_err(), - BeaconChainError::HistoricalBlockError(HistoricalBlockError::InvalidSignature) + HistoricalBlockError::InvalidSignature )); // Importing the batch with valid signatures should succeed. 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 6d32806713..c4944078fe 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,7 @@ 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, WhenSlotSkipped}; use itertools::process_results; use lighthouse_network::discovery::ConnectionId; use lighthouse_network::rpc::methods::{ @@ -682,12 +682,10 @@ impl NetworkBeaconProcessor { .forwards_iter_block_roots(Slot::from(*req.start_slot())) { Ok(iter) => iter, - Err(BeaconChainError::HistoricalBlockError( - HistoricalBlockError::BlockOutOfRange { - slot, - oldest_block_slot, - }, - )) => { + Err(BeaconChainError::HistoricalBlockOutOfRange { + slot, + oldest_block_slot, + }) => { debug!(self.log, "Range request failed during backfill"; "requested_slot" => slot, "oldest_known_slot" => oldest_block_slot @@ -941,12 +939,10 @@ impl NetworkBeaconProcessor { 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, - }, - )) => { + Err(BeaconChainError::HistoricalBlockOutOfRange { + slot, + oldest_block_slot, + }) => { debug!(self.log, "Range request failed during backfill"; "requested_slot" => slot, "oldest_known_slot" => oldest_block_slot @@ -1147,12 +1143,10 @@ impl NetworkBeaconProcessor { 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, - }, - )) => { + Err(BeaconChainError::HistoricalBlockOutOfRange { + slot, + oldest_block_slot, + }) => { debug!(self.log, "Range request failed during backfill"; "requested_slot" => slot, "oldest_known_slot" => oldest_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 82d06c20f8..d86dfae63a 100644 --- a/beacon_node/network/src/network_beacon_processor/sync_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/sync_methods.rs @@ -10,8 +10,8 @@ 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::{ - validator_monitor::get_slot_delay_ms, AvailabilityProcessingStatus, BeaconChainError, - BeaconChainTypes, BlockError, ChainSegmentResult, HistoricalBlockError, NotifyExecutionLayer, + validator_monitor::get_slot_delay_ms, AvailabilityProcessingStatus, BeaconChainTypes, + BlockError, ChainSegmentResult, HistoricalBlockError, NotifyExecutionLayer, }; use beacon_processor::{ work_reprocessing_queue::{QueuedRpcBlock, ReprocessQueueMessage}, @@ -606,103 +606,75 @@ impl NetworkBeaconProcessor { ); (imported_blocks, Ok(())) } - Err(error) => { + Err(e) => { metrics::inc_counter( &metrics::BEACON_PROCESSOR_BACKFILL_CHAIN_SEGMENT_FAILED_TOTAL, ); - let err = match error { - // Handle the historical block errors specifically - BeaconChainError::HistoricalBlockError(e) => match e { - HistoricalBlockError::MismatchedBlockRoot { - block_root, - expected_block_root, - } => { - debug!( - self.log, - "Backfill batch processing error"; - "error" => "mismatched_block_root", - "block_root" => ?block_root, - "expected_root" => ?expected_block_root - ); - - ChainSegmentFailed { - message: String::from("mismatched_block_root"), - // The peer is faulty if they send blocks with bad roots. - peer_action: Some(PeerAction::LowToleranceError), - } - } - HistoricalBlockError::InvalidSignature - | HistoricalBlockError::SignatureSet(_) => { - warn!( - self.log, - "Backfill batch processing error"; - "error" => ?e - ); - - ChainSegmentFailed { - message: "invalid_signature".into(), - // The peer is faulty if they bad signatures. - peer_action: Some(PeerAction::LowToleranceError), - } - } - HistoricalBlockError::ValidatorPubkeyCacheTimeout => { - warn!( - self.log, - "Backfill batch processing error"; - "error" => "pubkey_cache_timeout" - ); - - ChainSegmentFailed { - message: "pubkey_cache_timeout".into(), - // This is an internal error, do not penalize the peer. - peer_action: None, - } - } - HistoricalBlockError::NoAnchorInfo => { - warn!(self.log, "Backfill not required"); - - ChainSegmentFailed { - message: String::from("no_anchor_info"), - // There is no need to do a historical sync, this is not a fault of - // the peer. - peer_action: None, - } - } - HistoricalBlockError::IndexOutOfBounds => { - error!( - self.log, - "Backfill batch OOB error"; - "error" => ?e, - ); - ChainSegmentFailed { - message: String::from("logic_error"), - // This should never occur, don't penalize the peer. - peer_action: None, - } - } - HistoricalBlockError::BlockOutOfRange { .. } => { - error!( - self.log, - "Backfill batch error"; - "error" => ?e, - ); - ChainSegmentFailed { - message: String::from("unexpected_error"), - // This should never occur, don't penalize the peer. - peer_action: None, - } - } - }, - other => { - warn!(self.log, "Backfill batch processing error"; "error" => ?other); - ChainSegmentFailed { - message: format!("{:?}", other), - // This is an internal error, don't penalize the peer. - peer_action: None, - } + let peer_action = match &e { + HistoricalBlockError::MismatchedBlockRoot { + block_root, + expected_block_root, + } => { + debug!( + self.log, + "Backfill batch processing error"; + "error" => "mismatched_block_root", + "block_root" => ?block_root, + "expected_root" => ?expected_block_root + ); + // The peer is faulty if they send blocks with bad roots. + Some(PeerAction::LowToleranceError) } + HistoricalBlockError::InvalidSignature + | HistoricalBlockError::SignatureSet(_) => { + warn!( + self.log, + "Backfill batch processing error"; + "error" => ?e + ); + // The peer is faulty if they bad signatures. + Some(PeerAction::LowToleranceError) + } + HistoricalBlockError::ValidatorPubkeyCacheTimeout => { + warn!( + self.log, + "Backfill batch processing error"; + "error" => "pubkey_cache_timeout" + ); + // This is an internal error, do not penalize the peer. + None + } + HistoricalBlockError::NoAnchorInfo => { + warn!(self.log, "Backfill not required"); + // There is no need to do a historical sync, this is not a fault of + // the peer. + None + } + HistoricalBlockError::IndexOutOfBounds => { + error!( + self.log, + "Backfill batch OOB error"; + "error" => ?e, + ); + // This should never occur, don't penalize the peer. + None + } + HistoricalBlockError::StoreError(e) => { + warn!(self.log, "Backfill batch processing error"; "error" => ?e); + // This is an internal error, don't penalize the peer. + None + } // + // Do not use a fallback match, handle all errors explicitly }; - (0, Err(err)) + let err_str: &'static str = e.into(); + ( + 0, + Err(ChainSegmentFailed { + message: format!("{:?}", err_str), + // This is an internal error, don't penalize the peer. + peer_action, + }), + ) } } } From d8dbda319dbe9b8eab9d5fbd9a877b62a409a551 Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Tue, 5 Nov 2024 09:39:58 +0200 Subject: [PATCH 004/254] Resolve some PeerDAS todos (#6434) * Resolve some PeerDAS todos --- beacon_node/network/src/sync/manager.rs | 12 ++---------- beacon_node/network/src/sync/network_context.rs | 2 -- beacon_node/network/src/sync/peer_sampling.rs | 15 ++++++++------- 3 files changed, 10 insertions(+), 19 deletions(-) diff --git a/beacon_node/network/src/sync/manager.rs b/beacon_node/network/src/sync/manager.rs index 882f199b52..344e91711c 100644 --- a/beacon_node/network/src/sync/manager.rs +++ b/beacon_node/network/src/sync/manager.rs @@ -1188,22 +1188,14 @@ impl SyncManager { } 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(_) => { + // Notify the fork-choice of a successful sampling result to mark the block + // branch as safe. if let Err(e) = self .network .beacon_processor() diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index 5f7778ffcc..c4d987e858 100644 --- a/beacon_node/network/src/sync/network_context.rs +++ b/beacon_node/network/src/sync/network_context.rs @@ -769,7 +769,6 @@ impl SyncNetworkContext { 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(_) => { @@ -779,7 +778,6 @@ impl SyncNetworkContext { self.custody_by_root_requests.insert(requester, request); Ok(LookupRequestResult::RequestSent(req_id)) } - // TODO(das): handle this error properly Err(e) => Err(RpcRequestSendError::CustodyRequestError(e)), } } diff --git a/beacon_node/network/src/sync/peer_sampling.rs b/beacon_node/network/src/sync/peer_sampling.rs index 7e725f5df5..289ed73cdd 100644 --- a/beacon_node/network/src/sync/peer_sampling.rs +++ b/beacon_node/network/src/sync/peer_sampling.rs @@ -24,7 +24,6 @@ pub type SamplingResult = Result<(), SamplingError>; type DataColumnSidecarList = Vec>>; pub struct Sampling { - // TODO(das): stalled sampling request are never cleaned up requests: HashMap>, sampling_config: SamplingConfig, log: slog::Logger, @@ -313,8 +312,8 @@ impl ActiveSamplingRequest { .iter() .position(|data| &data.index == column_index) else { - // Peer does not have the requested data. - // TODO(das) what to do? + // Peer does not have the requested data, mark peer as "dont have" and try + // again with a different peer. debug!(self.log, "Sampling peer claims to not have the data"; "block_root" => %self.block_root, @@ -373,7 +372,9 @@ impl ActiveSamplingRequest { sampling_request_id, }, ) { - // TODO(das): Beacon processor is overloaded, what should we do? + // Beacon processor is overloaded, drop sampling attempt. Failing to sample + // is not a permanent state so we should recover once the node has capacity + // and receives a descendant block. error!(self.log, "Dropping sampling"; "block" => %self.block_root, @@ -391,8 +392,8 @@ impl ActiveSamplingRequest { ); 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? + // Error downloading, malicious network errors are already penalized before + // reaching this function. Mark the peer as failed and try again with another. for column_index in column_indexes { let Some(request) = self.column_requests.get_mut(column_index) else { warn!(self.log, @@ -453,7 +454,7 @@ impl ActiveSamplingRequest { 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 + // Peer sent invalid data, penalize and try again from different peer // TODO(das): Count individual failures for column_index in column_indexes { let Some(request) = self.column_requests.get_mut(column_index) else { From 9c42b12d06a79d9194b3711d88e6190f13e7f46d Mon Sep 17 00:00:00 2001 From: Pawan Dhananjay Date: Thu, 7 Nov 2024 10:29:39 +0530 Subject: [PATCH 005/254] Fix rpc decoding for blobs by range/root (#6569) * Fix rpc decoding for blobs by range/root --- .../lighthouse_network/src/rpc/codec.rs | 74 +++++++++++++++++-- 1 file changed, 66 insertions(+), 8 deletions(-) diff --git a/beacon_node/lighthouse_network/src/rpc/codec.rs b/beacon_node/lighthouse_network/src/rpc/codec.rs index 9bdecab70b..5d86936d41 100644 --- a/beacon_node/lighthouse_network/src/rpc/codec.rs +++ b/beacon_node/lighthouse_network/src/rpc/codec.rs @@ -682,10 +682,15 @@ fn handle_rpc_response( SignedBeaconBlock::Base(SignedBeaconBlockBase::from_ssz_bytes(decoded_buffer)?), )))), SupportedProtocol::BlobsByRangeV1 => match fork_name { - Some(ForkName::Deneb) => Ok(Some(RpcSuccessResponse::BlobsByRange(Arc::new( - BlobSidecar::from_ssz_bytes(decoded_buffer)?, - )))), - Some(_) => Err(RPCError::ErrorResponse( + Some(ForkName::Deneb) | Some(ForkName::Electra) => { + Ok(Some(RpcSuccessResponse::BlobsByRange(Arc::new( + BlobSidecar::from_ssz_bytes(decoded_buffer)?, + )))) + } + Some(ForkName::Base) + | Some(ForkName::Altair) + | Some(ForkName::Bellatrix) + | Some(ForkName::Capella) => Err(RPCError::ErrorResponse( RpcErrorResponse::InvalidRequest, "Invalid fork name for blobs by range".to_string(), )), @@ -698,10 +703,15 @@ fn handle_rpc_response( )), }, SupportedProtocol::BlobsByRootV1 => match fork_name { - Some(ForkName::Deneb) => Ok(Some(RpcSuccessResponse::BlobsByRoot(Arc::new( - BlobSidecar::from_ssz_bytes(decoded_buffer)?, - )))), - Some(_) => Err(RPCError::ErrorResponse( + Some(ForkName::Deneb) | Some(ForkName::Electra) => { + Ok(Some(RpcSuccessResponse::BlobsByRoot(Arc::new( + BlobSidecar::from_ssz_bytes(decoded_buffer)?, + )))) + } + Some(ForkName::Base) + | Some(ForkName::Altair) + | Some(ForkName::Bellatrix) + | Some(ForkName::Capella) => Err(RPCError::ErrorResponse( RpcErrorResponse::InvalidRequest, "Invalid fork name for blobs by root".to_string(), )), @@ -1376,6 +1386,16 @@ mod tests { Ok(Some(RpcSuccessResponse::BlobsByRange(empty_blob_sidecar()))), ); + assert_eq!( + encode_then_decode_response( + SupportedProtocol::BlobsByRangeV1, + RpcResponse::Success(RpcSuccessResponse::BlobsByRange(empty_blob_sidecar())), + ForkName::Electra, + &chain_spec + ), + Ok(Some(RpcSuccessResponse::BlobsByRange(empty_blob_sidecar()))), + ); + assert_eq!( encode_then_decode_response( SupportedProtocol::BlobsByRootV1, @@ -1386,6 +1406,16 @@ mod tests { Ok(Some(RpcSuccessResponse::BlobsByRoot(empty_blob_sidecar()))), ); + assert_eq!( + encode_then_decode_response( + SupportedProtocol::BlobsByRootV1, + RpcResponse::Success(RpcSuccessResponse::BlobsByRoot(empty_blob_sidecar())), + ForkName::Electra, + &chain_spec + ), + Ok(Some(RpcSuccessResponse::BlobsByRoot(empty_blob_sidecar()))), + ); + assert_eq!( encode_then_decode_response( SupportedProtocol::DataColumnsByRangeV1, @@ -1400,6 +1430,20 @@ mod tests { ))), ); + assert_eq!( + encode_then_decode_response( + SupportedProtocol::DataColumnsByRangeV1, + RpcResponse::Success(RpcSuccessResponse::DataColumnsByRange( + empty_data_column_sidecar() + )), + ForkName::Electra, + &chain_spec + ), + Ok(Some(RpcSuccessResponse::DataColumnsByRange( + empty_data_column_sidecar() + ))), + ); + assert_eq!( encode_then_decode_response( SupportedProtocol::DataColumnsByRootV1, @@ -1413,6 +1457,20 @@ mod tests { empty_data_column_sidecar() ))), ); + + assert_eq!( + encode_then_decode_response( + SupportedProtocol::DataColumnsByRootV1, + RpcResponse::Success(RpcSuccessResponse::DataColumnsByRoot( + empty_data_column_sidecar() + )), + ForkName::Electra, + &chain_spec + ), + Ok(Some(RpcSuccessResponse::DataColumnsByRoot( + empty_data_column_sidecar() + ))), + ); } // Test RPCResponse encoding/decoding for V1 messages From ae160ebf0754fcc9fab4b787ab366f0d974246f0 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Fri, 8 Nov 2024 09:19:43 +1100 Subject: [PATCH 006/254] Remove `yq` installation on CI (#6574) * Use `snap` to install `yq` on CI. * Remove yq install. --- .github/workflows/local-testnet.yml | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/.github/workflows/local-testnet.yml b/.github/workflows/local-testnet.yml index f719360c6a..d496cc6348 100644 --- a/.github/workflows/local-testnet.yml +++ b/.github/workflows/local-testnet.yml @@ -36,12 +36,11 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Install dependencies + - name: Install Kurtosis run: | - sudo add-apt-repository ppa:rmescandon/yq echo "deb [trusted=yes] https://apt.fury.io/kurtosis-tech/ /" | sudo tee /etc/apt/sources.list.d/kurtosis.list sudo apt update - sudo apt install -y kurtosis-cli=1.3.1 yq + sudo apt install -y kurtosis-cli=1.3.1 kurtosis analytics disable - name: Download Docker image artifact @@ -83,12 +82,11 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Install dependencies + - name: Install Kurtosis run: | - sudo add-apt-repository ppa:rmescandon/yq echo "deb [trusted=yes] https://apt.fury.io/kurtosis-tech/ /" | sudo tee /etc/apt/sources.list.d/kurtosis.list sudo apt update - sudo apt install -y kurtosis-cli=1.3.1 yq + sudo apt install -y kurtosis-cli=1.3.1 kurtosis analytics disable - name: Download Docker image artifact @@ -119,12 +117,11 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Install dependencies + - name: Install Kurtosis run: | - sudo add-apt-repository ppa:rmescandon/yq echo "deb [trusted=yes] https://apt.fury.io/kurtosis-tech/ /" | sudo tee /etc/apt/sources.list.d/kurtosis.list sudo apt update - sudo apt install -y kurtosis-cli=1.3.1 yq + sudo apt install -y kurtosis-cli=1.3.1 kurtosis analytics disable - name: Download Docker image artifact From 8e95024945de9b9eb3a77ad74c0c594385f2ecb2 Mon Sep 17 00:00:00 2001 From: Age Manning Date: Fri, 8 Nov 2024 12:01:46 +1100 Subject: [PATCH 007/254] Split the VC into crates making it more modular (#6453) * Starting to modularize the VC * Revert changes to eth2 * More progress * More progress * Compiles * Merge latest unstable and make it compile * Fix some lints * Tests compile * Merge latest unstable * Remove unnecessary deps * Merge latest unstable * Correct release tests * Merge latest unstable * Merge remote-tracking branch 'origin/unstable' into modularize-vc * Merge branch 'unstable' into modularize-vc * Revert unnecessary cargo lock changes * Update validator_client/beacon_node_fallback/Cargo.toml * Update validator_client/http_metrics/Cargo.toml * Update validator_client/http_metrics/src/lib.rs * Update validator_client/initialized_validators/Cargo.toml * Update validator_client/signing_method/Cargo.toml * Update validator_client/validator_metrics/Cargo.toml * Update validator_client/validator_services/Cargo.toml * Update validator_client/validator_store/Cargo.toml * Update validator_client/validator_store/src/lib.rs * Merge remote-tracking branch 'origin/unstable' into modularize-vc * Fix format string * Rename doppelganger trait * Don't drop the tempdir * Cargo fmt --- Cargo.lock | 242 +++++++++++++++--- Cargo.toml | 24 +- lighthouse/Cargo.toml | 3 + lighthouse/tests/validator_client.rs | 32 +-- testing/node_test_rig/Cargo.toml | 1 + testing/node_test_rig/src/lib.rs | 3 +- testing/simulator/src/basic_sim.rs | 3 +- testing/simulator/src/fallback_sim.rs | 3 +- testing/web3signer_tests/Cargo.toml | 4 +- testing/web3signer_tests/src/lib.rs | 16 +- validator_client/Cargo.toml | 68 ++--- .../beacon_node_fallback/Cargo.toml | 22 ++ .../src/beacon_node_health.rs | 29 ++- .../src/lib.rs} | 12 +- .../doppelganger_service/Cargo.toml | 20 ++ .../src/lib.rs} | 23 +- validator_client/graffiti_file/Cargo.toml | 19 ++ .../src/lib.rs} | 22 ++ validator_client/http_api/Cargo.toml | 50 ++++ .../http_api => http_api/src}/api_secret.rs | 0 .../src}/create_signed_voluntary_exit.rs | 2 +- .../src}/create_validator.rs | 2 +- .../http_api => http_api/src}/graffiti.rs | 2 +- .../http_api => http_api/src}/keystores.rs | 7 +- .../http_api/mod.rs => http_api/src/lib.rs} | 11 +- .../http_api => http_api/src}/remotekeys.rs | 3 +- .../http_api => http_api/src}/test_utils.rs | 19 +- .../{src/http_api => http_api/src}/tests.rs | 46 ++-- .../src}/tests/keystores.rs | 2 +- validator_client/http_metrics/Cargo.toml | 20 ++ .../mod.rs => http_metrics/src/lib.rs} | 66 ++++- .../initialized_validators/Cargo.toml | 26 ++ .../src/key_cache.rs | 0 .../src/lib.rs} | 33 ++- validator_client/signing_method/Cargo.toml | 17 ++ .../src/lib.rs} | 13 +- .../src}/web3signer.rs | 0 validator_client/src/check_synced.rs | 27 -- validator_client/src/config.rs | 65 ++--- validator_client/src/latency.rs | 10 +- validator_client/src/lib.rs | 110 +++----- validator_client/src/notifier.rs | 11 +- validator_client/validator_metrics/Cargo.toml | 12 + .../src/lib.rs} | 58 ----- .../validator_services/Cargo.toml | 23 ++ .../src/attestation_service.rs | 46 ++-- .../src/block_service.rs | 50 ++-- .../src/duties_service.rs | 92 +++---- .../validator_services/src/lib.rs | 6 + .../src/preparation_service.rs | 14 +- .../src}/sync.rs | 16 +- .../src/sync_committee_service.rs | 8 +- validator_client/validator_store/Cargo.toml | 23 ++ .../src/lib.rs} | 129 +++++++--- validator_manager/Cargo.toml | 2 +- validator_manager/src/delete_validators.rs | 2 +- validator_manager/src/import_validators.rs | 2 +- validator_manager/src/list_validators.rs | 2 +- validator_manager/src/move_validators.rs | 2 +- 59 files changed, 1021 insertions(+), 554 deletions(-) create mode 100644 validator_client/beacon_node_fallback/Cargo.toml rename validator_client/{ => beacon_node_fallback}/src/beacon_node_health.rs (95%) rename validator_client/{src/beacon_node_fallback.rs => beacon_node_fallback/src/lib.rs} (99%) create mode 100644 validator_client/doppelganger_service/Cargo.toml rename validator_client/{src/doppelganger_service.rs => doppelganger_service/src/lib.rs} (98%) create mode 100644 validator_client/graffiti_file/Cargo.toml rename validator_client/{src/graffiti_file.rs => graffiti_file/src/lib.rs} (89%) create mode 100644 validator_client/http_api/Cargo.toml rename validator_client/{src/http_api => http_api/src}/api_secret.rs (100%) rename validator_client/{src/http_api => http_api/src}/create_signed_voluntary_exit.rs (98%) rename validator_client/{src/http_api => http_api/src}/create_validator.rs (99%) rename validator_client/{src/http_api => http_api/src}/graffiti.rs (98%) rename validator_client/{src/http_api => http_api/src}/keystores.rs (99%) rename validator_client/{src/http_api/mod.rs => http_api/src/lib.rs} (99%) rename validator_client/{src/http_api => http_api/src}/remotekeys.rs (98%) rename validator_client/{src/http_api => http_api/src}/test_utils.rs (97%) rename validator_client/{src/http_api => http_api/src}/tests.rs (97%) rename validator_client/{src/http_api => http_api/src}/tests/keystores.rs (99%) create mode 100644 validator_client/http_metrics/Cargo.toml rename validator_client/{src/http_metrics/mod.rs => http_metrics/src/lib.rs} (68%) create mode 100644 validator_client/initialized_validators/Cargo.toml rename validator_client/{ => initialized_validators}/src/key_cache.rs (100%) rename validator_client/{src/initialized_validators.rs => initialized_validators/src/lib.rs} (98%) create mode 100644 validator_client/signing_method/Cargo.toml rename validator_client/{src/signing_method.rs => signing_method/src/lib.rs} (96%) rename validator_client/{src/signing_method => signing_method/src}/web3signer.rs (100%) delete mode 100644 validator_client/src/check_synced.rs create mode 100644 validator_client/validator_metrics/Cargo.toml rename validator_client/{src/http_metrics/metrics.rs => validator_metrics/src/lib.rs} (82%) create mode 100644 validator_client/validator_services/Cargo.toml rename validator_client/{ => validator_services}/src/attestation_service.rs (95%) rename validator_client/{ => validator_services}/src/block_service.rs (94%) rename validator_client/{ => validator_services}/src/duties_service.rs (95%) create mode 100644 validator_client/validator_services/src/lib.rs rename validator_client/{ => validator_services}/src/preparation_service.rs (97%) rename validator_client/{src/duties_service => validator_services/src}/sync.rs (98%) rename validator_client/{ => validator_services}/src/sync_committee_service.rs (99%) create mode 100644 validator_client/validator_store/Cargo.toml rename validator_client/{src/validator_store.rs => validator_store/src/lib.rs} (90%) diff --git a/Cargo.lock b/Cargo.lock index 0d9da0c7fe..71b5f7e7d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -857,6 +857,23 @@ dependencies = [ "unused_port", ] +[[package]] +name = "beacon_node_fallback" +version = "0.1.0" +dependencies = [ + "environment", + "eth2", + "futures", + "itertools 0.10.5", + "serde", + "slog", + "slot_clock", + "strum", + "tokio", + "types", + "validator_metrics", +] + [[package]] name = "beacon_processor" version = "0.1.0" @@ -2208,6 +2225,23 @@ dependencies = [ "syn 2.0.77", ] +[[package]] +name = "doppelganger_service" +version = "0.1.0" +dependencies = [ + "beacon_node_fallback", + "environment", + "eth2", + "futures", + "logging", + "parking_lot 0.12.3", + "slog", + "slot_clock", + "task_executor", + "tokio", + "types", +] + [[package]] name = "dsl_auto_type" version = "0.1.2" @@ -3498,6 +3532,18 @@ dependencies = [ "web-time", ] +[[package]] +name = "graffiti_file" +version = "0.1.0" +dependencies = [ + "bls", + "hex", + "serde", + "slog", + "tempfile", + "types", +] + [[package]] name = "group" version = "0.12.1" @@ -4200,6 +4246,31 @@ dependencies = [ "hashbrown 0.14.5", ] +[[package]] +name = "initialized_validators" +version = "0.1.0" +dependencies = [ + "account_utils", + "bincode", + "bls", + "eth2_keystore", + "filesystem", + "lockfile", + "metrics", + "parking_lot 0.12.3", + "rand", + "reqwest", + "serde", + "serde_json", + "signing_method", + "slog", + "tokio", + "types", + "url", + "validator_dir", + "validator_metrics", +] + [[package]] name = "inout" version = "0.1.3" @@ -5019,6 +5090,7 @@ dependencies = [ "account_manager", "account_utils", "beacon_node", + "beacon_node_fallback", "beacon_processor", "bls", "boot_node", @@ -5032,6 +5104,7 @@ dependencies = [ "eth2_network_config", "ethereum_hashing", "futures", + "initialized_validators", "lighthouse_network", "lighthouse_version", "logging", @@ -5697,6 +5770,7 @@ name = "node_test_rig" version = "0.2.0" dependencies = [ "beacon_node", + "beacon_node_fallback", "environment", "eth2", "execution_layer", @@ -7712,6 +7786,22 @@ dependencies = [ "rand_core", ] +[[package]] +name = "signing_method" +version = "0.1.0" +dependencies = [ + "eth2_keystore", + "ethereum_serde_utils", + "lockfile", + "parking_lot 0.12.3", + "reqwest", + "serde", + "task_executor", + "types", + "url", + "validator_metrics", +] + [[package]] name = "simple_asn1" version = "0.6.2" @@ -9147,54 +9237,34 @@ name = "validator_client" version = "0.3.5" dependencies = [ "account_utils", - "bincode", - "bls", + "beacon_node_fallback", "clap", "clap_utils", - "deposit_contract", "directory", "dirs", + "doppelganger_service", "environment", "eth2", - "eth2_keystore", - "ethereum_serde_utils", "fdlimit", - "filesystem", - "futures", - "hex", + "graffiti_file", "hyper 1.4.1", - "itertools 0.10.5", - "libsecp256k1", - "lighthouse_version", - "lockfile", - "logging", - "malloc_utils", + "initialized_validators", "metrics", "monitoring_api", "parking_lot 0.12.3", - "rand", "reqwest", - "ring 0.16.20", - "safe_arith", "sensitive_url", "serde", - "serde_json", "slashing_protection", "slog", "slot_clock", - "strum", - "sysinfo", - "system_health", - "task_executor", - "tempfile", "tokio", - "tokio-stream", - "tree_hash", "types", - "url", - "validator_dir", - "warp", - "warp_utils", + "validator_http_api", + "validator_http_metrics", + "validator_metrics", + "validator_services", + "validator_store", ] [[package]] @@ -9215,6 +9285,67 @@ dependencies = [ "types", ] +[[package]] +name = "validator_http_api" +version = "0.1.0" +dependencies = [ + "account_utils", + "beacon_node_fallback", + "bls", + "deposit_contract", + "doppelganger_service", + "eth2", + "eth2_keystore", + "ethereum_serde_utils", + "filesystem", + "futures", + "graffiti_file", + "initialized_validators", + "itertools 0.10.5", + "lighthouse_version", + "logging", + "parking_lot 0.12.3", + "rand", + "sensitive_url", + "serde", + "signing_method", + "slashing_protection", + "slog", + "slot_clock", + "sysinfo", + "system_health", + "task_executor", + "tempfile", + "tokio", + "tokio-stream", + "types", + "url", + "validator_dir", + "validator_services", + "validator_store", + "warp", + "warp_utils", +] + +[[package]] +name = "validator_http_metrics" +version = "0.1.0" +dependencies = [ + "lighthouse_version", + "malloc_utils", + "metrics", + "parking_lot 0.12.3", + "serde", + "slog", + "slot_clock", + "types", + "validator_metrics", + "validator_services", + "validator_store", + "warp", + "warp_utils", +] + [[package]] name = "validator_manager" version = "0.1.0" @@ -9236,7 +9367,54 @@ dependencies = [ "tokio", "tree_hash", "types", - "validator_client", + "validator_http_api", +] + +[[package]] +name = "validator_metrics" +version = "0.1.0" +dependencies = [ + "metrics", +] + +[[package]] +name = "validator_services" +version = "0.1.0" +dependencies = [ + "beacon_node_fallback", + "bls", + "doppelganger_service", + "environment", + "eth2", + "futures", + "graffiti_file", + "parking_lot 0.12.3", + "safe_arith", + "slog", + "slot_clock", + "tokio", + "tree_hash", + "types", + "validator_metrics", + "validator_store", +] + +[[package]] +name = "validator_store" +version = "0.1.0" +dependencies = [ + "account_utils", + "doppelganger_service", + "initialized_validators", + "parking_lot 0.12.3", + "serde", + "signing_method", + "slashing_protection", + "slog", + "slot_clock", + "task_executor", + "types", + "validator_metrics", ] [[package]] @@ -9516,19 +9694,21 @@ dependencies = [ "eth2_keystore", "eth2_network_config", "futures", + "initialized_validators", "logging", "parking_lot 0.12.3", "reqwest", "serde", "serde_json", "serde_yaml", + "slashing_protection", "slot_clock", "task_executor", "tempfile", "tokio", "types", "url", - "validator_client", + "validator_store", "zip", ] diff --git a/Cargo.toml b/Cargo.toml index 7094ff6077..83f3903ed4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -83,7 +83,17 @@ members = [ "testing/web3signer_tests", "validator_client", + "validator_client/beacon_node_fallback", + "validator_client/doppelganger_service", + "validator_client/graffiti_file", + "validator_client/http_api", + "validator_client/http_metrics", + "validator_client/initialized_validators", + "validator_client/signing_method", "validator_client/slashing_protection", + "validator_client/validator_metrics", + "validator_client/validator_services", + "validator_client/validator_store", "validator_manager", @@ -101,6 +111,7 @@ alloy-consensus = "0.3.0" anyhow = "1" arbitrary = { version = "1", features = ["derive"] } async-channel = "1.9.0" +axum = "0.7.7" bincode = "1" bitvec = "1" byteorder = "1" @@ -129,6 +140,7 @@ exit-future = "0.2" fnv = "1" fs2 = "0.4" futures = "0.3" +graffiti_file = { path = "validator_client/graffiti_file" } hex = "0.4" hashlink = "0.9.0" hyper = "1" @@ -170,7 +182,7 @@ superstruct = "0.8" syn = "1" sysinfo = "0.26" tempfile = "3" -tokio = { version = "1", features = ["rt-multi-thread", "sync", "signal"] } +tokio = { version = "1", features = ["rt-multi-thread", "sync", "signal", "macros"] } tokio-stream = { version = "0.1", features = ["sync"] } tokio-util = { version = "0.7", features = ["codec", "compat", "time"] } tracing = "0.1.40" @@ -190,12 +202,15 @@ zip = "0.6" account_utils = { path = "common/account_utils" } beacon_chain = { path = "beacon_node/beacon_chain" } beacon_node = { path = "beacon_node" } +beacon_node_fallback = { path = "validator_client/beacon_node_fallback" } beacon_processor = { path = "beacon_node/beacon_processor" } bls = { path = "crypto/bls" } clap_utils = { path = "common/clap_utils" } compare_fields = { path = "common/compare_fields" } deposit_contract = { path = "common/deposit_contract" } directory = { path = "common/directory" } +doppelganger_service = { path = "validator_client/doppelganger_service" } +validator_services = { path = "validator_client/validator_services" } environment = { path = "lighthouse/environment" } eth1 = { path = "beacon_node/eth1" } eth1_test_rig = { path = "testing/eth1_test_rig" } @@ -212,6 +227,7 @@ fork_choice = { path = "consensus/fork_choice" } genesis = { path = "beacon_node/genesis" } gossipsub = { path = "beacon_node/lighthouse_network/gossipsub/" } http_api = { path = "beacon_node/http_api" } +initialized_validators = { path = "validator_client/initialized_validators" } int_to_bytes = { path = "consensus/int_to_bytes" } kzg = { path = "crypto/kzg" } metrics = { path = "common/metrics" } @@ -229,17 +245,23 @@ pretty_reqwest_error = { path = "common/pretty_reqwest_error" } proto_array = { path = "consensus/proto_array" } safe_arith = { path = "consensus/safe_arith" } sensitive_url = { path = "common/sensitive_url" } +signing_method = { path = "validator_client/signing_method" } slasher = { path = "slasher", default-features = false } slashing_protection = { path = "validator_client/slashing_protection" } slot_clock = { path = "common/slot_clock" } state_processing = { path = "consensus/state_processing" } store = { path = "beacon_node/store" } swap_or_not_shuffle = { path = "consensus/swap_or_not_shuffle" } +system_health = { path = "common/system_health" } task_executor = { path = "common/task_executor" } types = { path = "consensus/types" } unused_port = { path = "common/unused_port" } validator_client = { path = "validator_client" } validator_dir = { path = "common/validator_dir" } +validator_http_api = { path = "validator_client/http_api" } +validator_http_metrics = { path = "validator_client/http_metrics" } +validator_metrics = { path = "validator_client/validator_metrics" } +validator_store= { path = "validator_client/validator_store" } warp_utils = { path = "common/warp_utils" } [profile.maxperf] diff --git a/lighthouse/Cargo.toml b/lighthouse/Cargo.toml index 1125697c7c..dd1cb68f06 100644 --- a/lighthouse/Cargo.toml +++ b/lighthouse/Cargo.toml @@ -71,6 +71,9 @@ sensitive_url = { workspace = true } eth1 = { workspace = true } eth2 = { workspace = true } beacon_processor = { workspace = true } +beacon_node_fallback = { workspace = true } +initialized_validators = { workspace = true } + [[test]] name = "lighthouse_tests" diff --git a/lighthouse/tests/validator_client.rs b/lighthouse/tests/validator_client.rs index 147a371f0e..34fe04cc45 100644 --- a/lighthouse/tests/validator_client.rs +++ b/lighthouse/tests/validator_client.rs @@ -1,9 +1,8 @@ -use validator_client::{ - config::DEFAULT_WEB3SIGNER_KEEP_ALIVE, ApiTopic, BeaconNodeSyncDistanceTiers, Config, -}; +use beacon_node_fallback::{beacon_node_health::BeaconNodeSyncDistanceTiers, ApiTopic}; use crate::exec::CommandLineTestExec; use bls::{Keypair, PublicKeyBytes}; +use initialized_validators::DEFAULT_WEB3SIGNER_KEEP_ALIVE; use sensitive_url::SensitiveUrl; use std::fs::File; use std::io::Write; @@ -15,6 +14,7 @@ use std::string::ToString; use std::time::Duration; use tempfile::TempDir; use types::{Address, Slot}; +use validator_client::Config; /// Returns the `lighthouse validator_client` command. fn base_cmd() -> Command { @@ -240,7 +240,7 @@ fn fee_recipient_flag() { .run() .with_config(|config| { assert_eq!( - config.fee_recipient, + config.validator_store.fee_recipient, Some(Address::from_str("0x00000000219ab540356cbb839cbe05303d7705fa").unwrap()) ) }); @@ -430,7 +430,7 @@ fn no_doppelganger_protection_flag() { fn no_gas_limit_flag() { CommandLineTest::new() .run() - .with_config(|config| assert!(config.gas_limit.is_none())); + .with_config(|config| assert!(config.validator_store.gas_limit.is_none())); } #[test] fn gas_limit_flag() { @@ -438,46 +438,46 @@ fn gas_limit_flag() { .flag("gas-limit", Some("600")) .flag("builder-proposals", None) .run() - .with_config(|config| assert_eq!(config.gas_limit, Some(600))); + .with_config(|config| assert_eq!(config.validator_store.gas_limit, Some(600))); } #[test] fn no_builder_proposals_flag() { CommandLineTest::new() .run() - .with_config(|config| assert!(!config.builder_proposals)); + .with_config(|config| assert!(!config.validator_store.builder_proposals)); } #[test] fn builder_proposals_flag() { CommandLineTest::new() .flag("builder-proposals", None) .run() - .with_config(|config| assert!(config.builder_proposals)); + .with_config(|config| assert!(config.validator_store.builder_proposals)); } #[test] fn builder_boost_factor_flag() { CommandLineTest::new() .flag("builder-boost-factor", Some("150")) .run() - .with_config(|config| assert_eq!(config.builder_boost_factor, Some(150))); + .with_config(|config| assert_eq!(config.validator_store.builder_boost_factor, Some(150))); } #[test] fn no_builder_boost_factor_flag() { CommandLineTest::new() .run() - .with_config(|config| assert_eq!(config.builder_boost_factor, None)); + .with_config(|config| assert_eq!(config.validator_store.builder_boost_factor, None)); } #[test] fn prefer_builder_proposals_flag() { CommandLineTest::new() .flag("prefer-builder-proposals", None) .run() - .with_config(|config| assert!(config.prefer_builder_proposals)); + .with_config(|config| assert!(config.validator_store.prefer_builder_proposals)); } #[test] fn no_prefer_builder_proposals_flag() { CommandLineTest::new() .run() - .with_config(|config| assert!(!config.prefer_builder_proposals)); + .with_config(|config| assert!(!config.validator_store.prefer_builder_proposals)); } #[test] fn no_builder_registration_timestamp_override_flag() { @@ -624,7 +624,7 @@ fn validator_registration_batch_size_zero_value() { #[test] fn validator_disable_web3_signer_slashing_protection_default() { CommandLineTest::new().run().with_config(|config| { - assert!(config.enable_web3signer_slashing_protection); + assert!(config.validator_store.enable_web3signer_slashing_protection); }); } @@ -634,7 +634,7 @@ fn validator_disable_web3_signer_slashing_protection() { .flag("disable-slashing-protection-web3signer", None) .run() .with_config(|config| { - assert!(!config.enable_web3signer_slashing_protection); + assert!(!config.validator_store.enable_web3signer_slashing_protection); }); } @@ -642,7 +642,7 @@ fn validator_disable_web3_signer_slashing_protection() { fn validator_web3_signer_keep_alive_default() { CommandLineTest::new().run().with_config(|config| { assert_eq!( - config.web3_signer_keep_alive_timeout, + config.initialized_validators.web3_signer_keep_alive_timeout, DEFAULT_WEB3SIGNER_KEEP_ALIVE ); }); @@ -655,7 +655,7 @@ fn validator_web3_signer_keep_alive_override() { .run() .with_config(|config| { assert_eq!( - config.web3_signer_keep_alive_timeout, + config.initialized_validators.web3_signer_keep_alive_timeout, Some(Duration::from_secs(1)) ); }); diff --git a/testing/node_test_rig/Cargo.toml b/testing/node_test_rig/Cargo.toml index 4696d8d2f1..97e73b8a2f 100644 --- a/testing/node_test_rig/Cargo.toml +++ b/testing/node_test_rig/Cargo.toml @@ -11,6 +11,7 @@ types = { workspace = true } tempfile = { workspace = true } eth2 = { workspace = true } validator_client = { workspace = true } +beacon_node_fallback = { workspace = true } validator_dir = { workspace = true, features = ["insecure_keys"] } sensitive_url = { workspace = true } execution_layer = { workspace = true } diff --git a/testing/node_test_rig/src/lib.rs b/testing/node_test_rig/src/lib.rs index 3320898642..6b453a8cbc 100644 --- a/testing/node_test_rig/src/lib.rs +++ b/testing/node_test_rig/src/lib.rs @@ -16,12 +16,13 @@ use validator_client::ProductionValidatorClient; use validator_dir::insecure_keys::build_deterministic_validator_dirs; pub use beacon_node::{ClientConfig, ClientGenesis, ProductionClient}; +pub use beacon_node_fallback::ApiTopic; pub use environment; pub use eth2; pub use execution_layer::test_utils::{ Config as MockServerConfig, MockExecutionConfig, MockServer, }; -pub use validator_client::{ApiTopic, Config as ValidatorConfig}; +pub use validator_client::Config as ValidatorConfig; /// The global timeout for HTTP requests to the beacon node. const HTTP_TIMEOUT: Duration = Duration::from_secs(8); diff --git a/testing/simulator/src/basic_sim.rs b/testing/simulator/src/basic_sim.rs index 5c9baa2349..8f659a893f 100644 --- a/testing/simulator/src/basic_sim.rs +++ b/testing/simulator/src/basic_sim.rs @@ -175,7 +175,8 @@ pub fn run_basic_sim(matches: &ArgMatches) -> Result<(), String> { executor.spawn( async move { let mut validator_config = testing_validator_config(); - validator_config.fee_recipient = Some(SUGGESTED_FEE_RECIPIENT.into()); + validator_config.validator_store.fee_recipient = + Some(SUGGESTED_FEE_RECIPIENT.into()); println!("Adding validator client {}", i); // Enable broadcast on every 4th node. diff --git a/testing/simulator/src/fallback_sim.rs b/testing/simulator/src/fallback_sim.rs index 0690ab242c..b3b9a46001 100644 --- a/testing/simulator/src/fallback_sim.rs +++ b/testing/simulator/src/fallback_sim.rs @@ -178,7 +178,8 @@ pub fn run_fallback_sim(matches: &ArgMatches) -> Result<(), String> { executor.spawn( async move { let mut validator_config = testing_validator_config(); - validator_config.fee_recipient = Some(SUGGESTED_FEE_RECIPIENT.into()); + validator_config.validator_store.fee_recipient = + Some(SUGGESTED_FEE_RECIPIENT.into()); println!("Adding validator client {}", i); network_1 .add_validator_client_with_fallbacks( diff --git a/testing/web3signer_tests/Cargo.toml b/testing/web3signer_tests/Cargo.toml index db5c53e0ac..0096d74f64 100644 --- a/testing/web3signer_tests/Cargo.toml +++ b/testing/web3signer_tests/Cargo.toml @@ -15,7 +15,6 @@ tempfile = { workspace = true } tokio = { workspace = true } reqwest = { workspace = true } url = { workspace = true } -validator_client = { workspace = true } slot_clock = { workspace = true } futures = { workspace = true } task_executor = { workspace = true } @@ -28,3 +27,6 @@ serde_json = { workspace = true } zip = { workspace = true } parking_lot = { workspace = true } logging = { workspace = true } +initialized_validators = { workspace = true } +slashing_protection = { workspace = true } +validator_store = { workspace = true } diff --git a/testing/web3signer_tests/src/lib.rs b/testing/web3signer_tests/src/lib.rs index 3a039d3c80..a58dcb5fa0 100644 --- a/testing/web3signer_tests/src/lib.rs +++ b/testing/web3signer_tests/src/lib.rs @@ -22,10 +22,14 @@ mod tests { }; use eth2_keystore::KeystoreBuilder; use eth2_network_config::Eth2NetworkConfig; + use initialized_validators::{ + load_pem_certificate, load_pkcs12_identity, InitializedValidators, + }; use logging::test_logger; use parking_lot::Mutex; use reqwest::Client; use serde::Serialize; + use slashing_protection::{SlashingDatabase, SLASHING_PROTECTION_FILENAME}; use slot_clock::{SlotClock, TestingSlotClock}; use std::env; use std::fmt::Debug; @@ -41,13 +45,7 @@ mod tests { use tokio::time::sleep; use types::{attestation::AttestationBase, *}; use url::Url; - use validator_client::{ - initialized_validators::{ - load_pem_certificate, load_pkcs12_identity, InitializedValidators, - }, - validator_store::{Error as ValidatorStoreError, ValidatorStore}, - SlashingDatabase, SLASHING_PROTECTION_FILENAME, - }; + use validator_store::{Error as ValidatorStoreError, ValidatorStore}; /// If the we are unable to reach the Web3Signer HTTP API within this time out then we will /// assume it failed to start. @@ -322,7 +320,7 @@ mod tests { let log = test_logger(); let validator_dir = TempDir::new().unwrap(); - let config = validator_client::Config::default(); + let config = initialized_validators::Config::default(); let validator_definitions = ValidatorDefinitions::from(validator_definitions); let initialized_validators = InitializedValidators::from_definitions( validator_definitions, @@ -354,7 +352,7 @@ mod tests { let slot_clock = TestingSlotClock::new(Slot::new(0), Duration::from_secs(0), Duration::from_secs(1)); - let config = validator_client::Config { + let config = validator_store::Config { enable_web3signer_slashing_protection: slashing_protection_config.local, ..Default::default() }; diff --git a/validator_client/Cargo.toml b/validator_client/Cargo.toml index 86825a9ee3..044a622d54 100644 --- a/validator_client/Cargo.toml +++ b/validator_client/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "validator_client" version = "0.3.5" -authors = ["Paul Hauner ", "Age Manning ", "Luke Anderson "] +authors = ["Sigma Prime "] edition = { workspace = true } [lib] @@ -12,52 +12,32 @@ path = "src/lib.rs" tokio = { workspace = true } [dependencies] -tree_hash = { workspace = true } -clap = { workspace = true } -slashing_protection = { workspace = true } -slot_clock = { workspace = true } -types = { workspace = true } -safe_arith = { workspace = true } -serde = { workspace = true } -bincode = { workspace = true } -serde_json = { workspace = true } -slog = { workspace = true } -tokio = { workspace = true } -tokio-stream = { workspace = true } -futures = { workspace = true } -dirs = { workspace = true } -directory = { workspace = true } -lockfile = { workspace = true } -environment = { workspace = true } -parking_lot = { workspace = true } -filesystem = { workspace = true } -hex = { workspace = true } -deposit_contract = { workspace = true } -bls = { workspace = true } -eth2 = { workspace = true } -tempfile = { workspace = true } -validator_dir = { workspace = true } -clap_utils = { workspace = true } -eth2_keystore = { workspace = true } account_utils = { workspace = true } -lighthouse_version = { workspace = true } -warp_utils = { workspace = true } -warp = { workspace = true } +beacon_node_fallback = { workspace = true } +clap = { workspace = true } +clap_utils = { workspace = true } +directory = { workspace = true } +doppelganger_service = { workspace = true } +dirs = { workspace = true } +eth2 = { workspace = true } +environment = { workspace = true } +graffiti_file = { workspace = true } hyper = { workspace = true } -ethereum_serde_utils = { workspace = true } -libsecp256k1 = { workspace = true } -ring = { workspace = true } -rand = { workspace = true, features = ["small_rng"] } +initialized_validators = { workspace = true } metrics = { workspace = true } monitoring_api = { workspace = true } +parking_lot = { workspace = true } +reqwest = { workspace = true } sensitive_url = { workspace = true } -task_executor = { workspace = true } -reqwest = { workspace = true, features = ["native-tls"] } -url = { workspace = true } -malloc_utils = { workspace = true } -sysinfo = { workspace = true } -system_health = { path = "../common/system_health" } -logging = { workspace = true } -strum = { workspace = true } -itertools = { workspace = true } +slashing_protection = { workspace = true } +serde = { workspace = true } +slog = { workspace = true } +slot_clock = { workspace = true } +types = { workspace = true } +validator_http_api = { workspace = true } +validator_http_metrics = { workspace = true } +validator_metrics = { workspace = true } +validator_services = { workspace = true } +validator_store = { workspace = true } +tokio = { workspace = true } fdlimit = "0.3.0" diff --git a/validator_client/beacon_node_fallback/Cargo.toml b/validator_client/beacon_node_fallback/Cargo.toml new file mode 100644 index 0000000000..c15ded43d7 --- /dev/null +++ b/validator_client/beacon_node_fallback/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "beacon_node_fallback" +version = "0.1.0" +edition = { workspace = true } +authors = ["Sigma Prime "] + +[lib] +name = "beacon_node_fallback" +path = "src/lib.rs" + +[dependencies] +environment = { workspace = true } +eth2 = { workspace = true } +futures = { workspace = true } +itertools = { workspace = true } +serde = { workspace = true } +slog = { workspace = true } +slot_clock = { workspace = true } +strum = { workspace = true } +tokio = { workspace = true } +types = { workspace = true } +validator_metrics = { workspace = true } diff --git a/validator_client/src/beacon_node_health.rs b/validator_client/beacon_node_fallback/src/beacon_node_health.rs similarity index 95% rename from validator_client/src/beacon_node_health.rs rename to validator_client/beacon_node_fallback/src/beacon_node_health.rs index 1783bb312c..e5b0487656 100644 --- a/validator_client/src/beacon_node_health.rs +++ b/validator_client/beacon_node_fallback/src/beacon_node_health.rs @@ -1,5 +1,8 @@ +use super::CandidateError; +use eth2::BeaconNodeHttpClient; use itertools::Itertools; use serde::{Deserialize, Serialize}; +use slog::{warn, Logger}; use std::cmp::Ordering; use std::fmt::{Debug, Display, Formatter}; use std::str::FromStr; @@ -285,6 +288,30 @@ impl BeaconNodeHealth { } } +pub async fn check_node_health( + beacon_node: &BeaconNodeHttpClient, + log: &Logger, +) -> Result<(Slot, bool, bool), CandidateError> { + let resp = match beacon_node.get_node_syncing().await { + Ok(resp) => resp, + Err(e) => { + warn!( + log, + "Unable connect to beacon node"; + "error" => %e + ); + + return Err(CandidateError::Offline); + } + }; + + Ok(( + resp.data.head_slot, + resp.data.is_optimistic, + resp.data.el_offline, + )) +} + #[cfg(test)] mod tests { use super::ExecutionEngineHealth::{Healthy, Unhealthy}; @@ -292,7 +319,7 @@ mod tests { BeaconNodeHealth, BeaconNodeHealthTier, BeaconNodeSyncDistanceTiers, IsOptimistic, SyncDistanceTier, }; - use crate::beacon_node_fallback::Config; + use crate::Config; use std::str::FromStr; use types::Slot; diff --git a/validator_client/src/beacon_node_fallback.rs b/validator_client/beacon_node_fallback/src/lib.rs similarity index 99% rename from validator_client/src/beacon_node_fallback.rs rename to validator_client/beacon_node_fallback/src/lib.rs index e5fe419983..95a221f189 100644 --- a/validator_client/src/beacon_node_fallback.rs +++ b/validator_client/beacon_node_fallback/src/lib.rs @@ -2,12 +2,11 @@ //! "fallback" behaviour; it will try a request on all of the nodes until one or none of them //! succeed. -use crate::beacon_node_health::{ - BeaconNodeHealth, BeaconNodeSyncDistanceTiers, ExecutionEngineHealth, IsOptimistic, - SyncDistanceTier, +pub mod beacon_node_health; +use beacon_node_health::{ + check_node_health, BeaconNodeHealth, BeaconNodeSyncDistanceTiers, ExecutionEngineHealth, + IsOptimistic, SyncDistanceTier, }; -use crate::check_synced::check_node_health; -use crate::http_metrics::metrics::{inc_counter_vec, ENDPOINT_ERRORS, ENDPOINT_REQUESTS}; use environment::RuntimeContext; use eth2::BeaconNodeHttpClient; use futures::future; @@ -24,6 +23,7 @@ use std::time::{Duration, Instant}; use strum::{EnumString, EnumVariantNames}; use tokio::{sync::RwLock, time::sleep}; use types::{ChainSpec, Config as ConfigSpec, EthSpec, Slot}; +use validator_metrics::{inc_counter_vec, ENDPOINT_ERRORS, ENDPOINT_REQUESTS}; /// Message emitted when the VC detects the BN is using a different spec. const UPDATE_REQUIRED_LOG_HINT: &str = "this VC or the remote BN may need updating"; @@ -739,7 +739,7 @@ impl ApiTopic { mod tests { use super::*; use crate::beacon_node_health::BeaconNodeHealthTier; - use crate::SensitiveUrl; + use eth2::SensitiveUrl; use eth2::Timeouts; use std::str::FromStr; use strum::VariantNames; diff --git a/validator_client/doppelganger_service/Cargo.toml b/validator_client/doppelganger_service/Cargo.toml new file mode 100644 index 0000000000..e5f7d3f2ba --- /dev/null +++ b/validator_client/doppelganger_service/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "doppelganger_service" +version = "0.1.0" +edition = { workspace = true } +authors = ["Sigma Prime "] + +[dependencies] +beacon_node_fallback = { workspace = true } +environment = { workspace = true } +eth2 = { workspace = true } +parking_lot = { workspace = true } +slog = { workspace = true } +slot_clock = { workspace = true } +task_executor = { workspace = true } +tokio = { workspace = true } +types = { workspace = true } + +[dev-dependencies] +futures = { workspace = true } +logging = {workspace = true } diff --git a/validator_client/src/doppelganger_service.rs b/validator_client/doppelganger_service/src/lib.rs similarity index 98% rename from validator_client/src/doppelganger_service.rs rename to validator_client/doppelganger_service/src/lib.rs index 1d552cc5ad..35228fe354 100644 --- a/validator_client/src/doppelganger_service.rs +++ b/validator_client/doppelganger_service/src/lib.rs @@ -29,8 +29,7 @@ //! //! Doppelganger protection is a best-effort, last-line-of-defence mitigation. Do not rely upon it. -use crate::beacon_node_fallback::BeaconNodeFallback; -use crate::validator_store::ValidatorStore; +use beacon_node_fallback::BeaconNodeFallback; use environment::RuntimeContext; use eth2::types::LivenessResponseData; use parking_lot::RwLock; @@ -114,6 +113,13 @@ struct LivenessResponses { /// validators on the network. pub const DEFAULT_REMAINING_DETECTION_EPOCHS: u64 = 1; +/// This crate cannot depend on ValidatorStore as validator_store depends on this crate and +/// initialises the doppelganger protection. For this reason, we abstract the validator store +/// functions this service needs through the following trait +pub trait DoppelgangerValidatorStore { + fn get_validator_index(&self, pubkey: &PublicKeyBytes) -> Option; +} + /// Store the per-validator status of doppelganger checking. #[derive(Debug, PartialEq)] pub struct DoppelgangerState { @@ -280,15 +286,20 @@ impl DoppelgangerService { /// Starts a reoccurring future which will try to keep the doppelganger service updated each /// slot. - pub fn start_update_service( + pub fn start_update_service( service: Arc, context: RuntimeContext, - validator_store: Arc>, + validator_store: Arc, beacon_nodes: Arc>, slot_clock: T, - ) -> Result<(), String> { + ) -> Result<(), String> + where + E: EthSpec, + T: 'static + SlotClock, + V: DoppelgangerValidatorStore + Send + Sync + 'static, + { // Define the `get_index` function as one that uses the validator store. - let get_index = move |pubkey| validator_store.validator_index(&pubkey); + let get_index = move |pubkey| validator_store.get_validator_index(&pubkey); // Define the `get_liveness` function as one that queries the beacon node API. let log = service.log.clone(); diff --git a/validator_client/graffiti_file/Cargo.toml b/validator_client/graffiti_file/Cargo.toml new file mode 100644 index 0000000000..02e48849d1 --- /dev/null +++ b/validator_client/graffiti_file/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "graffiti_file" +version = "0.1.0" +edition = { workspace = true } +authors = ["Sigma Prime "] + +[lib] +name = "graffiti_file" +path = "src/lib.rs" + +[dependencies] +serde = { workspace = true } +bls = { workspace = true } +types = { workspace = true } +slog = { workspace = true } + +[dev-dependencies] +tempfile = { workspace = true } +hex = { workspace = true } diff --git a/validator_client/src/graffiti_file.rs b/validator_client/graffiti_file/src/lib.rs similarity index 89% rename from validator_client/src/graffiti_file.rs rename to validator_client/graffiti_file/src/lib.rs index 29da3dca5a..0328c14eeb 100644 --- a/validator_client/src/graffiti_file.rs +++ b/validator_client/graffiti_file/src/lib.rs @@ -1,4 +1,5 @@ use serde::{Deserialize, Serialize}; +use slog::warn; use std::collections::HashMap; use std::fs::File; use std::io::{prelude::*, BufReader}; @@ -100,6 +101,27 @@ fn read_line(line: &str) -> Result<(Option, Graffiti), Error> { } } +// Given the various graffiti control methods, determine the graffiti that will be used for +// the next block produced by the validator with the given public key. +pub fn determine_graffiti( + validator_pubkey: &PublicKeyBytes, + log: &slog::Logger, + graffiti_file: Option, + validator_definition_graffiti: Option, + graffiti_flag: Option, +) -> Option { + graffiti_file + .and_then(|mut g| match g.load_graffiti(validator_pubkey) { + Ok(g) => g, + Err(e) => { + warn!(log, "Failed to read graffiti file"; "error" => ?e); + None + } + }) + .or(validator_definition_graffiti) + .or(graffiti_flag) +} + #[cfg(test)] mod tests { use super::*; diff --git a/validator_client/http_api/Cargo.toml b/validator_client/http_api/Cargo.toml new file mode 100644 index 0000000000..b83acdc782 --- /dev/null +++ b/validator_client/http_api/Cargo.toml @@ -0,0 +1,50 @@ +[package] +name = "validator_http_api" +version = "0.1.0" +edition = { workspace = true } +authors = ["Sigma Prime "] + +[lib] +name = "validator_http_api" +path = "src/lib.rs" + +[dependencies] +account_utils = { workspace = true } +bls = { workspace = true } +beacon_node_fallback = { workspace = true } +deposit_contract = { workspace = true } +doppelganger_service = { workspace = true } +graffiti_file = { workspace = true } +eth2 = { workspace = true } +eth2_keystore = { workspace = true } +ethereum_serde_utils = { workspace = true } +initialized_validators = { workspace = true } +lighthouse_version = { workspace = true } +logging = { workspace = true } +parking_lot = { workspace = true } +filesystem = { workspace = true } +rand = { workspace = true } +serde = { workspace = true } +signing_method = { workspace = true } +sensitive_url = { workspace = true } +slashing_protection = { workspace = true } +slog = { workspace = true } +slot_clock = { workspace = true } +sysinfo = { workspace = true } +system_health = { workspace = true } +task_executor = { workspace = true } +tempfile = { workspace = true } +tokio = { workspace = true } +tokio-stream = { workspace = true } +types = { workspace = true } +validator_dir = { workspace = true } +validator_store = { workspace = true } +validator_services = { workspace = true } +url = { workspace = true } +warp_utils = { workspace = true } +warp = { workspace = true } + +[dev-dependencies] +itertools = { workspace = true } +futures = { workspace = true } +rand = { workspace = true, features = ["small_rng"] } diff --git a/validator_client/src/http_api/api_secret.rs b/validator_client/http_api/src/api_secret.rs similarity index 100% rename from validator_client/src/http_api/api_secret.rs rename to validator_client/http_api/src/api_secret.rs diff --git a/validator_client/src/http_api/create_signed_voluntary_exit.rs b/validator_client/http_api/src/create_signed_voluntary_exit.rs similarity index 98% rename from validator_client/src/http_api/create_signed_voluntary_exit.rs rename to validator_client/http_api/src/create_signed_voluntary_exit.rs index a9586da57e..32269b202b 100644 --- a/validator_client/src/http_api/create_signed_voluntary_exit.rs +++ b/validator_client/http_api/src/create_signed_voluntary_exit.rs @@ -1,10 +1,10 @@ -use crate::validator_store::ValidatorStore; use bls::{PublicKey, PublicKeyBytes}; use eth2::types::GenericResponse; use slog::{info, Logger}; use slot_clock::SlotClock; use std::sync::Arc; use types::{Epoch, EthSpec, SignedVoluntaryExit, VoluntaryExit}; +use validator_store::ValidatorStore; pub async fn create_signed_voluntary_exit( pubkey: PublicKey, diff --git a/validator_client/src/http_api/create_validator.rs b/validator_client/http_api/src/create_validator.rs similarity index 99% rename from validator_client/src/http_api/create_validator.rs rename to validator_client/http_api/src/create_validator.rs index afa5d4fed1..dfd092e8b4 100644 --- a/validator_client/src/http_api/create_validator.rs +++ b/validator_client/http_api/src/create_validator.rs @@ -1,4 +1,3 @@ -use crate::ValidatorStore; use account_utils::validator_definitions::{PasswordStorage, ValidatorDefinition}; use account_utils::{ eth2_keystore::Keystore, @@ -11,6 +10,7 @@ use std::path::{Path, PathBuf}; use types::ChainSpec; use types::EthSpec; use validator_dir::{keystore_password_path, Builder as ValidatorDirBuilder}; +use validator_store::ValidatorStore; /// Create some validator EIP-2335 keystores and store them on disk. Then, enroll the validators in /// this validator client. diff --git a/validator_client/src/http_api/graffiti.rs b/validator_client/http_api/src/graffiti.rs similarity index 98% rename from validator_client/src/http_api/graffiti.rs rename to validator_client/http_api/src/graffiti.rs index 79d4fd61f3..86238a697c 100644 --- a/validator_client/src/http_api/graffiti.rs +++ b/validator_client/http_api/src/graffiti.rs @@ -1,8 +1,8 @@ -use crate::validator_store::ValidatorStore; use bls::PublicKey; use slot_clock::SlotClock; use std::sync::Arc; use types::{graffiti::GraffitiString, EthSpec, Graffiti}; +use validator_store::ValidatorStore; pub fn get_graffiti( validator_pubkey: PublicKey, diff --git a/validator_client/src/http_api/keystores.rs b/validator_client/http_api/src/keystores.rs similarity index 99% rename from validator_client/src/http_api/keystores.rs rename to validator_client/http_api/src/keystores.rs index e5477ff8df..5822c89cb8 100644 --- a/validator_client/src/http_api/keystores.rs +++ b/validator_client/http_api/src/keystores.rs @@ -1,8 +1,4 @@ //! Implementation of the standard keystore management API. -use crate::{ - initialized_validators::Error, signing_method::SigningMethod, InitializedValidators, - ValidatorStore, -}; use account_utils::{validator_definitions::PasswordStorage, ZeroizeString}; use eth2::lighthouse_vc::{ std_types::{ @@ -13,6 +9,8 @@ use eth2::lighthouse_vc::{ types::{ExportKeystoresResponse, SingleExportKeystoresResponse}, }; use eth2_keystore::Keystore; +use initialized_validators::{Error, InitializedValidators}; +use signing_method::SigningMethod; use slog::{info, warn, Logger}; use slot_clock::SlotClock; use std::path::PathBuf; @@ -21,6 +19,7 @@ use task_executor::TaskExecutor; use tokio::runtime::Handle; use types::{EthSpec, PublicKeyBytes}; use validator_dir::{keystore_password_path, Builder as ValidatorDirBuilder}; +use validator_store::ValidatorStore; use warp::Rejection; use warp_utils::reject::{custom_bad_request, custom_server_error}; diff --git a/validator_client/src/http_api/mod.rs b/validator_client/http_api/src/lib.rs similarity index 99% rename from validator_client/src/http_api/mod.rs rename to validator_client/http_api/src/lib.rs index ded25abbcd..b58c7ccec0 100644 --- a/validator_client/src/http_api/mod.rs +++ b/validator_client/http_api/src/lib.rs @@ -8,16 +8,18 @@ mod tests; pub mod test_utils; -use crate::beacon_node_fallback::CandidateInfo; -use crate::http_api::graffiti::{delete_graffiti, get_graffiti, set_graffiti}; +use graffiti::{delete_graffiti, get_graffiti, set_graffiti}; + +use create_signed_voluntary_exit::create_signed_voluntary_exit; +use graffiti_file::{determine_graffiti, GraffitiFile}; +use validator_store::ValidatorStore; -use crate::http_api::create_signed_voluntary_exit::create_signed_voluntary_exit; -use crate::{determine_graffiti, BlockService, GraffitiFile, ValidatorStore}; use account_utils::{ mnemonic_from_phrase, validator_definitions::{SigningDefinition, ValidatorDefinition, Web3SignerDefinition}, }; pub use api_secret::ApiSecret; +use beacon_node_fallback::CandidateInfo; use create_validator::{ create_validators_mnemonic, create_validators_web3signer, get_voting_password_storage, }; @@ -46,6 +48,7 @@ use task_executor::TaskExecutor; use tokio_stream::{wrappers::BroadcastStream, StreamExt}; use types::{ChainSpec, ConfigAndPreset, EthSpec}; use validator_dir::Builder as ValidatorDirBuilder; +use validator_services::block_service::BlockService; use warp::{sse::Event, Filter}; use warp_utils::task::blocking_json_task; diff --git a/validator_client/src/http_api/remotekeys.rs b/validator_client/http_api/src/remotekeys.rs similarity index 98% rename from validator_client/src/http_api/remotekeys.rs rename to validator_client/http_api/src/remotekeys.rs index 053bbcb4b2..289be57182 100644 --- a/validator_client/src/http_api/remotekeys.rs +++ b/validator_client/http_api/src/remotekeys.rs @@ -1,5 +1,4 @@ //! Implementation of the standard remotekey management API. -use crate::{initialized_validators::Error, InitializedValidators, ValidatorStore}; use account_utils::validator_definitions::{ SigningDefinition, ValidatorDefinition, Web3SignerDefinition, }; @@ -8,6 +7,7 @@ use eth2::lighthouse_vc::std_types::{ ImportRemotekeyStatus, ImportRemotekeysRequest, ImportRemotekeysResponse, ListRemotekeysResponse, SingleListRemotekeysResponse, Status, }; +use initialized_validators::{Error, InitializedValidators}; use slog::{info, warn, Logger}; use slot_clock::SlotClock; use std::sync::Arc; @@ -15,6 +15,7 @@ use task_executor::TaskExecutor; use tokio::runtime::Handle; use types::{EthSpec, PublicKeyBytes}; use url::Url; +use validator_store::ValidatorStore; use warp::Rejection; use warp_utils::reject::custom_server_error; diff --git a/validator_client/src/http_api/test_utils.rs b/validator_client/http_api/src/test_utils.rs similarity index 97% rename from validator_client/src/http_api/test_utils.rs rename to validator_client/http_api/src/test_utils.rs index 119c611553..931c4ea08e 100644 --- a/validator_client/src/http_api/test_utils.rs +++ b/validator_client/http_api/src/test_utils.rs @@ -1,21 +1,19 @@ -use crate::doppelganger_service::DoppelgangerService; -use crate::key_cache::{KeyCache, CACHE_FILENAME}; -use crate::{ - http_api::{ApiSecret, Config as HttpConfig, Context}, - initialized_validators::{InitializedValidators, OnDecryptFailure}, - Config, ValidatorDefinitions, ValidatorStore, -}; +use crate::{ApiSecret, Config as HttpConfig, Context}; +use account_utils::validator_definitions::ValidatorDefinitions; use account_utils::{ eth2_wallet::WalletBuilder, mnemonic_from_phrase, random_mnemonic, random_password, ZeroizeString, }; use deposit_contract::decode_eth1_tx_data; +use doppelganger_service::DoppelgangerService; use eth2::{ lighthouse_vc::{http_client::ValidatorClientHttpClient, types::*}, types::ErrorMessage as ApiErrorMessage, Error as ApiError, }; use eth2_keystore::KeystoreBuilder; +use initialized_validators::key_cache::{KeyCache, CACHE_FILENAME}; +use initialized_validators::{InitializedValidators, OnDecryptFailure}; use logging::test_logger; use parking_lot::RwLock; use sensitive_url::SensitiveUrl; @@ -29,6 +27,7 @@ use std::time::Duration; use task_executor::test_utils::TestRuntime; use tempfile::{tempdir, TempDir}; use tokio::sync::oneshot; +use validator_store::{Config as ValidatorStoreConfig, ValidatorStore}; pub const PASSWORD_BYTES: &[u8] = &[42, 50, 37]; pub const TEST_DEFAULT_FEE_RECIPIENT: Address = Address::repeat_byte(42); @@ -89,16 +88,14 @@ impl ApiTester { let api_secret = ApiSecret::create_or_open(validator_dir.path()).unwrap(); let api_pubkey = api_secret.api_token(); - let config = Config { - validator_dir: validator_dir.path().into(), - secrets_dir: secrets_dir.path().into(), + let config = ValidatorStoreConfig { fee_recipient: Some(TEST_DEFAULT_FEE_RECIPIENT), ..Default::default() }; let spec = Arc::new(E::default_spec()); - let slashing_db_path = config.validator_dir.join(SLASHING_PROTECTION_FILENAME); + let slashing_db_path = validator_dir.path().join(SLASHING_PROTECTION_FILENAME); let slashing_protection = SlashingDatabase::open_or_create(&slashing_db_path).unwrap(); let slot_clock = diff --git a/validator_client/src/http_api/tests.rs b/validator_client/http_api/src/tests.rs similarity index 97% rename from validator_client/src/http_api/tests.rs rename to validator_client/http_api/src/tests.rs index ba3b7f685b..76a6952153 100644 --- a/validator_client/src/http_api/tests.rs +++ b/validator_client/http_api/src/tests.rs @@ -3,15 +3,13 @@ mod keystores; -use crate::doppelganger_service::DoppelgangerService; -use crate::{ - http_api::{ApiSecret, Config as HttpConfig, Context}, - initialized_validators::InitializedValidators, - Config, ValidatorDefinitions, ValidatorStore, -}; +use doppelganger_service::DoppelgangerService; +use initialized_validators::{Config as InitializedValidatorsConfig, InitializedValidators}; + +use crate::{ApiSecret, Config as HttpConfig, Context}; use account_utils::{ eth2_wallet::WalletBuilder, mnemonic_from_phrase, random_mnemonic, random_password, - random_password_string, ZeroizeString, + random_password_string, validator_definitions::ValidatorDefinitions, ZeroizeString, }; use deposit_contract::decode_eth1_tx_data; use eth2::{ @@ -34,6 +32,7 @@ use std::time::Duration; use task_executor::test_utils::TestRuntime; use tempfile::{tempdir, TempDir}; use types::graffiti::GraffitiString; +use validator_store::{Config as ValidatorStoreConfig, ValidatorStore}; const PASSWORD_BYTES: &[u8] = &[42, 50, 37]; pub const TEST_DEFAULT_FEE_RECIPIENT: Address = Address::repeat_byte(42); @@ -47,17 +46,18 @@ struct ApiTester { url: SensitiveUrl, slot_clock: TestingSlotClock, _validator_dir: TempDir, + _secrets_dir: TempDir, _test_runtime: TestRuntime, } impl ApiTester { pub async fn new() -> Self { - let mut config = Config::default(); + let mut config = ValidatorStoreConfig::default(); config.fee_recipient = Some(TEST_DEFAULT_FEE_RECIPIENT); Self::new_with_config(config).await } - pub async fn new_with_config(mut config: Config) -> Self { + pub async fn new_with_config(config: ValidatorStoreConfig) -> Self { let log = test_logger(); let validator_dir = tempdir().unwrap(); @@ -68,7 +68,7 @@ impl ApiTester { let initialized_validators = InitializedValidators::from_definitions( validator_defs, validator_dir.path().into(), - Config::default(), + InitializedValidatorsConfig::default(), log.clone(), ) .await @@ -77,12 +77,9 @@ impl ApiTester { let api_secret = ApiSecret::create_or_open(validator_dir.path()).unwrap(); let api_pubkey = api_secret.api_token(); - config.validator_dir = validator_dir.path().into(); - config.secrets_dir = secrets_dir.path().into(); - let spec = Arc::new(E::default_spec()); - let slashing_db_path = config.validator_dir.join(SLASHING_PROTECTION_FILENAME); + let slashing_db_path = validator_dir.path().join(SLASHING_PROTECTION_FILENAME); let slashing_protection = SlashingDatabase::open_or_create(&slashing_db_path).unwrap(); let genesis_time: u64 = 0; @@ -157,6 +154,7 @@ impl ApiTester { url, slot_clock, _validator_dir: validator_dir, + _secrets_dir: secrets_dir, _test_runtime: test_runtime, } } @@ -1147,11 +1145,11 @@ async fn validator_builder_boost_factor() { /// `prefer_builder_proposals` and `builder_boost_factor` values. #[tokio::test] async fn validator_derived_builder_boost_factor_with_process_defaults() { - let config = Config { + let config = ValidatorStoreConfig { builder_proposals: true, prefer_builder_proposals: false, builder_boost_factor: Some(80), - ..Config::default() + ..ValidatorStoreConfig::default() }; ApiTester::new_with_config(config) .await @@ -1181,11 +1179,11 @@ async fn validator_derived_builder_boost_factor_with_process_defaults() { #[tokio::test] async fn validator_builder_boost_factor_global_builder_proposals_true() { - let config = Config { + let config = ValidatorStoreConfig { builder_proposals: true, prefer_builder_proposals: false, builder_boost_factor: None, - ..Config::default() + ..ValidatorStoreConfig::default() }; ApiTester::new_with_config(config) .await @@ -1194,11 +1192,11 @@ async fn validator_builder_boost_factor_global_builder_proposals_true() { #[tokio::test] async fn validator_builder_boost_factor_global_builder_proposals_false() { - let config = Config { + let config = ValidatorStoreConfig { builder_proposals: false, prefer_builder_proposals: false, builder_boost_factor: None, - ..Config::default() + ..ValidatorStoreConfig::default() }; ApiTester::new_with_config(config) .await @@ -1207,11 +1205,11 @@ async fn validator_builder_boost_factor_global_builder_proposals_false() { #[tokio::test] async fn validator_builder_boost_factor_global_prefer_builder_proposals_true() { - let config = Config { + let config = ValidatorStoreConfig { builder_proposals: true, prefer_builder_proposals: true, builder_boost_factor: None, - ..Config::default() + ..ValidatorStoreConfig::default() }; ApiTester::new_with_config(config) .await @@ -1220,11 +1218,11 @@ async fn validator_builder_boost_factor_global_prefer_builder_proposals_true() { #[tokio::test] async fn validator_builder_boost_factor_global_prefer_builder_proposals_true_override() { - let config = Config { + let config = ValidatorStoreConfig { builder_proposals: false, prefer_builder_proposals: true, builder_boost_factor: None, - ..Config::default() + ..ValidatorStoreConfig::default() }; ApiTester::new_with_config(config) .await diff --git a/validator_client/src/http_api/tests/keystores.rs b/validator_client/http_api/src/tests/keystores.rs similarity index 99% rename from validator_client/src/http_api/tests/keystores.rs rename to validator_client/http_api/src/tests/keystores.rs index b6923d1c78..f3f6de548b 100644 --- a/validator_client/src/http_api/tests/keystores.rs +++ b/validator_client/http_api/src/tests/keystores.rs @@ -1,4 +1,3 @@ -use super::super::super::validator_store::DEFAULT_GAS_LIMIT; use super::*; use account_utils::random_password_string; use bls::PublicKeyBytes; @@ -14,6 +13,7 @@ use slashing_protection::interchange::{Interchange, InterchangeMetadata}; use std::{collections::HashMap, path::Path}; use tokio::runtime::Handle; use types::{attestation::AttestationBase, Address}; +use validator_store::DEFAULT_GAS_LIMIT; fn new_keystore(password: ZeroizeString) -> Keystore { let keypair = Keypair::random(); diff --git a/validator_client/http_metrics/Cargo.toml b/validator_client/http_metrics/Cargo.toml new file mode 100644 index 0000000000..a9de26a55b --- /dev/null +++ b/validator_client/http_metrics/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "validator_http_metrics" +version = "0.1.0" +edition = { workspace = true } +authors = ["Sigma Prime "] + +[dependencies] +malloc_utils = { workspace = true } +slot_clock = { workspace = true } +metrics = { workspace = true } +parking_lot = { workspace = true } +serde = { workspace = true } +slog = { workspace = true } +warp_utils = { workspace = true } +warp = { workspace = true } +lighthouse_version = { workspace = true } +validator_services = { workspace = true } +validator_store = { workspace = true } +validator_metrics = { workspace = true } +types = { workspace = true } diff --git a/validator_client/src/http_metrics/mod.rs b/validator_client/http_metrics/src/lib.rs similarity index 68% rename from validator_client/src/http_metrics/mod.rs rename to validator_client/http_metrics/src/lib.rs index 67cab2bdc3..984b752e5a 100644 --- a/validator_client/src/http_metrics/mod.rs +++ b/validator_client/http_metrics/src/lib.rs @@ -1,18 +1,20 @@ //! This crate provides a HTTP server that is solely dedicated to serving the `/metrics` endpoint. //! //! For other endpoints, see the `http_api` crate. -pub mod metrics; -use crate::{DutiesService, ValidatorStore}; use lighthouse_version::version_with_platform; +use malloc_utils::scrape_allocator_metrics; use parking_lot::RwLock; use serde::{Deserialize, Serialize}; use slog::{crit, info, Logger}; -use slot_clock::SystemTimeSlotClock; +use slot_clock::{SlotClock, SystemTimeSlotClock}; use std::future::Future; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::sync::Arc; +use std::time::{SystemTime, UNIX_EPOCH}; use types::EthSpec; +use validator_services::duties_service::DutiesService; +use validator_store::ValidatorStore; use warp::{http::Response, Filter}; #[derive(Debug)] @@ -120,7 +122,7 @@ pub fn serve( .map(move || inner_ctx.clone()) .and_then(|ctx: Arc>| async move { Ok::<_, warp::Rejection>( - metrics::gather_prometheus_metrics(&ctx) + gather_prometheus_metrics(&ctx) .map(|body| { Response::builder() .status(200) @@ -156,3 +158,59 @@ pub fn serve( Ok((listening_socket, server)) } + +pub fn gather_prometheus_metrics( + ctx: &Context, +) -> std::result::Result { + use validator_metrics::*; + let mut buffer = vec![]; + let encoder = TextEncoder::new(); + + { + let shared = ctx.shared.read(); + + if let Some(genesis_time) = shared.genesis_time { + if let Ok(now) = SystemTime::now().duration_since(UNIX_EPOCH) { + let distance = now.as_secs() as i64 - genesis_time as i64; + set_gauge(&GENESIS_DISTANCE, distance); + } + } + + if let Some(duties_service) = &shared.duties_service { + if let Some(slot) = duties_service.slot_clock.now() { + let current_epoch = slot.epoch(E::slots_per_epoch()); + let next_epoch = current_epoch + 1; + + set_int_gauge( + &PROPOSER_COUNT, + &[CURRENT_EPOCH], + duties_service.proposer_count(current_epoch) as i64, + ); + set_int_gauge( + &ATTESTER_COUNT, + &[CURRENT_EPOCH], + duties_service.attester_count(current_epoch) as i64, + ); + set_int_gauge( + &ATTESTER_COUNT, + &[NEXT_EPOCH], + duties_service.attester_count(next_epoch) as i64, + ); + } + } + } + + // It's important to ensure these metrics are explicitly enabled in the case that users aren't + // using glibc and this function causes panics. + if ctx.config.allocator_metrics_enabled { + scrape_allocator_metrics(); + } + + warp_utils::metrics::scrape_health_metrics(); + + encoder + .encode(&metrics::gather(), &mut buffer) + .map_err(|e| format!("{e:?}"))?; + + String::from_utf8(buffer).map_err(|e| format!("Failed to encode prometheus info: {:?}", e)) +} diff --git a/validator_client/initialized_validators/Cargo.toml b/validator_client/initialized_validators/Cargo.toml new file mode 100644 index 0000000000..426cb303f6 --- /dev/null +++ b/validator_client/initialized_validators/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "initialized_validators" +version = "0.1.0" +edition = { workspace = true } +authors = ["Sigma Prime "] + +[dependencies] +signing_method = { workspace = true } +account_utils = { workspace = true } +eth2_keystore = { workspace = true } +metrics = { workspace = true } +lockfile = { workspace = true } +parking_lot = { workspace = true } +reqwest = { workspace = true } +slog = { workspace = true } +types = { workspace = true } +url = { workspace = true } +validator_dir = { workspace = true } +rand = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +bls = { workspace = true } +tokio = { workspace = true } +bincode = { workspace = true } +filesystem = { workspace = true } +validator_metrics = { workspace = true } diff --git a/validator_client/src/key_cache.rs b/validator_client/initialized_validators/src/key_cache.rs similarity index 100% rename from validator_client/src/key_cache.rs rename to validator_client/initialized_validators/src/key_cache.rs diff --git a/validator_client/src/initialized_validators.rs b/validator_client/initialized_validators/src/lib.rs similarity index 98% rename from validator_client/src/initialized_validators.rs rename to validator_client/initialized_validators/src/lib.rs index 0ef9a6a13d..0b36dbd62c 100644 --- a/validator_client/src/initialized_validators.rs +++ b/validator_client/initialized_validators/src/lib.rs @@ -6,7 +6,8 @@ //! The `InitializedValidators` struct in this file serves as the source-of-truth of which //! validators are managed by this validator client. -use crate::signing_method::SigningMethod; +pub mod key_cache; + use account_utils::{ read_password, read_password_from_user, read_password_string, validator_definitions::{ @@ -20,6 +21,8 @@ use lockfile::{Lockfile, LockfileError}; use metrics::set_gauge; use parking_lot::{MappedMutexGuard, Mutex, MutexGuard}; use reqwest::{Certificate, Client, Error as ReqwestError, Identity}; +use serde::{Deserialize, Serialize}; +use signing_method::SigningMethod; use slog::{debug, error, info, warn, Logger}; use std::collections::{HashMap, HashSet}; use std::fs::{self, File}; @@ -32,9 +35,7 @@ use types::{Address, Graffiti, Keypair, PublicKey, PublicKeyBytes}; use url::{ParseError, Url}; use validator_dir::Builder as ValidatorDirBuilder; -use crate::key_cache; -use crate::key_cache::KeyCache; -use crate::Config; +use key_cache::KeyCache; /// Default timeout for a request to a remote signer for a signature. /// @@ -45,6 +46,24 @@ const DEFAULT_REMOTE_SIGNER_REQUEST_TIMEOUT: Duration = Duration::from_secs(12); // Use TTY instead of stdin to capture passwords from users. const USE_STDIN: bool = false; +pub const DEFAULT_WEB3SIGNER_KEEP_ALIVE: Option = Some(Duration::from_secs(20)); + +// The configuration for initialised validators. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Config { + pub web3_signer_keep_alive_timeout: Option, + pub web3_signer_max_idle_connections: Option, +} + +impl Default for Config { + fn default() -> Self { + Config { + web3_signer_keep_alive_timeout: DEFAULT_WEB3SIGNER_KEEP_ALIVE, + web3_signer_max_idle_connections: None, + } + } +} + pub enum OnDecryptFailure { /// If the key cache fails to decrypt, create a new cache. CreateNew, @@ -1194,7 +1213,7 @@ impl InitializedValidators { /// A validator is considered "already known" and skipped if the public key is already known. /// I.e., if there are two different definitions with the same public key then the second will /// be ignored. - pub(crate) async fn update_validators(&mut self) -> Result<(), Error> { + pub async fn update_validators(&mut self) -> Result<(), Error> { //use key cache if available let mut key_stores = HashMap::new(); @@ -1380,11 +1399,11 @@ impl InitializedValidators { // Update the enabled and total validator counts set_gauge( - &crate::http_metrics::metrics::ENABLED_VALIDATORS_COUNT, + &validator_metrics::ENABLED_VALIDATORS_COUNT, self.num_enabled() as i64, ); set_gauge( - &crate::http_metrics::metrics::TOTAL_VALIDATORS_COUNT, + &validator_metrics::TOTAL_VALIDATORS_COUNT, self.num_total() as i64, ); Ok(()) diff --git a/validator_client/signing_method/Cargo.toml b/validator_client/signing_method/Cargo.toml new file mode 100644 index 0000000000..0f3852eff6 --- /dev/null +++ b/validator_client/signing_method/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "signing_method" +version = "0.1.0" +edition = { workspace = true } +authors = ["Sigma Prime "] + +[dependencies] +eth2_keystore = { workspace = true } +lockfile = { workspace = true } +parking_lot = { workspace = true } +reqwest = { workspace = true } +task_executor = { workspace = true } +types = { workspace = true } +url = { workspace = true } +validator_metrics = { workspace = true } +serde = { workspace = true } +ethereum_serde_utils = { workspace = true } diff --git a/validator_client/src/signing_method.rs b/validator_client/signing_method/src/lib.rs similarity index 96% rename from validator_client/src/signing_method.rs rename to validator_client/signing_method/src/lib.rs index d89c9b8229..2fe4af39d3 100644 --- a/validator_client/src/signing_method.rs +++ b/validator_client/signing_method/src/lib.rs @@ -3,7 +3,6 @@ //! - Via a local `Keypair`. //! - Via a remote signer (Web3Signer) -use crate::http_metrics::metrics; use eth2_keystore::Keystore; use lockfile::Lockfile; use parking_lot::Mutex; @@ -166,8 +165,10 @@ impl SigningMethod { ) -> Result { match self { SigningMethod::LocalKeystore { voting_keypair, .. } => { - let _timer = - metrics::start_timer_vec(&metrics::SIGNING_TIMES, &[metrics::LOCAL_KEYSTORE]); + let _timer = validator_metrics::start_timer_vec( + &validator_metrics::SIGNING_TIMES, + &[validator_metrics::LOCAL_KEYSTORE], + ); let voting_keypair = voting_keypair.clone(); // Spawn a blocking task to produce the signature. This avoids blocking the core @@ -187,8 +188,10 @@ impl SigningMethod { http_client, .. } => { - let _timer = - metrics::start_timer_vec(&metrics::SIGNING_TIMES, &[metrics::WEB3SIGNER]); + let _timer = validator_metrics::start_timer_vec( + &validator_metrics::SIGNING_TIMES, + &[validator_metrics::WEB3SIGNER], + ); // Map the message into a Web3Signer type. let object = match signable_message { diff --git a/validator_client/src/signing_method/web3signer.rs b/validator_client/signing_method/src/web3signer.rs similarity index 100% rename from validator_client/src/signing_method/web3signer.rs rename to validator_client/signing_method/src/web3signer.rs diff --git a/validator_client/src/check_synced.rs b/validator_client/src/check_synced.rs deleted file mode 100644 index 2e9a62ff65..0000000000 --- a/validator_client/src/check_synced.rs +++ /dev/null @@ -1,27 +0,0 @@ -use crate::beacon_node_fallback::CandidateError; -use eth2::{types::Slot, BeaconNodeHttpClient}; -use slog::{warn, Logger}; - -pub async fn check_node_health( - beacon_node: &BeaconNodeHttpClient, - log: &Logger, -) -> Result<(Slot, bool, bool), CandidateError> { - let resp = match beacon_node.get_node_syncing().await { - Ok(resp) => resp, - Err(e) => { - warn!( - log, - "Unable connect to beacon node"; - "error" => %e - ); - - return Err(CandidateError::Offline); - } - }; - - Ok(( - resp.data.head_slot, - resp.data.is_optimistic, - resp.data.el_offline, - )) -} diff --git a/validator_client/src/config.rs b/validator_client/src/config.rs index f42ed55146..abdadeb393 100644 --- a/validator_client/src/config.rs +++ b/validator_client/src/config.rs @@ -1,8 +1,4 @@ -use crate::beacon_node_fallback::ApiTopic; -use crate::graffiti_file::GraffitiFile; -use crate::{ - beacon_node_fallback, beacon_node_health::BeaconNodeSyncDistanceTiers, http_api, http_metrics, -}; +use beacon_node_fallback::{beacon_node_health::BeaconNodeSyncDistanceTiers, ApiTopic}; use clap::ArgMatches; use clap_utils::{flags::DISABLE_MALLOC_TUNING_FLAG, parse_optional, parse_required}; use directory::{ @@ -10,6 +6,8 @@ use directory::{ DEFAULT_VALIDATOR_DIR, }; use eth2::types::Graffiti; +use graffiti_file::GraffitiFile; +use initialized_validators::Config as InitializedValidatorsConfig; use sensitive_url::SensitiveUrl; use serde::{Deserialize, Serialize}; use slog::{info, warn, Logger}; @@ -19,13 +17,18 @@ use std::path::PathBuf; use std::str::FromStr; use std::time::Duration; use types::{Address, GRAFFITI_BYTES_LEN}; +use validator_http_api; +use validator_http_metrics; +use validator_store::Config as ValidatorStoreConfig; pub const DEFAULT_BEACON_NODE: &str = "http://localhost:5052/"; -pub const DEFAULT_WEB3SIGNER_KEEP_ALIVE: Option = Some(Duration::from_secs(20)); /// Stores the core configuration for this validator instance. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Config { + /// Configuration parameters for the validator store. + #[serde(flatten)] + pub validator_store: ValidatorStoreConfig, /// The data directory, which stores all validator databases pub validator_dir: PathBuf, /// The directory containing the passwords to unlock validator keystores. @@ -49,12 +52,10 @@ pub struct Config { pub graffiti: Option, /// Graffiti file to load per validator graffitis. pub graffiti_file: Option, - /// Fallback fallback address. - pub fee_recipient: Option
, /// Configuration for the HTTP REST API. - pub http_api: http_api::Config, + pub http_api: validator_http_api::Config, /// Configuration for the HTTP REST API. - pub http_metrics: http_metrics::Config, + pub http_metrics: validator_http_metrics::Config, /// Configuration for the Beacon Node fallback. pub beacon_node_fallback: beacon_node_fallback::Config, /// Configuration for sending metrics to a remote explorer endpoint. @@ -68,11 +69,7 @@ pub struct Config { /// (<= 64 validators) pub enable_high_validator_count_metrics: bool, /// Enable use of the blinded block endpoints during proposals. - pub builder_proposals: bool, - /// Overrides the timestamp field in builder api ValidatorRegistrationV1 pub builder_registration_timestamp_override: Option, - /// Fallback gas limit. - pub gas_limit: Option, /// A list of custom certificates that the validator client will additionally use when /// connecting to a beacon node over SSL/TLS. pub beacon_nodes_tls_certs: Option>, @@ -82,16 +79,11 @@ pub struct Config { pub enable_latency_measurement_service: bool, /// Defines the number of validators per `validator/register_validator` request sent to the BN. pub validator_registration_batch_size: usize, - /// Enable slashing protection even while using web3signer keys. - pub enable_web3signer_slashing_protection: bool, - /// Specifies the boost factor, a percentage multiplier to apply to the builder's payload value. - pub builder_boost_factor: Option, - /// If true, Lighthouse will prefer builder proposals, if available. - pub prefer_builder_proposals: bool, /// Whether we are running with distributed network support. pub distributed: bool, - pub web3_signer_keep_alive_timeout: Option, - pub web3_signer_max_idle_connections: Option, + /// Configuration for the initialized validators + #[serde(flatten)] + pub initialized_validators: InitializedValidatorsConfig, } impl Default for Config { @@ -109,6 +101,7 @@ impl Default for Config { let beacon_nodes = vec![SensitiveUrl::parse(DEFAULT_BEACON_NODE) .expect("beacon_nodes must always be a valid url.")]; Self { + validator_store: ValidatorStoreConfig::default(), validator_dir, secrets_dir, beacon_nodes, @@ -119,7 +112,6 @@ impl Default for Config { use_long_timeouts: false, graffiti: None, graffiti_file: None, - fee_recipient: None, http_api: <_>::default(), http_metrics: <_>::default(), beacon_node_fallback: <_>::default(), @@ -127,18 +119,12 @@ impl Default for Config { enable_doppelganger_protection: false, enable_high_validator_count_metrics: false, beacon_nodes_tls_certs: None, - builder_proposals: false, builder_registration_timestamp_override: None, - gas_limit: None, broadcast_topics: vec![ApiTopic::Subscriptions], enable_latency_measurement_service: true, validator_registration_batch_size: 500, - enable_web3signer_slashing_protection: true, - builder_boost_factor: None, - prefer_builder_proposals: false, distributed: false, - web3_signer_keep_alive_timeout: DEFAULT_WEB3SIGNER_KEEP_ALIVE, - web3_signer_max_idle_connections: None, + initialized_validators: <_>::default(), } } } @@ -233,7 +219,7 @@ impl Config { if let Some(input_fee_recipient) = parse_optional::
(cli_args, "suggested-fee-recipient")? { - config.fee_recipient = Some(input_fee_recipient); + config.validator_store.fee_recipient = Some(input_fee_recipient); } if let Some(tls_certs) = parse_optional::(cli_args, "beacon-nodes-tls-certs")? { @@ -270,7 +256,7 @@ impl Config { * Web3 signer */ if let Some(s) = parse_optional::(cli_args, "web3-signer-keep-alive-timeout")? { - config.web3_signer_keep_alive_timeout = if s == "null" { + config.initialized_validators.web3_signer_keep_alive_timeout = if s == "null" { None } else { Some(Duration::from_millis( @@ -279,7 +265,9 @@ impl Config { } } if let Some(n) = parse_optional::(cli_args, "web3-signer-max-idle-connections")? { - config.web3_signer_max_idle_connections = Some(n); + config + .initialized_validators + .web3_signer_max_idle_connections = Some(n); } /* @@ -382,14 +370,14 @@ impl Config { } if cli_args.get_flag("builder-proposals") { - config.builder_proposals = true; + config.validator_store.builder_proposals = true; } if cli_args.get_flag("prefer-builder-proposals") { - config.prefer_builder_proposals = true; + config.validator_store.prefer_builder_proposals = true; } - config.gas_limit = cli_args + config.validator_store.gas_limit = cli_args .get_one::("gas-limit") .map(|gas_limit| { gas_limit @@ -408,7 +396,8 @@ impl Config { ); } - config.builder_boost_factor = parse_optional(cli_args, "builder-boost-factor")?; + config.validator_store.builder_boost_factor = + parse_optional(cli_args, "builder-boost-factor")?; config.enable_latency_measurement_service = !cli_args.get_flag("disable-latency-measurement-service"); @@ -419,7 +408,7 @@ impl Config { return Err("validator-registration-batch-size cannot be 0".to_string()); } - config.enable_web3signer_slashing_protection = + config.validator_store.enable_web3signer_slashing_protection = if cli_args.get_flag("disable-slashing-protection-web3signer") { warn!( log, diff --git a/validator_client/src/latency.rs b/validator_client/src/latency.rs index 7e752f2923..22f02c7c0b 100644 --- a/validator_client/src/latency.rs +++ b/validator_client/src/latency.rs @@ -1,4 +1,4 @@ -use crate::{http_metrics::metrics, BeaconNodeFallback}; +use beacon_node_fallback::BeaconNodeFallback; use environment::RuntimeContext; use slog::debug; use slot_clock::SlotClock; @@ -44,14 +44,14 @@ pub fn start_latency_service( "node" => &measurement.beacon_node_id, "latency" => latency.as_millis(), ); - metrics::observe_timer_vec( - &metrics::VC_BEACON_NODE_LATENCY, + validator_metrics::observe_timer_vec( + &validator_metrics::VC_BEACON_NODE_LATENCY, &[&measurement.beacon_node_id], latency, ); if i == 0 { - metrics::observe_duration( - &metrics::VC_BEACON_NODE_LATENCY_PRIMARY_ENDPOINT, + validator_metrics::observe_duration( + &validator_metrics::VC_BEACON_NODE_LATENCY_PRIMARY_ENDPOINT, latency, ); } diff --git a/validator_client/src/lib.rs b/validator_client/src/lib.rs index 05ec1e53aa..2cc22357fb 100644 --- a/validator_client/src/lib.rs +++ b/validator_client/src/lib.rs @@ -1,52 +1,28 @@ -mod attestation_service; -mod beacon_node_fallback; -mod beacon_node_health; -mod block_service; -mod check_synced; mod cli; -mod duties_service; -mod graffiti_file; -mod http_metrics; -mod key_cache; +pub mod config; mod latency; mod notifier; -mod preparation_service; -mod signing_method; -mod sync_committee_service; -pub mod config; -mod doppelganger_service; -pub mod http_api; -pub mod initialized_validators; -pub mod validator_store; - -pub use beacon_node_fallback::ApiTopic; -pub use beacon_node_health::BeaconNodeSyncDistanceTiers; pub use cli::cli_app; pub use config::Config; use initialized_validators::InitializedValidators; use metrics::set_gauge; use monitoring_api::{MonitoringHttpClient, ProcessType}; use sensitive_url::SensitiveUrl; -pub use slashing_protection::{SlashingDatabase, SLASHING_PROTECTION_FILENAME}; +use slashing_protection::{SlashingDatabase, SLASHING_PROTECTION_FILENAME}; -use crate::beacon_node_fallback::{ +use beacon_node_fallback::{ start_fallback_updater_service, BeaconNodeFallback, CandidateBeaconNode, }; -use crate::doppelganger_service::DoppelgangerService; -use crate::graffiti_file::GraffitiFile; -use crate::initialized_validators::Error::UnableToOpenVotingKeystore; + use account_utils::validator_definitions::ValidatorDefinitions; -use attestation_service::{AttestationService, AttestationServiceBuilder}; -use block_service::{BlockService, BlockServiceBuilder}; use clap::ArgMatches; -use duties_service::{sync::SyncDutiesMap, DutiesService}; +use doppelganger_service::DoppelgangerService; use environment::RuntimeContext; -use eth2::{reqwest::ClientBuilder, types::Graffiti, BeaconNodeHttpClient, StatusCode, Timeouts}; -use http_api::ApiSecret; +use eth2::{reqwest::ClientBuilder, BeaconNodeHttpClient, StatusCode, Timeouts}; +use initialized_validators::Error::UnableToOpenVotingKeystore; use notifier::spawn_notifier; use parking_lot::RwLock; -use preparation_service::{PreparationService, PreparationServiceBuilder}; use reqwest::Certificate; use slog::{debug, error, info, warn, Logger}; use slot_clock::SlotClock; @@ -58,12 +34,20 @@ use std::net::SocketAddr; use std::path::Path; use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; -use sync_committee_service::SyncCommitteeService; use tokio::{ sync::mpsc, time::{sleep, Duration}, }; -use types::{EthSpec, Hash256, PublicKeyBytes}; +use types::{EthSpec, Hash256}; +use validator_http_api::ApiSecret; +use validator_services::{ + attestation_service::{AttestationService, AttestationServiceBuilder}, + block_service::{BlockService, BlockServiceBuilder}, + duties_service::{self, DutiesService}, + preparation_service::{PreparationService, PreparationServiceBuilder}, + sync::SyncDutiesMap, + sync_committee_service::SyncCommitteeService, +}; use validator_store::ValidatorStore; /// The interval between attempts to contact the beacon node during startup. @@ -152,22 +136,23 @@ impl ProductionValidatorClient { ); // Optionally start the metrics server. - let http_metrics_ctx = if config.http_metrics.enabled { - let shared = http_metrics::Shared { + let validator_metrics_ctx = if config.http_metrics.enabled { + let shared = validator_http_metrics::Shared { validator_store: None, genesis_time: None, duties_service: None, }; - let ctx: Arc> = Arc::new(http_metrics::Context { - config: config.http_metrics.clone(), - shared: RwLock::new(shared), - log: log.clone(), - }); + let ctx: Arc> = + Arc::new(validator_http_metrics::Context { + config: config.http_metrics.clone(), + shared: RwLock::new(shared), + log: log.clone(), + }); let exit = context.executor.exit(); - let (_listen_addr, server) = http_metrics::serve(ctx.clone(), exit) + let (_listen_addr, server) = validator_http_metrics::serve(ctx.clone(), exit) .map_err(|e| format!("Unable to start metrics API server: {:?}", e))?; context @@ -215,7 +200,7 @@ impl ProductionValidatorClient { let validators = InitializedValidators::from_definitions( validator_defs, config.validator_dir.clone(), - config.clone(), + config.initialized_validators.clone(), log.clone(), ) .await @@ -384,20 +369,20 @@ impl ProductionValidatorClient { // Set the count for beacon node fallbacks excluding the primary beacon node. set_gauge( - &http_metrics::metrics::ETH2_FALLBACK_CONFIGURED, + &validator_metrics::ETH2_FALLBACK_CONFIGURED, num_nodes.saturating_sub(1) as i64, ); // Set the total beacon node count. set_gauge( - &http_metrics::metrics::TOTAL_BEACON_NODES_COUNT, + &validator_metrics::TOTAL_BEACON_NODES_COUNT, num_nodes as i64, ); // Initialize the number of connected, synced beacon nodes to 0. - set_gauge(&http_metrics::metrics::ETH2_FALLBACK_CONNECTED, 0); - set_gauge(&http_metrics::metrics::SYNCED_BEACON_NODES_COUNT, 0); + set_gauge(&validator_metrics::ETH2_FALLBACK_CONNECTED, 0); + set_gauge(&validator_metrics::SYNCED_BEACON_NODES_COUNT, 0); // Initialize the number of connected, avaliable beacon nodes to 0. - set_gauge(&http_metrics::metrics::AVAILABLE_BEACON_NODES_COUNT, 0); + set_gauge(&validator_metrics::AVAILABLE_BEACON_NODES_COUNT, 0); let mut beacon_nodes: BeaconNodeFallback<_, E> = BeaconNodeFallback::new( candidates, @@ -422,7 +407,7 @@ impl ProductionValidatorClient { }; // Update the metrics server. - if let Some(ctx) = &http_metrics_ctx { + if let Some(ctx) = &validator_metrics_ctx { ctx.shared.write().genesis_time = Some(genesis_time); } @@ -459,7 +444,7 @@ impl ProductionValidatorClient { context.eth2_config.spec.clone(), doppelganger_service.clone(), slot_clock.clone(), - &config, + &config.validator_store, context.executor.clone(), log.clone(), )); @@ -496,7 +481,7 @@ impl ProductionValidatorClient { }); // Update the metrics server. - if let Some(ctx) = &http_metrics_ctx { + if let Some(ctx) = &validator_metrics_ctx { ctx.shared.write().validator_store = Some(validator_store.clone()); ctx.shared.write().duties_service = Some(duties_service.clone()); } @@ -569,7 +554,7 @@ impl ProductionValidatorClient { let api_secret = ApiSecret::create_or_open(&self.config.validator_dir)?; self.http_api_listen_addr = if self.config.http_api.enabled { - let ctx = Arc::new(http_api::Context { + let ctx = Arc::new(validator_http_api::Context { task_executor: self.context.executor.clone(), api_secret, block_service: Some(self.block_service.clone()), @@ -588,7 +573,7 @@ impl ProductionValidatorClient { let exit = self.context.executor.exit(); - let (listen_addr, server) = http_api::serve(ctx, exit) + let (listen_addr, server) = validator_http_api::serve(ctx, exit) .map_err(|e| format!("Unable to start HTTP API server: {:?}", e))?; self.context @@ -850,24 +835,3 @@ pub fn load_pem_certificate>(pem_path: P) -> Result, - validator_definition_graffiti: Option, - graffiti_flag: Option, -) -> Option { - graffiti_file - .and_then(|mut g| match g.load_graffiti(validator_pubkey) { - Ok(g) => g, - Err(e) => { - warn!(log, "Failed to read graffiti file"; "error" => ?e); - None - } - }) - .or(validator_definition_graffiti) - .or(graffiti_flag) -} diff --git a/validator_client/src/notifier.rs b/validator_client/src/notifier.rs index cda13a5e63..ff66517795 100644 --- a/validator_client/src/notifier.rs +++ b/validator_client/src/notifier.rs @@ -1,4 +1,3 @@ -use crate::http_metrics; use crate::{DutiesService, ProductionValidatorClient}; use metrics::set_gauge; use slog::{debug, error, info, Logger}; @@ -45,15 +44,15 @@ async fn notify( let num_synced_fallback = num_synced.saturating_sub(1); set_gauge( - &http_metrics::metrics::AVAILABLE_BEACON_NODES_COUNT, + &validator_metrics::AVAILABLE_BEACON_NODES_COUNT, num_available as i64, ); set_gauge( - &http_metrics::metrics::SYNCED_BEACON_NODES_COUNT, + &validator_metrics::SYNCED_BEACON_NODES_COUNT, num_synced as i64, ); set_gauge( - &http_metrics::metrics::TOTAL_BEACON_NODES_COUNT, + &validator_metrics::TOTAL_BEACON_NODES_COUNT, num_total as i64, ); if num_synced > 0 { @@ -79,9 +78,9 @@ async fn notify( ) } if num_synced_fallback > 0 { - set_gauge(&http_metrics::metrics::ETH2_FALLBACK_CONNECTED, 1); + set_gauge(&validator_metrics::ETH2_FALLBACK_CONNECTED, 1); } else { - set_gauge(&http_metrics::metrics::ETH2_FALLBACK_CONNECTED, 0); + set_gauge(&validator_metrics::ETH2_FALLBACK_CONNECTED, 0); } for info in candidate_info { diff --git a/validator_client/validator_metrics/Cargo.toml b/validator_client/validator_metrics/Cargo.toml new file mode 100644 index 0000000000..b3cf665b26 --- /dev/null +++ b/validator_client/validator_metrics/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "validator_metrics" +version = "0.1.0" +edition = { workspace = true } +authors = ["Sigma Prime "] + +[lib] +name = "validator_metrics" +path = "src/lib.rs" + +[dependencies] +metrics = { workspace = true } diff --git a/validator_client/src/http_metrics/metrics.rs b/validator_client/validator_metrics/src/lib.rs similarity index 82% rename from validator_client/src/http_metrics/metrics.rs rename to validator_client/validator_metrics/src/lib.rs index 57e1080fd9..060d8a4edd 100644 --- a/validator_client/src/http_metrics/metrics.rs +++ b/validator_client/validator_metrics/src/lib.rs @@ -1,9 +1,4 @@ -use super::Context; -use malloc_utils::scrape_allocator_metrics; -use slot_clock::SlotClock; use std::sync::LazyLock; -use std::time::{SystemTime, UNIX_EPOCH}; -use types::EthSpec; pub const SUCCESS: &str = "success"; pub const SLASHABLE: &str = "slashable"; @@ -267,56 +262,3 @@ pub static VC_BEACON_NODE_LATENCY_PRIMARY_ENDPOINT: LazyLock> "Round-trip latency for the primary BN endpoint", ) }); - -pub fn gather_prometheus_metrics( - ctx: &Context, -) -> std::result::Result { - let mut buffer = vec![]; - let encoder = TextEncoder::new(); - - { - let shared = ctx.shared.read(); - - if let Some(genesis_time) = shared.genesis_time { - if let Ok(now) = SystemTime::now().duration_since(UNIX_EPOCH) { - let distance = now.as_secs() as i64 - genesis_time as i64; - set_gauge(&GENESIS_DISTANCE, distance); - } - } - - if let Some(duties_service) = &shared.duties_service { - if let Some(slot) = duties_service.slot_clock.now() { - let current_epoch = slot.epoch(E::slots_per_epoch()); - let next_epoch = current_epoch + 1; - - set_int_gauge( - &PROPOSER_COUNT, - &[CURRENT_EPOCH], - duties_service.proposer_count(current_epoch) as i64, - ); - set_int_gauge( - &ATTESTER_COUNT, - &[CURRENT_EPOCH], - duties_service.attester_count(current_epoch) as i64, - ); - set_int_gauge( - &ATTESTER_COUNT, - &[NEXT_EPOCH], - duties_service.attester_count(next_epoch) as i64, - ); - } - } - } - - // It's important to ensure these metrics are explicitly enabled in the case that users aren't - // using glibc and this function causes panics. - if ctx.config.allocator_metrics_enabled { - scrape_allocator_metrics(); - } - - warp_utils::metrics::scrape_health_metrics(); - - encoder.encode(&metrics::gather(), &mut buffer).unwrap(); - - String::from_utf8(buffer).map_err(|e| format!("Failed to encode prometheus info: {:?}", e)) -} diff --git a/validator_client/validator_services/Cargo.toml b/validator_client/validator_services/Cargo.toml new file mode 100644 index 0000000000..7dcd815541 --- /dev/null +++ b/validator_client/validator_services/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "validator_services" +version = "0.1.0" +edition = { workspace = true } +authors = ["Sigma Prime "] + +[dependencies] +beacon_node_fallback = { workspace = true } +validator_metrics = { workspace = true } +validator_store = { workspace = true } +graffiti_file = { workspace = true } +doppelganger_service = { workspace = true } +environment = { workspace = true } +eth2 = { workspace = true } +futures = { workspace = true } +parking_lot = { workspace = true } +safe_arith = { workspace = true } +slog = { workspace = true } +slot_clock = { workspace = true } +tokio = { workspace = true } +types = { workspace = true } +tree_hash = { workspace = true } +bls = { workspace = true } diff --git a/validator_client/src/attestation_service.rs b/validator_client/validator_services/src/attestation_service.rs similarity index 95% rename from validator_client/src/attestation_service.rs rename to validator_client/validator_services/src/attestation_service.rs index 5363f36f66..e31ad4f661 100644 --- a/validator_client/src/attestation_service.rs +++ b/validator_client/validator_services/src/attestation_service.rs @@ -1,9 +1,5 @@ -use crate::beacon_node_fallback::{ApiTopic, BeaconNodeFallback}; -use crate::{ - duties_service::{DutiesService, DutyAndProof}, - http_metrics::metrics, - validator_store::{Error as ValidatorStoreError, ValidatorStore}, -}; +use crate::duties_service::{DutiesService, DutyAndProof}; +use beacon_node_fallback::{ApiTopic, BeaconNodeFallback}; use environment::RuntimeContext; use futures::future::join_all; use slog::{crit, debug, error, info, trace, warn}; @@ -14,8 +10,10 @@ use std::sync::Arc; use tokio::time::{sleep, sleep_until, Duration, Instant}; use tree_hash::TreeHash; use types::{Attestation, AttestationData, ChainSpec, CommitteeIndex, EthSpec, Slot}; +use validator_store::{Error as ValidatorStoreError, ValidatorStore}; /// Builds an `AttestationService`. +#[derive(Default)] pub struct AttestationServiceBuilder { duties_service: Option>>, validator_store: Option>>, @@ -238,9 +236,9 @@ impl AttestationService { aggregate_production_instant: Instant, ) -> Result<(), ()> { let log = self.context.log(); - let attestations_timer = metrics::start_timer_vec( - &metrics::ATTESTATION_SERVICE_TIMES, - &[metrics::ATTESTATIONS], + let attestations_timer = validator_metrics::start_timer_vec( + &validator_metrics::ATTESTATION_SERVICE_TIMES, + &[validator_metrics::ATTESTATIONS], ); // There's not need to produce `Attestation` or `SignedAggregateAndProof` if we do not have @@ -278,9 +276,9 @@ impl AttestationService { sleep_until(aggregate_production_instant).await; // Start the metrics timer *after* we've done the delay. - let _aggregates_timer = metrics::start_timer_vec( - &metrics::ATTESTATION_SERVICE_TIMES, - &[metrics::AGGREGATES], + let _aggregates_timer = validator_metrics::start_timer_vec( + &validator_metrics::ATTESTATION_SERVICE_TIMES, + &[validator_metrics::AGGREGATES], ); // Then download, sign and publish a `SignedAggregateAndProof` for each @@ -339,9 +337,9 @@ impl AttestationService { let attestation_data = self .beacon_nodes .first_success(|beacon_node| async move { - let _timer = metrics::start_timer_vec( - &metrics::ATTESTATION_SERVICE_TIMES, - &[metrics::ATTESTATIONS_HTTP_GET], + let _timer = validator_metrics::start_timer_vec( + &validator_metrics::ATTESTATION_SERVICE_TIMES, + &[validator_metrics::ATTESTATIONS_HTTP_GET], ); beacon_node .get_validator_attestation_data(slot, committee_index) @@ -454,9 +452,9 @@ impl AttestationService { match self .beacon_nodes .request(ApiTopic::Attestations, |beacon_node| async move { - let _timer = metrics::start_timer_vec( - &metrics::ATTESTATION_SERVICE_TIMES, - &[metrics::ATTESTATIONS_HTTP_POST], + let _timer = validator_metrics::start_timer_vec( + &validator_metrics::ATTESTATION_SERVICE_TIMES, + &[validator_metrics::ATTESTATIONS_HTTP_POST], ); if fork_name.electra_enabled() { beacon_node @@ -531,9 +529,9 @@ impl AttestationService { let aggregated_attestation = &self .beacon_nodes .first_success(|beacon_node| async move { - let _timer = metrics::start_timer_vec( - &metrics::ATTESTATION_SERVICE_TIMES, - &[metrics::AGGREGATES_HTTP_GET], + let _timer = validator_metrics::start_timer_vec( + &validator_metrics::ATTESTATION_SERVICE_TIMES, + &[validator_metrics::AGGREGATES_HTTP_GET], ); if fork_name.electra_enabled() { beacon_node @@ -620,9 +618,9 @@ impl AttestationService { match self .beacon_nodes .first_success(|beacon_node| async move { - let _timer = metrics::start_timer_vec( - &metrics::ATTESTATION_SERVICE_TIMES, - &[metrics::AGGREGATES_HTTP_POST], + let _timer = validator_metrics::start_timer_vec( + &validator_metrics::ATTESTATION_SERVICE_TIMES, + &[validator_metrics::AGGREGATES_HTTP_POST], ); if fork_name.electra_enabled() { beacon_node diff --git a/validator_client/src/block_service.rs b/validator_client/validator_services/src/block_service.rs similarity index 94% rename from validator_client/src/block_service.rs rename to validator_client/validator_services/src/block_service.rs index 9903324cad..60eb0361ad 100644 --- a/validator_client/src/block_service.rs +++ b/validator_client/validator_services/src/block_service.rs @@ -1,17 +1,9 @@ -use crate::beacon_node_fallback::{Error as FallbackError, Errors}; -use crate::{ - beacon_node_fallback::{ApiTopic, BeaconNodeFallback}, - determine_graffiti, - graffiti_file::GraffitiFile, -}; -use crate::{ - http_metrics::metrics, - validator_store::{Error as ValidatorStoreError, ValidatorStore}, -}; +use beacon_node_fallback::{ApiTopic, BeaconNodeFallback, Error as FallbackError, Errors}; use bls::SignatureBytes; use environment::RuntimeContext; use eth2::types::{FullBlockContents, PublishBlockRequest}; use eth2::{BeaconNodeHttpClient, StatusCode}; +use graffiti_file::{determine_graffiti, GraffitiFile}; use slog::{crit, debug, error, info, trace, warn, Logger}; use slot_clock::SlotClock; use std::fmt::Debug; @@ -24,6 +16,7 @@ use types::{ BlindedBeaconBlock, BlockType, EthSpec, Graffiti, PublicKeyBytes, SignedBlindedBeaconBlock, Slot, }; +use validator_store::{Error as ValidatorStoreError, ValidatorStore}; #[derive(Debug)] pub enum BlockError { @@ -50,6 +43,7 @@ impl From> for BlockError { } /// Builds a `BlockService`. +#[derive(Default)] pub struct BlockServiceBuilder { validator_store: Option>>, slot_clock: Option>, @@ -186,8 +180,8 @@ impl ProposerFallback { pub struct Inner { validator_store: Arc>, slot_clock: Arc, - pub(crate) beacon_nodes: Arc>, - pub(crate) proposer_nodes: Option>>, + pub beacon_nodes: Arc>, + pub proposer_nodes: Option>>, context: RuntimeContext, graffiti: Option, graffiti_file: Option, @@ -247,8 +241,10 @@ impl BlockService { /// Attempt to produce a block for any block producers in the `ValidatorStore`. async fn do_update(&self, notification: BlockServiceNotification) -> Result<(), ()> { let log = self.context.log(); - let _timer = - metrics::start_timer_vec(&metrics::BLOCK_SERVICE_TIMES, &[metrics::FULL_UPDATE]); + let _timer = validator_metrics::start_timer_vec( + &validator_metrics::BLOCK_SERVICE_TIMES, + &[validator_metrics::FULL_UPDATE], + ); let slot = self.slot_clock.now().ok_or_else(move || { crit!(log, "Duties manager failed to read slot clock"); @@ -337,7 +333,7 @@ impl BlockService { unsigned_block: UnsignedBlock, ) -> Result<(), BlockError> { let log = self.context.log(); - let signing_timer = metrics::start_timer(&metrics::BLOCK_SIGNING_TIMES); + let signing_timer = validator_metrics::start_timer(&validator_metrics::BLOCK_SIGNING_TIMES); let res = match unsigned_block { UnsignedBlock::Full(block_contents) => { @@ -418,8 +414,10 @@ impl BlockService { builder_boost_factor: Option, ) -> Result<(), BlockError> { let log = self.context.log(); - let _timer = - metrics::start_timer_vec(&metrics::BLOCK_SERVICE_TIMES, &[metrics::BEACON_BLOCK]); + let _timer = validator_metrics::start_timer_vec( + &validator_metrics::BLOCK_SERVICE_TIMES, + &[validator_metrics::BEACON_BLOCK], + ); let randao_reveal = match self .validator_store @@ -475,9 +473,9 @@ impl BlockService { // great view of attestations on the network. let unsigned_block = proposer_fallback .request_proposers_last(|beacon_node| async move { - let _get_timer = metrics::start_timer_vec( - &metrics::BLOCK_SERVICE_TIMES, - &[metrics::BEACON_BLOCK_HTTP_GET], + let _get_timer = validator_metrics::start_timer_vec( + &validator_metrics::BLOCK_SERVICE_TIMES, + &[validator_metrics::BEACON_BLOCK_HTTP_GET], ); Self::get_validator_block( &beacon_node, @@ -520,9 +518,9 @@ impl BlockService { let slot = signed_block.slot(); match signed_block { SignedBlock::Full(signed_block) => { - let _post_timer = metrics::start_timer_vec( - &metrics::BLOCK_SERVICE_TIMES, - &[metrics::BEACON_BLOCK_HTTP_POST], + let _post_timer = validator_metrics::start_timer_vec( + &validator_metrics::BLOCK_SERVICE_TIMES, + &[validator_metrics::BEACON_BLOCK_HTTP_POST], ); beacon_node .post_beacon_blocks_v2_ssz(signed_block, None) @@ -530,9 +528,9 @@ impl BlockService { .or_else(|e| handle_block_post_error(e, slot, log))? } SignedBlock::Blinded(signed_block) => { - let _post_timer = metrics::start_timer_vec( - &metrics::BLOCK_SERVICE_TIMES, - &[metrics::BLINDED_BEACON_BLOCK_HTTP_POST], + let _post_timer = validator_metrics::start_timer_vec( + &validator_metrics::BLOCK_SERVICE_TIMES, + &[validator_metrics::BLINDED_BEACON_BLOCK_HTTP_POST], ); beacon_node .post_beacon_blinded_blocks_v2_ssz(signed_block, None) diff --git a/validator_client/src/duties_service.rs b/validator_client/validator_services/src/duties_service.rs similarity index 95% rename from validator_client/src/duties_service.rs rename to validator_client/validator_services/src/duties_service.rs index cf8d499792..187eb4feb5 100644 --- a/validator_client/src/duties_service.rs +++ b/validator_client/validator_services/src/duties_service.rs @@ -6,15 +6,11 @@ //! The `DutiesService` is also responsible for sending events to the `BlockService` which trigger //! block production. -pub mod sync; - -use crate::beacon_node_fallback::{ApiTopic, BeaconNodeFallback}; -use crate::http_metrics::metrics::{get_int_gauge, set_int_gauge, ATTESTATION_DUTY}; -use crate::{ - block_service::BlockServiceNotification, - http_metrics::metrics, - validator_store::{DoppelgangerStatus, Error as ValidatorStoreError, ValidatorStore}, -}; +use crate::block_service::BlockServiceNotification; +use crate::sync::poll_sync_committee_duties; +use crate::sync::SyncDutiesMap; +use beacon_node_fallback::{ApiTopic, BeaconNodeFallback}; +use doppelganger_service::DoppelgangerStatus; use environment::RuntimeContext; use eth2::types::{ AttesterData, BeaconCommitteeSubscription, DutiesResponse, ProposerData, StateId, ValidatorId, @@ -29,10 +25,10 @@ use std::collections::{hash_map, BTreeMap, HashMap, HashSet}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::time::Duration; -use sync::poll_sync_committee_duties; -use sync::SyncDutiesMap; use tokio::{sync::mpsc::Sender, time::sleep}; use types::{ChainSpec, Epoch, EthSpec, Hash256, PublicKeyBytes, SelectionProof, Slot}; +use validator_metrics::{get_int_gauge, set_int_gauge, ATTESTATION_DUTY}; +use validator_store::{Error as ValidatorStoreError, ValidatorStore}; /// Only retain `HISTORICAL_DUTIES_EPOCHS` duties prior to the current epoch. const HISTORICAL_DUTIES_EPOCHS: u64 = 2; @@ -473,8 +469,10 @@ pub fn start_update_service( async fn poll_validator_indices( duties_service: &DutiesService, ) { - let _timer = - metrics::start_timer_vec(&metrics::DUTIES_SERVICE_TIMES, &[metrics::UPDATE_INDICES]); + let _timer = validator_metrics::start_timer_vec( + &validator_metrics::DUTIES_SERVICE_TIMES, + &[validator_metrics::UPDATE_INDICES], + ); let log = duties_service.context.log(); @@ -518,9 +516,9 @@ async fn poll_validator_indices( let download_result = duties_service .beacon_nodes .first_success(|beacon_node| async move { - let _timer = metrics::start_timer_vec( - &metrics::DUTIES_SERVICE_TIMES, - &[metrics::VALIDATOR_ID_HTTP_GET], + let _timer = validator_metrics::start_timer_vec( + &validator_metrics::DUTIES_SERVICE_TIMES, + &[validator_metrics::VALIDATOR_ID_HTTP_GET], ); beacon_node .get_beacon_states_validator_id( @@ -604,9 +602,9 @@ async fn poll_validator_indices( async fn poll_beacon_attesters( duties_service: &Arc>, ) -> Result<(), Error> { - let current_epoch_timer = metrics::start_timer_vec( - &metrics::DUTIES_SERVICE_TIMES, - &[metrics::UPDATE_ATTESTERS_CURRENT_EPOCH], + let current_epoch_timer = validator_metrics::start_timer_vec( + &validator_metrics::DUTIES_SERVICE_TIMES, + &[validator_metrics::UPDATE_ATTESTERS_CURRENT_EPOCH], ); let log = duties_service.context.log(); @@ -660,9 +658,9 @@ async fn poll_beacon_attesters( update_per_validator_duty_metrics::(duties_service, current_epoch, current_slot); drop(current_epoch_timer); - let next_epoch_timer = metrics::start_timer_vec( - &metrics::DUTIES_SERVICE_TIMES, - &[metrics::UPDATE_ATTESTERS_NEXT_EPOCH], + let next_epoch_timer = validator_metrics::start_timer_vec( + &validator_metrics::DUTIES_SERVICE_TIMES, + &[validator_metrics::UPDATE_ATTESTERS_NEXT_EPOCH], ); // Download the duties and update the duties for the next epoch. @@ -682,8 +680,10 @@ async fn poll_beacon_attesters( update_per_validator_duty_metrics::(duties_service, next_epoch, current_slot); drop(next_epoch_timer); - let subscriptions_timer = - metrics::start_timer_vec(&metrics::DUTIES_SERVICE_TIMES, &[metrics::SUBSCRIPTIONS]); + let subscriptions_timer = validator_metrics::start_timer_vec( + &validator_metrics::DUTIES_SERVICE_TIMES, + &[validator_metrics::SUBSCRIPTIONS], + ); // This vector is intentionally oversized by 10% so that it won't reallocate. // Each validator has 2 attestation duties occuring in the current and next epoch, for which @@ -741,9 +741,9 @@ async fn poll_beacon_attesters( let subscription_result = duties_service .beacon_nodes .request(ApiTopic::Subscriptions, |beacon_node| async move { - let _timer = metrics::start_timer_vec( - &metrics::DUTIES_SERVICE_TIMES, - &[metrics::SUBSCRIPTIONS_HTTP_POST], + let _timer = validator_metrics::start_timer_vec( + &validator_metrics::DUTIES_SERVICE_TIMES, + &[validator_metrics::SUBSCRIPTIONS_HTTP_POST], ); beacon_node .post_validator_beacon_committee_subscriptions(subscriptions_ref) @@ -815,9 +815,9 @@ async fn poll_beacon_attesters_for_epoch( return Ok(()); } - let fetch_timer = metrics::start_timer_vec( - &metrics::DUTIES_SERVICE_TIMES, - &[metrics::UPDATE_ATTESTERS_FETCH], + let fetch_timer = validator_metrics::start_timer_vec( + &validator_metrics::DUTIES_SERVICE_TIMES, + &[validator_metrics::UPDATE_ATTESTERS_FETCH], ); // Request duties for all uninitialized validators. If there isn't any, we will just request for @@ -883,9 +883,9 @@ async fn poll_beacon_attesters_for_epoch( drop(fetch_timer); - let _store_timer = metrics::start_timer_vec( - &metrics::DUTIES_SERVICE_TIMES, - &[metrics::UPDATE_ATTESTERS_STORE], + let _store_timer = validator_metrics::start_timer_vec( + &validator_metrics::DUTIES_SERVICE_TIMES, + &[validator_metrics::UPDATE_ATTESTERS_STORE], ); debug!( @@ -1029,9 +1029,9 @@ async fn post_validator_duties_attester( duties_service .beacon_nodes .first_success(|beacon_node| async move { - let _timer = metrics::start_timer_vec( - &metrics::DUTIES_SERVICE_TIMES, - &[metrics::ATTESTER_DUTIES_HTTP_POST], + let _timer = validator_metrics::start_timer_vec( + &validator_metrics::DUTIES_SERVICE_TIMES, + &[validator_metrics::ATTESTER_DUTIES_HTTP_POST], ); beacon_node .post_validator_duties_attester(epoch, validator_indices) @@ -1089,9 +1089,9 @@ async fn fill_in_selection_proofs( continue; } - let timer = metrics::start_timer_vec( - &metrics::DUTIES_SERVICE_TIMES, - &[metrics::ATTESTATION_SELECTION_PROOFS], + let timer = validator_metrics::start_timer_vec( + &validator_metrics::DUTIES_SERVICE_TIMES, + &[validator_metrics::ATTESTATION_SELECTION_PROOFS], ); // Sign selection proofs (serially). @@ -1223,8 +1223,10 @@ async fn poll_beacon_proposers( duties_service: &DutiesService, block_service_tx: &mut Sender, ) -> Result<(), Error> { - let _timer = - metrics::start_timer_vec(&metrics::DUTIES_SERVICE_TIMES, &[metrics::UPDATE_PROPOSERS]); + let _timer = validator_metrics::start_timer_vec( + &validator_metrics::DUTIES_SERVICE_TIMES, + &[validator_metrics::UPDATE_PROPOSERS], + ); let log = duties_service.context.log(); @@ -1261,9 +1263,9 @@ async fn poll_beacon_proposers( let download_result = duties_service .beacon_nodes .first_success(|beacon_node| async move { - let _timer = metrics::start_timer_vec( - &metrics::DUTIES_SERVICE_TIMES, - &[metrics::PROPOSER_DUTIES_HTTP_GET], + let _timer = validator_metrics::start_timer_vec( + &validator_metrics::DUTIES_SERVICE_TIMES, + &[validator_metrics::PROPOSER_DUTIES_HTTP_GET], ); beacon_node .get_validator_duties_proposer(current_epoch) @@ -1341,7 +1343,7 @@ async fn poll_beacon_proposers( "Detected new block proposer"; "current_slot" => current_slot, ); - metrics::inc_counter(&metrics::PROPOSAL_CHANGED); + validator_metrics::inc_counter(&validator_metrics::PROPOSAL_CHANGED); } } diff --git a/validator_client/validator_services/src/lib.rs b/validator_client/validator_services/src/lib.rs new file mode 100644 index 0000000000..abf8fab3cb --- /dev/null +++ b/validator_client/validator_services/src/lib.rs @@ -0,0 +1,6 @@ +pub mod attestation_service; +pub mod block_service; +pub mod duties_service; +pub mod preparation_service; +pub mod sync; +pub mod sync_committee_service; diff --git a/validator_client/src/preparation_service.rs b/validator_client/validator_services/src/preparation_service.rs similarity index 97% rename from validator_client/src/preparation_service.rs rename to validator_client/validator_services/src/preparation_service.rs index 010c651c25..480f4af2b3 100644 --- a/validator_client/src/preparation_service.rs +++ b/validator_client/validator_services/src/preparation_service.rs @@ -1,6 +1,6 @@ -use crate::beacon_node_fallback::{ApiTopic, BeaconNodeFallback}; -use crate::validator_store::{DoppelgangerStatus, Error as ValidatorStoreError, ValidatorStore}; +use beacon_node_fallback::{ApiTopic, BeaconNodeFallback}; use bls::PublicKeyBytes; +use doppelganger_service::DoppelgangerStatus; use environment::RuntimeContext; use parking_lot::RwLock; use slog::{debug, error, info, warn}; @@ -15,6 +15,7 @@ use types::{ Address, ChainSpec, EthSpec, ProposerPreparationData, SignedValidatorRegistrationData, ValidatorRegistrationData, }; +use validator_store::{Error as ValidatorStoreError, ProposalData, ValidatorStore}; /// Number of epochs before the Bellatrix hard fork to begin posting proposer preparations. const PROPOSER_PREPARATION_LOOKAHEAD_EPOCHS: u64 = 2; @@ -23,6 +24,7 @@ const PROPOSER_PREPARATION_LOOKAHEAD_EPOCHS: u64 = 2; const EPOCHS_PER_VALIDATOR_REGISTRATION_SUBMISSION: u64 = 1; /// Builds an `PreparationService`. +#[derive(Default)] pub struct PreparationServiceBuilder { validator_store: Option>>, slot_clock: Option, @@ -492,11 +494,3 @@ impl PreparationService { Ok(()) } } - -/// A helper struct, used for passing data from the validator store to services. -pub struct ProposalData { - pub(crate) validator_index: Option, - pub(crate) fee_recipient: Option
, - pub(crate) gas_limit: u64, - pub(crate) builder_proposals: bool, -} diff --git a/validator_client/src/duties_service/sync.rs b/validator_client/validator_services/src/sync.rs similarity index 98% rename from validator_client/src/duties_service/sync.rs rename to validator_client/validator_services/src/sync.rs index 0bd99dc638..af501326f4 100644 --- a/validator_client/src/duties_service/sync.rs +++ b/validator_client/validator_services/src/sync.rs @@ -1,10 +1,5 @@ -use crate::{ - doppelganger_service::DoppelgangerStatus, - duties_service::{DutiesService, Error}, - http_metrics::metrics, - validator_store::Error as ValidatorStoreError, -}; - +use crate::duties_service::{DutiesService, Error}; +use doppelganger_service::DoppelgangerStatus; use futures::future::join_all; use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard, RwLockWriteGuard}; use slog::{crit, debug, info, warn}; @@ -13,6 +8,7 @@ use std::collections::{HashMap, HashSet}; use std::marker::PhantomData; use std::sync::Arc; use types::{ChainSpec, EthSpec, PublicKeyBytes, Slot, SyncDuty, SyncSelectionProof, SyncSubnetId}; +use validator_store::Error as ValidatorStoreError; /// Number of epochs in advance to compute selection proofs when not in `distributed` mode. pub const AGGREGATION_PRE_COMPUTE_EPOCHS: u64 = 2; @@ -442,9 +438,9 @@ pub async fn poll_sync_committee_duties_for_period"] + +[lib] +name = "validator_store" +path = "src/lib.rs" + +[dependencies] +account_utils = { workspace = true } +doppelganger_service = { workspace = true } +initialized_validators = { workspace = true } +parking_lot = { workspace = true } +serde = { workspace = true } +signing_method = { workspace = true } +slashing_protection = { workspace = true } +slog = { workspace = true } +slot_clock = { workspace = true } +task_executor = { workspace = true } +types = { workspace = true } +validator_metrics = { workspace = true } diff --git a/validator_client/src/validator_store.rs b/validator_client/validator_store/src/lib.rs similarity index 90% rename from validator_client/src/validator_store.rs rename to validator_client/validator_store/src/lib.rs index af59ad9892..837af5b51d 100644 --- a/validator_client/src/validator_store.rs +++ b/validator_client/validator_store/src/lib.rs @@ -1,12 +1,9 @@ -use crate::{ - doppelganger_service::DoppelgangerService, - http_metrics::metrics, - initialized_validators::InitializedValidators, - signing_method::{Error as SigningError, SignableMessage, SigningContext, SigningMethod}, - Config, -}; use account_utils::validator_definitions::{PasswordStorage, ValidatorDefinition}; +use doppelganger_service::{DoppelgangerService, DoppelgangerStatus, DoppelgangerValidatorStore}; +use initialized_validators::InitializedValidators; use parking_lot::{Mutex, RwLock}; +use serde::{Deserialize, Serialize}; +use signing_method::{Error as SigningError, SignableMessage, SigningContext, SigningMethod}; use slashing_protection::{ interchange::Interchange, InterchangeError, NotSafe, Safe, SlashingDatabase, }; @@ -26,9 +23,6 @@ use types::{ ValidatorRegistrationData, VoluntaryExit, }; -pub use crate::doppelganger_service::DoppelgangerStatus; -use crate::preparation_service::ProposalData; - #[derive(Debug, PartialEq)] pub enum Error { DoppelgangerProtected(PublicKeyBytes), @@ -48,6 +42,30 @@ impl From for Error { } } +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct Config { + /// Fallback fee recipient address. + pub fee_recipient: Option
, + /// Fallback gas limit. + pub gas_limit: Option, + /// Enable use of the blinded block endpoints during proposals. + pub builder_proposals: bool, + /// Enable slashing protection even while using web3signer keys. + pub enable_web3signer_slashing_protection: bool, + /// If true, Lighthouse will prefer builder proposals, if available. + pub prefer_builder_proposals: bool, + /// Specifies the boost factor, a percentage multiplier to apply to the builder's payload value. + pub builder_boost_factor: Option, +} + +/// A helper struct, used for passing data from the validator store to services. +pub struct ProposalData { + pub validator_index: Option, + pub fee_recipient: Option
, + pub gas_limit: u64, + pub builder_proposals: bool, +} + /// Number of epochs of slashing protection history to keep. /// /// This acts as a maximum safe-guard against clock drift. @@ -77,6 +95,12 @@ pub struct ValidatorStore { _phantom: PhantomData, } +impl DoppelgangerValidatorStore for ValidatorStore { + fn get_validator_index(&self, pubkey: &PublicKeyBytes) -> Option { + self.validator_index(pubkey) + } +} + impl ValidatorStore { // All arguments are different types. Making the fields `pub` is undesired. A builder seems // unnecessary. @@ -590,7 +614,10 @@ impl ValidatorStore { match slashing_status { // We can safely sign this block without slashing. Ok(Safe::Valid) => { - metrics::inc_counter_vec(&metrics::SIGNED_BLOCKS_TOTAL, &[metrics::SUCCESS]); + validator_metrics::inc_counter_vec( + &validator_metrics::SIGNED_BLOCKS_TOTAL, + &[validator_metrics::SUCCESS], + ); let signature = signing_method .get_signature::( @@ -607,7 +634,10 @@ impl ValidatorStore { self.log, "Skipping signing of previously signed block"; ); - metrics::inc_counter_vec(&metrics::SIGNED_BLOCKS_TOTAL, &[metrics::SAME_DATA]); + validator_metrics::inc_counter_vec( + &validator_metrics::SIGNED_BLOCKS_TOTAL, + &[validator_metrics::SAME_DATA], + ); Err(Error::SameData) } Err(NotSafe::UnregisteredValidator(pk)) => { @@ -617,7 +647,10 @@ impl ValidatorStore { "msg" => "Carefully consider running with --init-slashing-protection (see --help)", "public_key" => format!("{:?}", pk) ); - metrics::inc_counter_vec(&metrics::SIGNED_BLOCKS_TOTAL, &[metrics::UNREGISTERED]); + validator_metrics::inc_counter_vec( + &validator_metrics::SIGNED_BLOCKS_TOTAL, + &[validator_metrics::UNREGISTERED], + ); Err(Error::Slashable(NotSafe::UnregisteredValidator(pk))) } Err(e) => { @@ -626,7 +659,10 @@ impl ValidatorStore { "Not signing slashable block"; "error" => format!("{:?}", e) ); - metrics::inc_counter_vec(&metrics::SIGNED_BLOCKS_TOTAL, &[metrics::SLASHABLE]); + validator_metrics::inc_counter_vec( + &validator_metrics::SIGNED_BLOCKS_TOTAL, + &[validator_metrics::SLASHABLE], + ); Err(Error::Slashable(e)) } } @@ -681,7 +717,10 @@ impl ValidatorStore { .add_signature(&signature, validator_committee_position) .map_err(Error::UnableToSignAttestation)?; - metrics::inc_counter_vec(&metrics::SIGNED_ATTESTATIONS_TOTAL, &[metrics::SUCCESS]); + validator_metrics::inc_counter_vec( + &validator_metrics::SIGNED_ATTESTATIONS_TOTAL, + &[validator_metrics::SUCCESS], + ); Ok(()) } @@ -690,9 +729,9 @@ impl ValidatorStore { self.log, "Skipping signing of previously signed attestation" ); - metrics::inc_counter_vec( - &metrics::SIGNED_ATTESTATIONS_TOTAL, - &[metrics::SAME_DATA], + validator_metrics::inc_counter_vec( + &validator_metrics::SIGNED_ATTESTATIONS_TOTAL, + &[validator_metrics::SAME_DATA], ); Err(Error::SameData) } @@ -703,9 +742,9 @@ impl ValidatorStore { "msg" => "Carefully consider running with --init-slashing-protection (see --help)", "public_key" => format!("{:?}", pk) ); - metrics::inc_counter_vec( - &metrics::SIGNED_ATTESTATIONS_TOTAL, - &[metrics::UNREGISTERED], + validator_metrics::inc_counter_vec( + &validator_metrics::SIGNED_ATTESTATIONS_TOTAL, + &[validator_metrics::UNREGISTERED], ); Err(Error::Slashable(NotSafe::UnregisteredValidator(pk))) } @@ -716,9 +755,9 @@ impl ValidatorStore { "attestation" => format!("{:?}", attestation.data()), "error" => format!("{:?}", e) ); - metrics::inc_counter_vec( - &metrics::SIGNED_ATTESTATIONS_TOTAL, - &[metrics::SLASHABLE], + validator_metrics::inc_counter_vec( + &validator_metrics::SIGNED_ATTESTATIONS_TOTAL, + &[validator_metrics::SLASHABLE], ); Err(Error::Slashable(e)) } @@ -743,7 +782,10 @@ impl ValidatorStore { ) .await?; - metrics::inc_counter_vec(&metrics::SIGNED_VOLUNTARY_EXITS_TOTAL, &[metrics::SUCCESS]); + validator_metrics::inc_counter_vec( + &validator_metrics::SIGNED_VOLUNTARY_EXITS_TOTAL, + &[validator_metrics::SUCCESS], + ); Ok(SignedVoluntaryExit { message: voluntary_exit, @@ -769,9 +811,9 @@ impl ValidatorStore { ) .await?; - metrics::inc_counter_vec( - &metrics::SIGNED_VALIDATOR_REGISTRATIONS_TOTAL, - &[metrics::SUCCESS], + validator_metrics::inc_counter_vec( + &validator_metrics::SIGNED_VALIDATOR_REGISTRATIONS_TOTAL, + &[validator_metrics::SUCCESS], ); Ok(SignedValidatorRegistrationData { @@ -807,7 +849,10 @@ impl ValidatorStore { ) .await?; - metrics::inc_counter_vec(&metrics::SIGNED_AGGREGATES_TOTAL, &[metrics::SUCCESS]); + validator_metrics::inc_counter_vec( + &validator_metrics::SIGNED_AGGREGATES_TOTAL, + &[validator_metrics::SUCCESS], + ); Ok(SignedAggregateAndProof::from_aggregate_and_proof( message, signature, @@ -843,7 +888,10 @@ impl ValidatorStore { .await .map_err(Error::UnableToSign)?; - metrics::inc_counter_vec(&metrics::SIGNED_SELECTION_PROOFS_TOTAL, &[metrics::SUCCESS]); + validator_metrics::inc_counter_vec( + &validator_metrics::SIGNED_SELECTION_PROOFS_TOTAL, + &[validator_metrics::SUCCESS], + ); Ok(signature.into()) } @@ -862,9 +910,9 @@ impl ValidatorStore { // Bypass `with_validator_signing_method`: sync committee messages are not slashable. let signing_method = self.doppelganger_bypassed_signing_method(*validator_pubkey)?; - metrics::inc_counter_vec( - &metrics::SIGNED_SYNC_SELECTION_PROOFS_TOTAL, - &[metrics::SUCCESS], + validator_metrics::inc_counter_vec( + &validator_metrics::SIGNED_SYNC_SELECTION_PROOFS_TOTAL, + &[validator_metrics::SUCCESS], ); let message = SyncAggregatorSelectionData { @@ -911,9 +959,9 @@ impl ValidatorStore { .await .map_err(Error::UnableToSign)?; - metrics::inc_counter_vec( - &metrics::SIGNED_SYNC_COMMITTEE_MESSAGES_TOTAL, - &[metrics::SUCCESS], + validator_metrics::inc_counter_vec( + &validator_metrics::SIGNED_SYNC_COMMITTEE_MESSAGES_TOTAL, + &[validator_metrics::SUCCESS], ); Ok(SyncCommitteeMessage { @@ -953,9 +1001,9 @@ impl ValidatorStore { .await .map_err(Error::UnableToSign)?; - metrics::inc_counter_vec( - &metrics::SIGNED_SYNC_COMMITTEE_CONTRIBUTIONS_TOTAL, - &[metrics::SUCCESS], + validator_metrics::inc_counter_vec( + &validator_metrics::SIGNED_SYNC_COMMITTEE_CONTRIBUTIONS_TOTAL, + &[validator_metrics::SUCCESS], ); Ok(SignedContributionAndProof { message, signature }) @@ -1029,7 +1077,8 @@ impl ValidatorStore { info!(self.log, "Pruning slashing protection DB"; "epoch" => current_epoch); } - let _timer = metrics::start_timer(&metrics::SLASHING_PROTECTION_PRUNE_TIMES); + let _timer = + validator_metrics::start_timer(&validator_metrics::SLASHING_PROTECTION_PRUNE_TIMES); let new_min_target_epoch = current_epoch.saturating_sub(SLASHING_PROTECTION_HISTORY_EPOCHS); let new_min_slot = new_min_target_epoch.start_slot(E::slots_per_epoch()); diff --git a/validator_manager/Cargo.toml b/validator_manager/Cargo.toml index 92267ad875..4f367b8f5b 100644 --- a/validator_manager/Cargo.toml +++ b/validator_manager/Cargo.toml @@ -25,4 +25,4 @@ derivative = { workspace = true } [dev-dependencies] tempfile = { workspace = true } regex = { workspace = true } -validator_client = { workspace = true } +validator_http_api = { workspace = true } diff --git a/validator_manager/src/delete_validators.rs b/validator_manager/src/delete_validators.rs index 6283279986..a2d6c062fa 100644 --- a/validator_manager/src/delete_validators.rs +++ b/validator_manager/src/delete_validators.rs @@ -148,7 +148,7 @@ mod test { use crate::{ common::ValidatorSpecification, import_validators::tests::TestBuilder as ImportTestBuilder, }; - use validator_client::http_api::{test_utils::ApiTester, Config as HttpConfig}; + use validator_http_api::{test_utils::ApiTester, Config as HttpConfig}; struct TestBuilder { delete_config: Option, diff --git a/validator_manager/src/import_validators.rs b/validator_manager/src/import_validators.rs index 6065ecb603..2a819a2a64 100644 --- a/validator_manager/src/import_validators.rs +++ b/validator_manager/src/import_validators.rs @@ -387,7 +387,7 @@ pub mod tests { str::FromStr, }; use tempfile::{tempdir, TempDir}; - use validator_client::http_api::{test_utils::ApiTester, Config as HttpConfig}; + use validator_http_api::{test_utils::ApiTester, Config as HttpConfig}; const VC_TOKEN_FILE_NAME: &str = "vc_token.json"; diff --git a/validator_manager/src/list_validators.rs b/validator_manager/src/list_validators.rs index 7df85a7eb9..e3deb0b21a 100644 --- a/validator_manager/src/list_validators.rs +++ b/validator_manager/src/list_validators.rs @@ -87,7 +87,7 @@ mod test { use crate::{ common::ValidatorSpecification, import_validators::tests::TestBuilder as ImportTestBuilder, }; - use validator_client::http_api::{test_utils::ApiTester, Config as HttpConfig}; + use validator_http_api::{test_utils::ApiTester, Config as HttpConfig}; struct TestBuilder { list_config: Option, diff --git a/validator_manager/src/move_validators.rs b/validator_manager/src/move_validators.rs index 7651917ea9..807a147ca1 100644 --- a/validator_manager/src/move_validators.rs +++ b/validator_manager/src/move_validators.rs @@ -668,7 +668,7 @@ mod test { use account_utils::validator_definitions::SigningDefinition; use std::fs; use tempfile::{tempdir, TempDir}; - use validator_client::http_api::{test_utils::ApiTester, Config as HttpConfig}; + use validator_http_api::{test_utils::ApiTester, Config as HttpConfig}; const SRC_VC_TOKEN_FILE_NAME: &str = "src_vc_token.json"; const DEST_VC_TOKEN_FILE_NAME: &str = "dest_vc_token.json"; From 5f053b0b6dfb7dba8f4455b6fea1d3e6777cea99 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Fri, 15 Nov 2024 10:34:13 +0700 Subject: [PATCH 008/254] Improving blob propagation post-PeerDAS with Decentralized Blob Building (#6268) * Get blobs from EL. Co-authored-by: Michael Sproul * Avoid cloning blobs after fetching blobs. * Address review comments and refactor code. * Fix lint. * Move blob computation metric to the right spot. * Merge branch 'unstable' into das-fetch-blobs * Merge branch 'unstable' into das-fetch-blobs # Conflicts: # beacon_node/beacon_chain/src/beacon_chain.rs # beacon_node/beacon_chain/src/block_verification.rs # beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs * Merge branch 'unstable' into das-fetch-blobs # Conflicts: # beacon_node/beacon_chain/src/beacon_chain.rs * Gradual publication of data columns for supernodes. * Recompute head after importing block with blobs from the EL. * Fix lint * Merge branch 'unstable' into das-fetch-blobs * Use blocking task instead of async when computing cells. * Merge branch 'das-fetch-blobs' of github.com:jimmygchen/lighthouse into das-fetch-blobs * Merge remote-tracking branch 'origin/unstable' into das-fetch-blobs * Fix semantic conflicts * Downgrade error log. * Merge branch 'unstable' into das-fetch-blobs # Conflicts: # beacon_node/beacon_chain/src/data_availability_checker.rs # beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs # beacon_node/execution_layer/src/engine_api.rs # beacon_node/execution_layer/src/engine_api/json_structures.rs # beacon_node/network/src/network_beacon_processor/gossip_methods.rs # beacon_node/network/src/network_beacon_processor/mod.rs # beacon_node/network/src/network_beacon_processor/sync_methods.rs * Merge branch 'unstable' into das-fetch-blobs * Publish block without waiting for blob and column proof computation. * Address review comments and refactor. * Merge branch 'unstable' into das-fetch-blobs * Fix test and docs. * Comment cleanups. * Merge branch 'unstable' into das-fetch-blobs * Address review comments and cleanup * Address review comments and cleanup * Refactor to de-duplicate gradual publication logic. * Add more logging. * Merge remote-tracking branch 'origin/unstable' into das-fetch-blobs # Conflicts: # Cargo.lock * Fix incorrect comparison on `num_fetched_blobs`. * Implement gradual blob publication. * Merge branch 'unstable' into das-fetch-blobs * Inline `publish_fn`. * Merge branch 'das-fetch-blobs' of github.com:jimmygchen/lighthouse into das-fetch-blobs * Gossip verify blobs before publishing * Avoid queries for 0 blobs and error for duplicates * Gossip verified engine blob before processing them, and use observe cache to detect duplicates before publishing. * Merge branch 'das-fetch-blobs' of github.com:jimmygchen/lighthouse into das-fetch-blobs # Conflicts: # beacon_node/network/src/network_beacon_processor/mod.rs * Merge branch 'unstable' into das-fetch-blobs * Fix invalid commitment inclusion proofs in blob sidecars created from EL blobs. * Only publish EL blobs triggered from gossip block, and not RPC block. * Downgrade gossip blob log to `debug`. * Merge branch 'unstable' into das-fetch-blobs * Merge branch 'unstable' into das-fetch-blobs * Grammar --- beacon_node/beacon_chain/benches/benches.rs | 13 +- beacon_node/beacon_chain/src/beacon_chain.rs | 287 +++++++---- .../beacon_chain/src/blob_verification.rs | 96 +++- .../beacon_chain/src/block_verification.rs | 6 +- beacon_node/beacon_chain/src/chain_config.rs | 8 + .../src/data_availability_checker.rs | 53 +- .../src/data_availability_checker/error.rs | 2 - .../overflow_lru_cache.rs | 174 +++---- .../state_lru_cache.rs | 5 + .../src/data_column_verification.rs | 70 ++- beacon_node/beacon_chain/src/fetch_blobs.rs | 308 +++++++++++ beacon_node/beacon_chain/src/kzg_utils.rs | 22 +- beacon_node/beacon_chain/src/lib.rs | 3 +- beacon_node/beacon_chain/src/metrics.rs | 35 ++ .../src/observed_data_sidecars.rs | 25 + beacon_node/beacon_chain/src/test_utils.rs | 7 +- .../beacon_chain/tests/block_verification.rs | 6 +- beacon_node/beacon_chain/tests/events.rs | 2 +- beacon_node/execution_layer/src/engine_api.rs | 6 +- .../execution_layer/src/engine_api/http.rs | 19 + .../src/engine_api/json_structures.rs | 10 +- beacon_node/execution_layer/src/lib.rs | 20 +- .../execution_layer/src/test_utils/mod.rs | 1 + beacon_node/http_api/src/publish_blocks.rs | 487 ++++++++++-------- .../tests/broadcast_validation_tests.rs | 8 +- .../gossip_methods.rs | 22 +- .../src/network_beacon_processor/mod.rs | 272 +++++++++- .../network_beacon_processor/sync_methods.rs | 48 +- beacon_node/src/cli.rs | 18 + beacon_node/src/config.rs | 9 + consensus/types/src/beacon_block_body.rs | 104 ++-- consensus/types/src/blob_sidecar.rs | 31 ++ consensus/types/src/signed_beacon_block.rs | 42 +- .../generate_random_block_and_blobs.rs | 29 ++ lighthouse/tests/beacon_node.rs | 21 + testing/ef_tests/src/cases/fork_choice.rs | 4 +- 36 files changed, 1660 insertions(+), 613 deletions(-) create mode 100644 beacon_node/beacon_chain/src/fetch_blobs.rs diff --git a/beacon_node/beacon_chain/benches/benches.rs b/beacon_node/beacon_chain/benches/benches.rs index b2f17062dc..c09af00be6 100644 --- a/beacon_node/beacon_chain/benches/benches.rs +++ b/beacon_node/beacon_chain/benches/benches.rs @@ -37,12 +37,15 @@ fn all_benches(c: &mut Criterion) { let kzg = get_kzg(&spec); for blob_count in [1, 2, 3, 6] { - let kzg = kzg.clone(); - let (signed_block, blob_sidecars) = create_test_block_and_blobs::(blob_count, &spec); + let (signed_block, blobs) = create_test_block_and_blobs::(blob_count, &spec); - let column_sidecars = - blobs_to_data_column_sidecars(&blob_sidecars, &signed_block, &kzg.clone(), &spec) - .unwrap(); + let column_sidecars = blobs_to_data_column_sidecars( + &blobs.iter().collect::>(), + &signed_block, + &kzg, + &spec, + ) + .unwrap(); let spec = spec.clone(); diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 90a203f722..6294ffef6a 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -88,7 +88,7 @@ use kzg::Kzg; use operation_pool::{ CompactAttestationRef, OperationPool, PersistedOperationPool, ReceivedPreCapella, }; -use parking_lot::{Mutex, RwLock}; +use parking_lot::{Mutex, RwLock, RwLockWriteGuard}; use proto_array::{DoNotReOrg, ProposerHeadError}; use safe_arith::SafeArith; use slasher::Slasher; @@ -120,6 +120,7 @@ use store::{ DatabaseBlock, Error as DBError, HotColdDB, KeyValueStore, KeyValueStoreOp, StoreItem, StoreOp, }; use task_executor::{ShutdownReason, TaskExecutor}; +use tokio::sync::mpsc::Receiver; use tokio_stream::Stream; use tree_hash::TreeHash; use types::blob_sidecar::FixedBlobSidecarList; @@ -2971,7 +2972,6 @@ impl BeaconChain { pub async fn process_gossip_blob( self: &Arc, blob: GossipVerifiedBlob, - publish_fn: impl FnOnce() -> Result<(), BlockError>, ) -> Result { let block_root = blob.block_root(); @@ -2990,17 +2990,9 @@ impl BeaconChain { 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( - blob.as_blob(), - ))); - } - } + self.emit_sse_blob_sidecar_events(&block_root, std::iter::once(blob.as_blob())); - let r = self - .check_gossip_blob_availability_and_import(blob, publish_fn) - .await; + let r = self.check_gossip_blob_availability_and_import(blob).await; self.remove_notified(&block_root, r) } @@ -3078,20 +3070,63 @@ impl BeaconChain { } } + self.emit_sse_blob_sidecar_events(&block_root, blobs.iter().flatten().map(Arc::as_ref)); + + let r = self + .check_rpc_blob_availability_and_import(slot, block_root, blobs) + .await; + self.remove_notified(&block_root, r) + } + + /// Process blobs retrieved from the EL and returns the `AvailabilityProcessingStatus`. + /// + /// `data_column_recv`: An optional receiver for `DataColumnSidecarList`. + /// If PeerDAS is enabled, this receiver will be provided and used to send + /// the `DataColumnSidecar`s once they have been successfully computed. + pub async fn process_engine_blobs( + self: &Arc, + slot: Slot, + block_root: Hash256, + blobs: FixedBlobSidecarList, + data_column_recv: Option>>, + ) -> 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::DuplicateFullyImported(block_root)); + } + + self.emit_sse_blob_sidecar_events(&block_root, blobs.iter().flatten().map(Arc::as_ref)); + + let r = self + .check_engine_blob_availability_and_import(slot, block_root, blobs, data_column_recv) + .await; + self.remove_notified(&block_root, r) + } + + fn emit_sse_blob_sidecar_events<'a, I>(self: &Arc, block_root: &Hash256, blobs_iter: I) + where + I: Iterator>, + { if let Some(event_handler) = self.event_handler.as_ref() { if event_handler.has_blob_sidecar_subscribers() { - for blob in blobs.iter().filter_map(|maybe_blob| maybe_blob.as_ref()) { + let imported_blobs = self + .data_availability_checker + .cached_blob_indexes(block_root) + .unwrap_or_default(); + let new_blobs = blobs_iter.filter(|b| !imported_blobs.contains(&b.index)); + + for blob in new_blobs { event_handler.register(EventKind::BlobSidecar( SseBlobSidecar::from_blob_sidecar(blob), )); } } } - - let r = self - .check_rpc_blob_availability_and_import(slot, block_root, blobs) - .await; - self.remove_notified(&block_root, r) } /// Cache the columns in the processing cache, process it, then evict it from the cache if it was @@ -3181,7 +3216,7 @@ impl BeaconChain { }; let r = self - .process_availability(slot, availability, || Ok(())) + .process_availability(slot, availability, None, || Ok(())) .await; self.remove_notified(&block_root, r) .map(|availability_processing_status| { @@ -3309,7 +3344,7 @@ impl BeaconChain { match executed_block { ExecutedBlock::Available(block) => { - self.import_available_block(Box::new(block)).await + self.import_available_block(Box::new(block), None).await } ExecutedBlock::AvailabilityPending(block) => { self.check_block_availability_and_import(block).await @@ -3441,7 +3476,7 @@ impl BeaconChain { let availability = self .data_availability_checker .put_pending_executed_block(block)?; - self.process_availability(slot, availability, || Ok(())) + self.process_availability(slot, availability, None, || Ok(())) .await } @@ -3450,7 +3485,6 @@ impl BeaconChain { async fn check_gossip_blob_availability_and_import( self: &Arc, blob: GossipVerifiedBlob, - publish_fn: impl FnOnce() -> Result<(), BlockError>, ) -> Result { let slot = blob.slot(); if let Some(slasher) = self.slasher.as_ref() { @@ -3458,7 +3492,7 @@ impl BeaconChain { } let availability = self.data_availability_checker.put_gossip_blob(blob)?; - self.process_availability(slot, availability, publish_fn) + self.process_availability(slot, availability, None, || Ok(())) .await } @@ -3477,16 +3511,41 @@ impl BeaconChain { } } - let availability = self.data_availability_checker.put_gossip_data_columns( - slot, - block_root, - data_columns, - )?; + let availability = self + .data_availability_checker + .put_gossip_data_columns(block_root, data_columns)?; - self.process_availability(slot, availability, publish_fn) + self.process_availability(slot, availability, None, publish_fn) .await } + fn check_blobs_for_slashability( + self: &Arc, + block_root: Hash256, + blobs: &FixedBlobSidecarList, + ) -> Result<(), BlockError> { + let mut slashable_cache = self.observed_slashable.write(); + for header in blobs + .iter() + .filter_map(|b| b.as_ref().map(|b| b.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); + } + } + } + Ok(()) + } + /// 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( @@ -3495,35 +3554,28 @@ impl BeaconChain { block_root: Hash256, blobs: FixedBlobSidecarList, ) -> 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 blobs - .iter() - .filter_map(|b| b.as_ref().map(|b| b.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 epoch = slot.epoch(T::EthSpec::slots_per_epoch()); + self.check_blobs_for_slashability(block_root, &blobs)?; let availability = self .data_availability_checker - .put_rpc_blobs(block_root, epoch, blobs)?; + .put_rpc_blobs(block_root, blobs)?; - self.process_availability(slot, availability, || Ok(())) + self.process_availability(slot, availability, None, || Ok(())) + .await + } + + async fn check_engine_blob_availability_and_import( + self: &Arc, + slot: Slot, + block_root: Hash256, + blobs: FixedBlobSidecarList, + data_column_recv: Option>>, + ) -> Result { + self.check_blobs_for_slashability(block_root, &blobs)?; + let availability = self + .data_availability_checker + .put_engine_blobs(block_root, blobs)?; + + self.process_availability(slot, availability, data_column_recv, || Ok(())) .await } @@ -3559,13 +3611,11 @@ impl BeaconChain { // This slot value is purely informative for the consumers of // `AvailabilityProcessingStatus::MissingComponents` to log an error with a slot. - let availability = self.data_availability_checker.put_rpc_custody_columns( - block_root, - slot.epoch(T::EthSpec::slots_per_epoch()), - custody_columns, - )?; + let availability = self + .data_availability_checker + .put_rpc_custody_columns(block_root, custody_columns)?; - self.process_availability(slot, availability, || Ok(())) + self.process_availability(slot, availability, None, || Ok(())) .await } @@ -3577,13 +3627,14 @@ impl BeaconChain { self: &Arc, slot: Slot, availability: Availability, + recv: Option>>, publish_fn: impl FnOnce() -> Result<(), BlockError>, ) -> Result { match availability { Availability::Available(block) => { publish_fn()?; // Block is fully available, import into fork choice - self.import_available_block(block).await + self.import_available_block(block, recv).await } Availability::MissingComponents(block_root) => Ok( AvailabilityProcessingStatus::MissingComponents(slot, block_root), @@ -3594,6 +3645,7 @@ impl BeaconChain { pub async fn import_available_block( self: &Arc, block: Box>, + data_column_recv: Option>>, ) -> Result { let AvailableExecutedBlock { block, @@ -3635,6 +3687,7 @@ impl BeaconChain { parent_block, parent_eth1_finalization_data, consensus_context, + data_column_recv, ) }, "payload_verification_handle", @@ -3673,6 +3726,7 @@ impl BeaconChain { parent_block: SignedBlindedBeaconBlock, parent_eth1_finalization_data: Eth1FinalizationData, mut consensus_context: ConsensusContext, + data_column_recv: Option>>, ) -> Result { // ----------------------------- BLOCK NOT YET ATTESTABLE ---------------------------------- // Everything in this initial section is on the hot path between processing the block and @@ -3818,7 +3872,6 @@ impl BeaconChain { // state if we returned early without committing. In other words, an error here would // corrupt the node's database permanently. // ----------------------------------------------------------------------------------------- - self.import_block_update_shuffling_cache(block_root, &mut state); self.import_block_observe_attestations( block, @@ -3835,15 +3888,53 @@ impl BeaconChain { ); self.import_block_update_slasher(block, &state, &mut consensus_context); - let db_write_timer = metrics::start_timer(&metrics::BLOCK_PROCESSING_DB_WRITE); - // Store the block and its state, and execute the confirmation batch for the intermediate // states, which will delete their temporary flags. // 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, data_columns) = signed_block.deconstruct(); + // TODO(das) we currently store all subnet sampled columns. Tracking issue to exclude non + // custody columns: https://github.com/sigp/lighthouse/issues/6465 + let custody_columns_count = self.data_availability_checker.get_sampling_column_count(); + // if block is made available via blobs, dropped the data columns. + let data_columns = data_columns.filter(|columns| columns.len() == custody_columns_count); + + let data_columns = match (data_columns, data_column_recv) { + // If the block was made available via custody columns received from gossip / rpc, use them + // since we already have them. + (Some(columns), _) => Some(columns), + // Otherwise, it means blobs were likely available via fetching from EL, in this case we + // wait for the data columns to be computed (blocking). + (None, Some(mut data_column_recv)) => { + let _column_recv_timer = + metrics::start_timer(&metrics::BLOCK_PROCESSING_DATA_COLUMNS_WAIT); + // Unable to receive data columns from sender, sender is either dropped or + // failed to compute data columns from blobs. We restore fork choice here and + // return to avoid inconsistency in database. + if let Some(columns) = data_column_recv.blocking_recv() { + Some(columns) + } else { + let err_msg = "Did not receive data columns from sender"; + error!( + self.log, + "Failed to store data columns into the database"; + "msg" => "Restoring fork choice from disk", + "error" => err_msg, + ); + return Err(self + .handle_import_block_db_write_error(fork_choice) + .err() + .unwrap_or(BlockError::InternalError(err_msg.to_string()))); + } + } + // No data columns present and compute data columns task was not spawned. + // Could either be no blobs in the block or before PeerDAS activation. + (None, None) => None, + }; + let block = signed_block.message(); + let db_write_timer = metrics::start_timer(&metrics::BLOCK_PROCESSING_DB_WRITE); ops.extend( confirmed_state_roots .into_iter() @@ -3885,33 +3976,10 @@ impl BeaconChain { "msg" => "Restoring fork choice from disk", "error" => ?e, ); - - // Clear the early attester cache to prevent attestations which we would later be unable - // to verify due to the failure. - self.early_attester_cache.clear(); - - // Since the write failed, try to revert the canonical head back to what was stored - // in the database. This attempts to prevent inconsistency between the database and - // fork choice. - if let Err(e) = self.canonical_head.restore_from_store( - fork_choice, - ResetPayloadStatuses::always_reset_conditionally( - self.config.always_reset_payload_statuses, - ), - &self.store, - &self.spec, - &self.log, - ) { - crit!( - self.log, - "No stored fork choice found to restore from"; - "error" => ?e, - "warning" => "The database is likely corrupt now, consider --purge-db" - ); - return Err(BlockError::BeaconChainError(e)); - } - - return Err(e.into()); + return Err(self + .handle_import_block_db_write_error(fork_choice) + .err() + .unwrap_or(e.into())); } drop(txn_lock); @@ -3979,6 +4047,41 @@ impl BeaconChain { Ok(block_root) } + fn handle_import_block_db_write_error( + &self, + // We don't actually need this value, however it's always present when we call this function + // and it needs to be dropped to prevent a dead-lock. Requiring it to be passed here is + // defensive programming. + fork_choice_write_lock: RwLockWriteGuard>, + ) -> Result<(), BlockError> { + // Clear the early attester cache to prevent attestations which we would later be unable + // to verify due to the failure. + self.early_attester_cache.clear(); + + // Since the write failed, try to revert the canonical head back to what was stored + // in the database. This attempts to prevent inconsistency between the database and + // fork choice. + if let Err(e) = self.canonical_head.restore_from_store( + fork_choice_write_lock, + ResetPayloadStatuses::always_reset_conditionally( + self.config.always_reset_payload_statuses, + ), + &self.store, + &self.spec, + &self.log, + ) { + crit!( + self.log, + "No stored fork choice found to restore from"; + "error" => ?e, + "warning" => "The database is likely corrupt now, consider --purge-db" + ); + Err(BlockError::BeaconChainError(e)) + } else { + Ok(()) + } + } + /// Check block's consistentency with any configured weak subjectivity checkpoint. fn check_block_against_weak_subjectivity_checkpoint( &self, diff --git a/beacon_node/beacon_chain/src/blob_verification.rs b/beacon_node/beacon_chain/src/blob_verification.rs index 743748a76d..6c87deb826 100644 --- a/beacon_node/beacon_chain/src/blob_verification.rs +++ b/beacon_node/beacon_chain/src/blob_verification.rs @@ -1,5 +1,6 @@ use derivative::Derivative; use slot_clock::SlotClock; +use std::marker::PhantomData; use std::sync::Arc; use crate::beacon_chain::{BeaconChain, BeaconChainTypes}; @@ -8,11 +9,11 @@ use crate::block_verification::{ BlockSlashInfo, }; use crate::kzg_utils::{validate_blob, validate_blobs}; +use crate::observed_data_sidecars::{DoNotObserve, ObservationStrategy, Observe}; use crate::{metrics, BeaconChainError}; use kzg::{Error as KzgError, Kzg, KzgCommitment}; use slog::debug; use ssz_derive::{Decode, Encode}; -use ssz_types::VariableList; use std::time::Duration; use tree_hash::TreeHash; use types::blob_sidecar::BlobIdentifier; @@ -156,20 +157,16 @@ impl From for GossipBlobError { } } -pub type GossipVerifiedBlobList = VariableList< - GossipVerifiedBlob, - <::EthSpec as EthSpec>::MaxBlobsPerBlock, ->; - /// A wrapper around a `BlobSidecar` that indicates it has been approved for re-gossiping on /// the p2p network. #[derive(Debug)] -pub struct GossipVerifiedBlob { +pub struct GossipVerifiedBlob { block_root: Hash256, blob: KzgVerifiedBlob, + _phantom: PhantomData, } -impl GossipVerifiedBlob { +impl GossipVerifiedBlob { pub fn new( blob: Arc>, subnet_id: u64, @@ -178,7 +175,7 @@ impl GossipVerifiedBlob { let header = blob.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_blob_sidecar_for_gossip(blob, subnet_id, chain).map_err(|e| { + validate_blob_sidecar_for_gossip::(blob, subnet_id, chain).map_err(|e| { process_block_slash_info::<_, GossipBlobError>( chain, BlockSlashInfo::from_early_error_blob(header, e), @@ -195,6 +192,7 @@ impl GossipVerifiedBlob { blob, seen_timestamp: Duration::from_secs(0), }, + _phantom: PhantomData, } } pub fn id(&self) -> BlobIdentifier { @@ -335,6 +333,25 @@ impl KzgVerifiedBlobList { verified_blobs: blobs, }) } + + /// Create a `KzgVerifiedBlobList` from `blobs` that are already KZG verified. + /// + /// This should be used with caution, as used incorrectly it could result in KZG verification + /// being skipped and invalid blobs being deemed valid. + pub fn from_verified>>>( + blobs: I, + seen_timestamp: Duration, + ) -> Self { + Self { + verified_blobs: blobs + .into_iter() + .map(|blob| KzgVerifiedBlob { + blob, + seen_timestamp, + }) + .collect(), + } + } } impl IntoIterator for KzgVerifiedBlobList { @@ -364,11 +381,11 @@ where validate_blobs::(kzg, commitments.as_slice(), blobs, proofs.as_slice()) } -pub fn validate_blob_sidecar_for_gossip( +pub fn validate_blob_sidecar_for_gossip( blob_sidecar: Arc>, subnet: u64, chain: &BeaconChain, -) -> Result, GossipBlobError> { +) -> Result, GossipBlobError> { let blob_slot = blob_sidecar.slot(); let blob_index = blob_sidecar.index; let block_parent_root = blob_sidecar.block_parent_root(); @@ -568,16 +585,45 @@ pub fn validate_blob_sidecar_for_gossip( ) .map_err(|e| GossipBlobError::BeaconChainError(e.into()))?; + if O::observe() { + observe_gossip_blob(&kzg_verified_blob.blob, chain)?; + } + + Ok(GossipVerifiedBlob { + block_root, + blob: kzg_verified_blob, + _phantom: PhantomData, + }) +} + +impl GossipVerifiedBlob { + pub fn observe( + self, + chain: &BeaconChain, + ) -> Result, GossipBlobError> { + observe_gossip_blob(&self.blob.blob, chain)?; + Ok(GossipVerifiedBlob { + block_root: self.block_root, + blob: self.blob, + _phantom: PhantomData, + }) + } +} + +fn observe_gossip_blob( + blob_sidecar: &BlobSidecar, + chain: &BeaconChain, +) -> Result<(), GossipBlobError> { // Now the signature is valid, store the proposal so we don't accept another blob sidecar - // with the same `BlobIdentifier`. - // It's important to double-check that the proposer still hasn't been observed so we don't - // have a race-condition when verifying two blocks simultaneously. + // with the same `BlobIdentifier`. It's important to double-check that the proposer still + // hasn't been observed so we don't have a race-condition when verifying two blocks + // simultaneously. // - // Note: If this BlobSidecar goes on to fail full verification, we do not evict it from the seen_cache - // as alternate blob_sidecars for the same identifier can still be retrieved - // over rpc. Evicting them from this cache would allow faster propagation over gossip. So we allow - // retrieval of potentially valid blocks over rpc, but try to punish the proposer for signing - // invalid messages. Issue for more background + // Note: If this BlobSidecar goes on to fail full verification, we do not evict it from the + // seen_cache as alternate blob_sidecars for the same identifier can still be retrieved over + // rpc. Evicting them from this cache would allow faster propagation over gossip. So we + // allow retrieval of potentially valid blocks over rpc, but try to punish the proposer for + // signing invalid messages. Issue for more background // https://github.com/ethereum/consensus-specs/issues/3261 if chain .observed_blob_sidecars @@ -586,16 +632,12 @@ pub fn validate_blob_sidecar_for_gossip( .map_err(|e| GossipBlobError::BeaconChainError(e.into()))? { return Err(GossipBlobError::RepeatBlob { - proposer: proposer_index as u64, - slot: blob_slot, - index: blob_index, + proposer: blob_sidecar.block_proposer_index(), + slot: blob_sidecar.slot(), + index: blob_sidecar.index, }); } - - Ok(GossipVerifiedBlob { - block_root, - blob: kzg_verified_blob, - }) + Ok(()) } /// Returns the canonical root of the given `blob`. diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 527462ab64..92eb45f9b0 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -683,7 +683,7 @@ pub struct SignatureVerifiedBlock { consensus_context: ConsensusContext, } -/// Used to await the result of executing payload with a remote EE. +/// Used to await the result of executing payload with an EE. type PayloadVerificationHandle = JoinHandle>>; /// A wrapper around a `SignedBeaconBlock` that indicates that this block is fully verified and @@ -750,7 +750,8 @@ pub fn build_blob_data_column_sidecars( &metrics::DATA_COLUMN_SIDECAR_COMPUTATION, &[&blobs.len().to_string()], ); - let sidecars = blobs_to_data_column_sidecars(&blobs, block, &chain.kzg, &chain.spec) + let blob_refs = blobs.iter().collect::>(); + let sidecars = blobs_to_data_column_sidecars(&blob_refs, block, &chain.kzg, &chain.spec) .discard_timer_on_break(&mut timer)?; drop(timer); Ok(sidecars) @@ -1343,7 +1344,6 @@ impl ExecutionPendingBlock { /* * Perform cursory checks to see if the block is even worth processing. */ - check_block_relevancy(block.as_block(), block_root, chain)?; // Define a future that will verify the execution payload with an execution engine. diff --git a/beacon_node/beacon_chain/src/chain_config.rs b/beacon_node/beacon_chain/src/chain_config.rs index 20edfbf31a..b8a607c886 100644 --- a/beacon_node/beacon_chain/src/chain_config.rs +++ b/beacon_node/beacon_chain/src/chain_config.rs @@ -88,6 +88,12 @@ pub struct ChainConfig { pub malicious_withhold_count: usize, /// Enable peer sampling on blocks. pub enable_sampling: bool, + /// Number of batches that the node splits blobs or data columns into during publication. + /// This doesn't apply if the node is the block proposer. For PeerDAS only. + pub blob_publication_batches: usize, + /// The delay in milliseconds applied by the node between sending each blob or data column batch. + /// This doesn't apply if the node is the block proposer. + pub blob_publication_batch_interval: Duration, } impl Default for ChainConfig { @@ -121,6 +127,8 @@ impl Default for ChainConfig { enable_light_client_server: false, malicious_withhold_count: 0, enable_sampling: false, + blob_publication_batches: 4, + blob_publication_batch_interval: Duration::from_millis(300), } } } diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index 047764d705..72806a74d2 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -18,7 +18,7 @@ use task_executor::TaskExecutor; use types::blob_sidecar::{BlobIdentifier, BlobSidecar, FixedBlobSidecarList}; use types::{ BlobSidecarList, ChainSpec, DataColumnIdentifier, DataColumnSidecar, DataColumnSidecarList, - Epoch, EthSpec, Hash256, RuntimeVariableList, SignedBeaconBlock, Slot, + Epoch, EthSpec, Hash256, RuntimeVariableList, SignedBeaconBlock, }; mod error; @@ -146,6 +146,10 @@ impl DataAvailabilityChecker { self.availability_cache.sampling_column_count() } + pub(crate) fn is_supernode(&self) -> bool { + self.get_sampling_column_count() == self.spec.number_of_columns + } + /// Checks if the block root is currenlty in the availability cache awaiting import because /// of missing components. pub fn get_execution_valid_block( @@ -201,7 +205,6 @@ impl DataAvailabilityChecker { pub fn put_rpc_blobs( &self, block_root: Hash256, - epoch: Epoch, blobs: FixedBlobSidecarList, ) -> Result, AvailabilityCheckError> { let seen_timestamp = self @@ -212,15 +215,12 @@ impl DataAvailabilityChecker { // Note: currently not reporting which specific blob is invalid because we fetch all blobs // from the same peer for both lookup and range sync. - let verified_blobs = KzgVerifiedBlobList::new( - Vec::from(blobs).into_iter().flatten(), - &self.kzg, - seen_timestamp, - ) - .map_err(AvailabilityCheckError::InvalidBlobs)?; + let verified_blobs = + KzgVerifiedBlobList::new(blobs.iter().flatten().cloned(), &self.kzg, seen_timestamp) + .map_err(AvailabilityCheckError::InvalidBlobs)?; self.availability_cache - .put_kzg_verified_blobs(block_root, epoch, verified_blobs, &self.log) + .put_kzg_verified_blobs(block_root, verified_blobs, &self.log) } /// Put a list of custody columns received via RPC into the availability cache. This performs KZG @@ -229,7 +229,6 @@ impl DataAvailabilityChecker { pub fn put_rpc_custody_columns( &self, block_root: Hash256, - epoch: Epoch, custody_columns: DataColumnSidecarList, ) -> Result, AvailabilityCheckError> { // TODO(das): report which column is invalid for proper peer scoring @@ -248,12 +247,32 @@ impl DataAvailabilityChecker { self.availability_cache.put_kzg_verified_data_columns( block_root, - epoch, verified_custody_columns, &self.log, ) } + /// Put a list of blobs received from the EL pool into the availability cache. + /// + /// This DOES NOT perform KZG verification because the KZG proofs should have been constructed + /// immediately prior to calling this function so they are assumed to be valid. + pub fn put_engine_blobs( + &self, + block_root: Hash256, + blobs: FixedBlobSidecarList, + ) -> Result, AvailabilityCheckError> { + let seen_timestamp = self + .slot_clock + .now_duration() + .ok_or(AvailabilityCheckError::SlotClockError)?; + + let verified_blobs = + KzgVerifiedBlobList::from_verified(blobs.iter().flatten().cloned(), seen_timestamp); + + self.availability_cache + .put_kzg_verified_blobs(block_root, verified_blobs, &self.log) + } + /// 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. @@ -265,7 +284,6 @@ impl DataAvailabilityChecker { ) -> Result, AvailabilityCheckError> { self.availability_cache.put_kzg_verified_blobs( gossip_blob.block_root(), - gossip_blob.epoch(), vec![gossip_blob.into_inner()], &self.log, ) @@ -279,12 +297,9 @@ impl DataAvailabilityChecker { #[allow(clippy::type_complexity)] pub fn put_gossip_data_columns( &self, - slot: Slot, block_root: Hash256, gossip_data_columns: Vec>, ) -> Result, AvailabilityCheckError> { - let epoch = slot.epoch(T::EthSpec::slots_per_epoch()); - let custody_columns = gossip_data_columns .into_iter() .map(|c| KzgVerifiedCustodyDataColumn::from_asserted_custody(c.into_inner())) @@ -292,7 +307,6 @@ impl DataAvailabilityChecker { self.availability_cache.put_kzg_verified_data_columns( block_root, - epoch, custody_columns, &self.log, ) @@ -595,12 +609,7 @@ impl DataAvailabilityChecker { ); self.availability_cache - .put_kzg_verified_data_columns( - *block_root, - slot.epoch(T::EthSpec::slots_per_epoch()), - data_columns_to_publish.clone(), - &self.log, - ) + .put_kzg_verified_data_columns(*block_root, data_columns_to_publish.clone(), &self.log) .map(|availability| { DataColumnReconstructionResult::Success(( availability, 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 dbfa00e6e2..cfdb3cfe91 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker/error.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker/error.rs @@ -10,7 +10,6 @@ pub enum Error { blob_commitment: KzgCommitment, block_commitment: KzgCommitment, }, - UnableToDetermineImportRequirement, Unexpected, SszTypes(ssz_types::Error), MissingBlobs, @@ -44,7 +43,6 @@ impl Error { | Error::Unexpected | Error::ParentStateMissing(_) | Error::BlockReplayError(_) - | Error::UnableToDetermineImportRequirement | Error::RebuildingStateCaches(_) | Error::SlotClockError => ErrorCategory::Internal, Error::InvalidBlobs { .. } 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 6d4636e8ed..40361574af 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 @@ -10,7 +10,7 @@ use crate::BeaconChainTypes; use lru::LruCache; use parking_lot::RwLock; use slog::{debug, Logger}; -use ssz_types::{FixedVector, VariableList}; +use ssz_types::FixedVector; use std::num::NonZeroUsize; use std::sync::Arc; use types::blob_sidecar::BlobIdentifier; @@ -34,11 +34,6 @@ pub struct PendingComponents { pub reconstruction_started: bool, } -pub enum BlockImportRequirement { - AllBlobs, - ColumnSampling(usize), -} - impl PendingComponents { /// Returns an immutable reference to the cached block. pub fn get_cached_block(&self) -> &Option> { @@ -199,63 +194,49 @@ impl PendingComponents { /// /// 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 { + pub fn is_available(&self, custody_column_count: usize, log: &Logger) -> bool { let block_kzg_commitments_count_opt = self.block_kzg_commitments_count(); + let expected_blobs_msg = block_kzg_commitments_count_opt + .as_ref() + .map(|num| num.to_string()) + .unwrap_or("unknown".to_string()); - match block_import_requirement { - BlockImportRequirement::AllBlobs => { - let received_blobs = self.num_received_blobs(); - let expected_blobs_msg = block_kzg_commitments_count_opt - .as_ref() - .map(|num| num.to_string()) - .unwrap_or("unknown".to_string()); - - debug!(log, - "Component(s) added to data availability checker"; - "block_root" => ?self.block_root, - "received_block" => block_kzg_commitments_count_opt.is_some(), - "received_blobs" => received_blobs, - "expected_blobs" => expected_blobs_msg, - ); - - block_kzg_commitments_count_opt.map_or(false, |num_expected_blobs| { - num_expected_blobs == received_blobs - }) + // No data columns when there are 0 blobs + let expected_columns_opt = block_kzg_commitments_count_opt.map(|blob_count| { + if blob_count > 0 { + custody_column_count + } else { + 0 } - BlockImportRequirement::ColumnSampling(num_expected_columns) => { - // No data columns when there are 0 blobs - let expected_columns_opt = block_kzg_commitments_count_opt.map(|blob_count| { - if blob_count > 0 { - *num_expected_columns - } else { - 0 - } - }); + }); + let expected_columns_msg = expected_columns_opt + .as_ref() + .map(|num| num.to_string()) + .unwrap_or("unknown".to_string()); - let expected_columns_msg = expected_columns_opt - .as_ref() - .map(|num| num.to_string()) - .unwrap_or("unknown".to_string()); + let num_received_blobs = self.num_received_blobs(); + let num_received_columns = self.num_received_data_columns(); - let num_received_columns = self.num_received_data_columns(); + debug!( + log, + "Component(s) added to data availability checker"; + "block_root" => ?self.block_root, + "received_blobs" => num_received_blobs, + "expected_blobs" => expected_blobs_msg, + "received_columns" => num_received_columns, + "expected_columns" => expected_columns_msg, + ); - debug!(log, - "Component(s) added to data availability checker"; - "block_root" => ?self.block_root, - "received_block" => block_kzg_commitments_count_opt.is_some(), - "received_columns" => num_received_columns, - "expected_columns" => expected_columns_msg, - ); + let all_blobs_received = block_kzg_commitments_count_opt + .map_or(false, |num_expected_blobs| { + num_expected_blobs == num_received_blobs + }); - expected_columns_opt.map_or(false, |num_expected_columns| { - num_expected_columns == num_received_columns - }) - } - } + let all_columns_received = expected_columns_opt.map_or(false, |num_expected_columns| { + num_expected_columns == num_received_columns + }); + + all_blobs_received || all_columns_received } /// Returns an empty `PendingComponents` object with the given block root. @@ -277,7 +258,6 @@ impl PendingComponents { /// reconstructed from disk. Ensure you are not holding any write locks while calling this. pub fn make_available( self, - block_import_requirement: BlockImportRequirement, spec: &Arc, recover: R, ) -> Result, AvailabilityCheckError> @@ -304,26 +284,25 @@ impl PendingComponents { return Err(AvailabilityCheckError::Unexpected); }; - let (blobs, data_columns) = match block_import_requirement { - BlockImportRequirement::AllBlobs => { - let num_blobs_expected = diet_executed_block.num_blobs_expected(); - let Some(verified_blobs) = verified_blobs - .into_iter() - .map(|b| b.map(|b| b.to_blob())) - .take(num_blobs_expected) - .collect::>>() - else { - return Err(AvailabilityCheckError::Unexpected); - }; - (Some(VariableList::new(verified_blobs)?), None) - } - BlockImportRequirement::ColumnSampling(_) => { - let verified_data_columns = verified_data_columns - .into_iter() - .map(|d| d.into_inner()) - .collect(); - (None, Some(verified_data_columns)) - } + let is_peer_das_enabled = spec.is_peer_das_enabled_for_epoch(diet_executed_block.epoch()); + let (blobs, data_columns) = if is_peer_das_enabled { + let data_columns = verified_data_columns + .into_iter() + .map(|d| d.into_inner()) + .collect::>(); + (None, Some(data_columns)) + } else { + let num_blobs_expected = diet_executed_block.num_blobs_expected(); + let Some(verified_blobs) = verified_blobs + .into_iter() + .map(|b| b.map(|b| b.to_blob())) + .take(num_blobs_expected) + .collect::>>() + .map(Into::into) + else { + return Err(AvailabilityCheckError::Unexpected); + }; + (Some(verified_blobs), None) }; let executed_block = recover(diet_executed_block)?; @@ -475,24 +454,9 @@ impl DataAvailabilityCheckerInner { f(self.critical.read().peek(block_root)) } - fn block_import_requirement( - &self, - epoch: Epoch, - ) -> Result { - let peer_das_enabled = self.spec.is_peer_das_enabled_for_epoch(epoch); - if peer_das_enabled { - Ok(BlockImportRequirement::ColumnSampling( - self.sampling_column_count, - )) - } else { - Ok(BlockImportRequirement::AllBlobs) - } - } - pub fn put_kzg_verified_blobs>>( &self, block_root: Hash256, - epoch: Epoch, kzg_verified_blobs: I, log: &Logger, ) -> Result, AvailabilityCheckError> { @@ -515,12 +479,11 @@ impl DataAvailabilityCheckerInner { // Merge in the blobs. pending_components.merge_blobs(fixed_blobs); - let block_import_requirement = self.block_import_requirement(epoch)?; - if pending_components.is_available(&block_import_requirement, log) { + if pending_components.is_available(self.sampling_column_count, log) { write_lock.put(block_root, pending_components.clone()); // No need to hold the write lock anymore drop(write_lock); - pending_components.make_available(block_import_requirement, &self.spec, |diet_block| { + pending_components.make_available(&self.spec, |diet_block| { self.state_cache.recover_pending_executed_block(diet_block) }) } else { @@ -535,7 +498,6 @@ impl DataAvailabilityCheckerInner { >( &self, block_root: Hash256, - epoch: Epoch, kzg_verified_data_columns: I, log: &Logger, ) -> Result, AvailabilityCheckError> { @@ -550,13 +512,11 @@ impl DataAvailabilityCheckerInner { // Merge in the data columns. pending_components.merge_data_columns(kzg_verified_data_columns)?; - let block_import_requirement = self.block_import_requirement(epoch)?; - - if pending_components.is_available(&block_import_requirement, log) { + if pending_components.is_available(self.sampling_column_count, log) { write_lock.put(block_root, pending_components.clone()); // No need to hold the write lock anymore drop(write_lock); - pending_components.make_available(block_import_requirement, &self.spec, |diet_block| { + pending_components.make_available(&self.spec, |diet_block| { self.state_cache.recover_pending_executed_block(diet_block) }) } else { @@ -625,7 +585,6 @@ impl DataAvailabilityCheckerInner { ) -> Result, AvailabilityCheckError> { let mut write_lock = self.critical.write(); let block_root = executed_block.import_data.block_root; - let epoch = executed_block.block.epoch(); // register the block to get the diet block let diet_executed_block = self @@ -642,12 +601,11 @@ impl DataAvailabilityCheckerInner { pending_components.merge_block(diet_executed_block); // Check if we have all components and entire set is consistent. - let block_import_requirement = self.block_import_requirement(epoch)?; - if pending_components.is_available(&block_import_requirement, log) { + if pending_components.is_available(self.sampling_column_count, log) { write_lock.put(block_root, pending_components.clone()); // No need to hold the write lock anymore drop(write_lock); - pending_components.make_available(block_import_requirement, &self.spec, |diet_block| { + pending_components.make_available(&self.spec, |diet_block| { self.state_cache.recover_pending_executed_block(diet_block) }) } else { @@ -703,6 +661,7 @@ impl DataAvailabilityCheckerInner { #[cfg(test)] mod test { use super::*; + use crate::{ blob_verification::GossipVerifiedBlob, block_verification::PayloadVerificationOutcome, @@ -712,6 +671,7 @@ mod test { test_utils::{BaseHarnessType, BeaconChainHarness, DiskHarnessType}, }; use fork_choice::PayloadVerificationStatus; + use logging::test_logger; use slog::{info, Logger}; use state_processing::ConsensusContext; @@ -931,7 +891,6 @@ mod test { let (pending_block, blobs) = availability_pending_block(&harness).await; let root = pending_block.import_data.block_root; - let epoch = pending_block.block.epoch(); let blobs_expected = pending_block.num_blobs_expected(); assert_eq!( @@ -980,7 +939,7 @@ mod test { for (blob_index, gossip_blob) in blobs.into_iter().enumerate() { kzg_verified_blobs.push(gossip_blob.into_inner()); let availability = cache - .put_kzg_verified_blobs(root, epoch, kzg_verified_blobs.clone(), harness.logger()) + .put_kzg_verified_blobs(root, kzg_verified_blobs.clone(), harness.logger()) .expect("should put blob"); if blob_index == blobs_expected - 1 { assert!(matches!(availability, Availability::Available(_))); @@ -1002,12 +961,11 @@ mod test { "should have expected number of blobs" ); let root = pending_block.import_data.block_root; - let epoch = pending_block.block.epoch(); let mut kzg_verified_blobs = vec![]; for gossip_blob in blobs { kzg_verified_blobs.push(gossip_blob.into_inner()); let availability = cache - .put_kzg_verified_blobs(root, epoch, kzg_verified_blobs.clone(), harness.logger()) + .put_kzg_verified_blobs(root, kzg_verified_blobs.clone(), harness.logger()) .expect("should put blob"); assert_eq!( availability, 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 03e3289118..5b9b7c7023 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 @@ -57,6 +57,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/beacon_chain/src/data_column_verification.rs b/beacon_node/beacon_chain/src/data_column_verification.rs index a4e83b2751..6cfd26786a 100644 --- a/beacon_node/beacon_chain/src/data_column_verification.rs +++ b/beacon_node/beacon_chain/src/data_column_verification.rs @@ -3,6 +3,7 @@ use crate::block_verification::{ BlockSlashInfo, }; use crate::kzg_utils::{reconstruct_data_columns, validate_data_columns}; +use crate::observed_data_sidecars::{ObservationStrategy, Observe}; use crate::{metrics, BeaconChain, BeaconChainError, BeaconChainTypes}; use derivative::Derivative; use fork_choice::ProtoBlock; @@ -13,6 +14,7 @@ use slog::debug; use slot_clock::SlotClock; use ssz_derive::{Decode, Encode}; use std::iter; +use std::marker::PhantomData; use std::sync::Arc; use types::data_column_sidecar::{ColumnIndex, DataColumnIdentifier}; use types::{ @@ -160,17 +162,16 @@ impl From for GossipDataColumnError { } } -pub type GossipVerifiedDataColumnList = RuntimeVariableList>; - /// A wrapper around a `DataColumnSidecar` that indicates it has been approved for re-gossiping on /// the p2p network. #[derive(Debug)] -pub struct GossipVerifiedDataColumn { +pub struct GossipVerifiedDataColumn { block_root: Hash256, data_column: KzgVerifiedDataColumn, + _phantom: PhantomData, } -impl GossipVerifiedDataColumn { +impl GossipVerifiedDataColumn { pub fn new( column_sidecar: Arc>, subnet_id: u64, @@ -179,12 +180,14 @@ impl GossipVerifiedDataColumn { 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), - ) - }) + 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 { @@ -375,11 +378,11 @@ where Ok(()) } -pub fn validate_data_column_sidecar_for_gossip( +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_data_column_sidecar(&data_column, &chain.spec)?; verify_index_matches_subnet(&data_column, subnet, &chain.spec)?; @@ -404,9 +407,14 @@ pub fn validate_data_column_sidecar_for_gossip( ) .map_err(|e| GossipDataColumnError::BeaconChainError(e.into()))?; + if O::observe() { + observe_gossip_data_column(&kzg_verified_data_column.data, chain)?; + } + Ok(GossipVerifiedDataColumn { block_root: data_column.block_root(), data_column: kzg_verified_data_column, + _phantom: PhantomData, }) } @@ -648,11 +656,42 @@ fn verify_sidecar_not_from_future_slot( Ok(()) } +pub fn observe_gossip_data_column( + data_column_sidecar: &DataColumnSidecar, + chain: &BeaconChain, +) -> Result<(), GossipDataColumnError> { + // Now the signature is valid, store the proposal so we don't accept another data column sidecar + // with the same `DataColumnIdentifier`. It's important to double-check that the proposer still + // hasn't been observed so we don't have a race-condition when verifying two blocks + // simultaneously. + // + // Note: If this DataColumnSidecar goes on to fail full verification, we do not evict it from the + // seen_cache as alternate data_column_sidecars for the same identifier can still be retrieved over + // rpc. Evicting them from this cache would allow faster propagation over gossip. So we + // allow retrieval of potentially valid blocks over rpc, but try to punish the proposer for + // signing invalid messages. Issue for more background + // https://github.com/ethereum/consensus-specs/issues/3261 + if chain + .observed_column_sidecars + .write() + .observe_sidecar(data_column_sidecar) + .map_err(|e| GossipDataColumnError::BeaconChainError(e.into()))? + { + return Err(GossipDataColumnError::PriorKnown { + proposer: data_column_sidecar.block_proposer_index(), + slot: data_column_sidecar.slot(), + index: data_column_sidecar.index, + }); + } + Ok(()) +} + #[cfg(test)] mod test { use crate::data_column_verification::{ validate_data_column_sidecar_for_gossip, GossipDataColumnError, }; + use crate::observed_data_sidecars::Observe; use crate::test_utils::BeaconChainHarness; use types::{DataColumnSidecar, EthSpec, ForkName, MainnetEthSpec}; @@ -691,8 +730,11 @@ mod test { .unwrap(), }; - let result = - validate_data_column_sidecar_for_gossip(column_sidecar.into(), index, &harness.chain); + let result = validate_data_column_sidecar_for_gossip::<_, Observe>( + column_sidecar.into(), + index, + &harness.chain, + ); assert!(matches!( result.err(), Some(GossipDataColumnError::UnexpectedDataColumn) diff --git a/beacon_node/beacon_chain/src/fetch_blobs.rs b/beacon_node/beacon_chain/src/fetch_blobs.rs new file mode 100644 index 0000000000..f740b693fb --- /dev/null +++ b/beacon_node/beacon_chain/src/fetch_blobs.rs @@ -0,0 +1,308 @@ +//! This module implements an optimisation to fetch blobs via JSON-RPC from the EL. +//! If a blob has already been seen in the public mempool, then it is often unnecessary to wait for +//! it to arrive on P2P gossip. This PR uses a new JSON-RPC method (`engine_getBlobsV1`) which +//! allows the CL to load the blobs quickly from the EL's blob pool. +//! +//! Once the node fetches the blobs from EL, it then publishes the remaining blobs that it hasn't seen +//! on P2P gossip to the network. From PeerDAS onwards, together with the increase in blob count, +//! broadcasting blobs requires a much higher bandwidth, and is only done by high capacity +//! supernodes. +use crate::blob_verification::{GossipBlobError, GossipVerifiedBlob}; +use crate::kzg_utils::blobs_to_data_column_sidecars; +use crate::observed_data_sidecars::DoNotObserve; +use crate::{metrics, AvailabilityProcessingStatus, BeaconChain, BeaconChainTypes, BlockError}; +use execution_layer::json_structures::BlobAndProofV1; +use execution_layer::Error as ExecutionLayerError; +use metrics::{inc_counter, inc_counter_by, TryExt}; +use slog::{debug, error, o, Logger}; +use ssz_types::FixedVector; +use state_processing::per_block_processing::deneb::kzg_commitment_to_versioned_hash; +use std::sync::Arc; +use tokio::sync::mpsc::Receiver; +use types::blob_sidecar::{BlobSidecarError, FixedBlobSidecarList}; +use types::{ + BeaconStateError, BlobSidecar, DataColumnSidecar, DataColumnSidecarList, EthSpec, FullPayload, + Hash256, SignedBeaconBlock, SignedBeaconBlockHeader, +}; + +pub enum BlobsOrDataColumns { + Blobs(Vec>), + DataColumns(DataColumnSidecarList), +} + +#[derive(Debug)] +pub enum FetchEngineBlobError { + BeaconStateError(BeaconStateError), + BlobProcessingError(BlockError), + BlobSidecarError(BlobSidecarError), + ExecutionLayerMissing, + InternalError(String), + GossipBlob(GossipBlobError), + RequestFailed(ExecutionLayerError), + RuntimeShutdown, +} + +/// Fetches blobs from the EL mempool and processes them. It also broadcasts unseen blobs or +/// data columns (PeerDAS onwards) to the network, using the supplied `publish_fn`. +pub async fn fetch_and_process_engine_blobs( + chain: Arc>, + block_root: Hash256, + block: Arc>>, + publish_fn: impl Fn(BlobsOrDataColumns) + Send + 'static, +) -> Result, FetchEngineBlobError> { + let block_root_str = format!("{:?}", block_root); + let log = chain + .log + .new(o!("service" => "fetch_engine_blobs", "block_root" => block_root_str)); + + let versioned_hashes = if let Some(kzg_commitments) = block + .message() + .body() + .blob_kzg_commitments() + .ok() + .filter(|blobs| !blobs.is_empty()) + { + kzg_commitments + .iter() + .map(kzg_commitment_to_versioned_hash) + .collect::>() + } else { + debug!( + log, + "Fetch blobs not triggered - none required"; + ); + return Ok(None); + }; + + let num_expected_blobs = versioned_hashes.len(); + + let execution_layer = chain + .execution_layer + .as_ref() + .ok_or(FetchEngineBlobError::ExecutionLayerMissing)?; + + debug!( + log, + "Fetching blobs from the EL"; + "num_expected_blobs" => num_expected_blobs, + ); + let response = execution_layer + .get_blobs(versioned_hashes) + .await + .map_err(FetchEngineBlobError::RequestFailed)?; + + if response.is_empty() { + debug!( + log, + "No blobs fetched from the EL"; + "num_expected_blobs" => num_expected_blobs, + ); + inc_counter(&metrics::BLOBS_FROM_EL_MISS_TOTAL); + return Ok(None); + } else { + inc_counter(&metrics::BLOBS_FROM_EL_HIT_TOTAL); + } + + let (signed_block_header, kzg_commitments_proof) = block + .signed_block_header_and_kzg_commitments_proof() + .map_err(FetchEngineBlobError::BeaconStateError)?; + + let fixed_blob_sidecar_list = build_blob_sidecars( + &block, + response, + signed_block_header, + &kzg_commitments_proof, + )?; + + let num_fetched_blobs = fixed_blob_sidecar_list + .iter() + .filter(|b| b.is_some()) + .count(); + + inc_counter_by( + &metrics::BLOBS_FROM_EL_EXPECTED_TOTAL, + num_expected_blobs as u64, + ); + inc_counter_by( + &metrics::BLOBS_FROM_EL_RECEIVED_TOTAL, + num_fetched_blobs as u64, + ); + + // Gossip verify blobs before publishing. This prevents blobs with invalid KZG proofs from + // the EL making it into the data availability checker. We do not immediately add these + // blobs to the observed blobs/columns cache because we want to allow blobs/columns to arrive on gossip + // and be accepted (and propagated) while we are waiting to publish. Just before publishing + // we will observe the blobs/columns and only proceed with publishing if they are not yet seen. + let blobs_to_import_and_publish = fixed_blob_sidecar_list + .iter() + .filter_map(|opt_blob| { + let blob = opt_blob.as_ref()?; + match GossipVerifiedBlob::::new(blob.clone(), blob.index, &chain) { + Ok(verified) => Some(Ok(verified)), + // Ignore already seen blobs. + Err(GossipBlobError::RepeatBlob { .. }) => None, + Err(e) => Some(Err(e)), + } + }) + .collect::, _>>() + .map_err(FetchEngineBlobError::GossipBlob)?; + + let peer_das_enabled = chain.spec.is_peer_das_enabled_for_epoch(block.epoch()); + + let data_columns_receiver_opt = if peer_das_enabled { + // Partial blobs response isn't useful for PeerDAS, so we don't bother building and publishing data columns. + if num_fetched_blobs != num_expected_blobs { + debug!( + log, + "Not all blobs fetched from the EL"; + "info" => "Unable to compute data columns", + "num_fetched_blobs" => num_fetched_blobs, + "num_expected_blobs" => num_expected_blobs, + ); + return Ok(None); + } + + let data_columns_receiver = spawn_compute_and_publish_data_columns_task( + &chain, + block.clone(), + fixed_blob_sidecar_list.clone(), + publish_fn, + log.clone(), + ); + + Some(data_columns_receiver) + } else { + if !blobs_to_import_and_publish.is_empty() { + publish_fn(BlobsOrDataColumns::Blobs(blobs_to_import_and_publish)); + } + + None + }; + + debug!( + log, + "Processing engine blobs"; + "num_fetched_blobs" => num_fetched_blobs, + ); + + let availability_processing_status = chain + .process_engine_blobs( + block.slot(), + block_root, + fixed_blob_sidecar_list.clone(), + data_columns_receiver_opt, + ) + .await + .map_err(FetchEngineBlobError::BlobProcessingError)?; + + Ok(Some(availability_processing_status)) +} + +/// Spawn a blocking task here for long computation tasks, so it doesn't block processing, and it +/// allows blobs / data columns to propagate without waiting for processing. +/// +/// An `mpsc::Sender` is then used to send the produced data columns to the `beacon_chain` for it +/// to be persisted, **after** the block is made attestable. +/// +/// The reason for doing this is to make the block available and attestable as soon as possible, +/// while maintaining the invariant that block and data columns are persisted atomically. +fn spawn_compute_and_publish_data_columns_task( + chain: &Arc>, + block: Arc>>, + blobs: FixedBlobSidecarList, + publish_fn: impl Fn(BlobsOrDataColumns) + Send + 'static, + log: Logger, +) -> Receiver>>> { + let chain_cloned = chain.clone(); + let (data_columns_sender, data_columns_receiver) = tokio::sync::mpsc::channel(1); + + chain.task_executor.spawn_blocking( + move || { + let mut timer = metrics::start_timer_vec( + &metrics::DATA_COLUMN_SIDECAR_COMPUTATION, + &[&blobs.len().to_string()], + ); + let blob_refs = blobs + .iter() + .filter_map(|b| b.as_ref().map(|b| &b.blob)) + .collect::>(); + let data_columns_result = blobs_to_data_column_sidecars( + &blob_refs, + &block, + &chain_cloned.kzg, + &chain_cloned.spec, + ) + .discard_timer_on_break(&mut timer); + drop(timer); + + let all_data_columns = match data_columns_result { + Ok(d) => d, + Err(e) => { + error!( + log, + "Failed to build data column sidecars from blobs"; + "error" => ?e + ); + return; + } + }; + + if let Err(e) = data_columns_sender.try_send(all_data_columns.clone()) { + error!(log, "Failed to send computed data columns"; "error" => ?e); + }; + + // Check indices from cache before sending the columns, to make sure we don't + // publish components already seen on gossip. + let is_supernode = chain_cloned.data_availability_checker.is_supernode(); + + // At the moment non supernodes are not required to publish any columns. + // TODO(das): we could experiment with having full nodes publish their custodied + // columns here. + if !is_supernode { + return; + } + + publish_fn(BlobsOrDataColumns::DataColumns(all_data_columns)); + }, + "compute_and_publish_data_columns", + ); + + data_columns_receiver +} + +fn build_blob_sidecars( + block: &Arc>>, + response: Vec>>, + signed_block_header: SignedBeaconBlockHeader, + kzg_commitments_inclusion_proof: &FixedVector, +) -> Result, FetchEngineBlobError> { + let mut fixed_blob_sidecar_list = FixedBlobSidecarList::default(); + for (index, blob_and_proof) in response + .into_iter() + .enumerate() + .filter_map(|(i, opt_blob)| Some((i, opt_blob?))) + { + match BlobSidecar::new_with_existing_proof( + index, + blob_and_proof.blob, + block, + signed_block_header.clone(), + kzg_commitments_inclusion_proof, + blob_and_proof.proof, + ) { + Ok(blob) => { + if let Some(blob_mut) = fixed_blob_sidecar_list.get_mut(index) { + *blob_mut = Some(Arc::new(blob)); + } else { + return Err(FetchEngineBlobError::InternalError(format!( + "Blobs from EL contains blob with invalid index {index}" + ))); + } + } + Err(e) => { + return Err(FetchEngineBlobError::BlobSidecarError(e)); + } + } + } + Ok(fixed_blob_sidecar_list) +} diff --git a/beacon_node/beacon_chain/src/kzg_utils.rs b/beacon_node/beacon_chain/src/kzg_utils.rs index 91c1098f81..1680c0298d 100644 --- a/beacon_node/beacon_chain/src/kzg_utils.rs +++ b/beacon_node/beacon_chain/src/kzg_utils.rs @@ -7,8 +7,8 @@ use std::sync::Arc; use types::beacon_block_body::KzgCommitments; use types::data_column_sidecar::{Cell, DataColumn, DataColumnSidecarError}; use types::{ - Blob, BlobsList, ChainSpec, ColumnIndex, DataColumnSidecar, DataColumnSidecarList, EthSpec, - Hash256, KzgCommitment, KzgProof, KzgProofs, SignedBeaconBlock, SignedBeaconBlockHeader, + Blob, ChainSpec, ColumnIndex, DataColumnSidecar, DataColumnSidecarList, EthSpec, Hash256, + KzgCommitment, KzgProof, KzgProofs, SignedBeaconBlock, SignedBeaconBlockHeader, }; /// Converts a blob ssz List object to an array to be used with the kzg @@ -146,7 +146,7 @@ pub fn verify_kzg_proof( /// Build data column sidecars from a signed beacon block and its blobs. pub fn blobs_to_data_column_sidecars( - blobs: &BlobsList, + blobs: &[&Blob], block: &SignedBeaconBlock, kzg: &Kzg, spec: &ChainSpec, @@ -154,6 +154,7 @@ pub fn blobs_to_data_column_sidecars( if blobs.is_empty() { return Ok(vec![]); } + let kzg_commitments = block .message() .body() @@ -312,19 +313,21 @@ mod test { #[track_caller] fn test_build_data_columns_empty(kzg: &Kzg, spec: &ChainSpec) { let num_of_blobs = 0; - let (signed_block, blob_sidecars) = create_test_block_and_blobs::(num_of_blobs, spec); + let (signed_block, blobs) = create_test_block_and_blobs::(num_of_blobs, spec); + let blob_refs = blobs.iter().collect::>(); let column_sidecars = - blobs_to_data_column_sidecars(&blob_sidecars, &signed_block, kzg, spec).unwrap(); + blobs_to_data_column_sidecars(&blob_refs, &signed_block, kzg, spec).unwrap(); assert!(column_sidecars.is_empty()); } #[track_caller] fn test_build_data_columns(kzg: &Kzg, spec: &ChainSpec) { let num_of_blobs = 6; - let (signed_block, blob_sidecars) = create_test_block_and_blobs::(num_of_blobs, spec); + let (signed_block, blobs) = create_test_block_and_blobs::(num_of_blobs, spec); + let blob_refs = blobs.iter().collect::>(); let column_sidecars = - blobs_to_data_column_sidecars(&blob_sidecars, &signed_block, kzg, spec).unwrap(); + blobs_to_data_column_sidecars(&blob_refs, &signed_block, kzg, spec).unwrap(); let block_kzg_commitments = signed_block .message() @@ -358,9 +361,10 @@ mod test { #[track_caller] fn test_reconstruct_data_columns(kzg: &Kzg, spec: &ChainSpec) { let num_of_blobs = 6; - let (signed_block, blob_sidecars) = create_test_block_and_blobs::(num_of_blobs, spec); + let (signed_block, blobs) = create_test_block_and_blobs::(num_of_blobs, spec); + let blob_refs = blobs.iter().collect::>(); let column_sidecars = - blobs_to_data_column_sidecars(&blob_sidecars, &signed_block, kzg, spec).unwrap(); + blobs_to_data_column_sidecars(&blob_refs, &signed_block, kzg, spec).unwrap(); // Now reconstruct let reconstructed_columns = reconstruct_data_columns( diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index b89c00e0af..2953516fb1 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -28,6 +28,7 @@ pub mod eth1_chain; mod eth1_finalization_cache; pub mod events; pub mod execution_payload; +pub mod fetch_blobs; pub mod fork_choice_signal; pub mod fork_revert; pub mod graffiti_calculator; @@ -43,7 +44,7 @@ mod naive_aggregation_pool; pub mod observed_aggregates; mod observed_attesters; pub mod observed_block_producers; -mod observed_data_sidecars; +pub 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 f73775d678..66b300f7f2 100644 --- a/beacon_node/beacon_chain/src/metrics.rs +++ b/beacon_node/beacon_chain/src/metrics.rs @@ -111,6 +111,13 @@ pub static BLOCK_PROCESSING_POST_EXEC_PROCESSING: LazyLock> = linear_buckets(5e-3, 5e-3, 10), ) }); +pub static BLOCK_PROCESSING_DATA_COLUMNS_WAIT: LazyLock> = LazyLock::new(|| { + try_create_histogram_with_buckets( + "beacon_block_processing_data_columns_wait_seconds", + "Time spent waiting for data columns to be computed before starting database write", + exponential_buckets(0.01, 2.0, 10), + ) +}); pub static BLOCK_PROCESSING_DB_WRITE: LazyLock> = LazyLock::new(|| { try_create_histogram( "beacon_block_processing_db_write_seconds", @@ -1691,6 +1698,34 @@ pub static DATA_COLUMNS_SIDECAR_PROCESSING_SUCCESSES: LazyLock> = LazyLock::new(|| { + try_create_int_counter( + "beacon_blobs_from_el_hit_total", + "Number of blob batches fetched from the execution layer", + ) +}); + +pub static BLOBS_FROM_EL_MISS_TOTAL: LazyLock> = LazyLock::new(|| { + try_create_int_counter( + "beacon_blobs_from_el_miss_total", + "Number of blob batches failed to fetch from the execution layer", + ) +}); + +pub static BLOBS_FROM_EL_EXPECTED_TOTAL: LazyLock> = LazyLock::new(|| { + try_create_int_counter( + "beacon_blobs_from_el_expected_total", + "Number of blobs expected from the execution layer", + ) +}); + +pub static BLOBS_FROM_EL_RECEIVED_TOTAL: LazyLock> = LazyLock::new(|| { + try_create_int_counter( + "beacon_blobs_from_el_received_total", + "Number of blobs fetched from the execution layer", + ) +}); + /* * Light server message verification */ diff --git a/beacon_node/beacon_chain/src/observed_data_sidecars.rs b/beacon_node/beacon_chain/src/observed_data_sidecars.rs index 9b59a8f85b..53f8c71f54 100644 --- a/beacon_node/beacon_chain/src/observed_data_sidecars.rs +++ b/beacon_node/beacon_chain/src/observed_data_sidecars.rs @@ -148,6 +148,31 @@ impl ObservedDataSidecars { } } +/// Abstraction to control "observation" of gossip messages (currently just blobs and data columns). +/// +/// If a type returns `false` for `observe` then the message will not be immediately added to its +/// respective gossip observation cache. Unobserved messages should usually be observed later. +pub trait ObservationStrategy { + fn observe() -> bool; +} + +/// Type for messages that are observed immediately. +pub struct Observe; +/// Type for messages that have not been observed. +pub struct DoNotObserve; + +impl ObservationStrategy for Observe { + fn observe() -> bool { + true + } +} + +impl ObservationStrategy for DoNotObserve { + fn observe() -> bool { + false + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index 9be3b4cc2f..093ee0c44b 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -2894,7 +2894,6 @@ 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, @@ -2902,12 +2901,12 @@ pub fn generate_rand_block_and_data_columns( spec: &ChainSpec, ) -> ( SignedBeaconBlock>, - Vec>>, + DataColumnSidecarList, ) { let kzg = get_kzg(spec); 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 blob_refs = blobs.iter().map(|b| &b.blob).collect::>(); + let data_columns = blobs_to_data_column_sidecars(&blob_refs, &block, &kzg, spec).unwrap(); (block, data_columns) } diff --git a/beacon_node/beacon_chain/tests/block_verification.rs b/beacon_node/beacon_chain/tests/block_verification.rs index d239f5089a..f094a173ee 100644 --- a/beacon_node/beacon_chain/tests/block_verification.rs +++ b/beacon_node/beacon_chain/tests/block_verification.rs @@ -976,7 +976,7 @@ async fn block_gossip_verification() { harness .chain - .process_gossip_blob(gossip_verified, || Ok(())) + .process_gossip_blob(gossip_verified) .await .expect("should import valid gossip verified blob"); } @@ -1247,7 +1247,7 @@ async fn verify_block_for_gossip_slashing_detection() { .unwrap(); harness .chain - .process_gossip_blob(verified_blob, || Ok(())) + .process_gossip_blob(verified_blob) .await .unwrap(); } @@ -1726,7 +1726,7 @@ async fn import_execution_pending_block( .unwrap() { ExecutedBlock::Available(block) => chain - .import_available_block(Box::from(block)) + .import_available_block(Box::from(block), None) .await .map_err(|e| format!("{e:?}")), ExecutedBlock::AvailabilityPending(_) => { diff --git a/beacon_node/beacon_chain/tests/events.rs b/beacon_node/beacon_chain/tests/events.rs index 31e69f0524..ab784d3be4 100644 --- a/beacon_node/beacon_chain/tests/events.rs +++ b/beacon_node/beacon_chain/tests/events.rs @@ -35,7 +35,7 @@ async fn blob_sidecar_event_on_process_gossip_blob() { let _ = harness .chain - .process_gossip_blob(gossip_verified_blob, || Ok(())) + .process_gossip_blob(gossip_verified_blob) .await .unwrap(); diff --git a/beacon_node/execution_layer/src/engine_api.rs b/beacon_node/execution_layer/src/engine_api.rs index 1c23c8ba66..083aaf2e25 100644 --- a/beacon_node/execution_layer/src/engine_api.rs +++ b/beacon_node/execution_layer/src/engine_api.rs @@ -1,7 +1,7 @@ use crate::engines::ForkchoiceState; use crate::http::{ ENGINE_FORKCHOICE_UPDATED_V1, ENGINE_FORKCHOICE_UPDATED_V2, ENGINE_FORKCHOICE_UPDATED_V3, - ENGINE_GET_CLIENT_VERSION_V1, ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1, + ENGINE_GET_BLOBS_V1, ENGINE_GET_CLIENT_VERSION_V1, ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1, ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1, ENGINE_GET_PAYLOAD_V1, ENGINE_GET_PAYLOAD_V2, ENGINE_GET_PAYLOAD_V3, ENGINE_GET_PAYLOAD_V4, ENGINE_NEW_PAYLOAD_V1, ENGINE_NEW_PAYLOAD_V2, ENGINE_NEW_PAYLOAD_V3, ENGINE_NEW_PAYLOAD_V4, @@ -507,6 +507,7 @@ pub struct EngineCapabilities { pub get_payload_v3: bool, pub get_payload_v4: bool, pub get_client_version_v1: bool, + pub get_blobs_v1: bool, } impl EngineCapabilities { @@ -554,6 +555,9 @@ impl EngineCapabilities { if self.get_client_version_v1 { response.push(ENGINE_GET_CLIENT_VERSION_V1); } + if self.get_blobs_v1 { + response.push(ENGINE_GET_BLOBS_V1); + } response } diff --git a/beacon_node/execution_layer/src/engine_api/http.rs b/beacon_node/execution_layer/src/engine_api/http.rs index 9c2c43bcf7..d4734be448 100644 --- a/beacon_node/execution_layer/src/engine_api/http.rs +++ b/beacon_node/execution_layer/src/engine_api/http.rs @@ -58,6 +58,9 @@ pub const ENGINE_EXCHANGE_CAPABILITIES_TIMEOUT: Duration = Duration::from_secs(1 pub const ENGINE_GET_CLIENT_VERSION_V1: &str = "engine_getClientVersionV1"; pub const ENGINE_GET_CLIENT_VERSION_TIMEOUT: Duration = Duration::from_secs(1); +pub const ENGINE_GET_BLOBS_V1: &str = "engine_getBlobsV1"; +pub const ENGINE_GET_BLOBS_TIMEOUT: Duration = Duration::from_secs(1); + /// This error is returned during a `chainId` call by Geth. pub const EIP155_ERROR_STR: &str = "chain not synced beyond EIP-155 replay-protection fork block"; /// This code is returned by all clients when a method is not supported @@ -79,6 +82,7 @@ pub static LIGHTHOUSE_CAPABILITIES: &[&str] = &[ ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1, ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1, ENGINE_GET_CLIENT_VERSION_V1, + ENGINE_GET_BLOBS_V1, ]; /// We opt to initialize the JsonClientVersionV1 rather than the ClientVersionV1 @@ -702,6 +706,20 @@ impl HttpJsonRpc { } } + pub async fn get_blobs( + &self, + versioned_hashes: Vec, + ) -> Result>>, Error> { + let params = json!([versioned_hashes]); + + self.rpc_request( + ENGINE_GET_BLOBS_V1, + params, + ENGINE_GET_BLOBS_TIMEOUT * self.execution_timeout_multiplier, + ) + .await + } + pub async fn get_block_by_number<'a>( &self, query: BlockByNumberQuery<'a>, @@ -1067,6 +1085,7 @@ impl HttpJsonRpc { get_payload_v3: capabilities.contains(ENGINE_GET_PAYLOAD_V3), get_payload_v4: capabilities.contains(ENGINE_GET_PAYLOAD_V4), get_client_version_v1: capabilities.contains(ENGINE_GET_CLIENT_VERSION_V1), + get_blobs_v1: capabilities.contains(ENGINE_GET_BLOBS_V1), }) } diff --git a/beacon_node/execution_layer/src/engine_api/json_structures.rs b/beacon_node/execution_layer/src/engine_api/json_structures.rs index 753554c149..efd68f1023 100644 --- a/beacon_node/execution_layer/src/engine_api/json_structures.rs +++ b/beacon_node/execution_layer/src/engine_api/json_structures.rs @@ -7,7 +7,7 @@ use superstruct::superstruct; use types::beacon_block_body::KzgCommitments; use types::blob_sidecar::BlobsList; use types::execution_requests::{ConsolidationRequests, DepositRequests, WithdrawalRequests}; -use types::{FixedVector, Unsigned}; +use types::{Blob, FixedVector, KzgProof, Unsigned}; #[derive(Debug, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -625,6 +625,14 @@ impl From> for BlobsBundle { } } +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(bound = "E: EthSpec", rename_all = "camelCase")] +pub struct BlobAndProofV1 { + #[serde(with = "ssz_types::serde_utils::hex_fixed_vec")] + pub blob: Blob, + pub proof: KzgProof, +} + #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct JsonForkchoiceStateV1 { diff --git a/beacon_node/execution_layer/src/lib.rs b/beacon_node/execution_layer/src/lib.rs index f7e490233f..08a00d7bf8 100644 --- a/beacon_node/execution_layer/src/lib.rs +++ b/beacon_node/execution_layer/src/lib.rs @@ -4,6 +4,7 @@ //! This crate only provides useful functionality for "The Merge", it does not provide any of the //! deposit-contract functionality that the `beacon_node/eth1` crate already provides. +use crate::json_structures::BlobAndProofV1; use crate::payload_cache::PayloadCache; use arc_swap::ArcSwapOption; use auth::{strip_prefix, Auth, JwtKey}; @@ -65,7 +66,7 @@ mod metrics; pub mod payload_cache; mod payload_status; pub mod test_utils; -mod versioned_hashes; +pub mod versioned_hashes; /// Indicates the default jwt authenticated execution endpoint. pub const DEFAULT_EXECUTION_ENDPOINT: &str = "http://localhost:8551/"; @@ -1857,6 +1858,23 @@ impl ExecutionLayer { } } + pub async fn get_blobs( + &self, + query: Vec, + ) -> Result>>, Error> { + let capabilities = self.get_engine_capabilities(None).await?; + + if capabilities.get_blobs_v1 { + self.engine() + .request(|engine| async move { engine.api.get_blobs(query).await }) + .await + .map_err(Box::new) + .map_err(Error::EngineError) + } else { + Ok(vec![None; query.len()]) + } + } + pub async fn get_block_by_number( &self, query: BlockByNumberQuery<'_>, diff --git a/beacon_node/execution_layer/src/test_utils/mod.rs b/beacon_node/execution_layer/src/test_utils/mod.rs index be99b38054..1e71fde255 100644 --- a/beacon_node/execution_layer/src/test_utils/mod.rs +++ b/beacon_node/execution_layer/src/test_utils/mod.rs @@ -53,6 +53,7 @@ pub const DEFAULT_ENGINE_CAPABILITIES: EngineCapabilities = EngineCapabilities { get_payload_v3: true, get_payload_v4: true, get_client_version_v1: true, + get_blobs_v1: true, }; pub static DEFAULT_CLIENT_VERSION: LazyLock = diff --git a/beacon_node/http_api/src/publish_blocks.rs b/beacon_node/http_api/src/publish_blocks.rs index fceeb2dd23..b5aa23acf8 100644 --- a/beacon_node/http_api/src/publish_blocks.rs +++ b/beacon_node/http_api/src/publish_blocks.rs @@ -1,4 +1,5 @@ use crate::metrics; +use std::future::Future; use beacon_chain::blob_verification::{GossipBlobError, GossipVerifiedBlob}; use beacon_chain::block_verification_types::AsBlock; @@ -13,9 +14,10 @@ use eth2::types::{ PublishBlockRequest, SignedBlockContents, }; use execution_layer::ProvenancedPayload; +use futures::TryFutureExt; use lighthouse_network::{NetworkGlobals, PubsubMessage}; use network::NetworkMessage; -use rand::seq::SliceRandom; +use rand::prelude::SliceRandom; use slog::{debug, error, info, warn, Logger}; use slot_clock::SlotClock; use std::marker::PhantomData; @@ -26,9 +28,8 @@ use tokio::sync::mpsc::UnboundedSender; use tree_hash::TreeHash; use types::{ AbstractExecPayload, BeaconBlockRef, BlobSidecar, BlobsList, BlockImportSource, - DataColumnSidecarList, DataColumnSubnetId, EthSpec, ExecPayload, ExecutionBlockHash, ForkName, - FullPayload, FullPayloadBellatrix, Hash256, KzgProofs, SignedBeaconBlock, - SignedBlindedBeaconBlock, + DataColumnSubnetId, EthSpec, ExecPayload, ExecutionBlockHash, ForkName, FullPayload, + FullPayloadBellatrix, Hash256, KzgProofs, SignedBeaconBlock, SignedBlindedBeaconBlock, }; use warp::http::StatusCode; use warp::{reply::Response, Rejection, Reply}; @@ -97,14 +98,9 @@ pub async fn publish_block>( }; let block = unverified_block.inner_block(); debug!(log, "Signed block received in HTTP API"; "slot" => block.slot()); - let malicious_withhold_count = chain.config.malicious_withhold_count; - let chain_cloned = chain.clone(); /* actually publish a block */ let publish_block_p2p = move |block: Arc>, - should_publish_block: bool, - blob_sidecars: Vec>>, - mut data_column_sidecars: DataColumnSidecarList, sender, log, seen_timestamp| @@ -120,53 +116,16 @@ pub async fn publish_block>( publish_delay, ); - let mut pubsub_messages = if should_publish_block { - info!( - log, - "Signed block published to network via HTTP API"; - "slot" => block.slot(), - "blobs_published" => blob_sidecars.len(), - "publish_delay_ms" => publish_delay.as_millis(), - ); - vec![PubsubMessage::BeaconBlock(block.clone())] - } else { - vec![] - }; + info!( + log, + "Signed block published to network via HTTP API"; + "slot" => block.slot(), + "publish_delay_ms" => publish_delay.as_millis(), + ); - match block.as_ref() { - SignedBeaconBlock::Base(_) - | SignedBeaconBlock::Altair(_) - | SignedBeaconBlock::Bellatrix(_) - | SignedBeaconBlock::Capella(_) => { - crate::publish_pubsub_messages(&sender, pubsub_messages) - .map_err(|_| BlockError::BeaconChainError(BeaconChainError::UnableToPublish))?; - } - SignedBeaconBlock::Deneb(_) | SignedBeaconBlock::Electra(_) => { - for blob in blob_sidecars.into_iter() { - pubsub_messages.push(PubsubMessage::BlobSidecar(Box::new((blob.index, blob)))); - } - if malicious_withhold_count > 0 { - let columns_to_keep = data_column_sidecars - .len() - .saturating_sub(malicious_withhold_count); - // Randomize columns before dropping the last malicious_withhold_count items - data_column_sidecars.shuffle(&mut rand::thread_rng()); - drop(data_column_sidecars.drain(columns_to_keep..)); - } + crate::publish_pubsub_message(&sender, PubsubMessage::BeaconBlock(block.clone())) + .map_err(|_| BlockError::BeaconChainError(BeaconChainError::UnableToPublish))?; - for data_col in data_column_sidecars { - let subnet = DataColumnSubnetId::from_column_index::( - data_col.index as usize, - &chain_cloned.spec, - ); - pubsub_messages.push(PubsubMessage::DataColumnSidecar(Box::new(( - subnet, data_col, - )))); - } - crate::publish_pubsub_messages(&sender, pubsub_messages) - .map_err(|_| BlockError::BeaconChainError(BeaconChainError::UnableToPublish))?; - } - }; Ok(()) }; @@ -174,145 +133,11 @@ pub async fn publish_block>( let slot = block.message().slot(); let sender_clone = network_tx.clone(); - // Convert blobs to either: - // - // 1. Blob sidecars if prior to peer DAS, or - // 2. Data column sidecars if post peer DAS. - let peer_das_enabled = chain.spec.is_peer_das_enabled_for_epoch(block.epoch()); - - let (blob_sidecars, data_column_sidecars) = match unverified_blobs { - // Pre-PeerDAS: construct blob sidecars for the network. - Some((kzg_proofs, blobs)) if !peer_das_enabled => { - let blob_sidecars = kzg_proofs - .into_iter() - .zip(blobs) - .enumerate() - .map(|(i, (proof, unverified_blob))| { - let _timer = metrics::start_timer( - &beacon_chain::metrics::BLOB_SIDECAR_INCLUSION_PROOF_COMPUTATION, - ); - let blob_sidecar = - BlobSidecar::new(i, unverified_blob, &block, proof).map(Arc::new); - blob_sidecar.map_err(|e| { - error!( - log, - "Invalid blob - not publishing block"; - "error" => ?e, - "blob_index" => i, - "slot" => slot, - ); - warp_utils::reject::custom_bad_request(format!("{e:?}")) - }) - }) - .collect::, Rejection>>()?; - (blob_sidecars, vec![]) - } - // Post PeerDAS: construct data columns. - Some((_, blobs)) => { - // TODO(das): this is sub-optimal and should likely not be happening prior to gossip - // block publishing. - let data_column_sidecars = build_blob_data_column_sidecars(&chain, &block, blobs) - .map_err(|e| { - error!( - log, - "Invalid data column - not publishing block"; - "error" => ?e, - "slot" => slot - ); - warp_utils::reject::custom_bad_request(format!("{e:?}")) - })?; - (vec![], data_column_sidecars) - } - None => (vec![], vec![]), - }; + let build_sidecar_task_handle = + spawn_build_data_sidecar_task(chain.clone(), block.clone(), unverified_blobs, log.clone())?; // Gossip verify the block and blobs/data columns separately. let gossip_verified_block_result = unverified_block.into_gossip_verified_block(&chain); - let gossip_verified_blobs = blob_sidecars - .into_iter() - .map(|blob_sidecar| { - let gossip_verified_blob = - GossipVerifiedBlob::new(blob_sidecar.clone(), blob_sidecar.index, &chain); - - match gossip_verified_blob { - Ok(blob) => Ok(Some(blob)), - Err(GossipBlobError::RepeatBlob { proposer, .. }) => { - // Log the error but do not abort publication, we may need to publish the block - // or some of the other blobs if the block & blobs are only partially published - // by the other publisher. - debug!( - log, - "Blob for publication already known"; - "blob_index" => blob_sidecar.index, - "slot" => slot, - "proposer" => proposer, - ); - Ok(None) - } - Err(e) => { - error!( - log, - "Blob for publication is gossip-invalid"; - "blob_index" => blob_sidecar.index, - "slot" => slot, - "error" => ?e, - ); - Err(warp_utils::reject::custom_bad_request(e.to_string())) - } - } - }) - .collect::, Rejection>>()?; - - let gossip_verified_data_columns = data_column_sidecars - .into_iter() - .map(|data_column_sidecar| { - let column_index = data_column_sidecar.index as usize; - let subnet = - DataColumnSubnetId::from_column_index::(column_index, &chain.spec); - let gossip_verified_column = - GossipVerifiedDataColumn::new(data_column_sidecar, subnet.into(), &chain); - - match gossip_verified_column { - Ok(blob) => Ok(Some(blob)), - Err(GossipDataColumnError::PriorKnown { proposer, .. }) => { - // Log the error but do not abort publication, we may need to publish the block - // or some of the other data columns if the block & data columns are only - // partially published by the other publisher. - debug!( - log, - "Data column for publication already known"; - "column_index" => column_index, - "slot" => slot, - "proposer" => proposer, - ); - Ok(None) - } - Err(e) => { - error!( - log, - "Data column for publication is gossip-invalid"; - "column_index" => column_index, - "slot" => slot, - "error" => ?e, - ); - Err(warp_utils::reject::custom_bad_request(format!("{e:?}"))) - } - } - }) - .collect::, Rejection>>()?; - - let publishable_blobs = gossip_verified_blobs - .iter() - .flatten() - .map(|b| b.clone_blob()) - .collect::>(); - - let publishable_data_columns = gossip_verified_data_columns - .iter() - .flatten() - .map(|b| b.clone_data_column()) - .collect::>(); - let block_root = block_root.unwrap_or_else(|| { gossip_verified_block_result.as_ref().map_or_else( |_| block.canonical_root(), @@ -321,12 +146,9 @@ pub async fn publish_block>( }); let should_publish_block = gossip_verified_block_result.is_ok(); - if let BroadcastValidation::Gossip = validation_level { + if BroadcastValidation::Gossip == validation_level && should_publish_block { publish_block_p2p( block.clone(), - should_publish_block, - publishable_blobs.clone(), - publishable_data_columns.clone(), sender_clone.clone(), log.clone(), seen_timestamp, @@ -337,38 +159,39 @@ pub async fn publish_block>( let publish_fn_completed = Arc::new(AtomicBool::new(false)); let block_to_publish = block.clone(); let publish_fn = || { - match validation_level { - BroadcastValidation::Gossip => (), - BroadcastValidation::Consensus => publish_block_p2p( - block_to_publish.clone(), - should_publish_block, - publishable_blobs.clone(), - publishable_data_columns.clone(), - sender_clone.clone(), - log.clone(), - seen_timestamp, - )?, - BroadcastValidation::ConsensusAndEquivocation => { - check_slashable(&chain, block_root, &block_to_publish, &log)?; - publish_block_p2p( + if should_publish_block { + match validation_level { + BroadcastValidation::Gossip => (), + BroadcastValidation::Consensus => publish_block_p2p( block_to_publish.clone(), - should_publish_block, - publishable_blobs.clone(), - publishable_data_columns.clone(), sender_clone.clone(), log.clone(), seen_timestamp, - )?; - } - }; + )?, + BroadcastValidation::ConsensusAndEquivocation => { + check_slashable(&chain, block_root, &block_to_publish, &log)?; + publish_block_p2p( + block_to_publish.clone(), + sender_clone.clone(), + log.clone(), + seen_timestamp, + )?; + } + }; + } + publish_fn_completed.store(true, Ordering::SeqCst); Ok(()) }; + // Wait for blobs/columns to get gossip verified before proceeding further as we need them for import. + let (gossip_verified_blobs, gossip_verified_columns) = build_sidecar_task_handle.await?; + for blob in gossip_verified_blobs.into_iter().flatten() { - // Importing the blobs could trigger block import and network publication in the case - // where the block was already seen on gossip. - if let Err(e) = Box::pin(chain.process_gossip_blob(blob, &publish_fn)).await { + publish_blob_sidecars(network_tx, &blob).map_err(|_| { + warp_utils::reject::custom_server_error("unable to publish blob sidecars".into()) + })?; + if let Err(e) = Box::pin(chain.process_gossip_blob(blob)).await { let msg = format!("Invalid blob: {e}"); return if let BroadcastValidation::Gossip = validation_level { Err(warp_utils::reject::broadcast_without_import(msg)) @@ -383,14 +206,12 @@ pub async fn publish_block>( } } - if gossip_verified_data_columns - .iter() - .map(Option::is_some) - .count() - > 0 - { + if gossip_verified_columns.iter().map(Option::is_some).count() > 0 { + publish_column_sidecars(network_tx, &gossip_verified_columns, &chain).map_err(|_| { + warp_utils::reject::custom_server_error("unable to publish data column sidecars".into()) + })?; let sampling_columns_indices = &network_globals.sampling_columns; - let sampling_columns = gossip_verified_data_columns + let sampling_columns = gossip_verified_columns .into_iter() .flatten() .filter(|data_column| sampling_columns_indices.contains(&data_column.index())) @@ -501,6 +322,224 @@ pub async fn publish_block>( } } +type BuildDataSidecarTaskResult = Result< + ( + Vec>>, + Vec>>, + ), + Rejection, +>; + +/// Convert blobs to either: +/// +/// 1. Blob sidecars if prior to peer DAS, or +/// 2. Data column sidecars if post peer DAS. +fn spawn_build_data_sidecar_task( + chain: Arc>, + block: Arc>>, + proofs_and_blobs: UnverifiedBlobs, + log: Logger, +) -> Result>, Rejection> { + chain + .clone() + .task_executor + .spawn_blocking_handle( + move || { + let Some((kzg_proofs, blobs)) = proofs_and_blobs else { + return Ok((vec![], vec![])); + }; + + let peer_das_enabled = chain.spec.is_peer_das_enabled_for_epoch(block.epoch()); + if !peer_das_enabled { + // Pre-PeerDAS: construct blob sidecars for the network. + let gossip_verified_blobs = + build_gossip_verified_blobs(&chain, &block, blobs, kzg_proofs, &log)?; + Ok((gossip_verified_blobs, vec![])) + } else { + // Post PeerDAS: construct data columns. + let gossip_verified_data_columns = + build_gossip_verified_data_columns(&chain, &block, blobs, &log)?; + Ok((vec![], gossip_verified_data_columns)) + } + }, + "build_data_sidecars", + ) + .ok_or(warp_utils::reject::custom_server_error( + "runtime shutdown".to_string(), + )) + .map(|r| { + r.map_err(|_| warp_utils::reject::custom_server_error("join error".to_string())) + .and_then(|output| async move { output }) + }) +} + +fn build_gossip_verified_data_columns( + chain: &BeaconChain, + block: &SignedBeaconBlock>, + blobs: BlobsList, + log: &Logger, +) -> Result>>, Rejection> { + let slot = block.slot(); + let data_column_sidecars = + build_blob_data_column_sidecars(chain, block, blobs).map_err(|e| { + error!( + log, + "Invalid data column - not publishing block"; + "error" => ?e, + "slot" => slot + ); + warp_utils::reject::custom_bad_request(format!("{e:?}")) + })?; + + let slot = block.slot(); + let gossip_verified_data_columns = data_column_sidecars + .into_iter() + .map(|data_column_sidecar| { + let column_index = data_column_sidecar.index as usize; + let subnet = + DataColumnSubnetId::from_column_index::(column_index, &chain.spec); + let gossip_verified_column = + GossipVerifiedDataColumn::new(data_column_sidecar, subnet.into(), chain); + + match gossip_verified_column { + Ok(blob) => Ok(Some(blob)), + Err(GossipDataColumnError::PriorKnown { proposer, .. }) => { + // Log the error but do not abort publication, we may need to publish the block + // or some of the other data columns if the block & data columns are only + // partially published by the other publisher. + debug!( + log, + "Data column for publication already known"; + "column_index" => column_index, + "slot" => slot, + "proposer" => proposer, + ); + Ok(None) + } + Err(e) => { + error!( + log, + "Data column for publication is gossip-invalid"; + "column_index" => column_index, + "slot" => slot, + "error" => ?e, + ); + Err(warp_utils::reject::custom_bad_request(format!("{e:?}"))) + } + } + }) + .collect::, Rejection>>()?; + + Ok(gossip_verified_data_columns) +} + +fn build_gossip_verified_blobs( + chain: &BeaconChain, + block: &SignedBeaconBlock>, + blobs: BlobsList, + kzg_proofs: KzgProofs, + log: &Logger, +) -> Result>>, Rejection> { + let slot = block.slot(); + let gossip_verified_blobs = kzg_proofs + .into_iter() + .zip(blobs) + .enumerate() + .map(|(i, (proof, unverified_blob))| { + let timer = metrics::start_timer( + &beacon_chain::metrics::BLOB_SIDECAR_INCLUSION_PROOF_COMPUTATION, + ); + let blob_sidecar = BlobSidecar::new(i, unverified_blob, block, proof) + .map(Arc::new) + .map_err(|e| { + error!( + log, + "Invalid blob - not publishing block"; + "error" => ?e, + "blob_index" => i, + "slot" => slot, + ); + warp_utils::reject::custom_bad_request(format!("{e:?}")) + })?; + drop(timer); + + let gossip_verified_blob = + GossipVerifiedBlob::new(blob_sidecar.clone(), blob_sidecar.index, chain); + + match gossip_verified_blob { + Ok(blob) => Ok(Some(blob)), + Err(GossipBlobError::RepeatBlob { proposer, .. }) => { + // Log the error but do not abort publication, we may need to publish the block + // or some of the other blobs if the block & blobs are only partially published + // by the other publisher. + debug!( + log, + "Blob for publication already known"; + "blob_index" => blob_sidecar.index, + "slot" => slot, + "proposer" => proposer, + ); + Ok(None) + } + Err(e) => { + error!( + log, + "Blob for publication is gossip-invalid"; + "blob_index" => blob_sidecar.index, + "slot" => slot, + "error" => ?e, + ); + Err(warp_utils::reject::custom_bad_request(e.to_string())) + } + } + }) + .collect::, Rejection>>()?; + + Ok(gossip_verified_blobs) +} + +fn publish_column_sidecars( + sender_clone: &UnboundedSender>, + data_column_sidecars: &[Option>], + chain: &BeaconChain, +) -> Result<(), BlockError> { + let malicious_withhold_count = chain.config.malicious_withhold_count; + let mut data_column_sidecars = data_column_sidecars + .iter() + .flatten() + .map(|d| d.clone_data_column()) + .collect::>(); + if malicious_withhold_count > 0 { + let columns_to_keep = data_column_sidecars + .len() + .saturating_sub(malicious_withhold_count); + // Randomize columns before dropping the last malicious_withhold_count items + data_column_sidecars.shuffle(&mut rand::thread_rng()); + data_column_sidecars.truncate(columns_to_keep); + } + let pubsub_messages = data_column_sidecars + .into_iter() + .map(|data_col| { + let subnet = DataColumnSubnetId::from_column_index::( + data_col.index as usize, + &chain.spec, + ); + PubsubMessage::DataColumnSidecar(Box::new((subnet, data_col))) + }) + .collect::>(); + crate::publish_pubsub_messages(sender_clone, pubsub_messages) + .map_err(|_| BlockError::BeaconChainError(BeaconChainError::UnableToPublish)) +} + +fn publish_blob_sidecars( + sender_clone: &UnboundedSender>, + blob: &GossipVerifiedBlob, +) -> Result<(), BlockError> { + let pubsub_message = PubsubMessage::BlobSidecar(Box::new((blob.index(), blob.clone_blob()))); + crate::publish_pubsub_message(sender_clone, pubsub_message) + .map_err(|_| BlockError::BeaconChainError(BeaconChainError::UnableToPublish)) +} + async fn post_block_import_logging_and_response( result: Result, validation_level: BroadcastValidation, diff --git a/beacon_node/http_api/tests/broadcast_validation_tests.rs b/beacon_node/http_api/tests/broadcast_validation_tests.rs index f55983ec66..1338f4f180 100644 --- a/beacon_node/http_api/tests/broadcast_validation_tests.rs +++ b/beacon_node/http_api/tests/broadcast_validation_tests.rs @@ -1486,7 +1486,7 @@ pub async fn block_seen_on_gossip_with_some_blobs() { tester .harness .chain - .process_gossip_blob(gossip_blob, || panic!("should not publish block yet")) + .process_gossip_blob(gossip_blob) .await .unwrap(); } @@ -1559,7 +1559,7 @@ pub async fn blobs_seen_on_gossip_without_block() { tester .harness .chain - .process_gossip_blob(gossip_blob, || panic!("should not publish block yet")) + .process_gossip_blob(gossip_blob) .await .unwrap(); } @@ -1633,7 +1633,7 @@ pub async fn blobs_seen_on_gossip_without_block_and_no_http_blobs() { tester .harness .chain - .process_gossip_blob(gossip_blob, || panic!("should not publish block yet")) + .process_gossip_blob(gossip_blob) .await .unwrap(); } @@ -1705,7 +1705,7 @@ pub async fn slashable_blobs_seen_on_gossip_cause_failure() { tester .harness .chain - .process_gossip_blob(gossip_blob, || panic!("should not publish block yet")) + .process_gossip_blob(gossip_blob) .await .unwrap(); } 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 4d875cb4a1..e92f450476 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -914,18 +914,15 @@ impl NetworkBeaconProcessor { let blob_slot = verified_blob.slot(); let blob_index = verified_blob.id().index; - let result = self - .chain - .process_gossip_blob(verified_blob, || Ok(())) - .await; + let result = self.chain.process_gossip_blob(verified_blob).await; match &result { Ok(AvailabilityProcessingStatus::Imported(block_root)) => { // Note: Reusing block imported metric here metrics::inc_counter(&metrics::BEACON_PROCESSOR_GOSSIP_BLOCK_IMPORTED_TOTAL); - info!( + debug!( self.log, - "Gossipsub blob processed, imported fully available block"; + "Gossipsub blob processed - imported fully available block"; "block_root" => %block_root ); self.chain.recompute_head_at_current_slot().await; @@ -936,9 +933,9 @@ impl NetworkBeaconProcessor { ); } Ok(AvailabilityProcessingStatus::MissingComponents(slot, block_root)) => { - trace!( + debug!( self.log, - "Processed blob, waiting for other components"; + "Processed gossip blob - waiting for other components"; "slot" => %slot, "blob_index" => %blob_index, "block_root" => %block_root, @@ -1079,7 +1076,7 @@ impl NetworkBeaconProcessor { message_id, peer_id, peer_client, - block, + block.clone(), reprocess_tx.clone(), seen_duration, ) @@ -1497,6 +1494,13 @@ impl NetworkBeaconProcessor { "slot" => slot, "block_root" => %block_root, ); + + // Block is valid, we can now attempt fetching blobs from EL using version hashes + // derived from kzg commitments from the block, without having to wait for all blobs + // to be sent from the peers if we already have them. + let publish_blobs = true; + self.fetch_engine_blobs_and_publish(block.clone(), *block_root, publish_blobs) + .await; } Err(BlockError::ParentUnknown { .. }) => { // This should not occur. It should be checked by `should_forward_block`. diff --git a/beacon_node/network/src/network_beacon_processor/mod.rs b/beacon_node/network/src/network_beacon_processor/mod.rs index 76f5e886ff..d81d964e7c 100644 --- a/beacon_node/network/src/network_beacon_processor/mod.rs +++ b/beacon_node/network/src/network_beacon_processor/mod.rs @@ -1,11 +1,17 @@ use crate::sync::manager::BlockProcessType; use crate::sync::SamplingId; use crate::{service::NetworkMessage, sync::manager::SyncMessage}; +use beacon_chain::blob_verification::{GossipBlobError, GossipVerifiedBlob}; use beacon_chain::block_verification_types::RpcBlock; +use beacon_chain::data_column_verification::{observe_gossip_data_column, GossipDataColumnError}; +use beacon_chain::fetch_blobs::{ + fetch_and_process_engine_blobs, BlobsOrDataColumns, FetchEngineBlobError, +}; +use beacon_chain::observed_data_sidecars::DoNotObserve; use beacon_chain::{ builder::Witness, eth1_chain::CachingEth1Backend, AvailabilityProcessingStatus, BeaconChain, + BeaconChainTypes, BlockError, NotifyExecutionLayer, }; -use beacon_chain::{BeaconChainTypes, NotifyExecutionLayer}; use beacon_processor::{ work_reprocessing_queue::ReprocessQueueMessage, BeaconProcessorChannels, BeaconProcessorSend, DuplicateCache, GossipAggregatePackage, GossipAttestationPackage, Work, @@ -21,7 +27,8 @@ use lighthouse_network::{ rpc::{BlocksByRangeRequest, BlocksByRootRequest, LightClientBootstrapRequest, StatusMessage}, Client, MessageId, NetworkGlobals, PeerId, PubsubMessage, }; -use slog::{debug, error, trace, Logger}; +use rand::prelude::SliceRandom; +use slog::{debug, error, trace, warn, Logger}; use slot_clock::ManualSlotClock; use std::path::PathBuf; use std::sync::Arc; @@ -67,6 +74,9 @@ pub struct NetworkBeaconProcessor { pub log: Logger, } +// Publish blobs in batches of exponentially increasing size. +const BLOB_PUBLICATION_EXP_FACTOR: usize = 2; + impl NetworkBeaconProcessor { fn try_send(&self, event: BeaconWorkEvent) -> Result<(), Error> { self.beacon_processor_send @@ -878,6 +888,79 @@ impl NetworkBeaconProcessor { }); } + pub async fn fetch_engine_blobs_and_publish( + self: &Arc, + block: Arc>>, + block_root: Hash256, + publish_blobs: bool, + ) { + let self_cloned = self.clone(); + let publish_fn = move |blobs_or_data_column| { + if publish_blobs { + match blobs_or_data_column { + BlobsOrDataColumns::Blobs(blobs) => { + self_cloned.publish_blobs_gradually(blobs, block_root); + } + BlobsOrDataColumns::DataColumns(columns) => { + self_cloned.publish_data_columns_gradually(columns, block_root); + } + }; + } + }; + + match fetch_and_process_engine_blobs( + self.chain.clone(), + block_root, + block.clone(), + publish_fn, + ) + .await + { + Ok(Some(availability)) => match availability { + AvailabilityProcessingStatus::Imported(_) => { + debug!( + self.log, + "Block components retrieved from EL"; + "result" => "imported block and custody columns", + "block_root" => %block_root, + ); + self.chain.recompute_head_at_current_slot().await; + } + AvailabilityProcessingStatus::MissingComponents(_, _) => { + debug!( + self.log, + "Still missing blobs after engine blobs processed successfully"; + "block_root" => %block_root, + ); + } + }, + Ok(None) => { + debug!( + self.log, + "Fetch blobs completed without import"; + "block_root" => %block_root, + ); + } + Err(FetchEngineBlobError::BlobProcessingError(BlockError::DuplicateFullyImported( + .., + ))) => { + debug!( + self.log, + "Fetch blobs duplicate import"; + "block_root" => %block_root, + ); + } + Err(e) => { + error!( + self.log, + "Error fetching or processing blobs from EL"; + "error" => ?e, + "block_root" => %block_root, + ); + } + } + } + /// Attempt to reconstruct all data columns if the following conditions satisfies: /// - Our custody requirement is all columns /// - We have >= 50% of columns, but not all columns @@ -885,25 +968,13 @@ impl NetworkBeaconProcessor { /// Returns `Some(AvailabilityProcessingStatus)` if reconstruction is successfully performed, /// otherwise returns `None`. async fn attempt_data_column_reconstruction( - &self, + self: &Arc, block_root: Hash256, ) -> Option { let result = self.chain.reconstruct_data_columns(block_root).await; match result { Ok(Some((availability_processing_status, 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, - &self.chain.spec, - ); - PubsubMessage::DataColumnSidecar(Box::new((subnet, d.clone()))) - }) - .collect(), - }); - + self.publish_data_columns_gradually(data_columns_to_publish, block_root); match &availability_processing_status { AvailabilityProcessingStatus::Imported(hash) => { debug!( @@ -946,6 +1017,175 @@ impl NetworkBeaconProcessor { } } } + + /// This function gradually publishes blobs to the network in randomised batches. + /// + /// This is an optimisation to reduce outbound bandwidth and ensures each blob is published + /// by some nodes on the network as soon as possible. Our hope is that some blobs arrive from + /// other nodes in the meantime, obviating the need for us to publish them. If no other + /// publisher exists for a blob, it will eventually get published here. + fn publish_blobs_gradually( + self: &Arc, + mut blobs: Vec>, + block_root: Hash256, + ) { + let self_clone = self.clone(); + + self.executor.spawn( + async move { + let chain = self_clone.chain.clone(); + let log = self_clone.chain.logger(); + let publish_fn = |blobs: Vec>>| { + self_clone.send_network_message(NetworkMessage::Publish { + messages: blobs + .into_iter() + .map(|blob| PubsubMessage::BlobSidecar(Box::new((blob.index, blob)))) + .collect(), + }); + }; + + // Permute the blobs and split them into batches. + // The hope is that we won't need to publish some blobs because we will receive them + // on gossip from other nodes. + blobs.shuffle(&mut rand::thread_rng()); + + let blob_publication_batch_interval = chain.config.blob_publication_batch_interval; + let mut publish_count = 0usize; + let blob_count = blobs.len(); + let mut blobs_iter = blobs.into_iter().peekable(); + let mut batch_size = 1usize; + + while blobs_iter.peek().is_some() { + let batch = blobs_iter.by_ref().take(batch_size); + let publishable = batch + .filter_map(|unobserved| match unobserved.observe(&chain) { + Ok(observed) => Some(observed.clone_blob()), + Err(GossipBlobError::RepeatBlob { .. }) => None, + Err(e) => { + warn!( + log, + "Previously verified blob is invalid"; + "error" => ?e + ); + None + } + }) + .collect::>(); + + if !publishable.is_empty() { + debug!( + log, + "Publishing blob batch"; + "publish_count" => publishable.len(), + "block_root" => ?block_root, + ); + publish_count += publishable.len(); + publish_fn(publishable); + } + + tokio::time::sleep(blob_publication_batch_interval).await; + batch_size *= BLOB_PUBLICATION_EXP_FACTOR; + } + + debug!( + log, + "Batch blob publication complete"; + "batch_interval" => blob_publication_batch_interval.as_millis(), + "blob_count" => blob_count, + "published_count" => publish_count, + "block_root" => ?block_root, + ) + }, + "gradual_blob_publication", + ); + } + + /// This function gradually publishes data columns to the network in randomised batches. + /// + /// This is an optimisation to reduce outbound bandwidth and ensures each column is published + /// by some nodes on the network as soon as possible. Our hope is that some columns arrive from + /// other supernodes in the meantime, obviating the need for us to publish them. If no other + /// publisher exists for a column, it will eventually get published here. + fn publish_data_columns_gradually( + self: &Arc, + mut data_columns_to_publish: DataColumnSidecarList, + block_root: Hash256, + ) { + let self_clone = self.clone(); + + self.executor.spawn( + async move { + let chain = self_clone.chain.clone(); + let log = self_clone.chain.logger(); + let publish_fn = |columns: DataColumnSidecarList| { + self_clone.send_network_message(NetworkMessage::Publish { + messages: columns + .into_iter() + .map(|d| { + let subnet = DataColumnSubnetId::from_column_index::( + d.index as usize, + &chain.spec, + ); + PubsubMessage::DataColumnSidecar(Box::new((subnet, d))) + }) + .collect(), + }); + }; + + // If this node is a super node, permute the columns and split them into batches. + // The hope is that we won't need to publish some columns because we will receive them + // on gossip from other supernodes. + data_columns_to_publish.shuffle(&mut rand::thread_rng()); + + let blob_publication_batch_interval = chain.config.blob_publication_batch_interval; + let blob_publication_batches = chain.config.blob_publication_batches; + let batch_size = chain.spec.number_of_columns / blob_publication_batches; + let mut publish_count = 0usize; + + for batch in data_columns_to_publish.chunks(batch_size) { + let publishable = batch + .iter() + .filter_map(|col| match observe_gossip_data_column(col, &chain) { + Ok(()) => Some(col.clone()), + Err(GossipDataColumnError::PriorKnown { .. }) => None, + Err(e) => { + warn!( + log, + "Previously verified data column is invalid"; + "error" => ?e + ); + None + } + }) + .collect::>(); + + if !publishable.is_empty() { + debug!( + log, + "Publishing data column batch"; + "publish_count" => publishable.len(), + "block_root" => ?block_root, + ); + publish_count += publishable.len(); + publish_fn(publishable); + } + + tokio::time::sleep(blob_publication_batch_interval).await; + } + + debug!( + log, + "Batch data column publishing complete"; + "batch_size" => batch_size, + "batch_interval" => blob_publication_batch_interval.as_millis(), + "data_columns_to_publish_count" => data_columns_to_publish.len(), + "published_count" => publish_count, + "block_root" => ?block_root, + ) + }, + "gradual_data_column_publication", + ); + } } type TestBeaconChainType = 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 d86dfae63a..8e943e6383 100644 --- a/beacon_node/network/src/network_beacon_processor/sync_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/sync_methods.rs @@ -153,6 +153,7 @@ impl NetworkBeaconProcessor { "process_type" => ?process_type, ); + let signed_beacon_block = block.block_cloned(); let result = self .chain .process_block_with_early_caching( @@ -166,26 +167,37 @@ impl NetworkBeaconProcessor { metrics::inc_counter(&metrics::BEACON_PROCESSOR_RPC_BLOCK_IMPORTED_TOTAL); // RPC block imported, regardless of process type - if let &Ok(AvailabilityProcessingStatus::Imported(hash)) = &result { - info!(self.log, "New RPC block received"; "slot" => slot, "hash" => %hash); + match result.as_ref() { + Ok(AvailabilityProcessingStatus::Imported(hash)) => { + info!(self.log, "New RPC block received"; "slot" => slot, "hash" => %hash); - // Trigger processing for work referencing this block. - let reprocess_msg = ReprocessQueueMessage::BlockImported { - block_root: hash, - parent_root, - }; - if reprocess_tx.try_send(reprocess_msg).is_err() { - error!(self.log, "Failed to inform block import"; "source" => "rpc", "block_root" => %hash) - }; - self.chain.block_times_cache.write().set_time_observed( - hash, - slot, - seen_timestamp, - None, - None, - ); + // Trigger processing for work referencing this block. + let reprocess_msg = ReprocessQueueMessage::BlockImported { + block_root: *hash, + parent_root, + }; + if reprocess_tx.try_send(reprocess_msg).is_err() { + error!(self.log, "Failed to inform block import"; "source" => "rpc", "block_root" => %hash) + }; + 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; + } + Ok(AvailabilityProcessingStatus::MissingComponents(..)) => { + // Block is valid, we can now attempt fetching blobs from EL using version hashes + // derived from kzg commitments from the block, without having to wait for all blobs + // to be sent from the peers if we already have them. + let publish_blobs = false; + self.fetch_engine_blobs_and_publish(signed_beacon_block, block_root, publish_blobs) + .await + } + _ => {} } // RPC block imported or execution validated. If the block was already imported by gossip we diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index 34b03a0955..cfda99325b 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -86,6 +86,24 @@ pub fn cli_app() -> Command { .hide(true) .display_order(0) ) + .arg( + Arg::new("blob-publication-batches") + .long("blob-publication-batches") + .action(ArgAction::Set) + .help_heading(FLAG_HEADER) + .help("Number of batches that the node splits blobs or data columns into during publication. This doesn't apply if the node is the block proposer. Used in PeerDAS only.") + .display_order(0) + .hide(true) + ) + .arg( + Arg::new("blob-publication-batch-interval") + .long("blob-publication-batch-interval") + .action(ArgAction::Set) + .help_heading(FLAG_HEADER) + .help("The delay in milliseconds applied by the node between sending each blob or data column batch. This doesn't apply if the node is the block proposer.") + .display_order(0) + .hide(true) + ) .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 ecadee5f47..d12c6d6681 100644 --- a/beacon_node/src/config.rs +++ b/beacon_node/src/config.rs @@ -192,6 +192,15 @@ pub fn get_config( client_config.chain.enable_sampling = true; } + if let Some(batches) = clap_utils::parse_optional(cli_args, "blob-publication-batches")? { + client_config.chain.blob_publication_batches = batches; + } + + if let Some(interval) = clap_utils::parse_optional(cli_args, "blob-publication-batch-interval")? + { + client_config.chain.blob_publication_batch_interval = Duration::from_millis(interval); + } + /* * Prometheus metrics HTTP server */ diff --git a/consensus/types/src/beacon_block_body.rs b/consensus/types/src/beacon_block_body.rs index c81e7bcde9..1090b2cc03 100644 --- a/consensus/types/src/beacon_block_body.rs +++ b/consensus/types/src/beacon_block_body.rs @@ -147,7 +147,7 @@ impl<'a, E: EthSpec, Payload: AbstractExecPayload> BeaconBlockBodyRef<'a, E, } } - fn body_merkle_leaves(&self) -> Vec { + pub(crate) fn body_merkle_leaves(&self) -> Vec { let mut leaves = vec![]; match self { Self::Base(body) => { @@ -178,57 +178,71 @@ impl<'a, E: EthSpec, Payload: AbstractExecPayload> BeaconBlockBodyRef<'a, E, leaves } - /// Produces the proof of inclusion for a `KzgCommitment` in `self.blob_kzg_commitments` - /// at `index`. + /// Calculate a KZG commitment merkle proof. + /// + /// Prefer to use `complete_kzg_commitment_merkle_proof` with a reused proof for the + /// `blob_kzg_commitments` field. pub fn kzg_commitment_merkle_proof( &self, index: usize, ) -> Result, Error> { - // We compute the branches by generating 2 merkle trees: - // 1. Merkle tree for the `blob_kzg_commitments` List object - // 2. Merkle tree for the `BeaconBlockBody` container - // We then merge the branches for both the trees all the way up to the root. + let kzg_commitments_proof = self.kzg_commitments_merkle_proof()?; + let proof = self.complete_kzg_commitment_merkle_proof(index, &kzg_commitments_proof)?; + Ok(proof) + } - // Part1 (Branches for the subtree rooted at `blob_kzg_commitments`) - // - // Branches for `blob_kzg_commitments` without length mix-in - let blob_leaves = self - .blob_kzg_commitments()? - .iter() - .map(|commitment| commitment.tree_hash_root()) - .collect::>(); - let depth = E::max_blob_commitments_per_block() - .next_power_of_two() - .ilog2(); - let tree = MerkleTree::create(&blob_leaves, depth as usize); - let (_, mut proof) = tree - .generate_proof(index, depth as usize) - .map_err(Error::MerkleTreeError)?; + /// Produces the proof of inclusion for a `KzgCommitment` in `self.blob_kzg_commitments` + /// at `index` using an existing proof for the `blob_kzg_commitments` field. + pub fn complete_kzg_commitment_merkle_proof( + &self, + index: usize, + kzg_commitments_proof: &[Hash256], + ) -> Result, Error> { + match self { + Self::Base(_) | Self::Altair(_) | Self::Bellatrix(_) | Self::Capella(_) => { + Err(Error::IncorrectStateVariant) + } + Self::Deneb(_) | Self::Electra(_) => { + // We compute the branches by generating 2 merkle trees: + // 1. Merkle tree for the `blob_kzg_commitments` List object + // 2. Merkle tree for the `BeaconBlockBody` container + // We then merge the branches for both the trees all the way up to the root. - // Add the branch corresponding to the length mix-in. - let length = blob_leaves.len(); - let usize_len = std::mem::size_of::(); - let mut length_bytes = [0; BYTES_PER_CHUNK]; - length_bytes - .get_mut(0..usize_len) - .ok_or(Error::MerkleTreeError(MerkleTreeError::PleaseNotifyTheDevs))? - .copy_from_slice(&length.to_le_bytes()); - let length_root = Hash256::from_slice(length_bytes.as_slice()); - proof.push(length_root); + // Part1 (Branches for the subtree rooted at `blob_kzg_commitments`) + // + // Branches for `blob_kzg_commitments` without length mix-in + let blob_leaves = self + .blob_kzg_commitments()? + .iter() + .map(|commitment| commitment.tree_hash_root()) + .collect::>(); + let depth = E::max_blob_commitments_per_block() + .next_power_of_two() + .ilog2(); + let tree = MerkleTree::create(&blob_leaves, depth as usize); + let (_, mut proof) = tree + .generate_proof(index, depth as usize) + .map_err(Error::MerkleTreeError)?; - // Part 2 - // Branches for `BeaconBlockBody` container - let body_leaves = self.body_merkle_leaves(); - let beacon_block_body_depth = body_leaves.len().next_power_of_two().ilog2() as usize; - let tree = MerkleTree::create(&body_leaves, beacon_block_body_depth); - let (_, mut proof_body) = tree - .generate_proof(BLOB_KZG_COMMITMENTS_INDEX, beacon_block_body_depth) - .map_err(Error::MerkleTreeError)?; - // Join the proofs for the subtree and the main tree - proof.append(&mut proof_body); - debug_assert_eq!(proof.len(), E::kzg_proof_inclusion_proof_depth()); + // Add the branch corresponding to the length mix-in. + let length = blob_leaves.len(); + let usize_len = std::mem::size_of::(); + let mut length_bytes = [0; BYTES_PER_CHUNK]; + length_bytes + .get_mut(0..usize_len) + .ok_or(Error::MerkleTreeError(MerkleTreeError::PleaseNotifyTheDevs))? + .copy_from_slice(&length.to_le_bytes()); + let length_root = Hash256::from_slice(length_bytes.as_slice()); + proof.push(length_root); - Ok(proof.into()) + // Part 2 + // Branches for `BeaconBlockBody` container + // Join the proofs for the subtree and the main tree + proof.extend_from_slice(kzg_commitments_proof); + + Ok(FixedVector::new(proof)?) + } + } } /// Produces the proof of inclusion for `self.blob_kzg_commitments`. @@ -241,7 +255,7 @@ impl<'a, E: EthSpec, Payload: AbstractExecPayload> BeaconBlockBodyRef<'a, E, let (_, proof) = tree .generate_proof(BLOB_KZG_COMMITMENTS_INDEX, beacon_block_body_depth) .map_err(Error::MerkleTreeError)?; - Ok(proof.into()) + Ok(FixedVector::new(proof)?) } pub fn block_body_merkle_proof(&self, generalized_index: usize) -> Result, Error> { diff --git a/consensus/types/src/blob_sidecar.rs b/consensus/types/src/blob_sidecar.rs index 0f7dbb2673..5a330388cc 100644 --- a/consensus/types/src/blob_sidecar.rs +++ b/consensus/types/src/blob_sidecar.rs @@ -150,6 +150,37 @@ impl BlobSidecar { }) } + pub fn new_with_existing_proof( + index: usize, + blob: Blob, + signed_block: &SignedBeaconBlock, + signed_block_header: SignedBeaconBlockHeader, + kzg_commitments_inclusion_proof: &[Hash256], + kzg_proof: KzgProof, + ) -> Result { + let expected_kzg_commitments = signed_block + .message() + .body() + .blob_kzg_commitments() + .map_err(|_e| BlobSidecarError::PreDeneb)?; + let kzg_commitment = *expected_kzg_commitments + .get(index) + .ok_or(BlobSidecarError::MissingKzgCommitment)?; + let kzg_commitment_inclusion_proof = signed_block + .message() + .body() + .complete_kzg_commitment_merkle_proof(index, kzg_commitments_inclusion_proof)?; + + Ok(Self { + index: index as u64, + blob, + kzg_commitment, + kzg_proof, + signed_block_header, + kzg_commitment_inclusion_proof, + }) + } + pub fn id(&self) -> BlobIdentifier { BlobIdentifier { block_root: self.block_root(), diff --git a/consensus/types/src/signed_beacon_block.rs b/consensus/types/src/signed_beacon_block.rs index b52adcfe41..bb5e1ea34b 100644 --- a/consensus/types/src/signed_beacon_block.rs +++ b/consensus/types/src/signed_beacon_block.rs @@ -1,6 +1,7 @@ -use crate::beacon_block_body::format_kzg_commitments; +use crate::beacon_block_body::{format_kzg_commitments, BLOB_KZG_COMMITMENTS_INDEX}; use crate::*; use derivative::Derivative; +use merkle_proof::MerkleTree; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use std::fmt; @@ -239,6 +240,45 @@ impl> SignedBeaconBlock } } + /// Produce a signed beacon block header AND a merkle proof for the KZG commitments. + /// + /// This method is more efficient than generating each part separately as it reuses hashing. + pub fn signed_block_header_and_kzg_commitments_proof( + &self, + ) -> Result< + ( + SignedBeaconBlockHeader, + FixedVector, + ), + Error, + > { + // Create the block body merkle tree + let body_leaves = self.message().body().body_merkle_leaves(); + let beacon_block_body_depth = body_leaves.len().next_power_of_two().ilog2() as usize; + let body_merkle_tree = MerkleTree::create(&body_leaves, beacon_block_body_depth); + + // Compute the KZG commitments inclusion proof + let (_, proof) = body_merkle_tree + .generate_proof(BLOB_KZG_COMMITMENTS_INDEX, beacon_block_body_depth) + .map_err(Error::MerkleTreeError)?; + let kzg_commitments_inclusion_proof = FixedVector::new(proof)?; + + let block_header = BeaconBlockHeader { + slot: self.slot(), + proposer_index: self.message().proposer_index(), + parent_root: self.parent_root(), + state_root: self.state_root(), + body_root: body_merkle_tree.hash(), + }; + + let signed_header = SignedBeaconBlockHeader { + message: block_header, + signature: self.signature().clone(), + }; + + Ok((signed_header, kzg_commitments_inclusion_proof)) + } + /// Convenience accessor for the block's slot. pub fn slot(&self) -> Slot { self.message().slot() diff --git a/consensus/types/src/test_utils/generate_random_block_and_blobs.rs b/consensus/types/src/test_utils/generate_random_block_and_blobs.rs index ab7ded0409..cf240c3f1f 100644 --- a/consensus/types/src/test_utils/generate_random_block_and_blobs.rs +++ b/consensus/types/src/test_utils/generate_random_block_and_blobs.rs @@ -83,6 +83,35 @@ mod test { } } + #[test] + fn test_verify_blob_inclusion_proof_from_existing_proof() { + let (block, mut blob_sidecars) = + generate_rand_block_and_blobs::(ForkName::Deneb, 1, &mut thread_rng()); + let BlobSidecar { + index, + blob, + kzg_proof, + .. + } = blob_sidecars.pop().unwrap(); + + // Compute the commitments inclusion proof and use it for building blob sidecar. + let (signed_block_header, kzg_commitments_inclusion_proof) = block + .signed_block_header_and_kzg_commitments_proof() + .unwrap(); + + let blob_sidecar = BlobSidecar::new_with_existing_proof( + index as usize, + blob, + &block, + signed_block_header, + &kzg_commitments_inclusion_proof, + kzg_proof, + ) + .unwrap(); + + assert!(blob_sidecar.verify_blob_sidecar_inclusion_proof()); + } + #[test] fn test_verify_blob_inclusion_proof_invalid() { let (_block, blobs) = diff --git a/lighthouse/tests/beacon_node.rs b/lighthouse/tests/beacon_node.rs index ffa6e300a7..100d12cba0 100644 --- a/lighthouse/tests/beacon_node.rs +++ b/lighthouse/tests/beacon_node.rs @@ -814,6 +814,27 @@ fn network_enable_sampling_flag() { .run_with_zero_port() .with_config(|config| assert!(config.chain.enable_sampling)); } +#[test] +fn blob_publication_batches() { + CommandLineTest::new() + .flag("blob-publication-batches", Some("3")) + .run_with_zero_port() + .with_config(|config| assert_eq!(config.chain.blob_publication_batches, 3)); +} + +#[test] +fn blob_publication_batch_interval() { + CommandLineTest::new() + .flag("blob-publication-batch-interval", Some("400")) + .run_with_zero_port() + .with_config(|config| { + assert_eq!( + config.chain.blob_publication_batch_interval, + Duration::from_millis(400) + ) + }); +} + #[test] fn network_enable_sampling_flag_default() { CommandLineTest::new() diff --git a/testing/ef_tests/src/cases/fork_choice.rs b/testing/ef_tests/src/cases/fork_choice.rs index 8d933a6fcd..33ae132e8a 100644 --- a/testing/ef_tests/src/cases/fork_choice.rs +++ b/testing/ef_tests/src/cases/fork_choice.rs @@ -505,8 +505,8 @@ impl Tester { } Err(_) => GossipVerifiedBlob::__assumed_valid(blob_sidecar), }; - let result = self - .block_on_dangerous(self.harness.chain.process_gossip_blob(blob, || Ok(())))?; + let result = + self.block_on_dangerous(self.harness.chain.process_gossip_blob(blob))?; if valid { assert!(result.is_ok()); } From 654fc6acdc07363e63475be224c583cc056aff95 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Fri, 15 Nov 2024 14:09:54 +0700 Subject: [PATCH 009/254] Additional light client metrics (#6545) * Fix db query and add some additional metrics * fmt * Update beacon_node/beacon_chain/src/metrics.rs Co-authored-by: Jimmy Chen * Update beacon_node/beacon_chain/src/metrics.rs Co-authored-by: Jimmy Chen --- .../src/light_client_server_cache.rs | 11 +++++++---- beacon_node/beacon_chain/src/metrics.rs | 16 ++++++++++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/beacon_node/beacon_chain/src/light_client_server_cache.rs b/beacon_node/beacon_chain/src/light_client_server_cache.rs index e0ddd8c882..78442d8df0 100644 --- a/beacon_node/beacon_chain/src/light_client_server_cache.rs +++ b/beacon_node/beacon_chain/src/light_client_server_cache.rs @@ -85,6 +85,7 @@ impl LightClientServerCache { log: &Logger, chain_spec: &ChainSpec, ) -> Result<(), BeaconChainError> { + metrics::inc_counter(&metrics::LIGHT_CLIENT_SERVER_CACHE_PROCESSING_REQUESTS); let _timer = metrics::start_timer(&metrics::LIGHT_CLIENT_SERVER_CACHE_RECOMPUTE_UPDATES_TIMES); @@ -205,6 +206,7 @@ impl LightClientServerCache { *self.latest_light_client_update.write() = Some(new_light_client_update); } + metrics::inc_counter(&metrics::LIGHT_CLIENT_SERVER_CACHE_PROCESSING_SUCCESSES); Ok(()) } @@ -280,6 +282,11 @@ impl LightClientServerCache { let (sync_committee_bytes, light_client_update_bytes) = res?; let sync_committee_period = u64::from_ssz_bytes(&sync_committee_bytes) .map_err(store::errors::Error::SszDecodeError)?; + + if sync_committee_period >= start_period + count { + break; + } + let epoch = sync_committee_period .safe_mul(chain_spec.epochs_per_sync_committee_period.into())?; @@ -290,10 +297,6 @@ impl LightClientServerCache { .map_err(store::errors::Error::SszDecodeError)?; light_client_updates.push(light_client_update); - - if sync_committee_period >= start_period + count { - break; - } } Ok(light_client_updates) } diff --git a/beacon_node/beacon_chain/src/metrics.rs b/beacon_node/beacon_chain/src/metrics.rs index 66b300f7f2..efc1fe7816 100644 --- a/beacon_node/beacon_chain/src/metrics.rs +++ b/beacon_node/beacon_chain/src/metrics.rs @@ -1972,6 +1972,22 @@ pub static LIGHT_CLIENT_SERVER_CACHE_PREV_BLOCK_CACHE_MISS: LazyLock> = + LazyLock::new(|| { + try_create_int_counter( + "beacon_light_client_server_cache_processing_requests", + "Count of all requests to recompute and cache updates", + ) + }); + +pub static LIGHT_CLIENT_SERVER_CACHE_PROCESSING_SUCCESSES: LazyLock> = + LazyLock::new(|| { + try_create_int_counter( + "beacon_light_client_server_cache_processing_successes", + "Count of all successful requests to recompute and cache updates", + ) + }); + /// Scrape the `beacon_chain` for metrics that are not constantly updated (e.g., the present slot, /// head state info, etc) and update the Prometheus `DEFAULT_REGISTRY`. pub fn scrape_for_metrics(beacon_chain: &BeaconChain) { From 9fdd53df5646aa8b98ea5943c979515ad0c602ac Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Mon, 18 Nov 2024 12:51:44 +1100 Subject: [PATCH 010/254] Hierarchical state diffs (#5978) * Start extracting freezer changes for tree-states * Remove unused config args * Add comments * Remove unwraps * Subjective more clear implementation * Clean up hdiff * Update xdelta3 * Tree states archive metrics (#6040) * Add store cache size metrics * Add compress timer metrics * Add diff apply compute timer metrics * Add diff buffer cache hit metrics * Add hdiff buffer load times * Add blocks replayed metric * Move metrics to store * Future proof some metrics --------- Co-authored-by: Michael Sproul * Port and clean up forwards iterator changes * Add and polish hierarchy-config flag * Merge remote-tracking branch 'origin/unstable' into tree-states-archive * Cleaner errors * Fix beacon_chain test compilation * Merge remote-tracking branch 'origin/unstable' into tree-states-archive * Patch a few more freezer block roots * Fix genesis block root bug * Fix test failing due to pending updates * Beacon chain tests passing * Merge remote-tracking branch 'origin/unstable' into tree-states-archive * Merge remote-tracking branch 'origin/unstable' into tree-states-archive * Fix doc lint * Implement DB schema upgrade for hierarchical state diffs (#6193) * DB upgrade * Add flag * Delete RestorePointHash * Update docs * Update docs * Implement hierarchical state diffs config migration (#6245) * Implement hierarchical state diffs config migration * Review PR * Remove TODO * Set CURRENT_SCHEMA_VERSION correctly * Fix genesis state loading * Re-delete some PartialBeaconState stuff --------- Co-authored-by: Michael Sproul * Merge remote-tracking branch 'origin/unstable' into tree-states-archive * Fix test compilation * Update schema downgrade test * Fix tests * Fix null anchor migration * Merge remote-tracking branch 'origin/unstable' into tree-states-archive * Fix tree states upgrade migration (#6328) * Towards crash safety * Fix compilation * Move cold summaries and state roots to new columns * Rename StateRoots chunked field * Update prune states * Clean hdiff CLI flag and metrics * Fix "staged reconstruction" * Merge remote-tracking branch 'origin/unstable' into tree-states-archive * Fix alloy issues * Fix staged reconstruction logic * Prevent weird slot drift * Remove "allow" flag * Update CLI help * Remove FIXME about downgrade * Merge remote-tracking branch 'origin/unstable' into tree-states-archive * Remove some unnecessary error variants * Fix new test * Tree states archive - review comments and metrics (#6386) * Review PR comments and metrics * Comments * Add anchor metrics * drop prev comment * Update metadata.rs * Apply suggestions from code review --------- Co-authored-by: Michael Sproul * Update beacon_node/store/src/hot_cold_store.rs Co-authored-by: Lion - dapplion <35266934+dapplion@users.noreply.github.com> * Merge remote-tracking branch 'origin/unstable' into tree-states-archive * Clarify comment and remove anchor_slot garbage * Simplify database anchor (#6397) * Simplify database anchor * Update beacon_node/store/src/reconstruct.rs * Add migration for anchor * Fix and simplify light_client store tests * Fix incompatible config test * Merge remote-tracking branch 'origin/unstable' into tree-states-archive * Merge remote-tracking branch 'origin/unstable' into tree-states-archive * More metrics * Merge remote-tracking branch 'origin/unstable' into tree-states-archive * New historic state cache (#6475) * New historic state cache * Add more metrics * State cache hit rate metrics * Fix store metrics * More logs and metrics * Fix logger * Ensure cached states have built caches :O * Replay blocks in preference to diffing * Two separate caches * Distribute cache build time to next slot * Re-plumb historic-state-cache flag * Clean up metrics * Update book * Update beacon_node/store/src/hdiff.rs Co-authored-by: Lion - dapplion <35266934+dapplion@users.noreply.github.com> * Update beacon_node/store/src/historic_state_cache.rs Co-authored-by: Lion - dapplion <35266934+dapplion@users.noreply.github.com> --------- Co-authored-by: Lion - dapplion <35266934+dapplion@users.noreply.github.com> * Update database docs * Update diagram * Merge remote-tracking branch 'origin/unstable' into tree-states-archive * Update lockbud to work with bindgen/etc * Correct pkg name for Debian * Remove vestigial epochs_per_state_diff * Merge remote-tracking branch 'origin/unstable' into tree-states-archive * Markdown lint * Merge remote-tracking branch 'origin/unstable' into tree-states-archive * Address Jimmy's review comments * Simplify ReplayFrom case * Fix and document genesis_state_root * Typo Co-authored-by: Jimmy Chen * Merge branch 'unstable' into tree-states-archive * Compute diff of validators list manually (#6556) * Split hdiff computation * Dedicated logic for historical roots and summaries * Benchmark against real states * Mutated source? * Version the hdiff * Add lighthouse DB config for hierarchy exponents * Tidy up hierarchy exponents flag * Apply suggestions from code review Co-authored-by: Michael Sproul * Address PR review * Remove hardcoded paths in benchmarks * Delete unused function in benches * lint --------- Co-authored-by: Michael Sproul * Test hdiff binary format stability (#6585) * Merge remote-tracking branch 'origin/unstable' into tree-states-archive * Add deprecation warning for SPRP * Update xdelta to get rid of duplicate deps * Document test --- .github/workflows/test-suite.yml | 4 +- Cargo.lock | 78 +- Cargo.toml | 2 + beacon_node/beacon_chain/src/beacon_chain.rs | 24 +- .../beacon_chain/src/block_verification.rs | 19 - beacon_node/beacon_chain/src/builder.rs | 4 + .../beacon_chain/src/historical_blocks.rs | 30 +- beacon_node/beacon_chain/src/metrics.rs | 3 + beacon_node/beacon_chain/src/migrate.rs | 63 +- beacon_node/beacon_chain/src/schema_change.rs | 34 +- .../src/schema_change/migration_schema_v22.rs | 210 +++ beacon_node/beacon_chain/tests/store_tests.rs | 343 +---- beacon_node/client/src/builder.rs | 12 +- beacon_node/client/src/notifier.rs | 36 +- beacon_node/http_api/src/lib.rs | 17 +- beacon_node/http_api/src/metrics.rs | 12 + beacon_node/http_api/src/state_id.rs | 2 + .../lighthouse_network/src/types/globals.rs | 2 +- .../src/types/sync_state.rs | 2 - .../network_beacon_processor/sync_methods.rs | 6 - .../network/src/sync/backfill_sync/mod.rs | 99 +- beacon_node/src/cli.rs | 36 +- beacon_node/src/config.rs | 45 +- beacon_node/src/lib.rs | 2 +- beacon_node/store/Cargo.toml | 12 + beacon_node/store/benches/hdiff.rs | 116 ++ beacon_node/store/src/chunk_writer.rs | 75 -- beacon_node/store/src/chunked_vector.rs | 16 +- beacon_node/store/src/config.rs | 286 +++- beacon_node/store/src/errors.rs | 41 +- beacon_node/store/src/forwards_iter.rs | 301 +++-- beacon_node/store/src/hdiff.rs | 914 +++++++++++++ beacon_node/store/src/historic_state_cache.rs | 92 ++ beacon_node/store/src/hot_cold_store.rs | 1146 ++++++++--------- beacon_node/store/src/lib.rs | 62 +- beacon_node/store/src/metadata.rs | 76 +- beacon_node/store/src/metrics.rs | 208 +++ beacon_node/store/src/partial_beacon_state.rs | 178 +-- beacon_node/store/src/reconstruct.rs | 100 +- beacon_node/tests/test.rs | 1 - book/src/advanced_database.md | 100 +- book/src/help_bn.md | 22 +- book/src/imgs/db-freezer-layout.png | Bin 0 -> 159462 bytes common/eth2/src/lighthouse.rs | 2 +- common/eth2_config/src/lib.rs | 6 + common/eth2_network_config/src/lib.rs | 28 + common/metrics/src/lib.rs | 8 + .../update_progressive_balances_cache.rs | 4 +- consensus/state_processing/src/epoch_cache.rs | 3 + consensus/state_processing/src/metrics.rs | 14 + consensus/types/src/beacon_state.rs | 1 - consensus/types/src/historical_summary.rs | 1 + consensus/types/src/validator.rs | 1 + database_manager/src/cli.rs | 12 +- database_manager/src/lib.rs | 80 +- lighthouse/tests/beacon_node.rs | 58 +- watch/README.md | 2 - 57 files changed, 3360 insertions(+), 1691 deletions(-) create mode 100644 beacon_node/beacon_chain/src/schema_change/migration_schema_v22.rs create mode 100644 beacon_node/store/benches/hdiff.rs delete mode 100644 beacon_node/store/src/chunk_writer.rs create mode 100644 beacon_node/store/src/hdiff.rs create mode 100644 beacon_node/store/src/historic_state_cache.rs create mode 100644 book/src/imgs/db-freezer-layout.png diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index a80470cf16..d6ef180934 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -63,8 +63,8 @@ jobs: - name: Checkout repository uses: actions/checkout@v3 - name: Install dependencies - run: apt update && apt install -y cmake - - name: Generate code coverage + run: apt update && apt install -y cmake libclang-dev + - name: Check for deadlocks run: | cargo lockbud -k deadlock -b -l tokio_util diff --git a/Cargo.lock b/Cargo.lock index 71b5f7e7d8..a2014728e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -917,12 +917,15 @@ dependencies = [ "itertools 0.12.1", "lazy_static", "lazycell", + "log", + "prettyplease", "proc-macro2", "quote", "regex", "rustc-hash 1.1.0", "shlex", "syn 2.0.77", + "which", ] [[package]] @@ -3828,6 +3831,15 @@ dependencies = [ "hmac 0.8.1", ] +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "hostname" version = "0.3.1" @@ -6461,6 +6473,16 @@ dependencies = [ "sensitive_url", ] +[[package]] +name = "prettyplease" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" +dependencies = [ + "proc-macro2", + "syn 2.0.77", +] + [[package]] name = "primeorder" version = "0.13.6" @@ -8184,23 +8206,31 @@ name = "store" version = "0.2.0" dependencies = [ "beacon_chain", + "bls", + "criterion", "db-key", "directory", "ethereum_ssz", "ethereum_ssz_derive", "itertools 0.10.5", "leveldb", + "logging", "lru", "metrics", "parking_lot 0.12.3", + "rand", "safe_arith", "serde", "slog", "sloggers", + "smallvec", "state_processing", "strum", + "superstruct", "tempfile", "types", + "xdelta3", + "zstd 0.13.1", ] [[package]] @@ -9718,6 +9748,18 @@ version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.37", +] + [[package]] name = "whoami" version = "1.5.2" @@ -10117,6 +10159,20 @@ dependencies = [ "time", ] +[[package]] +name = "xdelta3" +version = "0.1.5" +source = "git+http://github.com/sigp/xdelta3-rs?rev=50d63cdf1878e5cf3538e9aae5eed34a22c64e4a#50d63cdf1878e5cf3538e9aae5eed34a22c64e4a" +dependencies = [ + "bindgen", + "cc", + "futures-io", + "futures-util", + "libc", + "log", + "rand", +] + [[package]] name = "xml-rs" version = "0.8.22" @@ -10241,7 +10297,7 @@ dependencies = [ "pbkdf2 0.11.0", "sha1", "time", - "zstd", + "zstd 0.11.2+zstd.1.5.2", ] [[package]] @@ -10250,7 +10306,16 @@ version = "0.11.2+zstd.1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" dependencies = [ - "zstd-safe", + "zstd-safe 5.0.2+zstd.1.5.2", +] + +[[package]] +name = "zstd" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d789b1514203a1120ad2429eae43a7bd32b90976a7bb8a05f7ec02fa88cc23a" +dependencies = [ + "zstd-safe 7.1.0", ] [[package]] @@ -10263,6 +10328,15 @@ dependencies = [ "zstd-sys", ] +[[package]] +name = "zstd-safe" +version = "7.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd99b45c6bc03a018c8b8a86025678c87e55526064e38f9df301989dce7ec0a" +dependencies = [ + "zstd-sys", +] + [[package]] name = "zstd-sys" version = "2.0.13+zstd.1.5.6" diff --git a/Cargo.toml b/Cargo.toml index 83f3903ed4..eedb8a0591 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -263,6 +263,8 @@ validator_http_metrics = { path = "validator_client/http_metrics" } validator_metrics = { path = "validator_client/validator_metrics" } validator_store= { path = "validator_client/validator_store" } warp_utils = { path = "common/warp_utils" } +xdelta3 = { git = "http://github.com/sigp/xdelta3-rs", rev = "50d63cdf1878e5cf3538e9aae5eed34a22c64e4a" } +zstd = "0.13" [profile.maxperf] inherits = "release" diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 6294ffef6a..a78ae266e5 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -767,7 +767,6 @@ impl BeaconChain { start_slot, local_head.beacon_state.clone(), local_head.beacon_block_root, - &self.spec, )?; Ok(iter.map(|result| result.map_err(Into::into))) @@ -790,12 +789,11 @@ impl BeaconChain { } self.with_head(move |head| { - let iter = self.store.forwards_block_roots_iterator_until( - start_slot, - end_slot, - || Ok((head.beacon_state.clone(), head.beacon_block_root)), - &self.spec, - )?; + let iter = + self.store + .forwards_block_roots_iterator_until(start_slot, end_slot, || { + Ok((head.beacon_state.clone(), head.beacon_block_root)) + })?; Ok(iter .map(|result| result.map_err(Into::into)) .take_while(move |result| { @@ -865,7 +863,6 @@ impl BeaconChain { start_slot, local_head.beacon_state_root(), local_head.beacon_state.clone(), - &self.spec, )?; Ok(iter.map(|result| result.map_err(Into::into))) @@ -882,12 +879,11 @@ impl BeaconChain { end_slot: Slot, ) -> Result> + '_, Error> { self.with_head(move |head| { - let iter = self.store.forwards_state_roots_iterator_until( - start_slot, - end_slot, - || Ok((head.beacon_state.clone(), head.beacon_state_root())), - &self.spec, - )?; + let iter = + self.store + .forwards_state_roots_iterator_until(start_slot, end_slot, || { + Ok((head.beacon_state.clone(), head.beacon_state_root())) + })?; Ok(iter .map(|result| result.map_err(Into::into)) .take_while(move |result| { diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 92eb45f9b0..3ae19430aa 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -839,9 +839,6 @@ impl GossipVerifiedBlock { let block_root = get_block_header_root(block_header); - // Disallow blocks that conflict with the anchor (weak subjectivity checkpoint), if any. - check_block_against_anchor_slot(block.message(), chain)?; - // Do not gossip a block from a finalized slot. check_block_against_finalized_slot(block.message(), block_root, chain)?; @@ -1074,9 +1071,6 @@ impl SignatureVerifiedBlock { .fork_name(&chain.spec) .map_err(BlockError::InconsistentFork)?; - // Check the anchor slot before loading the parent, to avoid spurious lookups. - check_block_against_anchor_slot(block.message(), chain)?; - let (mut parent, block) = load_parent(block, chain)?; let state = cheap_state_advance_to_obtain_committees::<_, BlockError>( @@ -1688,19 +1682,6 @@ impl ExecutionPendingBlock { } } -/// Returns `Ok(())` if the block's slot is greater than the anchor block's slot (if any). -fn check_block_against_anchor_slot( - block: BeaconBlockRef<'_, T::EthSpec>, - chain: &BeaconChain, -) -> Result<(), BlockError> { - if let Some(anchor_slot) = chain.store.get_anchor_slot() { - if block.slot() <= anchor_slot { - return Err(BlockError::WeakSubjectivityConflict); - } - } - Ok(()) -} - /// Returns `Ok(())` if the block is later than the finalized slot on `chain`. /// /// Returns an error if the block is earlier or equal to the finalized slot, or there was an error diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index 5f1e94fc8c..589db0af50 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -363,6 +363,10 @@ where store .put_block(&beacon_block_root, beacon_block.clone()) .map_err(|e| format!("Failed to store genesis block: {:?}", e))?; + store + .store_frozen_block_root_at_skip_slots(Slot::new(0), Slot::new(1), beacon_block_root) + .and_then(|ops| store.cold_db.do_atomically(ops)) + .map_err(|e| format!("Failed to store genesis block root: {e:?}"))?; // Store the genesis block under the `ZERO_HASH` key. store diff --git a/beacon_node/beacon_chain/src/historical_blocks.rs b/beacon_node/beacon_chain/src/historical_blocks.rs index 813eb906b9..ddae54f464 100644 --- a/beacon_node/beacon_chain/src/historical_blocks.rs +++ b/beacon_node/beacon_chain/src/historical_blocks.rs @@ -11,8 +11,8 @@ use std::iter; use std::time::Duration; use store::metadata::DataColumnInfo; use store::{ - chunked_vector::BlockRoots, AnchorInfo, BlobInfo, ChunkWriter, Error as StoreError, - KeyValueStore, + get_key_for_col, AnchorInfo, BlobInfo, DBColumn, Error as StoreError, KeyValueStore, + KeyValueStoreOp, }; use strum::IntoStaticStr; use types::{FixedBytesExtended, Hash256, Slot}; @@ -35,8 +35,6 @@ pub enum HistoricalBlockError { InvalidSignature, /// Transitory error, caller should retry with the same blocks. ValidatorPubkeyCacheTimeout, - /// No historical sync needed. - NoAnchorInfo, /// Logic error: should never occur. IndexOutOfBounds, /// Internal store error @@ -72,10 +70,7 @@ impl BeaconChain { &self, mut blocks: Vec>, ) -> Result { - let anchor_info = self - .store - .get_anchor_info() - .ok_or(HistoricalBlockError::NoAnchorInfo)?; + let anchor_info = self.store.get_anchor_info(); let blob_info = self.store.get_blob_info(); let data_column_info = self.store.get_data_column_info(); @@ -119,8 +114,6 @@ impl BeaconChain { let mut expected_block_root = anchor_info.oldest_block_parent; let mut prev_block_slot = anchor_info.oldest_block_slot; - 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; @@ -158,8 +151,11 @@ impl BeaconChain { } // 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() { - chunk_writer.set(slot, block_root, &mut cold_batch)?; + for slot in (block.slot().as_u64()..prev_block_slot.as_u64()).rev() { + cold_batch.push(KeyValueStoreOp::PutKeyValue( + get_key_for_col(DBColumn::BeaconBlockRoots.into(), &slot.to_be_bytes()), + block_root.as_slice().to_vec(), + )); } prev_block_slot = block.slot(); @@ -171,15 +167,17 @@ impl BeaconChain { // completion. if expected_block_root == self.genesis_block_root { let genesis_slot = self.spec.genesis_slot; - for slot in genesis_slot.as_usize()..prev_block_slot.as_usize() { - chunk_writer.set(slot, self.genesis_block_root, &mut cold_batch)?; + for slot in genesis_slot.as_u64()..prev_block_slot.as_u64() { + cold_batch.push(KeyValueStoreOp::PutKeyValue( + get_key_for_col(DBColumn::BeaconBlockRoots.into(), &slot.to_be_bytes()), + self.genesis_block_root.as_slice().to_vec(), + )); } prev_block_slot = genesis_slot; expected_block_root = Hash256::zero(); break; } } - chunk_writer.write(&mut cold_batch)?; // these were pushed in reverse order so we reverse again signed_blocks.reverse(); @@ -271,7 +269,7 @@ impl BeaconChain { let backfill_complete = new_anchor.block_backfill_complete(self.genesis_backfill_slot); anchor_and_blob_batch.push( self.store - .compare_and_set_anchor_info(Some(anchor_info), Some(new_anchor))?, + .compare_and_set_anchor_info(anchor_info, new_anchor)?, ); self.store.hot_db.do_atomically(anchor_and_blob_batch)?; diff --git a/beacon_node/beacon_chain/src/metrics.rs b/beacon_node/beacon_chain/src/metrics.rs index efc1fe7816..c6aa9fbcac 100644 --- a/beacon_node/beacon_chain/src/metrics.rs +++ b/beacon_node/beacon_chain/src/metrics.rs @@ -2004,6 +2004,7 @@ pub fn scrape_for_metrics(beacon_chain: &BeaconChain) { let attestation_stats = beacon_chain.op_pool.attestation_stats(); let chain_metrics = beacon_chain.metrics(); + // Kept duplicated for backwards compatibility set_gauge_by_usize( &BLOCK_PROCESSING_SNAPSHOT_CACHE_SIZE, beacon_chain.store.state_cache_len(), @@ -2067,6 +2068,8 @@ pub fn scrape_for_metrics(beacon_chain: &BeaconChain) { .canonical_head .fork_choice_read_lock() .scrape_for_metrics(); + + beacon_chain.store.register_metrics(); } /// Scrape the given `state` assuming it's the head state, updating the `DEFAULT_REGISTRY`. diff --git a/beacon_node/beacon_chain/src/migrate.rs b/beacon_node/beacon_chain/src/migrate.rs index f83df7b446..37a2e8917b 100644 --- a/beacon_node/beacon_chain/src/migrate.rs +++ b/beacon_node/beacon_chain/src/migrate.rs @@ -24,6 +24,10 @@ const MAX_COMPACTION_PERIOD_SECONDS: u64 = 604800; const MIN_COMPACTION_PERIOD_SECONDS: u64 = 7200; /// Compact after a large finality gap, if we respect `MIN_COMPACTION_PERIOD_SECONDS`. const COMPACTION_FINALITY_DISTANCE: u64 = 1024; +/// Maximum number of blocks applied in each reconstruction burst. +/// +/// This limits the amount of time that the finalization migration is paused for. +const BLOCKS_PER_RECONSTRUCTION: usize = 8192 * 4; /// Default number of epochs to wait between finalization migrations. pub const DEFAULT_EPOCHS_PER_MIGRATION: u64 = 1; @@ -188,7 +192,9 @@ impl, Cold: ItemStore> BackgroundMigrator, Cold: ItemStore> BackgroundMigrator>, log: &Logger) { - if let Err(e) = db.reconstruct_historic_states() { - error!( - log, - "State reconstruction failed"; - "error" => ?e, - ); + pub fn run_reconstruction( + db: Arc>, + opt_tx: Option>, + log: &Logger, + ) { + match db.reconstruct_historic_states(Some(BLOCKS_PER_RECONSTRUCTION)) { + Ok(()) => { + // Schedule another reconstruction batch if required and we have access to the + // channel for requeueing. + if let Some(tx) = opt_tx { + if !db.get_anchor_info().all_historic_states_stored() { + if let Err(e) = tx.send(Notification::Reconstruction) { + error!( + log, + "Unable to requeue reconstruction notification"; + "error" => ?e + ); + } + } + } + } + Err(e) => { + error!( + log, + "State reconstruction failed"; + "error" => ?e, + ); + } } } @@ -388,6 +415,7 @@ impl, Cold: ItemStore> BackgroundMigrator (mpsc::Sender, thread::JoinHandle<()>) { let (tx, rx) = mpsc::channel(); + let inner_tx = tx.clone(); let thread = thread::spawn(move || { while let Ok(notif) = rx.recv() { let mut reconstruction_notif = None; @@ -418,16 +446,17 @@ impl, Cold: ItemStore> BackgroundMigrator( db: Arc>, - deposit_contract_deploy_block: u64, + genesis_state_root: Option, from: SchemaVersion, to: SchemaVersion, log: Logger, - spec: &ChainSpec, ) -> Result<(), StoreError> { match (from, to) { // Migrating from the current schema version to itself is always OK, a no-op. @@ -26,28 +25,14 @@ pub fn migrate_schema( // Upgrade across multiple versions by recursively migrating one step at a time. (_, _) if from.as_u64() + 1 < to.as_u64() => { let next = SchemaVersion(from.as_u64() + 1); - migrate_schema::( - db.clone(), - deposit_contract_deploy_block, - from, - next, - log.clone(), - spec, - )?; - migrate_schema::(db, deposit_contract_deploy_block, next, to, log, spec) + migrate_schema::(db.clone(), genesis_state_root, from, next, log.clone())?; + migrate_schema::(db, genesis_state_root, next, to, log) } // Downgrade across multiple versions by recursively migrating one step at a time. (_, _) if to.as_u64() + 1 < from.as_u64() => { let next = SchemaVersion(from.as_u64() - 1); - migrate_schema::( - db.clone(), - deposit_contract_deploy_block, - from, - next, - log.clone(), - spec, - )?; - migrate_schema::(db, deposit_contract_deploy_block, next, to, log, spec) + migrate_schema::(db.clone(), genesis_state_root, from, next, log.clone())?; + migrate_schema::(db, genesis_state_root, next, to, log) } // @@ -69,6 +54,11 @@ pub fn migrate_schema( let ops = migration_schema_v21::downgrade_from_v21::(db.clone(), log)?; db.store_schema_version_atomically(to, ops) } + (SchemaVersion(21), SchemaVersion(22)) => { + // This migration needs to sync data between hot and cold DBs. The schema version is + // bumped inside the upgrade_to_v22 fn + migration_schema_v22::upgrade_to_v22::(db.clone(), genesis_state_root, log) + } // Anything else is an error. (_, _) => Err(HotColdDBError::UnsupportedSchemaVersion { target_version: to, diff --git a/beacon_node/beacon_chain/src/schema_change/migration_schema_v22.rs b/beacon_node/beacon_chain/src/schema_change/migration_schema_v22.rs new file mode 100644 index 0000000000..fcb78ab801 --- /dev/null +++ b/beacon_node/beacon_chain/src/schema_change/migration_schema_v22.rs @@ -0,0 +1,210 @@ +use crate::beacon_chain::BeaconChainTypes; +use slog::{info, Logger}; +use std::sync::Arc; +use store::chunked_iter::ChunkedVectorIter; +use store::{ + chunked_vector::BlockRootsChunked, + get_key_for_col, + metadata::{ + SchemaVersion, ANCHOR_FOR_ARCHIVE_NODE, ANCHOR_UNINITIALIZED, STATE_UPPER_LIMIT_NO_RETAIN, + }, + partial_beacon_state::PartialBeaconState, + AnchorInfo, DBColumn, Error, HotColdDB, KeyValueStore, KeyValueStoreOp, +}; +use types::{BeaconState, Hash256, Slot}; + +const LOG_EVERY: usize = 200_000; + +fn load_old_schema_frozen_state( + db: &HotColdDB, + state_root: Hash256, +) -> Result>, Error> { + let Some(partial_state_bytes) = db + .cold_db + .get_bytes(DBColumn::BeaconState.into(), state_root.as_slice())? + else { + return Ok(None); + }; + let mut partial_state: PartialBeaconState = + PartialBeaconState::from_ssz_bytes(&partial_state_bytes, db.get_chain_spec())?; + + // Fill in the fields of the partial state. + partial_state.load_block_roots(&db.cold_db, db.get_chain_spec())?; + partial_state.load_state_roots(&db.cold_db, db.get_chain_spec())?; + partial_state.load_historical_roots(&db.cold_db, db.get_chain_spec())?; + partial_state.load_randao_mixes(&db.cold_db, db.get_chain_spec())?; + partial_state.load_historical_summaries(&db.cold_db, db.get_chain_spec())?; + + partial_state.try_into().map(Some) +} + +pub fn upgrade_to_v22( + db: Arc>, + genesis_state_root: Option, + log: Logger, +) -> Result<(), Error> { + info!(log, "Upgrading from v21 to v22"); + + let mut old_anchor = db.get_anchor_info(); + + // If the anchor was uninitialized in the old schema (`None`), this represents a full archive + // node. + if old_anchor == ANCHOR_UNINITIALIZED { + old_anchor = ANCHOR_FOR_ARCHIVE_NODE; + } + + let split_slot = db.get_split_slot(); + let genesis_state_root = genesis_state_root.ok_or(Error::GenesisStateUnknown)?; + + let mut cold_ops = vec![]; + + // Load the genesis state in the previous chunked format, BEFORE we go deleting or rewriting + // anything. + let mut genesis_state = load_old_schema_frozen_state::(&db, genesis_state_root)? + .ok_or(Error::MissingGenesisState)?; + let genesis_state_root = genesis_state.update_tree_hash_cache()?; + let genesis_block_root = genesis_state.get_latest_block_root(genesis_state_root); + + // Store the genesis state in the new format, prior to updating the schema version on disk. + // In case of a crash no data is lost because we will re-load it in the old format and re-do + // this write. + if split_slot > 0 { + info!( + log, + "Re-storing genesis state"; + "state_root" => ?genesis_state_root, + ); + db.store_cold_state(&genesis_state_root, &genesis_state, &mut cold_ops)?; + } + + // Write the block roots in the new format in a new column. Similar to above, we do this + // separately from deleting the old format block roots so that this is crash safe. + let oldest_block_slot = old_anchor.oldest_block_slot; + write_new_schema_block_roots::( + &db, + genesis_block_root, + oldest_block_slot, + split_slot, + &mut cold_ops, + &log, + )?; + + // Commit this first batch of non-destructive cold database ops. + db.cold_db.do_atomically(cold_ops)?; + + // Now we update the anchor and the schema version atomically in the hot database. + // + // If we crash after commiting this change, then there will be some leftover cruft left in the + // freezer database, but no corruption because all the new-format data has already been written + // above. + let new_anchor = AnchorInfo { + state_upper_limit: STATE_UPPER_LIMIT_NO_RETAIN, + state_lower_limit: Slot::new(0), + ..old_anchor.clone() + }; + let hot_ops = vec![db.compare_and_set_anchor_info(old_anchor, new_anchor)?]; + db.store_schema_version_atomically(SchemaVersion(22), hot_ops)?; + + // Finally, clean up the old-format data from the freezer database. + delete_old_schema_freezer_data::(&db, &log)?; + + Ok(()) +} + +pub fn delete_old_schema_freezer_data( + db: &Arc>, + log: &Logger, +) -> Result<(), Error> { + let mut cold_ops = vec![]; + + let columns = [ + DBColumn::BeaconState, + // Cold state summaries indexed by state root were stored in this column. + DBColumn::BeaconStateSummary, + // Mapping from restore point number to state root was stored in this column. + DBColumn::BeaconRestorePoint, + // Chunked vector values were stored in these columns. + DBColumn::BeaconHistoricalRoots, + DBColumn::BeaconRandaoMixes, + DBColumn::BeaconHistoricalSummaries, + DBColumn::BeaconBlockRootsChunked, + DBColumn::BeaconStateRootsChunked, + ]; + + for column in columns { + for res in db.cold_db.iter_column_keys::>(column) { + let key = res?; + cold_ops.push(KeyValueStoreOp::DeleteKey(get_key_for_col( + column.as_str(), + &key, + ))); + } + } + let delete_ops = cold_ops.len(); + + info!( + log, + "Deleting historic states"; + "delete_ops" => delete_ops, + ); + db.cold_db.do_atomically(cold_ops)?; + + // In order to reclaim space, we need to compact the freezer DB as well. + db.cold_db.compact()?; + + Ok(()) +} + +pub fn write_new_schema_block_roots( + db: &HotColdDB, + genesis_block_root: Hash256, + oldest_block_slot: Slot, + split_slot: Slot, + cold_ops: &mut Vec, + log: &Logger, +) -> Result<(), Error> { + info!( + log, + "Starting beacon block root migration"; + "oldest_block_slot" => oldest_block_slot, + "genesis_block_root" => ?genesis_block_root, + ); + + // Store the genesis block root if it would otherwise not be stored. + if oldest_block_slot != 0 { + cold_ops.push(KeyValueStoreOp::PutKeyValue( + get_key_for_col(DBColumn::BeaconBlockRoots.into(), &0u64.to_be_bytes()), + genesis_block_root.as_slice().to_vec(), + )); + } + + // Block roots are available from the `oldest_block_slot` to the `split_slot`. + let start_vindex = oldest_block_slot.as_usize(); + let block_root_iter = ChunkedVectorIter::::new( + db, + start_vindex, + split_slot, + db.get_chain_spec(), + ); + + // OK to hold these in memory (10M slots * 43 bytes per KV ~= 430 MB). + for (i, (slot, block_root)) in block_root_iter.enumerate() { + cold_ops.push(KeyValueStoreOp::PutKeyValue( + get_key_for_col( + DBColumn::BeaconBlockRoots.into(), + &(slot as u64).to_be_bytes(), + ), + block_root.as_slice().to_vec(), + )); + + if i > 0 && i % LOG_EVERY == 0 { + info!( + log, + "Beacon block root migration in progress"; + "roots_migrated" => i + ); + } + } + + Ok(()) +} diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index a241d752fc..522020e476 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -25,13 +25,10 @@ use std::collections::HashSet; use std::convert::TryInto; use std::sync::{Arc, LazyLock}; use std::time::Duration; -use store::chunked_vector::Chunk; use store::metadata::{SchemaVersion, CURRENT_SCHEMA_VERSION, STATE_UPPER_LIMIT_NO_RETAIN}; use store::{ - chunked_vector::{chunk_key, Field}, - get_key_for_col, iter::{BlockRootsIterator, StateRootsIterator}, - BlobInfo, DBColumn, HotColdDB, KeyValueStore, KeyValueStoreOp, LevelDB, StoreConfig, + BlobInfo, DBColumn, HotColdDB, LevelDB, StoreConfig, }; use tempfile::{tempdir, TempDir}; use tokio::time::sleep; @@ -58,8 +55,8 @@ fn get_store_generic( config: StoreConfig, spec: ChainSpec, ) -> Arc, LevelDB>> { - let hot_path = db_path.path().join("hot_db"); - let cold_path = db_path.path().join("cold_db"); + let hot_path = db_path.path().join("chain_db"); + let cold_path = db_path.path().join("freezer_db"); let blobs_path = db_path.path().join("blobs_db"); let log = test_logger(); @@ -232,253 +229,6 @@ async fn light_client_updates_test() { assert_eq!(lc_updates.len(), 2); } -/// Tests that `store.heal_freezer_block_roots_at_split` inserts block roots between last restore point -/// slot and the split slot. -#[tokio::test] -async fn heal_freezer_block_roots_at_split() { - // chunk_size is hard-coded to 128 - let num_blocks_produced = E::slots_per_epoch() * 20; - let db_path = tempdir().unwrap(); - let store = get_store_generic( - &db_path, - StoreConfig { - slots_per_restore_point: 2 * E::slots_per_epoch(), - ..Default::default() - }, - test_spec::(), - ); - let harness = get_harness(store.clone(), LOW_VALIDATOR_COUNT); - - harness - .extend_chain( - num_blocks_produced as usize, - BlockStrategy::OnCanonicalHead, - AttestationStrategy::AllValidators, - ) - .await; - - let split_slot = store.get_split_slot(); - assert_eq!(split_slot, 18 * E::slots_per_epoch()); - - // Do a heal before deleting to make sure that it doesn't break. - let last_restore_point_slot = Slot::new(16 * E::slots_per_epoch()); - store.heal_freezer_block_roots_at_split().unwrap(); - check_freezer_block_roots(&harness, last_restore_point_slot, split_slot); - - // Delete block roots between `last_restore_point_slot` and `split_slot`. - let chunk_index = >::chunk_index( - last_restore_point_slot.as_usize(), - ); - let key_chunk = get_key_for_col(DBColumn::BeaconBlockRoots.as_str(), &chunk_key(chunk_index)); - store - .cold_db - .do_atomically(vec![KeyValueStoreOp::DeleteKey(key_chunk)]) - .unwrap(); - - let block_root_err = store - .forwards_block_roots_iterator_until( - last_restore_point_slot, - last_restore_point_slot + 1, - || unreachable!(), - &harness.chain.spec, - ) - .unwrap() - .next() - .unwrap() - .unwrap_err(); - - assert!(matches!(block_root_err, store::Error::NoContinuationData)); - - // Re-insert block roots - store.heal_freezer_block_roots_at_split().unwrap(); - check_freezer_block_roots(&harness, last_restore_point_slot, split_slot); - - // Run for another two epochs to check that the invariant is maintained. - let additional_blocks_produced = 2 * E::slots_per_epoch(); - harness - .extend_slots(additional_blocks_produced as usize) - .await; - - check_finalization(&harness, num_blocks_produced + additional_blocks_produced); - check_split_slot(&harness, store); - check_chain_dump( - &harness, - num_blocks_produced + additional_blocks_produced + 1, - ); - check_iterators(&harness); -} - -/// Tests that `store.heal_freezer_block_roots` inserts block roots between last restore point -/// slot and the split slot. -#[tokio::test] -async fn heal_freezer_block_roots_with_skip_slots() { - // chunk_size is hard-coded to 128 - let num_blocks_produced = E::slots_per_epoch() * 20; - let db_path = tempdir().unwrap(); - let store = get_store_generic( - &db_path, - StoreConfig { - slots_per_restore_point: 2 * E::slots_per_epoch(), - ..Default::default() - }, - test_spec::(), - ); - let harness = get_harness(store.clone(), LOW_VALIDATOR_COUNT); - - let mut current_state = harness.get_current_state(); - let state_root = current_state.canonical_root().unwrap(); - let all_validators = &harness.get_all_validators(); - harness - .add_attested_blocks_at_slots( - current_state, - state_root, - &(1..=num_blocks_produced) - .filter(|i| i % 12 != 0) - .map(Slot::new) - .collect::>(), - all_validators, - ) - .await; - - // split slot should be 18 here - let split_slot = store.get_split_slot(); - assert_eq!(split_slot, 18 * E::slots_per_epoch()); - - let last_restore_point_slot = Slot::new(16 * E::slots_per_epoch()); - let chunk_index = >::chunk_index( - last_restore_point_slot.as_usize(), - ); - let key_chunk = get_key_for_col(DBColumn::BeaconBlockRoots.as_str(), &chunk_key(chunk_index)); - store - .cold_db - .do_atomically(vec![KeyValueStoreOp::DeleteKey(key_chunk)]) - .unwrap(); - - let block_root_err = store - .forwards_block_roots_iterator_until( - last_restore_point_slot, - last_restore_point_slot + 1, - || unreachable!(), - &harness.chain.spec, - ) - .unwrap() - .next() - .unwrap() - .unwrap_err(); - - assert!(matches!(block_root_err, store::Error::NoContinuationData)); - - // heal function - store.heal_freezer_block_roots_at_split().unwrap(); - check_freezer_block_roots(&harness, last_restore_point_slot, split_slot); - - // Run for another two epochs to check that the invariant is maintained. - let additional_blocks_produced = 2 * E::slots_per_epoch(); - harness - .extend_slots(additional_blocks_produced as usize) - .await; - - check_finalization(&harness, num_blocks_produced + additional_blocks_produced); - check_split_slot(&harness, store); - check_iterators(&harness); -} - -/// Tests that `store.heal_freezer_block_roots_at_genesis` replaces 0x0 block roots between slot -/// 0 and the first non-skip slot with genesis block root. -#[tokio::test] -async fn heal_freezer_block_roots_at_genesis() { - // Run for a few epochs to ensure we're past finalization. - let num_blocks_produced = E::slots_per_epoch() * 4; - let db_path = tempdir().unwrap(); - let store = get_store(&db_path); - let harness = get_harness(store.clone(), LOW_VALIDATOR_COUNT); - - // Start with 2 skip slots. - harness.advance_slot(); - harness.advance_slot(); - - harness - .extend_chain( - num_blocks_produced as usize, - BlockStrategy::OnCanonicalHead, - AttestationStrategy::AllValidators, - ) - .await; - - // Do a heal before deleting to make sure that it doesn't break. - store.heal_freezer_block_roots_at_genesis().unwrap(); - check_freezer_block_roots( - &harness, - Slot::new(0), - Epoch::new(1).end_slot(E::slots_per_epoch()), - ); - - // Write 0x0 block roots at slot 1 and slot 2. - let chunk_index = 0; - let chunk_db_key = chunk_key(chunk_index); - let mut chunk = - Chunk::::load(&store.cold_db, DBColumn::BeaconBlockRoots, &chunk_db_key) - .unwrap() - .unwrap(); - - chunk.values[1] = Hash256::zero(); - chunk.values[2] = Hash256::zero(); - - let mut ops = vec![]; - chunk - .store(DBColumn::BeaconBlockRoots, &chunk_db_key, &mut ops) - .unwrap(); - store.cold_db.do_atomically(ops).unwrap(); - - // Ensure the DB is corrupted - let block_roots = store - .forwards_block_roots_iterator_until( - Slot::new(1), - Slot::new(2), - || unreachable!(), - &harness.chain.spec, - ) - .unwrap() - .map(Result::unwrap) - .take(2) - .collect::>(); - assert_eq!( - block_roots, - vec![ - (Hash256::zero(), Slot::new(1)), - (Hash256::zero(), Slot::new(2)) - ] - ); - - // Insert genesis block roots at skip slots before first block slot - store.heal_freezer_block_roots_at_genesis().unwrap(); - check_freezer_block_roots( - &harness, - Slot::new(0), - Epoch::new(1).end_slot(E::slots_per_epoch()), - ); -} - -fn check_freezer_block_roots(harness: &TestHarness, start_slot: Slot, end_slot: Slot) { - for slot in (start_slot.as_u64()..end_slot.as_u64()).map(Slot::new) { - let (block_root, result_slot) = harness - .chain - .store - .forwards_block_roots_iterator_until(slot, slot, || unreachable!(), &harness.chain.spec) - .unwrap() - .next() - .unwrap() - .unwrap(); - assert_eq!(slot, result_slot); - let expected_block_root = harness - .chain - .block_root_at_slot(slot, WhenSlotSkipped::Prev) - .unwrap() - .unwrap(); - assert_eq!(expected_block_root, block_root); - } -} - #[tokio::test] async fn full_participation_no_skips() { let num_blocks_produced = E::slots_per_epoch() * 5; @@ -741,11 +491,12 @@ async fn epoch_boundary_state_attestation_processing() { .load_epoch_boundary_state(&block.state_root()) .expect("no error") .expect("epoch boundary state exists"); - let ebs_state_root = epoch_boundary_state.canonical_root().unwrap(); - let ebs_of_ebs = store + let ebs_state_root = epoch_boundary_state.update_tree_hash_cache().unwrap(); + let mut ebs_of_ebs = store .load_epoch_boundary_state(&ebs_state_root) .expect("no error") .expect("ebs of ebs exists"); + ebs_of_ebs.apply_pending_mutations().unwrap(); assert_eq!(epoch_boundary_state, ebs_of_ebs); // If the attestation is pre-finalization it should be rejected. @@ -807,10 +558,19 @@ async fn forwards_iter_block_and_state_roots_until() { check_finalization(&harness, num_blocks_produced); check_split_slot(&harness, store.clone()); - // The last restore point slot is the point at which the hybrid forwards iterator behaviour + // The freezer upper bound slot is the point at which the hybrid forwards iterator behaviour // changes. - let last_restore_point_slot = store.get_latest_restore_point_slot().unwrap(); - assert!(last_restore_point_slot > 0); + let block_upper_bound = store + .freezer_upper_bound_for_column(DBColumn::BeaconBlockRoots, Slot::new(0)) + .unwrap() + .unwrap(); + assert!(block_upper_bound > 0); + let state_upper_bound = store + .freezer_upper_bound_for_column(DBColumn::BeaconStateRoots, Slot::new(0)) + .unwrap() + .unwrap(); + assert!(state_upper_bound > 0); + assert_eq!(state_upper_bound, block_upper_bound); let chain = &harness.chain; let head_state = harness.get_current_state(); @@ -835,14 +595,12 @@ async fn forwards_iter_block_and_state_roots_until() { }; let split_slot = store.get_split_slot(); - assert!(split_slot > last_restore_point_slot); + assert_eq!(split_slot, block_upper_bound); - test_range(Slot::new(0), last_restore_point_slot); - test_range(last_restore_point_slot, last_restore_point_slot); - test_range(last_restore_point_slot - 1, last_restore_point_slot); - test_range(Slot::new(0), last_restore_point_slot - 1); test_range(Slot::new(0), split_slot); - test_range(last_restore_point_slot - 1, split_slot); + test_range(split_slot, split_slot); + test_range(split_slot - 1, split_slot); + test_range(Slot::new(0), split_slot - 1); test_range(Slot::new(0), head_state.slot()); } @@ -2567,7 +2325,7 @@ async fn weak_subjectivity_sync_test(slots: Vec, checkpoint_slot: Slot) { .await; let (shutdown_tx, _shutdown_rx) = futures::channel::mpsc::channel(1); - let log = test_logger(); + let log = harness.chain.logger().clone(); let temp2 = tempdir().unwrap(); let store = get_store(&temp2); let spec = test_spec::(); @@ -2792,11 +2550,11 @@ async fn weak_subjectivity_sync_test(slots: Vec, checkpoint_slot: Slot) { } // Anchor slot is still set to the slot of the checkpoint block. - assert_eq!(store.get_anchor_slot(), Some(wss_block.slot())); + assert_eq!(store.get_anchor_info().anchor_slot, wss_block.slot()); // Reconstruct states. - store.clone().reconstruct_historic_states().unwrap(); - assert_eq!(store.get_anchor_slot(), None); + store.clone().reconstruct_historic_states(None).unwrap(); + assert_eq!(store.get_anchor_info().anchor_slot, 0); } /// Test that blocks and attestations that refer to states around an unaligned split state are @@ -3222,7 +2980,6 @@ async fn schema_downgrade_to_min_version() { let db_path = tempdir().unwrap(); let store = get_store(&db_path); let harness = get_harness(store.clone(), LOW_VALIDATOR_COUNT); - let spec = &harness.chain.spec.clone(); harness .extend_chain( @@ -3232,7 +2989,8 @@ async fn schema_downgrade_to_min_version() { ) .await; - let min_version = SchemaVersion(19); + let min_version = SchemaVersion(22); + let genesis_state_root = Some(harness.chain.genesis_state_root); // Save the slot clock so that the new harness doesn't revert in time. let slot_clock = harness.chain.slot_clock.clone(); @@ -3245,25 +3003,22 @@ async fn schema_downgrade_to_min_version() { let store = get_store(&db_path); // Downgrade. - let deposit_contract_deploy_block = 0; migrate_schema::>( store.clone(), - deposit_contract_deploy_block, + genesis_state_root, CURRENT_SCHEMA_VERSION, min_version, store.logger().clone(), - spec, ) .expect("schema downgrade to minimum version should work"); // Upgrade back. migrate_schema::>( store.clone(), - deposit_contract_deploy_block, + genesis_state_root, min_version, CURRENT_SCHEMA_VERSION, store.logger().clone(), - spec, ) .expect("schema upgrade from minimum version should work"); @@ -3286,11 +3041,10 @@ async fn schema_downgrade_to_min_version() { let min_version_sub_1 = SchemaVersion(min_version.as_u64().checked_sub(1).unwrap()); migrate_schema::>( store.clone(), - deposit_contract_deploy_block, + genesis_state_root, CURRENT_SCHEMA_VERSION, min_version_sub_1, harness.logger().clone(), - spec, ) .expect_err("should not downgrade below minimum version"); } @@ -3622,15 +3376,15 @@ async fn prune_historic_states() { ) .await; - // Check historical state is present. - let state_roots_iter = harness + // Check historical states are present. + let first_epoch_state_roots = harness .chain .forwards_iter_state_roots(Slot::new(0)) - .unwrap(); - for (state_root, slot) in state_roots_iter + .unwrap() .take(E::slots_per_epoch() as usize) .map(Result::unwrap) - { + .collect::>(); + for &(state_root, slot) in &first_epoch_state_roots { assert!(store.get_state(&state_root, Some(slot)).unwrap().is_some()); } @@ -3639,29 +3393,18 @@ async fn prune_historic_states() { .unwrap(); // Check that anchor info is updated. - let anchor_info = store.get_anchor_info().unwrap(); + let anchor_info = store.get_anchor_info(); assert_eq!(anchor_info.state_lower_limit, 0); assert_eq!(anchor_info.state_upper_limit, STATE_UPPER_LIMIT_NO_RETAIN); - // Historical states should be pruned. - let state_roots_iter = harness - .chain - .forwards_iter_state_roots(Slot::new(1)) - .unwrap(); - for (state_root, slot) in state_roots_iter - .take(E::slots_per_epoch() as usize) - .map(Result::unwrap) - { - assert!(store.get_state(&state_root, Some(slot)).unwrap().is_none()); + // Ensure all epoch 0 states other than the genesis have been pruned. + for &(state_root, slot) in &first_epoch_state_roots { + assert_eq!( + store.get_state(&state_root, Some(slot)).unwrap().is_some(), + slot == 0 + ); } - // Ensure that genesis state is still accessible - let genesis_state_root = harness.chain.genesis_state_root; - assert!(store - .get_state(&genesis_state_root, Some(Slot::new(0))) - .unwrap() - .is_some()); - // Run for another two epochs. let additional_blocks_produced = 2 * E::slots_per_epoch(); harness diff --git a/beacon_node/client/src/builder.rs b/beacon_node/client/src/builder.rs index 2fe482d4d2..961f5140f9 100644 --- a/beacon_node/client/src/builder.rs +++ b/beacon_node/client/src/builder.rs @@ -1060,21 +1060,21 @@ where self.db_path = Some(hot_path.into()); self.freezer_db_path = Some(cold_path.into()); - let inner_spec = spec.clone(); - let deposit_contract_deploy_block = context + // Optionally grab the genesis state root. + // This will only be required if a DB upgrade to V22 is needed. + let genesis_state_root = context .eth2_network_config .as_ref() - .map(|config| config.deposit_contract_deploy_block) - .unwrap_or(0); + .and_then(|config| config.genesis_state_root::().transpose()) + .transpose()?; let schema_upgrade = |db, from, to| { migrate_schema::>( db, - deposit_contract_deploy_block, + genesis_state_root, from, to, log, - &inner_spec, ) }; diff --git a/beacon_node/client/src/notifier.rs b/beacon_node/client/src/notifier.rs index 839d296c76..f686c2c650 100644 --- a/beacon_node/client/src/notifier.rs +++ b/beacon_node/client/src/notifier.rs @@ -45,10 +45,7 @@ pub fn spawn_notifier( let mut current_sync_state = network.sync_state(); // Store info if we are required to do a backfill sync. - let original_anchor_slot = beacon_chain - .store - .get_anchor_info() - .map(|ai| ai.oldest_block_slot); + let original_oldest_block_slot = beacon_chain.store.get_anchor_info().oldest_block_slot; let interval_future = async move { // Perform pre-genesis logging. @@ -141,22 +138,17 @@ pub fn spawn_notifier( match current_sync_state { SyncState::BackFillSyncing { .. } => { // Observe backfilling sync info. - if let Some(oldest_slot) = original_anchor_slot { - if let Some(current_anchor_slot) = beacon_chain - .store - .get_anchor_info() - .map(|ai| ai.oldest_block_slot) - { - sync_distance = current_anchor_slot - .saturating_sub(beacon_chain.genesis_backfill_slot); - speedo - // For backfill sync use a fake slot which is the distance we've progressed from the starting `oldest_block_slot`. - .observe( - oldest_slot.saturating_sub(current_anchor_slot), - Instant::now(), - ); - } - } + let current_oldest_block_slot = + beacon_chain.store.get_anchor_info().oldest_block_slot; + sync_distance = current_oldest_block_slot + .saturating_sub(beacon_chain.genesis_backfill_slot); + speedo + // For backfill sync use a fake slot which is the distance we've progressed + // from the starting `original_oldest_block_slot`. + .observe( + original_oldest_block_slot.saturating_sub(current_oldest_block_slot), + Instant::now(), + ); } SyncState::SyncingFinalized { .. } | SyncState::SyncingHead { .. } @@ -213,14 +205,14 @@ pub fn spawn_notifier( "Downloading historical blocks"; "distance" => distance, "speed" => sync_speed_pretty(speed), - "est_time" => estimated_time_pretty(speedo.estimated_time_till_slot(original_anchor_slot.unwrap_or(current_slot).saturating_sub(beacon_chain.genesis_backfill_slot))), + "est_time" => estimated_time_pretty(speedo.estimated_time_till_slot(original_oldest_block_slot.saturating_sub(beacon_chain.genesis_backfill_slot))), ); } else { info!( log, "Downloading historical blocks"; "distance" => distance, - "est_time" => estimated_time_pretty(speedo.estimated_time_till_slot(original_anchor_slot.unwrap_or(current_slot).saturating_sub(beacon_chain.genesis_backfill_slot))), + "est_time" => estimated_time_pretty(speedo.estimated_time_till_slot(original_oldest_block_slot.saturating_sub(beacon_chain.genesis_backfill_slot))), ); } } else if !is_backfilling && last_backfill_log_slot.is_some() { diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index 307584b82d..fe05f55a01 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -2693,24 +2693,37 @@ pub fn serve( .and(warp::header::optional::("accept")) .and(task_spawner_filter.clone()) .and(chain_filter.clone()) + .and(log_filter.clone()) .then( |endpoint_version: EndpointVersion, state_id: StateId, accept_header: Option, task_spawner: TaskSpawner, - chain: Arc>| { + chain: Arc>, + log: Logger| { task_spawner.blocking_response_task(Priority::P1, move || match accept_header { Some(api_types::Accept::Ssz) => { // We can ignore the optimistic status for the "fork" since it's a // specification constant that doesn't change across competing heads of the // beacon chain. + let t = std::time::Instant::now(); let (state, _execution_optimistic, _finalized) = state_id.state(&chain)?; let fork_name = state .fork_name(&chain.spec) .map_err(inconsistent_fork_rejection)?; + let timer = metrics::start_timer(&metrics::HTTP_API_STATE_SSZ_ENCODE_TIMES); + let response_bytes = state.as_ssz_bytes(); + drop(timer); + debug!( + log, + "HTTP state load"; + "total_time_ms" => t.elapsed().as_millis(), + "target_slot" => state.slot() + ); + Response::builder() .status(200) - .body(state.as_ssz_bytes().into()) + .body(response_bytes.into()) .map(|res: Response| add_ssz_content_type_header(res)) .map(|resp: warp::reply::Response| { add_consensus_version_header(resp, fork_name) diff --git a/beacon_node/http_api/src/metrics.rs b/beacon_node/http_api/src/metrics.rs index b6a53b26c6..767931a747 100644 --- a/beacon_node/http_api/src/metrics.rs +++ b/beacon_node/http_api/src/metrics.rs @@ -39,3 +39,15 @@ pub static HTTP_API_BLOCK_GOSSIP_TIMES: LazyLock> = LazyLoc &["provenance"], ) }); +pub static HTTP_API_STATE_SSZ_ENCODE_TIMES: LazyLock> = LazyLock::new(|| { + try_create_histogram( + "http_api_state_ssz_encode_times", + "Time to SSZ encode a BeaconState for a response", + ) +}); +pub static HTTP_API_STATE_ROOT_TIMES: LazyLock> = LazyLock::new(|| { + try_create_histogram( + "http_api_state_root_times", + "Time to load a state root for a request", + ) +}); diff --git a/beacon_node/http_api/src/state_id.rs b/beacon_node/http_api/src/state_id.rs index fdc99fa954..ddacde9a3f 100644 --- a/beacon_node/http_api/src/state_id.rs +++ b/beacon_node/http_api/src/state_id.rs @@ -1,3 +1,4 @@ +use crate::metrics; use crate::ExecutionOptimistic; use beacon_chain::{BeaconChain, BeaconChainError, BeaconChainTypes}; use eth2::types::StateId as CoreStateId; @@ -23,6 +24,7 @@ impl StateId { &self, chain: &BeaconChain, ) -> Result<(Hash256, ExecutionOptimistic, Finalized), warp::Rejection> { + let _t = metrics::start_timer(&metrics::HTTP_API_STATE_ROOT_TIMES); let (slot, execution_optimistic, finalized) = match &self.0 { CoreStateId::Head => { let (cached_head, execution_status) = chain diff --git a/beacon_node/lighthouse_network/src/types/globals.rs b/beacon_node/lighthouse_network/src/types/globals.rs index bcebd02a0e..92583b7b5d 100644 --- a/beacon_node/lighthouse_network/src/types/globals.rs +++ b/beacon_node/lighthouse_network/src/types/globals.rs @@ -82,7 +82,7 @@ impl NetworkGlobals { peers: RwLock::new(PeerDB::new(trusted_peers, disable_peer_scoring, log)), gossipsub_subscriptions: RwLock::new(HashSet::new()), sync_state: RwLock::new(SyncState::Stalled), - backfill_state: RwLock::new(BackFillState::NotRequired), + backfill_state: RwLock::new(BackFillState::Paused), sampling_subnets, sampling_columns, config, diff --git a/beacon_node/lighthouse_network/src/types/sync_state.rs b/beacon_node/lighthouse_network/src/types/sync_state.rs index 4322763fc5..0519d6f4b0 100644 --- a/beacon_node/lighthouse_network/src/types/sync_state.rs +++ b/beacon_node/lighthouse_network/src/types/sync_state.rs @@ -35,8 +35,6 @@ pub enum BackFillState { Syncing, /// A backfill sync has completed. Completed, - /// A backfill sync is not required. - NotRequired, /// Too many failed attempts at backfilling. Consider it failed. Failed, } 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 8e943e6383..6c6bb26ee0 100644 --- a/beacon_node/network/src/network_beacon_processor/sync_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/sync_methods.rs @@ -656,12 +656,6 @@ impl NetworkBeaconProcessor { // This is an internal error, do not penalize the peer. None } - HistoricalBlockError::NoAnchorInfo => { - warn!(self.log, "Backfill not required"); - // There is no need to do a historical sync, this is not a fault of - // the peer. - None - } HistoricalBlockError::IndexOutOfBounds => { error!( self.log, diff --git a/beacon_node/network/src/sync/backfill_sync/mod.rs b/beacon_node/network/src/sync/backfill_sync/mod.rs index 946d25237b..5703ed3504 100644 --- a/beacon_node/network/src/sync/backfill_sync/mod.rs +++ b/beacon_node/network/src/sync/backfill_sync/mod.rs @@ -158,26 +158,20 @@ impl BackFillSync { log: slog::Logger, ) -> Self { // Determine if backfill is enabled or not. - // Get the anchor info, if this returns None, then backfill is not required for this - // running instance. // If, for some reason a backfill has already been completed (or we've used a trusted // genesis root) then backfill has been completed. - - let (state, current_start) = match beacon_chain.store.get_anchor_info() { - Some(anchor_info) => { - if anchor_info.block_backfill_complete(beacon_chain.genesis_backfill_slot) { - (BackFillState::Completed, Epoch::new(0)) - } else { - ( - BackFillState::Paused, - anchor_info - .oldest_block_slot - .epoch(T::EthSpec::slots_per_epoch()), - ) - } - } - None => (BackFillState::NotRequired, Epoch::new(0)), - }; + let anchor_info = beacon_chain.store.get_anchor_info(); + let (state, current_start) = + if anchor_info.block_backfill_complete(beacon_chain.genesis_backfill_slot) { + (BackFillState::Completed, Epoch::new(0)) + } else { + ( + BackFillState::Paused, + anchor_info + .oldest_block_slot + .epoch(T::EthSpec::slots_per_epoch()), + ) + }; let bfs = BackFillSync { batches: BTreeMap::new(), @@ -253,25 +247,15 @@ impl BackFillSync { self.set_state(BackFillState::Syncing); // Obtain a new start slot, from the beacon chain and handle possible errors. - match self.reset_start_epoch() { - Err(ResetEpochError::SyncCompleted) => { - error!(self.log, "Backfill sync completed whilst in failed status"); - self.set_state(BackFillState::Completed); - return Err(BackFillError::InvalidSyncState(String::from( - "chain completed", - ))); - } - Err(ResetEpochError::NotRequired) => { - error!( - self.log, - "Backfill sync not required whilst in failed status" - ); - self.set_state(BackFillState::NotRequired); - return Err(BackFillError::InvalidSyncState(String::from( - "backfill not required", - ))); - } - Ok(_) => {} + if let Err(e) = self.reset_start_epoch() { + // This infallible match exists to force us to update this code if a future + // refactor of `ResetEpochError` adds a variant. + let ResetEpochError::SyncCompleted = e; + error!(self.log, "Backfill sync completed whilst in failed status"); + self.set_state(BackFillState::Completed); + return Err(BackFillError::InvalidSyncState(String::from( + "chain completed", + ))); } debug!(self.log, "Resuming a failed backfill sync"; "start_epoch" => self.current_start); @@ -279,9 +263,7 @@ impl BackFillSync { // begin requesting blocks from the peer pool, until all peers are exhausted. self.request_batches(network)?; } - BackFillState::Completed | BackFillState::NotRequired => { - return Ok(SyncStart::NotSyncing) - } + BackFillState::Completed => return Ok(SyncStart::NotSyncing), } Ok(SyncStart::Syncing { @@ -313,10 +295,7 @@ impl BackFillSync { peer_id: &PeerId, network: &mut SyncNetworkContext, ) -> Result<(), BackFillError> { - if matches!( - self.state(), - BackFillState::Failed | BackFillState::NotRequired - ) { + if matches!(self.state(), BackFillState::Failed) { return Ok(()); } @@ -1142,17 +1121,14 @@ impl BackFillSync { /// This errors if the beacon chain indicates that backfill sync has already completed or is /// not required. fn reset_start_epoch(&mut self) -> Result<(), ResetEpochError> { - if let Some(anchor_info) = self.beacon_chain.store.get_anchor_info() { - if anchor_info.block_backfill_complete(self.beacon_chain.genesis_backfill_slot) { - Err(ResetEpochError::SyncCompleted) - } else { - self.current_start = anchor_info - .oldest_block_slot - .epoch(T::EthSpec::slots_per_epoch()); - Ok(()) - } + let anchor_info = self.beacon_chain.store.get_anchor_info(); + if anchor_info.block_backfill_complete(self.beacon_chain.genesis_backfill_slot) { + Err(ResetEpochError::SyncCompleted) } else { - Err(ResetEpochError::NotRequired) + self.current_start = anchor_info + .oldest_block_slot + .epoch(T::EthSpec::slots_per_epoch()); + Ok(()) } } @@ -1160,13 +1136,12 @@ impl BackFillSync { fn check_completed(&mut self) -> bool { if self.would_complete(self.current_start) { // Check that the beacon chain agrees - if let Some(anchor_info) = self.beacon_chain.store.get_anchor_info() { - // Conditions that we have completed a backfill sync - if anchor_info.block_backfill_complete(self.beacon_chain.genesis_backfill_slot) { - return true; - } else { - error!(self.log, "Backfill out of sync with beacon chain"); - } + let anchor_info = self.beacon_chain.store.get_anchor_info(); + // Conditions that we have completed a backfill sync + if anchor_info.block_backfill_complete(self.beacon_chain.genesis_backfill_slot) { + return true; + } else { + error!(self.log, "Backfill out of sync with beacon chain"); } } false @@ -1195,6 +1170,4 @@ impl BackFillSync { enum ResetEpochError { /// The chain has already completed. SyncCompleted, - /// Backfill is not required. - NotRequired, } diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index cfda99325b..87c6e84ba7 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -756,9 +756,23 @@ pub fn cli_app() -> Command { Arg::new("slots-per-restore-point") .long("slots-per-restore-point") .value_name("SLOT_COUNT") - .help("Specifies how often a freezer DB restore point should be stored. \ - Cannot be changed after initialization. \ - [default: 8192 (mainnet) or 64 (minimal)]") + .help("DEPRECATED. This flag has no effect.") + .action(ArgAction::Set) + .display_order(0) + ) + .arg( + Arg::new("hierarchy-exponents") + .long("hierarchy-exponents") + .value_name("EXPONENTS") + .help("Specifies the frequency for storing full state snapshots and hierarchical \ + diffs in the freezer DB. Accepts a comma-separated list of ascending \ + exponents. Each exponent defines an interval for storing diffs to the layer \ + above. The last exponent defines the interval for full snapshots. \ + For example, a config of '4,8,12' would store a full snapshot every \ + 4096 (2^12) slots, first-level diffs every 256 (2^8) slots, and second-level \ + diffs every 16 (2^4) slots. \ + Cannot be changed after initialization. \ + [default: 5,9,11,13,16,18,21]") .action(ArgAction::Set) .display_order(0) ) @@ -786,11 +800,24 @@ pub fn cli_app() -> Command { Arg::new("historic-state-cache-size") .long("historic-state-cache-size") .value_name("SIZE") - .help("Specifies how many states from the freezer database should cache in memory") + .help("Specifies how many states from the freezer database should be cached in \ + memory") .default_value("1") .action(ArgAction::Set) .display_order(0) ) + .arg( + Arg::new("hdiff-buffer-cache-size") + .long("hdiff-buffer-cache-size") + .value_name("SIZE") + .help("Number of hierarchical diff (hdiff) buffers to cache in memory. Each buffer \ + is around the size of a BeaconState so you should be cautious about setting \ + this value too high. This flag is irrelevant for most nodes, which run with \ + state pruning enabled.") + .default_value("16") + .action(ArgAction::Set) + .display_order(0) + ) .arg( Arg::new("state-cache-size") .long("state-cache-size") @@ -1006,7 +1033,6 @@ pub fn cli_app() -> Command { .default_value("0") .display_order(0) ) - /* * Misc. */ diff --git a/beacon_node/src/config.rs b/beacon_node/src/config.rs index d12c6d6681..adcb591aed 100644 --- a/beacon_node/src/config.rs +++ b/beacon_node/src/config.rs @@ -411,13 +411,6 @@ pub fn get_config( client_config.blobs_db_path = Some(PathBuf::from(blobs_db_dir)); } - let (sprp, sprp_explicit) = get_slots_per_restore_point::(clap_utils::parse_optional( - cli_args, - "slots-per-restore-point", - )?)?; - client_config.store.slots_per_restore_point = sprp; - client_config.store.slots_per_restore_point_set_explicitly = sprp_explicit; - if let Some(block_cache_size) = cli_args.get_one::("block-cache-size") { client_config.store.block_cache_size = block_cache_size .parse() @@ -430,11 +423,16 @@ pub fn get_config( .map_err(|_| "state-cache-size is not a valid integer".to_string())?; } - if let Some(historic_state_cache_size) = cli_args.get_one::("historic-state-cache-size") + if let Some(historic_state_cache_size) = + clap_utils::parse_optional(cli_args, "historic-state-cache-size")? { - client_config.store.historic_state_cache_size = historic_state_cache_size - .parse() - .map_err(|_| "historic-state-cache-size is not a valid integer".to_string())?; + client_config.store.historic_state_cache_size = historic_state_cache_size; + } + + if let Some(hdiff_buffer_cache_size) = + clap_utils::parse_optional(cli_args, "hdiff-buffer-cache-size")? + { + client_config.store.hdiff_buffer_cache_size = hdiff_buffer_cache_size; } client_config.store.compact_on_init = cli_args.get_flag("compact-db"); @@ -448,6 +446,14 @@ pub fn get_config( client_config.store.prune_payloads = prune_payloads; } + if clap_utils::parse_optional::(cli_args, "slots-per-restore-point")?.is_some() { + warn!(log, "The slots-per-restore-point flag is deprecated"); + } + + if let Some(hierarchy_config) = clap_utils::parse_optional(cli_args, "hierarchy-exponents")? { + client_config.store.hierarchy_config = hierarchy_config; + } + if let Some(epochs_per_migration) = clap_utils::parse_optional(cli_args, "epochs-per-migration")? { @@ -1495,23 +1501,6 @@ pub fn get_data_dir(cli_args: &ArgMatches) -> PathBuf { .unwrap_or_else(|| PathBuf::from(".")) } -/// Get the `slots_per_restore_point` value to use for the database. -/// -/// Return `(sprp, set_explicitly)` where `set_explicitly` is `true` if the user provided the value. -pub fn get_slots_per_restore_point( - slots_per_restore_point: Option, -) -> Result<(u64, bool), String> { - if let Some(slots_per_restore_point) = slots_per_restore_point { - Ok((slots_per_restore_point, true)) - } else { - let default = std::cmp::min( - E::slots_per_historical_root() as u64, - store::config::DEFAULT_SLOTS_PER_RESTORE_POINT, - ); - Ok((default, false)) - } -} - /// Parses the `cli_value` as a comma-separated string of values to be parsed with `parser`. /// /// If there is more than one value, log a warning. If there are no values, return an error. diff --git a/beacon_node/src/lib.rs b/beacon_node/src/lib.rs index 5bc0f9dc6a..9413eb3924 100644 --- a/beacon_node/src/lib.rs +++ b/beacon_node/src/lib.rs @@ -9,7 +9,7 @@ use beacon_chain::{ use clap::ArgMatches; pub use cli::cli_app; pub use client::{Client, ClientBuilder, ClientConfig, ClientGenesis}; -pub use config::{get_config, get_data_dir, get_slots_per_restore_point, set_network_config}; +pub use config::{get_config, get_data_dir, set_network_config}; use environment::RuntimeContext; pub use eth2_config::Eth2Config; use slasher::{DatabaseBackendOverride, Slasher}; diff --git a/beacon_node/store/Cargo.toml b/beacon_node/store/Cargo.toml index aac1ee26e1..7cee16c353 100644 --- a/beacon_node/store/Cargo.toml +++ b/beacon_node/store/Cargo.toml @@ -7,6 +7,8 @@ edition = { workspace = true } [dev-dependencies] tempfile = { workspace = true } beacon_chain = { workspace = true } +criterion = { workspace = true } +rand = { workspace = true, features = ["small_rng"] } [dependencies] db-key = "0.0.5" @@ -15,6 +17,7 @@ parking_lot = { workspace = true } itertools = { workspace = true } ethereum_ssz = { workspace = true } ethereum_ssz_derive = { workspace = true } +superstruct = { workspace = true } types = { workspace = true } safe_arith = { workspace = true } state_processing = { workspace = true } @@ -25,3 +28,12 @@ lru = { workspace = true } sloggers = { workspace = true } directory = { workspace = true } strum = { workspace = true } +xdelta3 = { workspace = true } +zstd = { workspace = true } +bls = { workspace = true } +smallvec = { workspace = true } +logging = { workspace = true } + +[[bench]] +name = "hdiff" +harness = false diff --git a/beacon_node/store/benches/hdiff.rs b/beacon_node/store/benches/hdiff.rs new file mode 100644 index 0000000000..2577f03f66 --- /dev/null +++ b/beacon_node/store/benches/hdiff.rs @@ -0,0 +1,116 @@ +use bls::PublicKeyBytes; +use criterion::{criterion_group, criterion_main, Criterion}; +use rand::Rng; +use ssz::Decode; +use store::{ + hdiff::{HDiff, HDiffBuffer}, + StoreConfig, +}; +use types::{BeaconState, Epoch, Eth1Data, EthSpec, MainnetEthSpec as E, Validator}; + +pub fn all_benches(c: &mut Criterion) { + let spec = E::default_spec(); + let genesis_time = 0; + let eth1_data = Eth1Data::default(); + let mut rng = rand::thread_rng(); + let validator_mutations = 1000; + let validator_additions = 100; + + for n in [1_000_000, 1_500_000, 2_000_000] { + let mut source_state = BeaconState::::new(genesis_time, eth1_data.clone(), &spec); + + for _ in 0..n { + append_validator(&mut source_state, &mut rng); + } + + let mut target_state = source_state.clone(); + // Change all balances + for i in 0..n { + let balance = target_state.balances_mut().get_mut(i).unwrap(); + *balance += rng.gen_range(1..=1_000_000); + } + // And some validator records + for _ in 0..validator_mutations { + let index = rng.gen_range(1..n); + // TODO: Only change a few things, and not the pubkey + *target_state.validators_mut().get_mut(index).unwrap() = rand_validator(&mut rng); + } + for _ in 0..validator_additions { + append_validator(&mut target_state, &mut rng); + } + + bench_against_states( + c, + source_state, + target_state, + &format!("n={n} v_mut={validator_mutations} v_add={validator_additions}"), + ); + } +} + +fn bench_against_states( + c: &mut Criterion, + source_state: BeaconState, + target_state: BeaconState, + id: &str, +) { + let slot_diff = target_state.slot() - source_state.slot(); + let config = StoreConfig::default(); + let source = HDiffBuffer::from_state(source_state); + let target = HDiffBuffer::from_state(target_state); + let diff = HDiff::compute(&source, &target, &config).unwrap(); + println!( + "state slot diff {slot_diff} - diff size {id} {}", + diff.size() + ); + + c.bench_function(&format!("compute hdiff {id}"), |b| { + b.iter(|| { + HDiff::compute(&source, &target, &config).unwrap(); + }) + }); + c.bench_function(&format!("apply hdiff {id}"), |b| { + b.iter(|| { + let mut source = source.clone(); + diff.apply(&mut source, &config).unwrap(); + }) + }); +} + +fn rand_validator(mut rng: impl Rng) -> Validator { + let mut pubkey = [0u8; 48]; + rng.fill_bytes(&mut pubkey); + let withdrawal_credentials: [u8; 32] = rng.gen(); + + Validator { + pubkey: PublicKeyBytes::from_ssz_bytes(&pubkey).unwrap(), + withdrawal_credentials: withdrawal_credentials.into(), + slashed: false, + effective_balance: 32_000_000_000, + activation_eligibility_epoch: Epoch::max_value(), + activation_epoch: Epoch::max_value(), + exit_epoch: Epoch::max_value(), + withdrawable_epoch: Epoch::max_value(), + } +} + +fn append_validator(state: &mut BeaconState, mut rng: impl Rng) { + state + .balances_mut() + .push(32_000_000_000 + rng.gen_range(1..=1_000_000_000)) + .unwrap(); + if let Ok(inactivity_scores) = state.inactivity_scores_mut() { + inactivity_scores.push(0).unwrap(); + } + state + .validators_mut() + .push(rand_validator(&mut rng)) + .unwrap(); +} + +criterion_group! { + name = benches; + config = Criterion::default().sample_size(10); + targets = all_benches +} +criterion_main!(benches); diff --git a/beacon_node/store/src/chunk_writer.rs b/beacon_node/store/src/chunk_writer.rs deleted file mode 100644 index 059b812e74..0000000000 --- a/beacon_node/store/src/chunk_writer.rs +++ /dev/null @@ -1,75 +0,0 @@ -use crate::chunked_vector::{chunk_key, Chunk, ChunkError, Field}; -use crate::{Error, KeyValueStore, KeyValueStoreOp}; -use types::EthSpec; - -/// Buffered writer for chunked vectors (block roots mainly). -pub struct ChunkWriter<'a, F, E, S> -where - F: Field, - E: EthSpec, - S: KeyValueStore, -{ - /// Buffered chunk awaiting writing to disk (always dirty). - chunk: Chunk, - /// Chunk index of `chunk`. - index: usize, - store: &'a S, -} - -impl<'a, F, E, S> ChunkWriter<'a, F, E, S> -where - F: Field, - E: EthSpec, - S: KeyValueStore, -{ - pub fn new(store: &'a S, vindex: usize) -> Result { - let chunk_index = F::chunk_index(vindex); - let chunk = Chunk::load(store, F::column(), &chunk_key(chunk_index))? - .unwrap_or_else(|| Chunk::new(vec![F::Value::default(); F::chunk_size()])); - - Ok(Self { - chunk, - index: chunk_index, - store, - }) - } - - /// Set the value at a given vector index, writing the current chunk and moving on if necessary. - pub fn set( - &mut self, - vindex: usize, - value: F::Value, - batch: &mut Vec, - ) -> Result<(), Error> { - let chunk_index = F::chunk_index(vindex); - - // Advance to the next chunk. - if chunk_index != self.index { - self.write(batch)?; - *self = Self::new(self.store, vindex)?; - } - - let i = vindex % F::chunk_size(); - let existing_value = &self.chunk.values[i]; - - if existing_value == &value || existing_value == &F::Value::default() { - self.chunk.values[i] = value; - Ok(()) - } else { - Err(ChunkError::Inconsistent { - field: F::column(), - chunk_index, - existing_value: format!("{:?}", existing_value), - new_value: format!("{:?}", value), - } - .into()) - } - } - - /// Write the current chunk to disk. - /// - /// Should be called before the writer is dropped, in order to write the final chunk to disk. - pub fn write(&self, batch: &mut Vec) -> Result<(), Error> { - self.chunk.store(F::column(), &chunk_key(self.index), batch) - } -} diff --git a/beacon_node/store/src/chunked_vector.rs b/beacon_node/store/src/chunked_vector.rs index 4450989d59..83b8da2a18 100644 --- a/beacon_node/store/src/chunked_vector.rs +++ b/beacon_node/store/src/chunked_vector.rs @@ -322,11 +322,11 @@ macro_rules! field { } field!( - BlockRoots, + BlockRootsChunked, FixedLengthField, Hash256, E::SlotsPerHistoricalRoot, - DBColumn::BeaconBlockRoots, + DBColumn::BeaconBlockRootsChunked, |_| OncePerNSlots { n: 1, activation_slot: Some(Slot::new(0)), @@ -336,11 +336,11 @@ field!( ); field!( - StateRoots, + StateRootsChunked, FixedLengthField, Hash256, E::SlotsPerHistoricalRoot, - DBColumn::BeaconStateRoots, + DBColumn::BeaconStateRootsChunked, |_| OncePerNSlots { n: 1, activation_slot: Some(Slot::new(0)), @@ -859,8 +859,8 @@ mod test { fn test_fixed_length>(_: F, expected: bool) { assert_eq!(F::is_fixed_length(), expected); } - test_fixed_length(BlockRoots, true); - test_fixed_length(StateRoots, true); + test_fixed_length(BlockRootsChunked, true); + test_fixed_length(StateRootsChunked, true); test_fixed_length(HistoricalRoots, false); test_fixed_length(RandaoMixes, true); } @@ -880,12 +880,12 @@ mod test { #[test] fn needs_genesis_value_block_roots() { - needs_genesis_value_once_per_slot(BlockRoots); + needs_genesis_value_once_per_slot(BlockRootsChunked); } #[test] fn needs_genesis_value_state_roots() { - needs_genesis_value_once_per_slot(StateRoots); + needs_genesis_value_once_per_slot(StateRootsChunked); } #[test] diff --git a/beacon_node/store/src/config.rs b/beacon_node/store/src/config.rs index d43999d822..4f67530570 100644 --- a/beacon_node/store/src/config.rs +++ b/beacon_node/store/src/config.rs @@ -1,38 +1,47 @@ -use crate::{DBColumn, Error, StoreItem}; +use crate::hdiff::HierarchyConfig; +use crate::{AnchorInfo, DBColumn, Error, Split, StoreItem}; use serde::{Deserialize, Serialize}; use ssz::{Decode, Encode}; use ssz_derive::{Decode, Encode}; +use std::io::Write; use std::num::NonZeroUsize; +use superstruct::superstruct; use types::non_zero_usize::new_non_zero_usize; -use types::{EthSpec, MinimalEthSpec}; +use types::EthSpec; +use zstd::Encoder; -pub const PREV_DEFAULT_SLOTS_PER_RESTORE_POINT: u64 = 2048; -pub const DEFAULT_SLOTS_PER_RESTORE_POINT: u64 = 8192; -pub const DEFAULT_BLOCK_CACHE_SIZE: NonZeroUsize = new_non_zero_usize(5); +// Only used in tests. Mainnet sets a higher default on the CLI. +pub const DEFAULT_EPOCHS_PER_STATE_DIFF: u64 = 8; +pub const DEFAULT_BLOCK_CACHE_SIZE: NonZeroUsize = new_non_zero_usize(64); pub const DEFAULT_STATE_CACHE_SIZE: NonZeroUsize = new_non_zero_usize(128); +pub const DEFAULT_COMPRESSION_LEVEL: i32 = 1; pub const DEFAULT_HISTORIC_STATE_CACHE_SIZE: NonZeroUsize = new_non_zero_usize(1); +pub const DEFAULT_HDIFF_BUFFER_CACHE_SIZE: NonZeroUsize = new_non_zero_usize(16); +const EST_COMPRESSION_FACTOR: usize = 2; pub const DEFAULT_EPOCHS_PER_BLOB_PRUNE: u64 = 1; pub const DEFAULT_BLOB_PUNE_MARGIN_EPOCHS: u64 = 0; /// Database configuration parameters. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct StoreConfig { - /// Number of slots to wait between storing restore points in the freezer database. - pub slots_per_restore_point: u64, - /// Flag indicating whether the `slots_per_restore_point` was set explicitly by the user. - pub slots_per_restore_point_set_explicitly: bool, /// Maximum number of blocks to store in the in-memory block cache. pub block_cache_size: NonZeroUsize, /// Maximum number of states to store in the in-memory state cache. pub state_cache_size: NonZeroUsize, - /// Maximum number of states from freezer database to store in the in-memory state cache. + /// Compression level for blocks, state diffs and other compressed values. + pub compression_level: i32, + /// Maximum number of historic states to store in the in-memory historic state cache. pub historic_state_cache_size: NonZeroUsize, + /// Maximum number of `HDiffBuffer`s to store in memory. + pub hdiff_buffer_cache_size: NonZeroUsize, /// Whether to compact the database on initialization. pub compact_on_init: bool, /// Whether to compact the database during database pruning. pub compact_on_prune: bool, /// Whether to prune payloads on initialization and finalization. pub prune_payloads: bool, + /// State diff hierarchy. + pub hierarchy_config: HierarchyConfig, /// Whether to prune blobs older than the blob data availability boundary. pub prune_blobs: bool, /// Frequency of blob pruning in epochs. Default: 1 (every epoch). @@ -43,28 +52,59 @@ pub struct StoreConfig { } /// Variant of `StoreConfig` that gets written to disk. Contains immutable configuration params. -#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] +#[superstruct( + variants(V1, V22), + variant_attributes(derive(Debug, Clone, PartialEq, Eq, Encode, Decode)) +)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct OnDiskStoreConfig { + #[superstruct(only(V1))] pub slots_per_restore_point: u64, + /// Prefix byte to future-proof versions of the `OnDiskStoreConfig` post V1 + #[superstruct(only(V22))] + version_byte: u8, + #[superstruct(only(V22))] + pub hierarchy_config: HierarchyConfig, +} + +impl OnDiskStoreConfigV22 { + fn new(hierarchy_config: HierarchyConfig) -> Self { + Self { + version_byte: 22, + hierarchy_config, + } + } } #[derive(Debug, Clone)] pub enum StoreConfigError { - MismatchedSlotsPerRestorePoint { config: u64, on_disk: u64 }, + MismatchedSlotsPerRestorePoint { + config: u64, + on_disk: u64, + }, + InvalidCompressionLevel { + level: i32, + }, + IncompatibleStoreConfig { + config: OnDiskStoreConfig, + on_disk: OnDiskStoreConfig, + }, + ZeroEpochsPerBlobPrune, + InvalidVersionByte(Option), } impl Default for StoreConfig { fn default() -> Self { Self { - // Safe default for tests, shouldn't ever be read by a CLI node. - slots_per_restore_point: MinimalEthSpec::slots_per_historical_root() as u64, - slots_per_restore_point_set_explicitly: false, block_cache_size: DEFAULT_BLOCK_CACHE_SIZE, state_cache_size: DEFAULT_STATE_CACHE_SIZE, historic_state_cache_size: DEFAULT_HISTORIC_STATE_CACHE_SIZE, + hdiff_buffer_cache_size: DEFAULT_HDIFF_BUFFER_CACHE_SIZE, + compression_level: DEFAULT_COMPRESSION_LEVEL, compact_on_init: false, compact_on_prune: true, prune_payloads: true, + hierarchy_config: HierarchyConfig::default(), prune_blobs: true, epochs_per_blob_prune: DEFAULT_EPOCHS_PER_BLOB_PRUNE, blob_prune_margin_epochs: DEFAULT_BLOB_PUNE_MARGIN_EPOCHS, @@ -74,22 +114,90 @@ impl Default for StoreConfig { impl StoreConfig { pub fn as_disk_config(&self) -> OnDiskStoreConfig { - OnDiskStoreConfig { - slots_per_restore_point: self.slots_per_restore_point, - } + OnDiskStoreConfig::V22(OnDiskStoreConfigV22::new(self.hierarchy_config.clone())) } pub fn check_compatibility( &self, on_disk_config: &OnDiskStoreConfig, + split: &Split, + anchor: &AnchorInfo, ) -> Result<(), StoreConfigError> { - if self.slots_per_restore_point != on_disk_config.slots_per_restore_point { - return Err(StoreConfigError::MismatchedSlotsPerRestorePoint { - config: self.slots_per_restore_point, - on_disk: on_disk_config.slots_per_restore_point, - }); + // Allow changing the hierarchy exponents if no historic states are stored. + let no_historic_states_stored = anchor.no_historic_states_stored(split.slot); + let hierarchy_config_changed = + if let Ok(on_disk_hierarchy_config) = on_disk_config.hierarchy_config() { + *on_disk_hierarchy_config != self.hierarchy_config + } else { + false + }; + + if hierarchy_config_changed && !no_historic_states_stored { + Err(StoreConfigError::IncompatibleStoreConfig { + config: self.as_disk_config(), + on_disk: on_disk_config.clone(), + }) + } else { + Ok(()) } - Ok(()) + } + + /// Check that the configuration is valid. + pub fn verify(&self) -> Result<(), StoreConfigError> { + self.verify_compression_level()?; + self.verify_epochs_per_blob_prune() + } + + /// Check that the compression level is valid. + fn verify_compression_level(&self) -> Result<(), StoreConfigError> { + if zstd::compression_level_range().contains(&self.compression_level) { + Ok(()) + } else { + Err(StoreConfigError::InvalidCompressionLevel { + level: self.compression_level, + }) + } + } + + /// Check that epochs_per_blob_prune is at least 1 epoch to avoid attempting to prune the same + /// epochs over and over again. + fn verify_epochs_per_blob_prune(&self) -> Result<(), StoreConfigError> { + if self.epochs_per_blob_prune > 0 { + Ok(()) + } else { + Err(StoreConfigError::ZeroEpochsPerBlobPrune) + } + } + + /// Estimate the size of `len` bytes after compression at the current compression level. + pub fn estimate_compressed_size(&self, len: usize) -> usize { + // This is a rough estimate, but for our data it seems that all non-zero compression levels + // provide a similar compression ratio. + if self.compression_level == 0 { + len + } else { + len / EST_COMPRESSION_FACTOR + } + } + + /// Estimate the size of `len` compressed bytes after decompression at the current compression + /// level. + pub fn estimate_decompressed_size(&self, len: usize) -> usize { + if self.compression_level == 0 { + len + } else { + len * EST_COMPRESSION_FACTOR + } + } + + pub fn compress_bytes(&self, ssz_bytes: &[u8]) -> Result, Error> { + let mut compressed_value = + Vec::with_capacity(self.estimate_compressed_size(ssz_bytes.len())); + let mut encoder = Encoder::new(&mut compressed_value, self.compression_level) + .map_err(Error::Compression)?; + encoder.write_all(ssz_bytes).map_err(Error::Compression)?; + encoder.finish().map_err(Error::Compression)?; + Ok(compressed_value) } } @@ -99,10 +207,136 @@ impl StoreItem for OnDiskStoreConfig { } fn as_store_bytes(&self) -> Vec { - self.as_ssz_bytes() + match self { + OnDiskStoreConfig::V1(value) => value.as_ssz_bytes(), + OnDiskStoreConfig::V22(value) => value.as_ssz_bytes(), + } } fn from_store_bytes(bytes: &[u8]) -> Result { - Ok(Self::from_ssz_bytes(bytes)?) + // NOTE: V22 config can never be deserialized as a V1 because the minimum length of its + // serialization is: 1 prefix byte + 1 offset (OnDiskStoreConfigV1 container) + + // 1 offset (HierarchyConfig container) = 9. + if let Ok(value) = OnDiskStoreConfigV1::from_ssz_bytes(bytes) { + return Ok(Self::V1(value)); + } + + Ok(Self::V22(OnDiskStoreConfigV22::from_ssz_bytes(bytes)?)) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{ + metadata::{ANCHOR_FOR_ARCHIVE_NODE, ANCHOR_UNINITIALIZED, STATE_UPPER_LIMIT_NO_RETAIN}, + AnchorInfo, Split, + }; + use ssz::DecodeError; + use types::{Hash256, Slot}; + + #[test] + fn check_compatibility_ok() { + let store_config = StoreConfig { + ..Default::default() + }; + let on_disk_config = OnDiskStoreConfig::V22(OnDiskStoreConfigV22::new( + store_config.hierarchy_config.clone(), + )); + let split = Split::default(); + assert!(store_config + .check_compatibility(&on_disk_config, &split, &ANCHOR_UNINITIALIZED) + .is_ok()); + } + + #[test] + fn check_compatibility_after_migration() { + let store_config = StoreConfig { + ..Default::default() + }; + let on_disk_config = OnDiskStoreConfig::V1(OnDiskStoreConfigV1 { + slots_per_restore_point: 8192, + }); + let split = Split::default(); + assert!(store_config + .check_compatibility(&on_disk_config, &split, &ANCHOR_UNINITIALIZED) + .is_ok()); + } + + #[test] + fn check_compatibility_hierarchy_config_incompatible() { + let store_config = StoreConfig::default(); + let on_disk_config = OnDiskStoreConfig::V22(OnDiskStoreConfigV22::new(HierarchyConfig { + exponents: vec![5, 8, 11, 13, 16, 18, 21], + })); + let split = Split { + slot: Slot::new(32), + ..Default::default() + }; + assert!(store_config + .check_compatibility(&on_disk_config, &split, &ANCHOR_FOR_ARCHIVE_NODE) + .is_err()); + } + + #[test] + fn check_compatibility_hierarchy_config_update() { + let store_config = StoreConfig { + ..Default::default() + }; + let on_disk_config = OnDiskStoreConfig::V22(OnDiskStoreConfigV22::new(HierarchyConfig { + exponents: vec![5, 8, 11, 13, 16, 18, 21], + })); + let split = Split::default(); + let anchor = AnchorInfo { + anchor_slot: Slot::new(0), + oldest_block_slot: Slot::new(0), + oldest_block_parent: Hash256::ZERO, + state_upper_limit: STATE_UPPER_LIMIT_NO_RETAIN, + state_lower_limit: Slot::new(0), + }; + assert!(store_config + .check_compatibility(&on_disk_config, &split, &anchor) + .is_ok()); + } + + #[test] + fn serde_on_disk_config_v0_from_v1_default() { + let config = OnDiskStoreConfig::V22(OnDiskStoreConfigV22::new(<_>::default())); + let config_bytes = config.as_store_bytes(); + // On a downgrade, the previous version of lighthouse will attempt to deserialize the + // prefixed V22 as just the V1 version. + assert_eq!( + OnDiskStoreConfigV1::from_ssz_bytes(&config_bytes).unwrap_err(), + DecodeError::InvalidByteLength { + len: 16, + expected: 8 + }, + ); + } + + #[test] + fn serde_on_disk_config_v0_from_v1_empty() { + let config = OnDiskStoreConfig::V22(OnDiskStoreConfigV22::new(HierarchyConfig { + exponents: vec![], + })); + let config_bytes = config.as_store_bytes(); + // On a downgrade, the previous version of lighthouse will attempt to deserialize the + // prefixed V22 as just the V1 version. + assert_eq!( + OnDiskStoreConfigV1::from_ssz_bytes(&config_bytes).unwrap_err(), + DecodeError::InvalidByteLength { + len: 9, + expected: 8 + }, + ); + } + + #[test] + fn serde_on_disk_config_v1_roundtrip() { + let config = OnDiskStoreConfig::V22(OnDiskStoreConfigV22::new(<_>::default())); + let bytes = config.as_store_bytes(); + assert_eq!(bytes[0], 22); + let config_out = OnDiskStoreConfig::from_store_bytes(&bytes).unwrap(); + assert_eq!(config_out, config); } } diff --git a/beacon_node/store/src/errors.rs b/beacon_node/store/src/errors.rs index c543a9c4e4..6bb4edee6b 100644 --- a/beacon_node/store/src/errors.rs +++ b/beacon_node/store/src/errors.rs @@ -1,9 +1,10 @@ use crate::chunked_vector::ChunkError; use crate::config::StoreConfigError; use crate::hot_cold_store::HotColdDBError; +use crate::{hdiff, DBColumn}; use ssz::DecodeError; use state_processing::BlockReplayError; -use types::{BeaconStateError, EpochCacheError, Hash256, InconsistentFork, Slot}; +use types::{milhouse, BeaconStateError, EpochCacheError, Hash256, InconsistentFork, Slot}; pub type Result = std::result::Result; @@ -38,27 +39,35 @@ pub enum Error { /// State reconstruction failed because it didn't reach the upper limit slot. /// /// This should never happen (it's a logic error). - StateReconstructionDidNotComplete, + StateReconstructionLogicError, StateReconstructionRootMismatch { slot: Slot, expected: Hash256, computed: Hash256, }, + MissingGenesisState, + MissingSnapshot(Slot), BlockReplayError(BlockReplayError), - AddPayloadLogicError, - SlotClockUnavailableForMigration, - InvalidKey, - InvalidBytes, - UnableToDowngrade, - InconsistentFork(InconsistentFork), - CacheBuildError(EpochCacheError), - RandaoMixOutOfBounds, + MilhouseError(milhouse::Error), + Compression(std::io::Error), FinalizedStateDecreasingSlot, FinalizedStateUnaligned, StateForCacheHasPendingUpdates { state_root: Hash256, slot: Slot, }, + AddPayloadLogicError, + InvalidKey, + InvalidBytes, + InconsistentFork(InconsistentFork), + Hdiff(hdiff::Error), + CacheBuildError(EpochCacheError), + ForwardsIterInvalidColumn(DBColumn), + ForwardsIterGap(DBColumn, Slot, Slot), + StateShouldNotBeRequired(Slot), + MissingBlock(Hash256), + RandaoMixOutOfBounds, + GenesisStateUnknown, ArithError(safe_arith::ArithError), } @@ -112,6 +121,18 @@ impl From for Error { } } +impl From for Error { + fn from(e: milhouse::Error) -> Self { + Self::MilhouseError(e) + } +} + +impl From for Error { + fn from(e: hdiff::Error) -> Self { + Self::Hdiff(e) + } +} + impl From for Error { fn from(e: BlockReplayError) -> Error { Error::BlockReplayError(e) diff --git a/beacon_node/store/src/forwards_iter.rs b/beacon_node/store/src/forwards_iter.rs index 1ccf1da1b7..e0f44f3aff 100644 --- a/beacon_node/store/src/forwards_iter.rs +++ b/beacon_node/store/src/forwards_iter.rs @@ -1,37 +1,34 @@ -use crate::chunked_iter::ChunkedVectorIter; -use crate::chunked_vector::{BlockRoots, Field, StateRoots}; use crate::errors::{Error, Result}; use crate::iter::{BlockRootsIterator, StateRootsIterator}; -use crate::{HotColdDB, ItemStore}; +use crate::{ColumnIter, DBColumn, HotColdDB, ItemStore}; use itertools::process_results; -use types::{BeaconState, ChainSpec, EthSpec, Hash256, Slot}; +use std::marker::PhantomData; +use types::{BeaconState, EthSpec, Hash256, Slot}; pub type HybridForwardsBlockRootsIterator<'a, E, Hot, Cold> = - HybridForwardsIterator<'a, E, BlockRoots, Hot, Cold>; + HybridForwardsIterator<'a, E, Hot, Cold>; pub type HybridForwardsStateRootsIterator<'a, E, Hot, Cold> = - HybridForwardsIterator<'a, E, StateRoots, Hot, Cold>; + HybridForwardsIterator<'a, E, Hot, Cold>; -/// Trait unifying `BlockRoots` and `StateRoots` for forward iteration. -pub trait Root: Field { - fn simple_forwards_iterator, Cold: ItemStore>( - store: &HotColdDB, +impl, Cold: ItemStore> HotColdDB { + fn simple_forwards_iterator( + &self, + column: DBColumn, start_slot: Slot, end_state: BeaconState, end_root: Hash256, - ) -> Result; + ) -> Result { + if column == DBColumn::BeaconBlockRoots { + self.forwards_iter_block_roots_using_state(start_slot, end_state, end_root) + } else if column == DBColumn::BeaconStateRoots { + self.forwards_iter_state_roots_using_state(start_slot, end_state, end_root) + } else { + Err(Error::ForwardsIterInvalidColumn(column)) + } + } - /// The first slot for which this field is *no longer* stored in the freezer database. - /// - /// If `None`, then this field is not stored in the freezer database at all due to pruning - /// configuration. - fn freezer_upper_limit, Cold: ItemStore>( - store: &HotColdDB, - ) -> Option; -} - -impl Root for BlockRoots { - fn simple_forwards_iterator, Cold: ItemStore>( - store: &HotColdDB, + fn forwards_iter_block_roots_using_state( + &self, start_slot: Slot, end_state: BeaconState, end_block_root: Hash256, @@ -39,7 +36,7 @@ impl Root for BlockRoots { // Iterate backwards from the end state, stopping at the start slot. let values = process_results( std::iter::once(Ok((end_block_root, end_state.slot()))) - .chain(BlockRootsIterator::owned(store, end_state)), + .chain(BlockRootsIterator::owned(self, end_state)), |iter| { iter.take_while(|(_, slot)| *slot >= start_slot) .collect::>() @@ -48,17 +45,8 @@ impl Root for BlockRoots { Ok(SimpleForwardsIterator { values }) } - fn freezer_upper_limit, Cold: ItemStore>( - store: &HotColdDB, - ) -> Option { - // Block roots are stored for all slots up to the split slot (exclusive). - Some(store.get_split_slot()) - } -} - -impl Root for StateRoots { - fn simple_forwards_iterator, Cold: ItemStore>( - store: &HotColdDB, + fn forwards_iter_state_roots_using_state( + &self, start_slot: Slot, end_state: BeaconState, end_state_root: Hash256, @@ -66,7 +54,7 @@ impl Root for StateRoots { // Iterate backwards from the end state, stopping at the start slot. let values = process_results( std::iter::once(Ok((end_state_root, end_state.slot()))) - .chain(StateRootsIterator::owned(store, end_state)), + .chain(StateRootsIterator::owned(self, end_state)), |iter| { iter.take_while(|(_, slot)| *slot >= start_slot) .collect::>() @@ -75,51 +63,123 @@ impl Root for StateRoots { Ok(SimpleForwardsIterator { values }) } - fn freezer_upper_limit, Cold: ItemStore>( - store: &HotColdDB, - ) -> Option { - // State roots are stored for all slots up to the latest restore point (exclusive). - // There may not be a latest restore point if state pruning is enabled, in which - // case this function will return `None`. - store.get_latest_restore_point_slot() - } -} - -/// Forwards root iterator that makes use of a flat field table in the freezer DB. -pub struct FrozenForwardsIterator<'a, E: EthSpec, F: Root, Hot: ItemStore, Cold: ItemStore> -{ - inner: ChunkedVectorIter<'a, F, E, Hot, Cold>, -} - -impl<'a, E: EthSpec, F: Root, Hot: ItemStore, Cold: ItemStore> - FrozenForwardsIterator<'a, E, F, Hot, Cold> -{ - pub fn new( - store: &'a HotColdDB, + /// Values in `column` are available in the range `start_slot..upper_bound`. + /// + /// If `None` is returned then no values are available from `start_slot` due to pruning or + /// incomplete backfill. + pub fn freezer_upper_bound_for_column( + &self, + column: DBColumn, start_slot: Slot, - last_restore_point_slot: Slot, - spec: &ChainSpec, - ) -> Self { - Self { - inner: ChunkedVectorIter::new( - store, - start_slot.as_usize(), - last_restore_point_slot, - spec, - ), + ) -> Result> { + if column == DBColumn::BeaconBlockRoots { + Ok(self.freezer_upper_bound_for_block_roots(start_slot)) + } else if column == DBColumn::BeaconStateRoots { + Ok(self.freezer_upper_bound_for_state_roots(start_slot)) + } else { + Err(Error::ForwardsIterInvalidColumn(column)) + } + } + + fn freezer_upper_bound_for_block_roots(&self, start_slot: Slot) -> Option { + let oldest_block_slot = self.get_oldest_block_slot(); + if start_slot < oldest_block_slot { + if start_slot == 0 { + // Slot 0 block root is always available. + Some(Slot::new(1)) + // Non-zero block roots are not available prior to the `oldest_block_slot`. + } else { + None + } + } else { + // Block roots are stored for all slots up to the split slot (exclusive). + Some(self.get_split_slot()) + } + } + + fn freezer_upper_bound_for_state_roots(&self, start_slot: Slot) -> Option { + let split_slot = self.get_split_slot(); + let anchor = self.get_anchor_info(); + + if start_slot >= anchor.state_upper_limit { + // Starting slot is after the upper limit, so the split is the upper limit. + // The split state's root is not available in the freezer so this is exclusive. + Some(split_slot) + } else if start_slot <= anchor.state_lower_limit { + // Starting slot is prior to lower limit, so that's the upper limit. We can't + // iterate past the lower limit into the gap. The +1 accounts for exclusivity. + Some(anchor.state_lower_limit + 1) + } else { + // In the gap, nothing is available. + None } } } -impl<'a, E: EthSpec, F: Root, Hot: ItemStore, Cold: ItemStore> Iterator - for FrozenForwardsIterator<'a, E, F, Hot, Cold> +/// Forwards root iterator that makes use of a slot -> root mapping in the freezer DB. +pub struct FrozenForwardsIterator<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> { + inner: ColumnIter<'a, Vec>, + column: DBColumn, + next_slot: Slot, + end_slot: Slot, + _phantom: PhantomData<(E, Hot, Cold)>, +} + +impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> + FrozenForwardsIterator<'a, E, Hot, Cold> { - type Item = (Hash256, Slot); + /// `end_slot` is EXCLUSIVE here. + pub fn new( + store: &'a HotColdDB, + column: DBColumn, + start_slot: Slot, + end_slot: Slot, + ) -> Result { + if column != DBColumn::BeaconBlockRoots && column != DBColumn::BeaconStateRoots { + return Err(Error::ForwardsIterInvalidColumn(column)); + } + let start = start_slot.as_u64().to_be_bytes(); + Ok(Self { + inner: store.cold_db.iter_column_from(column, &start), + column, + next_slot: start_slot, + end_slot, + _phantom: PhantomData, + }) + } +} + +impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> Iterator + for FrozenForwardsIterator<'a, E, Hot, Cold> +{ + type Item = Result<(Hash256, Slot)>; fn next(&mut self) -> Option { + if self.next_slot == self.end_slot { + return None; + } self.inner - .next() - .map(|(slot, root)| (root, Slot::from(slot))) + .next()? + .and_then(|(slot_bytes, root_bytes)| { + let slot = slot_bytes + .clone() + .try_into() + .map(u64::from_be_bytes) + .map(Slot::new) + .map_err(|_| Error::InvalidBytes)?; + if root_bytes.len() != std::mem::size_of::() { + return Err(Error::InvalidBytes); + } + let root = Hash256::from_slice(&root_bytes); + + if slot != self.next_slot { + return Err(Error::ForwardsIterGap(self.column, slot, self.next_slot)); + } + self.next_slot += 1; + + Ok(Some((root, slot))) + }) + .transpose() } } @@ -139,10 +199,12 @@ impl Iterator for SimpleForwardsIterator { } /// Fusion of the above two approaches to forwards iteration. Fast and efficient. -pub enum HybridForwardsIterator<'a, E: EthSpec, F: Root, Hot: ItemStore, Cold: ItemStore> { +pub enum HybridForwardsIterator<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> { PreFinalization { - iter: Box>, + iter: Box>, + store: &'a HotColdDB, end_slot: Option, + column: DBColumn, /// Data required by the `PostFinalization` iterator when we get to it. continuation_data: Option, Hash256)>>, }, @@ -150,6 +212,7 @@ pub enum HybridForwardsIterator<'a, E: EthSpec, F: Root, Hot: ItemStore, C continuation_data: Option, Hash256)>>, store: &'a HotColdDB, start_slot: Slot, + column: DBColumn, }, PostFinalization { iter: SimpleForwardsIterator, @@ -157,8 +220,8 @@ pub enum HybridForwardsIterator<'a, E: EthSpec, F: Root, Hot: ItemStore, C Finished, } -impl<'a, E: EthSpec, F: Root, Hot: ItemStore, Cold: ItemStore> - HybridForwardsIterator<'a, E, F, Hot, Cold> +impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> + HybridForwardsIterator<'a, E, Hot, Cold> { /// Construct a new hybrid iterator. /// @@ -174,48 +237,54 @@ impl<'a, E: EthSpec, F: Root, Hot: ItemStore, Cold: ItemStore> /// function may block for some time while `get_state` runs. pub fn new( store: &'a HotColdDB, + column: DBColumn, start_slot: Slot, end_slot: Option, get_state: impl FnOnce() -> Result<(BeaconState, Hash256)>, - spec: &ChainSpec, ) -> Result { use HybridForwardsIterator::*; // First slot at which this field is *not* available in the freezer. i.e. all slots less // than this slot have their data available in the freezer. - let freezer_upper_limit = F::freezer_upper_limit(store).unwrap_or(Slot::new(0)); + let opt_freezer_upper_bound = store.freezer_upper_bound_for_column(column, start_slot)?; - let result = if start_slot < freezer_upper_limit { - let iter = Box::new(FrozenForwardsIterator::new( - store, - start_slot, - freezer_upper_limit, - spec, - )); + match opt_freezer_upper_bound { + Some(freezer_upper_bound) if start_slot < freezer_upper_bound => { + // EXCLUSIVE end slot for the frozen portion of the iterator. + let frozen_end_slot = end_slot.map_or(freezer_upper_bound, |end_slot| { + std::cmp::min(end_slot + 1, freezer_upper_bound) + }); + let iter = Box::new(FrozenForwardsIterator::new( + store, + column, + start_slot, + frozen_end_slot, + )?); - // No continuation data is needed if the forwards iterator plans to halt before - // `end_slot`. If it tries to continue further a `NoContinuationData` error will be - // returned. - let continuation_data = - if end_slot.map_or(false, |end_slot| end_slot < freezer_upper_limit) { - None - } else { - Some(Box::new(get_state()?)) - }; - PreFinalization { - iter, - end_slot, - continuation_data, + // No continuation data is needed if the forwards iterator plans to halt before + // `end_slot`. If it tries to continue further a `NoContinuationData` error will be + // returned. + let continuation_data = + if end_slot.map_or(false, |end_slot| end_slot < freezer_upper_bound) { + None + } else { + Some(Box::new(get_state()?)) + }; + Ok(PreFinalization { + iter, + store, + end_slot, + column, + continuation_data, + }) } - } else { - PostFinalizationLazy { + _ => Ok(PostFinalizationLazy { continuation_data: Some(Box::new(get_state()?)), store, start_slot, - } - }; - - Ok(result) + column, + }), + } } fn do_next(&mut self) -> Result> { @@ -225,29 +294,31 @@ impl<'a, E: EthSpec, F: Root, Hot: ItemStore, Cold: ItemStore> PreFinalization { iter, end_slot, + store, continuation_data, + column, } => { match iter.next() { - Some(x) => Ok(Some(x)), + Some(x) => x.map(Some), // Once the pre-finalization iterator is consumed, transition // to a post-finalization iterator beginning from the last slot // of the pre iterator. None => { // If the iterator has an end slot (inclusive) which has already been // covered by the (exclusive) frozen forwards iterator, then we're done! - let iter_end_slot = Slot::from(iter.inner.end_vindex); - if end_slot.map_or(false, |end_slot| iter_end_slot == end_slot + 1) { + if end_slot.map_or(false, |end_slot| iter.end_slot == end_slot + 1) { *self = Finished; return Ok(None); } let continuation_data = continuation_data.take(); - let store = iter.inner.store; - let start_slot = iter_end_slot; + let start_slot = iter.end_slot; + *self = PostFinalizationLazy { continuation_data, store, start_slot, + column: *column, }; self.do_next() @@ -258,11 +329,17 @@ impl<'a, E: EthSpec, F: Root, Hot: ItemStore, Cold: ItemStore> continuation_data, store, start_slot, + column, } => { let (end_state, end_root) = *continuation_data.take().ok_or(Error::NoContinuationData)?; *self = PostFinalization { - iter: F::simple_forwards_iterator(store, *start_slot, end_state, end_root)?, + iter: store.simple_forwards_iterator( + *column, + *start_slot, + end_state, + end_root, + )?, }; self.do_next() } @@ -272,8 +349,8 @@ impl<'a, E: EthSpec, F: Root, Hot: ItemStore, Cold: ItemStore> } } -impl<'a, E: EthSpec, F: Root, Hot: ItemStore, Cold: ItemStore> Iterator - for HybridForwardsIterator<'a, E, F, Hot, Cold> +impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> Iterator + for HybridForwardsIterator<'a, E, Hot, Cold> { type Item = Result<(Hash256, Slot)>; diff --git a/beacon_node/store/src/hdiff.rs b/beacon_node/store/src/hdiff.rs new file mode 100644 index 0000000000..a29e680eb5 --- /dev/null +++ b/beacon_node/store/src/hdiff.rs @@ -0,0 +1,914 @@ +//! Hierarchical diff implementation. +use crate::{metrics, DBColumn, StoreConfig, StoreItem}; +use bls::PublicKeyBytes; +use itertools::Itertools; +use serde::{Deserialize, Serialize}; +use ssz::{Decode, Encode}; +use ssz_derive::{Decode, Encode}; +use std::cmp::Ordering; +use std::io::{Read, Write}; +use std::ops::RangeInclusive; +use std::str::FromStr; +use std::sync::LazyLock; +use superstruct::superstruct; +use types::historical_summary::HistoricalSummary; +use types::{BeaconState, ChainSpec, Epoch, EthSpec, Hash256, List, Slot, Validator}; +use zstd::{Decoder, Encoder}; + +static EMPTY_PUBKEY: LazyLock = LazyLock::new(PublicKeyBytes::empty); + +#[derive(Debug)] +pub enum Error { + InvalidHierarchy, + DiffDeletionsNotSupported, + UnableToComputeDiff, + UnableToApplyDiff, + BalancesIncompleteChunk, + Compression(std::io::Error), + InvalidSszState(ssz::DecodeError), + InvalidBalancesLength, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Encode, Decode)] +pub struct HierarchyConfig { + /// A sequence of powers of two to define how frequently to store each layer of state diffs. + /// The last value always represents the frequency of full state snapshots. Adding more + /// exponents increases the number of diff layers. This value allows to customize the trade-off + /// between reconstruction speed and disk space. + /// + /// Consider an example `exponents value of `[5,13,21]`. This means we have 3 layers: + /// - Full state stored every 2^21 slots (2097152 slots or 291 days) + /// - First diff layer stored every 2^13 slots (8192 slots or 2.3 hours) + /// - Second diff layer stored every 2^5 slots (32 slots or 1 epoch) + /// + /// To reconstruct a state at slot 3,000,003 we load each closest layer + /// - Layer 0: 3000003 - (3000003 mod 2^21) = 2097152 + /// - Layer 1: 3000003 - (3000003 mod 2^13) = 2998272 + /// - Layer 2: 3000003 - (3000003 mod 2^5) = 3000000 + /// + /// Layer 0 is full state snapshot, apply layer 1 diff, then apply layer 2 diff and then replay + /// blocks 3,000,001 to 3,000,003. + pub exponents: Vec, +} + +impl FromStr for HierarchyConfig { + type Err = String; + + fn from_str(s: &str) -> Result { + let exponents = s + .split(',') + .map(|s| { + s.parse() + .map_err(|e| format!("invalid hierarchy-exponents: {e:?}")) + }) + .collect::, _>>()?; + + if exponents.windows(2).any(|w| w[0] >= w[1]) { + return Err("hierarchy-exponents must be in ascending order".to_string()); + } + + Ok(HierarchyConfig { exponents }) + } +} + +impl std::fmt::Display for HierarchyConfig { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", self.exponents.iter().join(",")) + } +} + +#[derive(Debug)] +pub struct HierarchyModuli { + moduli: Vec, +} + +#[derive(Debug, PartialEq, Eq)] +pub enum StorageStrategy { + ReplayFrom(Slot), + DiffFrom(Slot), + Snapshot, +} + +/// Hierarchical diff output and working buffer. +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct HDiffBuffer { + state: Vec, + balances: Vec, + inactivity_scores: Vec, + validators: Vec, + historical_roots: Vec, + historical_summaries: Vec, +} + +/// Hierarchical state diff. +/// +/// Splits the diff into two data sections: +/// +/// - **balances**: The balance of each active validator is almost certain to change every epoch. +/// So this is the field in the state with most entropy. However the balance changes are small. +/// We can optimize the diff significantly by computing the balance difference first and then +/// compressing the result to squash those leading zero bytes. +/// +/// - **everything else**: Instead of trying to apply heuristics and be clever on each field, +/// running a generic binary diff algorithm on the rest of fields yields very good results. With +/// this strategy the HDiff code is easily mantainable across forks, as new fields are covered +/// automatically. xdelta3 algorithm showed diff compute and apply times of ~200 ms on a mainnet +/// state from Apr 2023 (570k indexes), and a 92kB diff size. +#[superstruct( + variants(V0), + variant_attributes(derive(Debug, PartialEq, Encode, Decode)) +)] +#[derive(Debug, PartialEq, Encode, Decode)] +#[ssz(enum_behaviour = "union")] +pub struct HDiff { + state_diff: BytesDiff, + balances_diff: CompressedU64Diff, + /// inactivity_scores are small integers that change slowly epoch to epoch. And are 0 for all + /// participants unless there's non-finality. Computing the diff and compressing the result is + /// much faster than running them through a binary patch algorithm. In the default case where + /// all values are 0 it should also result in a tiny output. + inactivity_scores_diff: CompressedU64Diff, + /// The validators array represents the vast majority of data in a BeaconState. Due to its big + /// size we have seen the performance of xdelta3 degrade. Comparing each entry of the + /// validators array manually significantly speeds up the computation of the diff (+10x faster) + /// and result in the same minimal diff. As the `Validator` record is unlikely to change, + /// maintaining this extra complexity should be okay. + validators_diff: ValidatorsDiff, + /// `historical_roots` is an unbounded forever growing (after Capella it's + /// historical_summaries) list of unique roots. This data is pure entropy so there's no point + /// in compressing it. As it's an append only list, the optimal diff + compression is just the + /// list of new entries. The size of `historical_roots` and `historical_summaries` in + /// non-trivial ~10 MB so throwing it to xdelta3 adds CPU cycles. With a bit of extra complexity + /// we can save those completely. + historical_roots: AppendOnlyDiff, + /// See historical_roots + historical_summaries: AppendOnlyDiff, +} + +#[derive(Debug, PartialEq, Encode, Decode)] +pub struct BytesDiff { + bytes: Vec, +} + +#[derive(Debug, PartialEq, Encode, Decode)] +pub struct CompressedU64Diff { + bytes: Vec, +} + +#[derive(Debug, PartialEq, Encode, Decode)] +pub struct ValidatorsDiff { + bytes: Vec, +} + +#[derive(Debug, PartialEq, Encode, Decode)] +pub struct AppendOnlyDiff { + values: Vec, +} + +impl HDiffBuffer { + pub fn from_state(mut beacon_state: BeaconState) -> Self { + let _t = metrics::start_timer(&metrics::STORE_BEACON_HDIFF_BUFFER_FROM_STATE_TIME); + // Set state.balances to empty list, and then serialize state as ssz + let balances_list = std::mem::take(beacon_state.balances_mut()); + let inactivity_scores = if let Ok(inactivity_scores) = beacon_state.inactivity_scores_mut() + { + std::mem::take(inactivity_scores).to_vec() + } else { + // If this state is pre-altair consider the list empty. If the target state + // is post altair, all its items will show up in the diff as is. + vec![] + }; + let validators = std::mem::take(beacon_state.validators_mut()).to_vec(); + let historical_roots = std::mem::take(beacon_state.historical_roots_mut()).to_vec(); + let historical_summaries = + if let Ok(historical_summaries) = beacon_state.historical_summaries_mut() { + std::mem::take(historical_summaries).to_vec() + } else { + // If this state is pre-capella consider the list empty. The diff will + // include all items in the target state. If both states are + // pre-capella the diff will be empty. + vec![] + }; + + let state = beacon_state.as_ssz_bytes(); + let balances = balances_list.to_vec(); + + HDiffBuffer { + state, + balances, + inactivity_scores, + validators, + historical_roots, + historical_summaries, + } + } + + pub fn as_state(&self, spec: &ChainSpec) -> Result, Error> { + let _t = metrics::start_timer(&metrics::STORE_BEACON_HDIFF_BUFFER_INTO_STATE_TIME); + let mut state = + BeaconState::from_ssz_bytes(&self.state, spec).map_err(Error::InvalidSszState)?; + + *state.balances_mut() = List::try_from_iter(self.balances.iter().copied()) + .map_err(|_| Error::InvalidBalancesLength)?; + + if let Ok(inactivity_scores) = state.inactivity_scores_mut() { + *inactivity_scores = List::try_from_iter(self.inactivity_scores.iter().copied()) + .map_err(|_| Error::InvalidBalancesLength)?; + } + + *state.validators_mut() = List::try_from_iter(self.validators.iter().cloned()) + .map_err(|_| Error::InvalidBalancesLength)?; + + *state.historical_roots_mut() = List::try_from_iter(self.historical_roots.iter().copied()) + .map_err(|_| Error::InvalidBalancesLength)?; + + if let Ok(historical_summaries) = state.historical_summaries_mut() { + *historical_summaries = List::try_from_iter(self.historical_summaries.iter().copied()) + .map_err(|_| Error::InvalidBalancesLength)?; + } + + Ok(state) + } + + /// Byte size of this instance + pub fn size(&self) -> usize { + self.state.len() + + self.balances.len() * std::mem::size_of::() + + self.inactivity_scores.len() * std::mem::size_of::() + + self.validators.len() * std::mem::size_of::() + + self.historical_roots.len() * std::mem::size_of::() + + self.historical_summaries.len() * std::mem::size_of::() + } +} + +impl HDiff { + pub fn compute( + source: &HDiffBuffer, + target: &HDiffBuffer, + config: &StoreConfig, + ) -> Result { + let state_diff = BytesDiff::compute(&source.state, &target.state)?; + let balances_diff = CompressedU64Diff::compute(&source.balances, &target.balances, config)?; + let inactivity_scores_diff = CompressedU64Diff::compute( + &source.inactivity_scores, + &target.inactivity_scores, + config, + )?; + let validators_diff = + ValidatorsDiff::compute(&source.validators, &target.validators, config)?; + let historical_roots = + AppendOnlyDiff::compute(&source.historical_roots, &target.historical_roots)?; + let historical_summaries = + AppendOnlyDiff::compute(&source.historical_summaries, &target.historical_summaries)?; + + Ok(HDiff::V0(HDiffV0 { + state_diff, + balances_diff, + inactivity_scores_diff, + validators_diff, + historical_roots, + historical_summaries, + })) + } + + pub fn apply(&self, source: &mut HDiffBuffer, config: &StoreConfig) -> Result<(), Error> { + let source_state = std::mem::take(&mut source.state); + self.state_diff().apply(&source_state, &mut source.state)?; + self.balances_diff().apply(&mut source.balances, config)?; + self.inactivity_scores_diff() + .apply(&mut source.inactivity_scores, config)?; + self.validators_diff() + .apply(&mut source.validators, config)?; + self.historical_roots().apply(&mut source.historical_roots); + self.historical_summaries() + .apply(&mut source.historical_summaries); + + Ok(()) + } + + /// Byte size of this instance + pub fn size(&self) -> usize { + self.sizes().iter().sum() + } + + pub fn sizes(&self) -> Vec { + vec![ + self.state_diff().size(), + self.balances_diff().size(), + self.inactivity_scores_diff().size(), + self.validators_diff().size(), + self.historical_roots().size(), + self.historical_summaries().size(), + ] + } +} + +impl StoreItem for HDiff { + fn db_column() -> DBColumn { + DBColumn::BeaconStateDiff + } + + fn as_store_bytes(&self) -> Vec { + self.as_ssz_bytes() + } + + fn from_store_bytes(bytes: &[u8]) -> Result { + Ok(Self::from_ssz_bytes(bytes)?) + } +} + +impl BytesDiff { + pub fn compute(source: &[u8], target: &[u8]) -> Result { + Self::compute_xdelta(source, target) + } + + pub fn compute_xdelta(source_bytes: &[u8], target_bytes: &[u8]) -> Result { + let bytes = xdelta3::encode(target_bytes, source_bytes) + .ok_or(Error::UnableToComputeDiff) + .unwrap(); + Ok(Self { bytes }) + } + + pub fn apply(&self, source: &[u8], target: &mut Vec) -> Result<(), Error> { + self.apply_xdelta(source, target) + } + + pub fn apply_xdelta(&self, source: &[u8], target: &mut Vec) -> Result<(), Error> { + *target = xdelta3::decode(&self.bytes, source).ok_or(Error::UnableToApplyDiff)?; + Ok(()) + } + + /// Byte size of this instance + pub fn size(&self) -> usize { + self.bytes.len() + } +} + +impl CompressedU64Diff { + pub fn compute(xs: &[u64], ys: &[u64], config: &StoreConfig) -> Result { + if xs.len() > ys.len() { + return Err(Error::DiffDeletionsNotSupported); + } + + let uncompressed_bytes: Vec = ys + .iter() + .enumerate() + .flat_map(|(i, y)| { + // Diff from 0 if the entry is new. + let x = xs.get(i).copied().unwrap_or(0); + y.wrapping_sub(x).to_be_bytes() + }) + .collect(); + + Ok(CompressedU64Diff { + bytes: compress_bytes(&uncompressed_bytes, config)?, + }) + } + + pub fn apply(&self, xs: &mut Vec, config: &StoreConfig) -> Result<(), Error> { + // Decompress balances diff. + let balances_diff_bytes = uncompress_bytes(&self.bytes, config)?; + + for (i, diff_bytes) in balances_diff_bytes + .chunks(u64::BITS as usize / 8) + .enumerate() + { + let diff = diff_bytes + .try_into() + .map(u64::from_be_bytes) + .map_err(|_| Error::BalancesIncompleteChunk)?; + + if let Some(x) = xs.get_mut(i) { + *x = x.wrapping_add(diff); + } else { + xs.push(diff); + } + } + + Ok(()) + } + + /// Byte size of this instance + pub fn size(&self) -> usize { + self.bytes.len() + } +} + +fn compress_bytes(input: &[u8], config: &StoreConfig) -> Result, Error> { + let compression_level = config.compression_level; + let mut out = Vec::with_capacity(config.estimate_compressed_size(input.len())); + let mut encoder = Encoder::new(&mut out, compression_level).map_err(Error::Compression)?; + encoder.write_all(input).map_err(Error::Compression)?; + encoder.finish().map_err(Error::Compression)?; + Ok(out) +} + +fn uncompress_bytes(input: &[u8], config: &StoreConfig) -> Result, Error> { + let mut out = Vec::with_capacity(config.estimate_decompressed_size(input.len())); + let mut decoder = Decoder::new(input).map_err(Error::Compression)?; + decoder.read_to_end(&mut out).map_err(Error::Compression)?; + Ok(out) +} + +impl ValidatorsDiff { + pub fn compute( + xs: &[Validator], + ys: &[Validator], + config: &StoreConfig, + ) -> Result { + if xs.len() > ys.len() { + return Err(Error::DiffDeletionsNotSupported); + } + + let uncompressed_bytes = ys + .iter() + .enumerate() + .filter_map(|(i, y)| { + let validator_diff = if let Some(x) = xs.get(i) { + if y == x { + return None; + } else { + let pubkey_changed = y.pubkey != x.pubkey; + // Note: If researchers attempt to change the Validator container, go quickly to + // All Core Devs and push hard to add another List in the BeaconState instead. + Validator { + // The pubkey can be changed on index re-use + pubkey: if pubkey_changed { + y.pubkey + } else { + PublicKeyBytes::empty() + }, + // withdrawal_credentials can be set to zero initially but can never be + // changed INTO zero. On index re-use it can be set to zero, but in that + // case the pubkey will also change. + withdrawal_credentials: if pubkey_changed + || y.withdrawal_credentials != x.withdrawal_credentials + { + y.withdrawal_credentials + } else { + Hash256::ZERO + }, + // effective_balance can increase and decrease + effective_balance: y.effective_balance - x.effective_balance, + // slashed can only change from false into true. In an index re-use it can + // switch back to false, but in that case the pubkey will also change. + slashed: y.slashed, + // activation_eligibility_epoch can never be zero under any case. It's + // set to either FAR_FUTURE_EPOCH or get_current_epoch(state) + 1 + activation_eligibility_epoch: if y.activation_eligibility_epoch + != x.activation_eligibility_epoch + { + y.activation_eligibility_epoch + } else { + Epoch::new(0) + }, + // activation_epoch can never be zero under any case. It's + // set to either FAR_FUTURE_EPOCH or epoch + 1 + MAX_SEED_LOOKAHEAD + activation_epoch: if y.activation_epoch != x.activation_epoch { + y.activation_epoch + } else { + Epoch::new(0) + }, + // exit_epoch can never be zero under any case. It's set to either + // FAR_FUTURE_EPOCH or > epoch + 1 + MAX_SEED_LOOKAHEAD + exit_epoch: if y.exit_epoch != x.exit_epoch { + y.exit_epoch + } else { + Epoch::new(0) + }, + // withdrawable_epoch can never be zero under any case. It's set to + // either FAR_FUTURE_EPOCH or > epoch + 1 + MAX_SEED_LOOKAHEAD + withdrawable_epoch: if y.withdrawable_epoch != x.withdrawable_epoch { + y.withdrawable_epoch + } else { + Epoch::new(0) + }, + } + } + } else { + y.clone() + }; + + Some(ValidatorDiffEntry { + index: i as u64, + validator_diff, + }) + }) + .flat_map(|v_diff| v_diff.as_ssz_bytes()) + .collect::>(); + + Ok(Self { + bytes: compress_bytes(&uncompressed_bytes, config)?, + }) + } + + pub fn apply(&self, xs: &mut Vec, config: &StoreConfig) -> Result<(), Error> { + let validator_diff_bytes = uncompress_bytes(&self.bytes, config)?; + + for diff_bytes in + validator_diff_bytes.chunks(::ssz_fixed_len()) + { + let ValidatorDiffEntry { + index, + validator_diff: diff, + } = ValidatorDiffEntry::from_ssz_bytes(diff_bytes) + .map_err(|_| Error::BalancesIncompleteChunk)?; + + if let Some(x) = xs.get_mut(index as usize) { + // Note: a pubkey change implies index re-use. In that case over-write + // withdrawal_credentials and slashed inconditionally as their default values + // are valid values. + let pubkey_changed = diff.pubkey != *EMPTY_PUBKEY; + if pubkey_changed { + x.pubkey = diff.pubkey; + } + if pubkey_changed || diff.withdrawal_credentials != Hash256::ZERO { + x.withdrawal_credentials = diff.withdrawal_credentials; + } + if diff.effective_balance != 0 { + x.effective_balance = x.effective_balance.wrapping_add(diff.effective_balance); + } + if pubkey_changed || diff.slashed { + x.slashed = diff.slashed; + } + if diff.activation_eligibility_epoch != Epoch::new(0) { + x.activation_eligibility_epoch = diff.activation_eligibility_epoch; + } + if diff.activation_epoch != Epoch::new(0) { + x.activation_epoch = diff.activation_epoch; + } + if diff.exit_epoch != Epoch::new(0) { + x.exit_epoch = diff.exit_epoch; + } + if diff.withdrawable_epoch != Epoch::new(0) { + x.withdrawable_epoch = diff.withdrawable_epoch; + } + } else { + xs.push(diff) + } + } + + Ok(()) + } + + /// Byte size of this instance + pub fn size(&self) -> usize { + self.bytes.len() + } +} + +#[derive(Debug, Encode, Decode)] +struct ValidatorDiffEntry { + index: u64, + validator_diff: Validator, +} + +impl AppendOnlyDiff { + pub fn compute(xs: &[T], ys: &[T]) -> Result { + match xs.len().cmp(&ys.len()) { + Ordering::Less => Ok(Self { + values: ys.iter().skip(xs.len()).copied().collect(), + }), + // Don't even create an iterator for this common case + Ordering::Equal => Ok(Self { values: vec![] }), + Ordering::Greater => Err(Error::DiffDeletionsNotSupported), + } + } + + pub fn apply(&self, xs: &mut Vec) { + xs.extend(self.values.iter().copied()); + } + + /// Byte size of this instance + pub fn size(&self) -> usize { + self.values.len() * size_of::() + } +} + +impl Default for HierarchyConfig { + fn default() -> Self { + HierarchyConfig { + exponents: vec![5, 9, 11, 13, 16, 18, 21], + } + } +} + +impl HierarchyConfig { + pub fn to_moduli(&self) -> Result { + self.validate()?; + let moduli = self.exponents.iter().map(|n| 1 << n).collect(); + Ok(HierarchyModuli { moduli }) + } + + pub fn validate(&self) -> Result<(), Error> { + if !self.exponents.is_empty() + && self + .exponents + .iter() + .tuple_windows() + .all(|(small, big)| small < big && *big < u64::BITS as u8) + { + Ok(()) + } else { + Err(Error::InvalidHierarchy) + } + } +} + +impl HierarchyModuli { + pub fn storage_strategy(&self, slot: Slot) -> Result { + // last = full snapshot interval + let last = self.moduli.last().copied().ok_or(Error::InvalidHierarchy)?; + // first = most frequent diff layer, need to replay blocks from this layer + let first = self + .moduli + .first() + .copied() + .ok_or(Error::InvalidHierarchy)?; + + if slot % last == 0 { + return Ok(StorageStrategy::Snapshot); + } + + Ok(self + .moduli + .iter() + .rev() + .tuple_windows() + .find_map(|(&n_big, &n_small)| { + if slot % n_small == 0 { + // Diff from the previous layer. + Some(StorageStrategy::DiffFrom(slot / n_big * n_big)) + } else { + // Keep trying with next layer + None + } + }) + // Exhausted layers, need to replay from most frequent layer + .unwrap_or(StorageStrategy::ReplayFrom(slot / first * first))) + } + + /// Return the smallest slot greater than or equal to `slot` at which a full snapshot should + /// be stored. + pub fn next_snapshot_slot(&self, slot: Slot) -> Result { + let last = self.moduli.last().copied().ok_or(Error::InvalidHierarchy)?; + if slot % last == 0 { + Ok(slot) + } else { + Ok((slot / last + 1) * last) + } + } + + /// Return `true` if the database ops for this slot should be committed immediately. + /// + /// This is the case for all diffs aside from the ones in the leaf layer. To store a diff + /// might require loading the state at the previous layer, in which case the diff for that + /// layer must already have been stored. + /// + /// In future we may be able to handle this differently (with proper transaction semantics + /// rather than LevelDB's "write batches"). + pub fn should_commit_immediately(&self, slot: Slot) -> Result { + // If there's only 1 layer of snapshots, then commit only when writing a snapshot. + self.moduli.get(1).map_or_else( + || Ok(slot == self.next_snapshot_slot(slot)?), + |second_layer_moduli| Ok(slot % *second_layer_moduli == 0), + ) + } +} + +impl StorageStrategy { + /// For the state stored with this `StorageStrategy` at `slot`, return the range of slots which + /// should be checked for ancestor states in the historic state cache. + /// + /// The idea is that for states which need to be built by replaying blocks we should scan + /// for any viable ancestor state between their `from` slot and `slot`. If we find such a + /// state it will save us from the slow reconstruction of the `from` state using diffs. + /// + /// Similarly for `DiffFrom` and `Snapshot` states, loading the prior state and replaying 1 + /// block is often going to be faster than loading and applying diffs/snapshots, so we may as + /// well check the cache for that 1 slot prior (in case the caller is iterating sequentially). + pub fn replay_from_range( + &self, + slot: Slot, + ) -> std::iter::Map, fn(u64) -> Slot> { + match self { + Self::ReplayFrom(from) => from.as_u64()..=slot.as_u64(), + Self::Snapshot | Self::DiffFrom(_) => { + if slot > 0 { + (slot - 1).as_u64()..=slot.as_u64() + } else { + slot.as_u64()..=slot.as_u64() + } + } + } + .map(Slot::from) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rand::{rngs::SmallRng, thread_rng, Rng, SeedableRng}; + + #[test] + fn default_storage_strategy() { + let config = HierarchyConfig::default(); + config.validate().unwrap(); + + let moduli = config.to_moduli().unwrap(); + + // Full snapshots at multiples of 2^21. + let snapshot_freq = Slot::new(1 << 21); + assert_eq!( + moduli.storage_strategy(Slot::new(0)).unwrap(), + StorageStrategy::Snapshot + ); + assert_eq!( + moduli.storage_strategy(snapshot_freq).unwrap(), + StorageStrategy::Snapshot + ); + assert_eq!( + moduli.storage_strategy(snapshot_freq * 3).unwrap(), + StorageStrategy::Snapshot + ); + + // Diffs should be from the previous layer (the snapshot in this case), and not the previous diff in the same layer. + let first_layer = Slot::new(1 << 18); + assert_eq!( + moduli.storage_strategy(first_layer * 2).unwrap(), + StorageStrategy::DiffFrom(Slot::new(0)) + ); + + let replay_strategy_slot = first_layer + 1; + assert_eq!( + moduli.storage_strategy(replay_strategy_slot).unwrap(), + StorageStrategy::ReplayFrom(first_layer) + ); + } + + #[test] + fn next_snapshot_slot() { + let config = HierarchyConfig::default(); + config.validate().unwrap(); + + let moduli = config.to_moduli().unwrap(); + let snapshot_freq = Slot::new(1 << 21); + + assert_eq!( + moduli.next_snapshot_slot(snapshot_freq).unwrap(), + snapshot_freq + ); + assert_eq!( + moduli.next_snapshot_slot(snapshot_freq + 1).unwrap(), + snapshot_freq * 2 + ); + assert_eq!( + moduli.next_snapshot_slot(snapshot_freq * 2 - 1).unwrap(), + snapshot_freq * 2 + ); + assert_eq!( + moduli.next_snapshot_slot(snapshot_freq * 2).unwrap(), + snapshot_freq * 2 + ); + assert_eq!( + moduli.next_snapshot_slot(snapshot_freq * 100).unwrap(), + snapshot_freq * 100 + ); + } + + #[test] + fn compressed_u64_vs_bytes_diff() { + let x_values = vec![99u64, 55, 123, 6834857, 0, 12]; + let y_values = vec![98u64, 55, 312, 1, 1, 2, 4, 5]; + let config = &StoreConfig::default(); + + let to_bytes = + |nums: &[u64]| -> Vec { nums.iter().flat_map(|x| x.to_be_bytes()).collect() }; + + let x_bytes = to_bytes(&x_values); + let y_bytes = to_bytes(&y_values); + + let u64_diff = CompressedU64Diff::compute(&x_values, &y_values, config).unwrap(); + + let mut y_from_u64_diff = x_values; + u64_diff.apply(&mut y_from_u64_diff, config).unwrap(); + + assert_eq!(y_values, y_from_u64_diff); + + let bytes_diff = BytesDiff::compute(&x_bytes, &y_bytes).unwrap(); + + let mut y_from_bytes = vec![]; + bytes_diff.apply(&x_bytes, &mut y_from_bytes).unwrap(); + + assert_eq!(y_bytes, y_from_bytes); + + // U64 diff wins by more than a factor of 3 + assert!(u64_diff.bytes.len() < 3 * bytes_diff.bytes.len()); + } + + #[test] + fn compressed_validators_diff() { + assert_eq!(::ssz_fixed_len(), 129); + + let mut rng = thread_rng(); + let config = &StoreConfig::default(); + let xs = (0..10) + .map(|_| rand_validator(&mut rng)) + .collect::>(); + let mut ys = xs.clone(); + ys[5] = rand_validator(&mut rng); + ys.push(rand_validator(&mut rng)); + let diff = ValidatorsDiff::compute(&xs, &ys, config).unwrap(); + + let mut xs_out = xs.clone(); + diff.apply(&mut xs_out, config).unwrap(); + assert_eq!(xs_out, ys); + } + + fn rand_validator(mut rng: impl Rng) -> Validator { + let mut pubkey = [0u8; 48]; + rng.fill_bytes(&mut pubkey); + let withdrawal_credentials: [u8; 32] = rng.gen(); + + Validator { + pubkey: PublicKeyBytes::from_ssz_bytes(&pubkey).unwrap(), + withdrawal_credentials: withdrawal_credentials.into(), + slashed: false, + effective_balance: 32_000_000_000, + activation_eligibility_epoch: Epoch::max_value(), + activation_epoch: Epoch::max_value(), + exit_epoch: Epoch::max_value(), + withdrawable_epoch: Epoch::max_value(), + } + } + + // This test checks that the hdiff algorithm doesn't accidentally change between releases. + // If it does, we need to ensure appropriate backwards compatibility measures are implemented + // before this test is updated. + #[test] + fn hdiff_version_stability() { + let mut rng = SmallRng::seed_from_u64(0xffeeccdd00aa); + + let pre_balances = vec![32_000_000_000, 16_000_000_000, 0]; + let post_balances = vec![31_000_000_000, 17_000_000, 0, 0]; + + let pre_inactivity_scores = vec![1, 1, 1]; + let post_inactivity_scores = vec![0, 0, 0, 1]; + + let pre_validators = (0..3).map(|_| rand_validator(&mut rng)).collect::>(); + let post_validators = pre_validators.clone(); + + let pre_historical_roots = vec![Hash256::repeat_byte(0xff)]; + let post_historical_roots = vec![Hash256::repeat_byte(0xff), Hash256::repeat_byte(0xee)]; + + let pre_historical_summaries = vec![HistoricalSummary::default()]; + let post_historical_summaries = pre_historical_summaries.clone(); + + let pre_buffer = HDiffBuffer { + state: vec![0, 1, 2, 3, 3, 2, 1, 0], + balances: pre_balances, + inactivity_scores: pre_inactivity_scores, + validators: pre_validators, + historical_roots: pre_historical_roots, + historical_summaries: pre_historical_summaries, + }; + let post_buffer = HDiffBuffer { + state: vec![0, 1, 3, 2, 2, 3, 1, 1], + balances: post_balances, + inactivity_scores: post_inactivity_scores, + validators: post_validators, + historical_roots: post_historical_roots, + historical_summaries: post_historical_summaries, + }; + + let config = StoreConfig::default(); + let hdiff = HDiff::compute(&pre_buffer, &post_buffer, &config).unwrap(); + let hdiff_ssz = hdiff.as_ssz_bytes(); + + // First byte should match enum version. + assert_eq!(hdiff_ssz[0], 0); + + // Should roundtrip. + assert_eq!(HDiff::from_ssz_bytes(&hdiff_ssz).unwrap(), hdiff); + + // Should roundtrip as V0 with enum selector stripped. + assert_eq!( + HDiff::V0(HDiffV0::from_ssz_bytes(&hdiff_ssz[1..]).unwrap()), + hdiff + ); + + assert_eq!( + hdiff_ssz, + vec![ + 0u8, 24, 0, 0, 0, 49, 0, 0, 0, 85, 0, 0, 0, 114, 0, 0, 0, 127, 0, 0, 0, 163, 0, 0, + 0, 4, 0, 0, 0, 214, 195, 196, 0, 0, 0, 14, 8, 0, 8, 1, 0, 0, 1, 3, 2, 2, 3, 1, 1, + 9, 4, 0, 0, 0, 40, 181, 47, 253, 0, 72, 189, 0, 0, 136, 255, 255, 255, 255, 196, + 101, 54, 0, 255, 255, 255, 252, 71, 86, 198, 64, 0, 1, 0, 59, 176, 4, 4, 0, 0, 0, + 40, 181, 47, 253, 0, 72, 133, 0, 0, 80, 255, 255, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 10, + 192, 2, 4, 0, 0, 0, 40, 181, 47, 253, 32, 0, 1, 0, 0, 4, 0, 0, 0, 238, 238, 238, + 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, + 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 4, 0, 0, 0 + ] + ); + } +} diff --git a/beacon_node/store/src/historic_state_cache.rs b/beacon_node/store/src/historic_state_cache.rs new file mode 100644 index 0000000000..c0e8f8346c --- /dev/null +++ b/beacon_node/store/src/historic_state_cache.rs @@ -0,0 +1,92 @@ +use crate::hdiff::{Error, HDiffBuffer}; +use crate::metrics; +use lru::LruCache; +use std::num::NonZeroUsize; +use types::{BeaconState, ChainSpec, EthSpec, Slot}; + +/// Holds a combination of finalized states in two formats: +/// - `hdiff_buffers`: Format close to an SSZ serialized state for rapid application of diffs on top +/// of it +/// - `states`: Deserialized states for direct use or for rapid application of blocks (replay) +/// +/// An example use: when requesting state data for consecutive slots, this cache allows the node to +/// apply diffs once on the first request, and latter just apply blocks one at a time. +#[derive(Debug)] +pub struct HistoricStateCache { + hdiff_buffers: LruCache, + states: LruCache>, +} + +#[derive(Debug, Default)] +pub struct Metrics { + pub num_hdiff: usize, + pub num_state: usize, + pub hdiff_byte_size: usize, +} + +impl HistoricStateCache { + pub fn new(hdiff_buffer_cache_size: NonZeroUsize, state_cache_size: NonZeroUsize) -> Self { + Self { + hdiff_buffers: LruCache::new(hdiff_buffer_cache_size), + states: LruCache::new(state_cache_size), + } + } + + pub fn get_hdiff_buffer(&mut self, slot: Slot) -> Option { + if let Some(buffer_ref) = self.hdiff_buffers.get(&slot) { + let _timer = metrics::start_timer(&metrics::BEACON_HDIFF_BUFFER_CLONE_TIMES); + Some(buffer_ref.clone()) + } else if let Some(state) = self.states.get(&slot) { + let buffer = HDiffBuffer::from_state(state.clone()); + let _timer = metrics::start_timer(&metrics::BEACON_HDIFF_BUFFER_CLONE_TIMES); + let cloned = buffer.clone(); + drop(_timer); + self.hdiff_buffers.put(slot, cloned); + Some(buffer) + } else { + None + } + } + + pub fn get_state( + &mut self, + slot: Slot, + spec: &ChainSpec, + ) -> Result>, Error> { + if let Some(state) = self.states.get(&slot) { + Ok(Some(state.clone())) + } else if let Some(buffer) = self.hdiff_buffers.get(&slot) { + let state = buffer.as_state(spec)?; + self.states.put(slot, state.clone()); + Ok(Some(state)) + } else { + Ok(None) + } + } + + pub fn put_state(&mut self, slot: Slot, state: BeaconState) { + self.states.put(slot, state); + } + + pub fn put_hdiff_buffer(&mut self, slot: Slot, buffer: HDiffBuffer) { + self.hdiff_buffers.put(slot, buffer); + } + + pub fn put_both(&mut self, slot: Slot, state: BeaconState, buffer: HDiffBuffer) { + self.put_state(slot, state); + self.put_hdiff_buffer(slot, buffer); + } + + pub fn metrics(&self) -> Metrics { + let hdiff_byte_size = self + .hdiff_buffers + .iter() + .map(|(_, buffer)| buffer.size()) + .sum::(); + Metrics { + num_hdiff: self.hdiff_buffers.len(), + num_state: self.states.len(), + hdiff_byte_size, + } + } +} diff --git a/beacon_node/store/src/hot_cold_store.rs b/beacon_node/store/src/hot_cold_store.rs index 5483c490dc..4942b14881 100644 --- a/beacon_node/store/src/hot_cold_store.rs +++ b/beacon_node/store/src/hot_cold_store.rs @@ -1,29 +1,24 @@ -use crate::chunked_vector::{ - store_updated_vector, BlockRoots, HistoricalRoots, HistoricalSummaries, RandaoMixes, StateRoots, -}; -use crate::config::{ - OnDiskStoreConfig, StoreConfig, DEFAULT_SLOTS_PER_RESTORE_POINT, - PREV_DEFAULT_SLOTS_PER_RESTORE_POINT, -}; +use crate::config::{OnDiskStoreConfig, StoreConfig}; use crate::forwards_iter::{HybridForwardsBlockRootsIterator, HybridForwardsStateRootsIterator}; +use crate::hdiff::{HDiff, HDiffBuffer, HierarchyModuli, StorageStrategy}; +use crate::historic_state_cache::HistoricStateCache; use crate::impls::beacon_state::{get_full_state, store_full_state}; use crate::iter::{BlockRootsIterator, ParentRootBlockIterator, RootsIterator}; -use crate::leveldb_store::BytesKey; -use crate::leveldb_store::LevelDB; +use crate::leveldb_store::{BytesKey, LevelDB}; use crate::memory_store::MemoryStore; use crate::metadata::{ 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, + ANCHOR_FOR_ARCHIVE_NODE, ANCHOR_INFO_KEY, ANCHOR_UNINITIALIZED, 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::state_cache::{PutStateOutcome, StateCache}; use crate::{ - get_data_column_key, get_key_for_col, ChunkWriter, DBColumn, DatabaseBlock, Error, ItemStore, - KeyValueStoreOp, PartialBeaconState, StoreItem, StoreOp, + get_data_column_key, get_key_for_col, DBColumn, DatabaseBlock, Error, ItemStore, + KeyValueStoreOp, StoreItem, StoreOp, }; use crate::{metrics, parse_data_column_key}; -use itertools::process_results; +use itertools::{process_results, Itertools}; use leveldb::iterator::LevelDBIterator; use lru::LruCache; use parking_lot::{Mutex, RwLock}; @@ -38,6 +33,7 @@ use state_processing::{ }; use std::cmp::min; use std::collections::{HashMap, HashSet}; +use std::io::{Read, Write}; use std::marker::PhantomData; use std::num::NonZeroUsize; use std::path::Path; @@ -45,6 +41,7 @@ use std::sync::Arc; use std::time::Duration; use types::data_column_sidecar::{ColumnIndex, DataColumnSidecar, DataColumnSidecarList}; use types::*; +use zstd::{Decoder, Encoder}; /// On-disk database that stores finalized states efficiently. /// @@ -58,12 +55,13 @@ pub struct HotColdDB, Cold: ItemStore> { /// greater than or equal are in the hot DB. pub(crate) split: RwLock, /// The starting slots for the range of blocks & states stored in the database. - anchor_info: RwLock>, + 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, + pub(crate) hierarchy: HierarchyModuli, /// Cold database containing compact historical data. pub cold_db: Cold, /// Database containing blobs. If None, store falls back to use `cold_db`. @@ -78,8 +76,11 @@ pub struct HotColdDB, Cold: ItemStore> { /// /// LOCK ORDERING: this lock must always be locked *after* the `split` if both are required. state_cache: Mutex>, - /// LRU cache of replayed states. - historic_state_cache: Mutex>>, + /// Cache of historic states and hierarchical diff buffers. + /// + /// This cache is never pruned. It is only populated in response to historical queries from the + /// HTTP API. + historic_state_cache: Mutex>, /// Chain spec. pub(crate) spec: Arc, /// Logger. @@ -155,22 +156,27 @@ pub enum HotColdDBError { proposed_split_slot: Slot, }, MissingStateToFreeze(Hash256), - MissingRestorePointHash(u64), + MissingRestorePointState(Slot), MissingRestorePoint(Hash256), MissingColdStateSummary(Hash256), MissingHotStateSummary(Hash256), MissingEpochBoundaryState(Hash256), + MissingPrevState(Hash256), MissingSplitState(Hash256, Slot), + MissingStateDiff(Hash256), + MissingHDiff(Slot), MissingExecutionPayload(Hash256), MissingFullBlockExecutionPayloadPruned(Hash256, Slot), MissingAnchorInfo, + MissingFrozenBlockSlot(Hash256), + MissingFrozenBlock(Slot), + MissingPathToBlobsDatabase, BlobsPreviouslyInDefaultStore, HotStateSummaryError(BeaconStateError), RestorePointDecodeError(ssz::DecodeError), BlockReplayBeaconError(BeaconStateError), BlockReplaySlotError(SlotProcessingError), BlockReplayBlockError(BlockProcessingError), - MissingLowerLimitState(Slot), InvalidSlotsPerRestorePoint { slots_per_restore_point: u64, slots_per_historical_root: u64, @@ -196,11 +202,13 @@ impl HotColdDB, MemoryStore> { spec: Arc, log: Logger, ) -> Result, MemoryStore>, Error> { - Self::verify_config(&config)?; + config.verify::()?; + + let hierarchy = config.hierarchy_config.to_moduli()?; let db = HotColdDB { split: RwLock::new(Split::default()), - anchor_info: RwLock::new(None), + anchor_info: RwLock::new(ANCHOR_UNINITIALIZED), blob_info: RwLock::new(BlobInfo::default()), data_column_info: RwLock::new(DataColumnInfo::default()), cold_db: MemoryStore::open(), @@ -208,8 +216,12 @@ impl HotColdDB, MemoryStore> { hot_db: MemoryStore::open(), block_cache: Mutex::new(BlockCache::new(config.block_cache_size)), state_cache: Mutex::new(StateCache::new(config.state_cache_size)), - historic_state_cache: Mutex::new(LruCache::new(config.historic_state_cache_size)), + historic_state_cache: Mutex::new(HistoricStateCache::new( + config.hdiff_buffer_cache_size, + config.historic_state_cache_size, + )), config, + hierarchy, spec, log, _phantom: PhantomData, @@ -233,51 +245,43 @@ impl HotColdDB, LevelDB> { spec: Arc, log: Logger, ) -> Result, Error> { - Self::verify_slots_per_restore_point(config.slots_per_restore_point)?; + config.verify::()?; - let mut db = HotColdDB { + let hierarchy = config.hierarchy_config.to_moduli()?; + + let hot_db = LevelDB::open(hot_path)?; + let anchor_info = RwLock::new(Self::load_anchor_info(&hot_db)?); + + let db = HotColdDB { split: RwLock::new(Split::default()), - anchor_info: RwLock::new(None), + anchor_info, 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)?, + hot_db, block_cache: Mutex::new(BlockCache::new(config.block_cache_size)), state_cache: Mutex::new(StateCache::new(config.state_cache_size)), - historic_state_cache: Mutex::new(LruCache::new(config.historic_state_cache_size)), + historic_state_cache: Mutex::new(HistoricStateCache::new( + config.hdiff_buffer_cache_size, + config.historic_state_cache_size, + )), config, + hierarchy, spec, log, _phantom: PhantomData, }; - // Allow the slots-per-restore-point value to stay at the previous default if the config - // uses the new default. Don't error on a failed read because the config itself may need - // migrating. - if let Ok(Some(disk_config)) = db.load_config() { - if !db.config.slots_per_restore_point_set_explicitly - && disk_config.slots_per_restore_point == PREV_DEFAULT_SLOTS_PER_RESTORE_POINT - && db.config.slots_per_restore_point == DEFAULT_SLOTS_PER_RESTORE_POINT - { - debug!( - db.log, - "Ignoring slots-per-restore-point config in favour of on-disk value"; - "config" => db.config.slots_per_restore_point, - "on_disk" => disk_config.slots_per_restore_point, - ); - - // Mutate the in-memory config so that it's compatible. - db.config.slots_per_restore_point = PREV_DEFAULT_SLOTS_PER_RESTORE_POINT; - } - } + // Load the config from disk but don't error on a failed read because the config itself may + // need migrating. + let _ = db.load_config(); // Load the previous split slot from the database (if any). This ensures we can // stop and restart correctly. This needs to occur *before* running any migrations // because some migrations load states and depend on the split. if let Some(split) = db.load_split()? { *db.split.write() = split; - *db.anchor_info.write() = db.load_anchor_info()?; info!( db.log, @@ -370,7 +374,22 @@ impl HotColdDB, LevelDB> { // Ensure that any on-disk config is compatible with the supplied config. if let Some(disk_config) = db.load_config()? { - db.config.check_compatibility(&disk_config)?; + let split = db.get_split_info(); + let anchor = db.get_anchor_info(); + db.config + .check_compatibility(&disk_config, &split, &anchor)?; + + // Inform user if hierarchy config is changing. + if let Ok(hierarchy_config) = disk_config.hierarchy_config() { + if &db.config.hierarchy_config != hierarchy_config { + info!( + db.log, + "Updating historic state config"; + "previous_config" => %hierarchy_config, + "new_config" => %db.config.hierarchy_config, + ); + } + } } db.store_config()?; @@ -425,6 +444,49 @@ impl, Cold: ItemStore> HotColdDB self.state_cache.lock().len() } + pub fn register_metrics(&self) { + let hsc_metrics = self.historic_state_cache.lock().metrics(); + + metrics::set_gauge( + &metrics::STORE_BEACON_BLOCK_CACHE_SIZE, + self.block_cache.lock().block_cache.len() as i64, + ); + metrics::set_gauge( + &metrics::STORE_BEACON_BLOB_CACHE_SIZE, + self.block_cache.lock().blob_cache.len() as i64, + ); + metrics::set_gauge( + &metrics::STORE_BEACON_STATE_CACHE_SIZE, + self.state_cache.lock().len() as i64, + ); + metrics::set_gauge( + &metrics::STORE_BEACON_HISTORIC_STATE_CACHE_SIZE, + hsc_metrics.num_state as i64, + ); + metrics::set_gauge( + &metrics::STORE_BEACON_HDIFF_BUFFER_CACHE_SIZE, + hsc_metrics.num_hdiff as i64, + ); + metrics::set_gauge( + &metrics::STORE_BEACON_HDIFF_BUFFER_CACHE_BYTE_SIZE, + hsc_metrics.hdiff_byte_size as i64, + ); + + let anchor_info = self.get_anchor_info(); + metrics::set_gauge( + &metrics::STORE_BEACON_ANCHOR_SLOT, + anchor_info.anchor_slot.as_u64() as i64, + ); + metrics::set_gauge( + &metrics::STORE_BEACON_OLDEST_BLOCK_SLOT, + anchor_info.oldest_block_slot.as_u64() as i64, + ); + metrics::set_gauge( + &metrics::STORE_BEACON_STATE_LOWER_LIMIT, + anchor_info.state_lower_limit.as_u64() as i64, + ); + } + /// Store a block and update the LRU cache. pub fn put_block( &self, @@ -1002,14 +1064,13 @@ impl, Cold: ItemStore> HotColdDB start_slot: Slot, end_state: BeaconState, end_block_root: Hash256, - spec: &ChainSpec, ) -> Result> + '_, Error> { HybridForwardsBlockRootsIterator::new( self, + DBColumn::BeaconBlockRoots, start_slot, None, || Ok((end_state, end_block_root)), - spec, ) } @@ -1018,9 +1079,14 @@ impl, Cold: ItemStore> HotColdDB start_slot: Slot, end_slot: Slot, get_state: impl FnOnce() -> Result<(BeaconState, Hash256), Error>, - spec: &ChainSpec, ) -> Result, Error> { - HybridForwardsBlockRootsIterator::new(self, start_slot, Some(end_slot), get_state, spec) + HybridForwardsBlockRootsIterator::new( + self, + DBColumn::BeaconBlockRoots, + start_slot, + Some(end_slot), + get_state, + ) } pub fn forwards_state_roots_iterator( @@ -1028,14 +1094,13 @@ impl, Cold: ItemStore> HotColdDB start_slot: Slot, end_state_root: Hash256, end_state: BeaconState, - spec: &ChainSpec, ) -> Result> + '_, Error> { HybridForwardsStateRootsIterator::new( self, + DBColumn::BeaconStateRoots, start_slot, None, || Ok((end_state, end_state_root)), - spec, ) } @@ -1044,9 +1109,14 @@ impl, Cold: ItemStore> HotColdDB start_slot: Slot, end_slot: Slot, get_state: impl FnOnce() -> Result<(BeaconState, Hash256), Error>, - spec: &ChainSpec, ) -> Result, Error> { - HybridForwardsStateRootsIterator::new(self, start_slot, Some(end_slot), get_state, spec) + HybridForwardsStateRootsIterator::new( + self, + DBColumn::BeaconStateRoots, + start_slot, + Some(end_slot), + get_state, + ) } /// Load an epoch boundary state by using the hot state summary look-up. @@ -1072,7 +1142,7 @@ impl, Cold: ItemStore> HotColdDB Some(state_slot) => { let epoch_boundary_slot = state_slot / E::slots_per_epoch() * E::slots_per_epoch(); - self.load_cold_state_by_slot(epoch_boundary_slot) + self.load_cold_state_by_slot(epoch_boundary_slot).map(Some) } None => Ok(None), } @@ -1497,7 +1567,6 @@ impl, Cold: ItemStore> HotColdDB state.build_all_caches(&self.spec)?; let latest_block_root = state.get_latest_block_root(state_root); - let state_slot = state.slot(); if let PutStateOutcome::New = self.state_cache .lock() @@ -1507,13 +1576,14 @@ impl, Cold: ItemStore> HotColdDB self.log, "Cached ancestor state"; "state_root" => ?state_root, - "slot" => state_slot, + "slot" => slot, ); } Ok(()) }; let blocks = self.load_blocks_to_replay(boundary_state.slot(), slot, latest_block_root)?; + let _t = metrics::start_timer(&metrics::STORE_BEACON_REPLAY_HOT_BLOCKS_TIME); self.replay_blocks( boundary_state, blocks, @@ -1530,48 +1600,142 @@ impl, Cold: ItemStore> HotColdDB } } + pub fn store_cold_state_summary( + &self, + state_root: &Hash256, + slot: Slot, + ops: &mut Vec, + ) -> Result<(), Error> { + ops.push(ColdStateSummary { slot }.as_kv_store_op(*state_root)); + ops.push(KeyValueStoreOp::PutKeyValue( + get_key_for_col( + DBColumn::BeaconStateRoots.into(), + &slot.as_u64().to_be_bytes(), + ), + state_root.as_slice().to_vec(), + )); + Ok(()) + } + /// Store a pre-finalization state in the freezer database. - /// - /// If the state doesn't lie on a restore point boundary then just its summary will be stored. pub fn store_cold_state( &self, state_root: &Hash256, state: &BeaconState, ops: &mut Vec, ) -> Result<(), Error> { - ops.push(ColdStateSummary { slot: state.slot() }.as_kv_store_op(*state_root)); + self.store_cold_state_summary(state_root, state.slot(), ops)?; - if state.slot() % self.config.slots_per_restore_point != 0 { - return Ok(()); + let slot = state.slot(); + match self.hierarchy.storage_strategy(slot)? { + StorageStrategy::ReplayFrom(from) => { + debug!( + self.log, + "Storing cold state"; + "strategy" => "replay", + "from_slot" => from, + "slot" => state.slot(), + ); + // Already have persisted the state summary, don't persist anything else + } + StorageStrategy::Snapshot => { + debug!( + self.log, + "Storing cold state"; + "strategy" => "snapshot", + "slot" => state.slot(), + ); + self.store_cold_state_as_snapshot(state, ops)?; + } + StorageStrategy::DiffFrom(from) => { + debug!( + self.log, + "Storing cold state"; + "strategy" => "diff", + "from_slot" => from, + "slot" => state.slot(), + ); + self.store_cold_state_as_diff(state, from, ops)?; + } } - trace!( - self.log, - "Creating restore point"; - "slot" => state.slot(), - "state_root" => format!("{:?}", state_root) + Ok(()) + } + + pub fn store_cold_state_as_snapshot( + &self, + state: &BeaconState, + ops: &mut Vec, + ) -> Result<(), Error> { + let bytes = state.as_ssz_bytes(); + let compressed_value = { + let _timer = metrics::start_timer(&metrics::STORE_BEACON_STATE_FREEZER_COMPRESS_TIME); + let mut out = Vec::with_capacity(self.config.estimate_compressed_size(bytes.len())); + let mut encoder = Encoder::new(&mut out, self.config.compression_level) + .map_err(Error::Compression)?; + encoder.write_all(&bytes).map_err(Error::Compression)?; + encoder.finish().map_err(Error::Compression)?; + out + }; + + let key = get_key_for_col( + DBColumn::BeaconStateSnapshot.into(), + &state.slot().as_u64().to_be_bytes(), ); + ops.push(KeyValueStoreOp::PutKeyValue(key, compressed_value)); + Ok(()) + } - // 1. Convert to PartialBeaconState and store that in the DB. - let partial_state = PartialBeaconState::from_state_forgetful(state); - let op = partial_state.as_kv_store_op(*state_root); - ops.push(op); + fn load_cold_state_bytes_as_snapshot(&self, slot: Slot) -> Result>, Error> { + match self.cold_db.get_bytes( + DBColumn::BeaconStateSnapshot.into(), + &slot.as_u64().to_be_bytes(), + )? { + Some(bytes) => { + let _timer = + metrics::start_timer(&metrics::STORE_BEACON_STATE_FREEZER_DECOMPRESS_TIME); + let mut ssz_bytes = + Vec::with_capacity(self.config.estimate_decompressed_size(bytes.len())); + let mut decoder = Decoder::new(&*bytes).map_err(Error::Compression)?; + decoder + .read_to_end(&mut ssz_bytes) + .map_err(Error::Compression)?; + Ok(Some(ssz_bytes)) + } + None => Ok(None), + } + } - // 2. Store updated vector entries. - // Block roots need to be written here as well as by the `ChunkWriter` in `migrate_db` - // because states may require older block roots, and the writer only stores block roots - // between the previous split point and the new split point. - let db = &self.cold_db; - store_updated_vector(BlockRoots, db, state, &self.spec, ops)?; - store_updated_vector(StateRoots, db, state, &self.spec, ops)?; - store_updated_vector(HistoricalRoots, db, state, &self.spec, ops)?; - store_updated_vector(RandaoMixes, db, state, &self.spec, ops)?; - store_updated_vector(HistoricalSummaries, db, state, &self.spec, ops)?; + fn load_cold_state_as_snapshot(&self, slot: Slot) -> Result>, Error> { + Ok(self + .load_cold_state_bytes_as_snapshot(slot)? + .map(|bytes| BeaconState::from_ssz_bytes(&bytes, &self.spec)) + .transpose()?) + } - // 3. Store restore point. - let restore_point_index = state.slot().as_u64() / self.config.slots_per_restore_point; - self.store_restore_point_hash(restore_point_index, *state_root, ops); + pub fn store_cold_state_as_diff( + &self, + state: &BeaconState, + from_slot: Slot, + ops: &mut Vec, + ) -> Result<(), Error> { + // Load diff base state bytes. + let (_, base_buffer) = { + let _t = metrics::start_timer(&metrics::STORE_BEACON_HDIFF_BUFFER_LOAD_FOR_STORE_TIME); + self.load_hdiff_buffer_for_slot(from_slot)? + }; + let target_buffer = HDiffBuffer::from_state(state.clone()); + let diff = { + let _timer = metrics::start_timer(&metrics::STORE_BEACON_HDIFF_BUFFER_COMPUTE_TIME); + HDiff::compute(&base_buffer, &target_buffer, &self.config)? + }; + let diff_bytes = diff.as_ssz_bytes(); + let key = get_key_for_col( + DBColumn::BeaconStateDiff.into(), + &state.slot().as_u64().to_be_bytes(), + ); + ops.push(KeyValueStoreOp::PutKeyValue(key, diff_bytes)); Ok(()) } @@ -1580,7 +1744,7 @@ impl, Cold: ItemStore> HotColdDB /// Return `None` if no state with `state_root` lies in the freezer. pub fn load_cold_state(&self, state_root: &Hash256) -> Result>, Error> { match self.load_cold_state_slot(state_root)? { - Some(slot) => self.load_cold_state_by_slot(slot), + Some(slot) => self.load_cold_state_by_slot(slot).map(Some), None => Ok(None), } } @@ -1588,149 +1752,214 @@ impl, Cold: ItemStore> HotColdDB /// Load a pre-finalization state from the freezer database. /// /// Will reconstruct the state if it lies between restore points. - pub fn load_cold_state_by_slot(&self, slot: Slot) -> Result>, Error> { - // Guard against fetching states that do not exist due to gaps in the historic state - // database, which can occur due to checkpoint sync or re-indexing. - // See the comments in `get_historic_state_limits` for more information. - let (lower_limit, upper_limit) = self.get_historic_state_limits(); + pub fn load_cold_state_by_slot(&self, slot: Slot) -> Result, Error> { + let storage_strategy = self.hierarchy.storage_strategy(slot)?; - if slot <= lower_limit || slot >= upper_limit { - if slot % self.config.slots_per_restore_point == 0 { - let restore_point_idx = slot.as_u64() / self.config.slots_per_restore_point; - self.load_restore_point_by_index(restore_point_idx) - } else { - self.load_cold_intermediate_state(slot) + // Search for a state from this slot or a recent prior slot in the historic state cache. + let mut historic_state_cache = self.historic_state_cache.lock(); + + let cached_state = itertools::process_results( + storage_strategy + .replay_from_range(slot) + .rev() + .map(|prior_slot| historic_state_cache.get_state(prior_slot, &self.spec)), + |mut iter| iter.find_map(|cached_state| cached_state), + )?; + drop(historic_state_cache); + + if let Some(cached_state) = cached_state { + if cached_state.slot() == slot { + metrics::inc_counter(&metrics::STORE_BEACON_HISTORIC_STATE_CACHE_HIT); + return Ok(cached_state); } - .map(Some) - } else { - Ok(None) - } - } + metrics::inc_counter(&metrics::STORE_BEACON_HISTORIC_STATE_CACHE_MISS); - /// Load a restore point state by its `state_root`. - fn load_restore_point(&self, state_root: &Hash256) -> Result, Error> { - let partial_state_bytes = self - .cold_db - .get_bytes(DBColumn::BeaconState.into(), state_root.as_slice())? - .ok_or(HotColdDBError::MissingRestorePoint(*state_root))?; - let mut partial_state: PartialBeaconState = - PartialBeaconState::from_ssz_bytes(&partial_state_bytes, &self.spec)?; - - // Fill in the fields of the partial state. - partial_state.load_block_roots(&self.cold_db, &self.spec)?; - partial_state.load_state_roots(&self.cold_db, &self.spec)?; - partial_state.load_historical_roots(&self.cold_db, &self.spec)?; - partial_state.load_randao_mixes(&self.cold_db, &self.spec)?; - partial_state.load_historical_summaries(&self.cold_db, &self.spec)?; - - let mut state: BeaconState = partial_state.try_into()?; - state.apply_pending_mutations()?; - Ok(state) - } - - /// Load a restore point state by its `restore_point_index`. - fn load_restore_point_by_index( - &self, - restore_point_index: u64, - ) -> Result, Error> { - let state_root = self.load_restore_point_hash(restore_point_index)?; - self.load_restore_point(&state_root) - } - - /// Load a frozen state that lies between restore points. - fn load_cold_intermediate_state(&self, slot: Slot) -> Result, Error> { - if let Some(state) = self.historic_state_cache.lock().get(&slot) { - return Ok(state.clone()); + return self.load_cold_state_by_slot_using_replay(cached_state, slot); } - // 1. Load the restore points either side of the intermediate state. - let low_restore_point_idx = slot.as_u64() / self.config.slots_per_restore_point; - let high_restore_point_idx = low_restore_point_idx + 1; + metrics::inc_counter(&metrics::STORE_BEACON_HISTORIC_STATE_CACHE_MISS); - // Use low restore point as the base state. - let mut low_slot: Slot = - Slot::new(low_restore_point_idx * self.config.slots_per_restore_point); - let mut low_state: Option> = None; + // Load using the diff hierarchy. For states that require replay we recurse into this + // function so that we can try to get their pre-state *as a state* rather than an hdiff + // buffer. + match self.hierarchy.storage_strategy(slot)? { + StorageStrategy::Snapshot | StorageStrategy::DiffFrom(_) => { + let buffer_timer = + metrics::start_timer(&metrics::STORE_BEACON_HDIFF_BUFFER_LOAD_TIME); + let (_, buffer) = self.load_hdiff_buffer_for_slot(slot)?; + drop(buffer_timer); + let state = buffer.as_state(&self.spec)?; - // Try to get a more recent state from the cache to avoid massive blocks replay. - for (s, state) in self.historic_state_cache.lock().iter() { - if s.as_u64() / self.config.slots_per_restore_point == low_restore_point_idx - && *s < slot - && low_slot < *s - { - low_slot = *s; - low_state = Some(state.clone()); + self.historic_state_cache + .lock() + .put_both(slot, state.clone(), buffer); + Ok(state) + } + StorageStrategy::ReplayFrom(from) => { + // No prior state found in cache (above), need to load by diffing and then + // replaying. + let base_state = self.load_cold_state_by_slot(from)?; + self.load_cold_state_by_slot_using_replay(base_state, slot) } } - - // If low_state is still None, use load_restore_point_by_index to load the state. - let low_state = match low_state { - Some(state) => state, - None => self.load_restore_point_by_index(low_restore_point_idx)?, - }; - - // Acquire the read lock, so that the split can't change while this is happening. - let split = self.split.read_recursive(); - - let high_restore_point = self.get_restore_point(high_restore_point_idx, &split)?; - - // 2. Load the blocks from the high restore point back to the low point. - let blocks = self.load_blocks_to_replay( - low_slot, - slot, - self.get_high_restore_point_block_root(&high_restore_point, slot)?, - )?; - - // 3. Replay the blocks on top of the low point. - // Use a forwards state root iterator to avoid doing any tree hashing. - // The state root of the high restore point should never be used, so is safely set to 0. - let state_root_iter = self.forwards_state_roots_iterator_until( - low_slot, - slot, - || Ok((high_restore_point, Hash256::zero())), - &self.spec, - )?; - - let mut state = self.replay_blocks(low_state, blocks, slot, Some(state_root_iter), None)?; - state.apply_pending_mutations()?; - - // If state is not error, put it in the cache. - self.historic_state_cache.lock().put(slot, state.clone()); - - Ok(state) } - /// Get the restore point with the given index, or if it is out of bounds, the split state. - pub(crate) fn get_restore_point( + fn load_cold_state_by_slot_using_replay( &self, - restore_point_idx: u64, - split: &Split, - ) -> Result, Error> { - if restore_point_idx * self.config.slots_per_restore_point >= split.slot.as_u64() { - self.get_state(&split.state_root, Some(split.slot))? - .ok_or(HotColdDBError::MissingSplitState( - split.state_root, - split.slot, - )) - .map_err(Into::into) - } else { - self.load_restore_point_by_index(restore_point_idx) - } - } - - /// Get a suitable block root for backtracking from `high_restore_point` to the state at `slot`. - /// - /// Defaults to the block root for `slot`, which *should* be in range. - fn get_high_restore_point_block_root( - &self, - high_restore_point: &BeaconState, + mut base_state: BeaconState, slot: Slot, - ) -> Result { - high_restore_point - .get_block_root(slot) - .or_else(|_| high_restore_point.get_oldest_block_root()) - .copied() - .map_err(HotColdDBError::RestorePointBlockHashError) + ) -> Result, Error> { + if !base_state.all_caches_built() { + // Build all caches and update the historic state cache so that these caches may be used + // at future slots. We do this lazily here rather than when populating the cache in + // order to speed up queries at snapshot/diff slots, which are already slow. + let cache_timer = + metrics::start_timer(&metrics::STORE_BEACON_COLD_BUILD_BEACON_CACHES_TIME); + base_state.build_all_caches(&self.spec)?; + debug!( + self.log, + "Built caches for historic state"; + "target_slot" => slot, + "build_time_ms" => metrics::stop_timer_with_duration(cache_timer).as_millis() + ); + self.historic_state_cache + .lock() + .put_state(base_state.slot(), base_state.clone()); + } + + if base_state.slot() == slot { + return Ok(base_state); + } + + let blocks = self.load_cold_blocks(base_state.slot() + 1, slot)?; + + // Include state root for base state as it is required by block processing to not + // have to hash the state. + let replay_timer = metrics::start_timer(&metrics::STORE_BEACON_REPLAY_COLD_BLOCKS_TIME); + let state_root_iter = + self.forwards_state_roots_iterator_until(base_state.slot(), slot, || { + Err(Error::StateShouldNotBeRequired(slot)) + })?; + let state = self.replay_blocks(base_state, blocks, slot, Some(state_root_iter), None)?; + debug!( + self.log, + "Replayed blocks for historic state"; + "target_slot" => slot, + "replay_time_ms" => metrics::stop_timer_with_duration(replay_timer).as_millis() + ); + + self.historic_state_cache + .lock() + .put_state(slot, state.clone()); + Ok(state) + } + + fn load_hdiff_for_slot(&self, slot: Slot) -> Result { + let bytes = { + let _t = metrics::start_timer(&metrics::BEACON_HDIFF_READ_TIMES); + self.cold_db + .get_bytes( + DBColumn::BeaconStateDiff.into(), + &slot.as_u64().to_be_bytes(), + )? + .ok_or(HotColdDBError::MissingHDiff(slot))? + }; + let hdiff = { + let _t = metrics::start_timer(&metrics::BEACON_HDIFF_DECODE_TIMES); + HDiff::from_ssz_bytes(&bytes)? + }; + Ok(hdiff) + } + + /// Returns `HDiffBuffer` for the specified slot, or `HDiffBuffer` for the `ReplayFrom` slot if + /// the diff for the specified slot is not stored. + fn load_hdiff_buffer_for_slot(&self, slot: Slot) -> Result<(Slot, HDiffBuffer), Error> { + if let Some(buffer) = self.historic_state_cache.lock().get_hdiff_buffer(slot) { + debug!( + self.log, + "Hit hdiff buffer cache"; + "slot" => slot + ); + metrics::inc_counter(&metrics::STORE_BEACON_HDIFF_BUFFER_CACHE_HIT); + return Ok((slot, buffer)); + } + metrics::inc_counter(&metrics::STORE_BEACON_HDIFF_BUFFER_CACHE_MISS); + + // Load buffer for the previous state. + // This amount of recursion (<10 levels) should be OK. + let t = std::time::Instant::now(); + match self.hierarchy.storage_strategy(slot)? { + // Base case. + StorageStrategy::Snapshot => { + let state = self + .load_cold_state_as_snapshot(slot)? + .ok_or(Error::MissingSnapshot(slot))?; + let buffer = HDiffBuffer::from_state(state.clone()); + + self.historic_state_cache + .lock() + .put_both(slot, state, buffer.clone()); + + let load_time_ms = t.elapsed().as_millis(); + debug!( + self.log, + "Cached state and hdiff buffer"; + "load_time_ms" => load_time_ms, + "slot" => slot + ); + + Ok((slot, buffer)) + } + // Recursive case. + StorageStrategy::DiffFrom(from) => { + let (_buffer_slot, mut buffer) = self.load_hdiff_buffer_for_slot(from)?; + + // Load diff and apply it to buffer. + let diff = self.load_hdiff_for_slot(slot)?; + { + let _timer = + metrics::start_timer(&metrics::STORE_BEACON_HDIFF_BUFFER_APPLY_TIME); + diff.apply(&mut buffer, &self.config)?; + } + + self.historic_state_cache + .lock() + .put_hdiff_buffer(slot, buffer.clone()); + + let load_time_ms = t.elapsed().as_millis(); + debug!( + self.log, + "Cached hdiff buffer"; + "load_time_ms" => load_time_ms, + "slot" => slot + ); + + Ok((slot, buffer)) + } + StorageStrategy::ReplayFrom(from) => self.load_hdiff_buffer_for_slot(from), + } + } + + /// Load cold blocks between `start_slot` and `end_slot` inclusive. + pub fn load_cold_blocks( + &self, + start_slot: Slot, + end_slot: Slot, + ) -> Result>, Error> { + let _t = metrics::start_timer(&metrics::STORE_BEACON_LOAD_COLD_BLOCKS_TIME); + let block_root_iter = + self.forwards_block_roots_iterator_until(start_slot, end_slot, || { + Err(Error::StateShouldNotBeRequired(end_slot)) + })?; + process_results(block_root_iter, |iter| { + iter.map(|(block_root, _slot)| block_root) + .dedup() + .map(|block_root| { + self.get_blinded_block(&block_root)? + .ok_or(Error::MissingBlock(block_root)) + }) + .collect() + })? } /// Load the blocks between `start_slot` and `end_slot` by backtracking from `end_block_hash`. @@ -1743,6 +1972,7 @@ impl, Cold: ItemStore> HotColdDB end_slot: Slot, end_block_hash: Hash256, ) -> Result>>, Error> { + let _t = metrics::start_timer(&metrics::STORE_BEACON_LOAD_HOT_BLOCKS_TIME); let mut blocks = ParentRootBlockIterator::new(self, end_block_hash) .map(|result| result.map(|(_, block)| block)) // Include the block at the end slot (if any), it needs to be @@ -1785,6 +2015,8 @@ impl, Cold: ItemStore> HotColdDB state_root_iter: Option>>, pre_slot_hook: Option>, ) -> Result, Error> { + metrics::inc_counter_by(&metrics::STORE_BEACON_REPLAYED_BLOCKS, blocks.len() as u64); + let mut block_replayer = BlockReplayer::new(state, &self.spec) .no_signature_verification() .minimal_block_root_verification(); @@ -1902,30 +2134,6 @@ impl, Cold: ItemStore> HotColdDB }; } - /// Fetch the slot of the most recently stored restore point (if any). - pub fn get_latest_restore_point_slot(&self) -> Option { - let split_slot = self.get_split_slot(); - let anchor = self.get_anchor_info(); - - // There are no restore points stored if the state upper limit lies in the hot database, - // and the lower limit is zero. It hasn't been reached yet, and may never be. - if anchor.as_ref().map_or(false, |a| { - a.state_upper_limit >= split_slot && a.state_lower_limit == 0 - }) { - None - } else if let Some(lower_limit) = anchor - .map(|a| a.state_lower_limit) - .filter(|limit| *limit > 0) - { - Some(lower_limit) - } else { - Some( - (split_slot - 1) / self.config.slots_per_restore_point - * self.config.slots_per_restore_point, - ) - } - } - /// Load the database schema version from disk. fn load_schema_version(&self) -> Result, Error> { self.hot_db.get(&SCHEMA_VERSION_KEY) @@ -1958,36 +2166,33 @@ impl, Cold: ItemStore> HotColdDB retain_historic_states: bool, ) -> Result { let anchor_slot = block.slot(); - let slots_per_restore_point = self.config.slots_per_restore_point; + // Set the `state_upper_limit` to the slot of the *next* checkpoint. + let next_snapshot_slot = self.hierarchy.next_snapshot_slot(anchor_slot)?; let state_upper_limit = if !retain_historic_states { STATE_UPPER_LIMIT_NO_RETAIN - } else if anchor_slot % slots_per_restore_point == 0 { - anchor_slot } else { - // Set the `state_upper_limit` to the slot of the *next* restore point. - // See `get_state_upper_limit` for rationale. - (anchor_slot / slots_per_restore_point + 1) * slots_per_restore_point + next_snapshot_slot }; let anchor_info = if state_upper_limit == 0 && anchor_slot == 0 { // Genesis archive node: no anchor because we *will* store all states. - None + ANCHOR_FOR_ARCHIVE_NODE } else { - Some(AnchorInfo { + AnchorInfo { anchor_slot, oldest_block_slot: anchor_slot, oldest_block_parent: block.parent_root(), state_upper_limit, state_lower_limit: self.spec.genesis_slot, - }) + } }; - self.compare_and_set_anchor_info(None, anchor_info) + self.compare_and_set_anchor_info(ANCHOR_UNINITIALIZED, anchor_info) } /// Get a clone of the store's anchor info. /// /// To do mutations, use `compare_and_set_anchor_info`. - pub fn get_anchor_info(&self) -> Option { + pub fn get_anchor_info(&self) -> AnchorInfo { self.anchor_info.read_recursive().clone() } @@ -2000,8 +2205,8 @@ impl, Cold: ItemStore> HotColdDB /// is not correct. pub fn compare_and_set_anchor_info( &self, - prev_value: Option, - new_value: Option, + prev_value: AnchorInfo, + new_value: AnchorInfo, ) -> Result { let mut anchor_info = self.anchor_info.write(); if *anchor_info == prev_value { @@ -2016,39 +2221,26 @@ impl, Cold: ItemStore> HotColdDB /// As for `compare_and_set_anchor_info`, but also writes the anchor to disk immediately. pub fn compare_and_set_anchor_info_with_write( &self, - prev_value: Option, - new_value: Option, + prev_value: AnchorInfo, + new_value: AnchorInfo, ) -> Result<(), Error> { let kv_store_op = self.compare_and_set_anchor_info(prev_value, new_value)?; self.hot_db.do_atomically(vec![kv_store_op]) } - /// Load the anchor info from disk, but do not set `self.anchor_info`. - fn load_anchor_info(&self) -> Result, Error> { - self.hot_db.get(&ANCHOR_INFO_KEY) + /// Load the anchor info from disk. + fn load_anchor_info(hot_db: &Hot) -> Result { + Ok(hot_db + .get(&ANCHOR_INFO_KEY)? + .unwrap_or(ANCHOR_UNINITIALIZED)) } /// Store the given `anchor_info` to disk. /// /// The argument is intended to be `self.anchor_info`, but is passed manually to avoid issues /// with recursive locking. - fn store_anchor_info_in_batch(&self, anchor_info: &Option) -> KeyValueStoreOp { - if let Some(ref anchor_info) = anchor_info { - anchor_info.as_kv_store_op(ANCHOR_INFO_KEY) - } else { - KeyValueStoreOp::DeleteKey(get_key_for_col( - DBColumn::BeaconMeta.into(), - ANCHOR_INFO_KEY.as_slice(), - )) - } - } - - /// If an anchor exists, return its `anchor_slot` field. - pub fn get_anchor_slot(&self) -> Option { - self.anchor_info - .read_recursive() - .as_ref() - .map(|a| a.anchor_slot) + fn store_anchor_info_in_batch(&self, anchor_info: &AnchorInfo) -> KeyValueStoreOp { + anchor_info.as_kv_store_op(ANCHOR_INFO_KEY) } /// Initialize the `BlobInfo` when starting from genesis or a checkpoint. @@ -2196,7 +2388,7 @@ impl, Cold: ItemStore> HotColdDB /// instance. pub fn get_historic_state_limits(&self) -> (Slot, Slot) { // If checkpoint sync is used then states in the hot DB will always be available, but may - // become unavailable as finalisation advances due to the lack of a restore point in the + // become unavailable as finalisation advances due to the lack of a snapshot in the // database. For this reason we take the minimum of the split slot and the // restore-point-aligned `state_upper_limit`, which should be set _ahead_ of the checkpoint // slot during initialisation. @@ -2207,20 +2399,16 @@ impl, Cold: ItemStore> HotColdDB // a new restore point will be created at that slot, making all states from 4096 onwards // permanently available. let split_slot = self.get_split_slot(); - self.anchor_info - .read_recursive() - .as_ref() - .map_or((split_slot, self.spec.genesis_slot), |a| { - (a.state_lower_limit, min(a.state_upper_limit, split_slot)) - }) + let anchor = self.anchor_info.read_recursive(); + ( + anchor.state_lower_limit, + min(anchor.state_upper_limit, split_slot), + ) } /// Return the minimum slot such that blocks are available for all subsequent slots. pub fn get_oldest_block_slot(&self) -> Slot { - self.anchor_info - .read_recursive() - .as_ref() - .map_or(self.spec.genesis_slot, |anchor| anchor.oldest_block_slot) + self.anchor_info.read_recursive().oldest_block_slot } /// Return the in-memory configuration used by the database. @@ -2263,32 +2451,6 @@ impl, Cold: ItemStore> HotColdDB self.split.read_recursive().as_kv_store_op(SPLIT_KEY) } - /// Load the state root of a restore point. - fn load_restore_point_hash(&self, restore_point_index: u64) -> Result { - let key = Self::restore_point_key(restore_point_index); - self.cold_db - .get(&key)? - .map(|r: RestorePointHash| r.state_root) - .ok_or_else(|| HotColdDBError::MissingRestorePointHash(restore_point_index).into()) - } - - /// Store the state root of a restore point. - fn store_restore_point_hash( - &self, - restore_point_index: u64, - state_root: Hash256, - ops: &mut Vec, - ) { - let value = &RestorePointHash { state_root }; - let op = value.as_kv_store_op(Self::restore_point_key(restore_point_index)); - ops.push(op); - } - - /// Convert a `restore_point_index` into a database key. - fn restore_point_key(restore_point_index: u64) -> Hash256 { - Hash256::from_low_u64_be(restore_point_index) - } - /// Load a frozen state's slot, given its root. pub fn load_cold_state_slot(&self, state_root: &Hash256) -> Result, Error> { Ok(self @@ -2316,52 +2478,6 @@ impl, Cold: ItemStore> HotColdDB self.hot_db.get(state_root) } - /// Verify that a parsed config is valid. - fn verify_config(config: &StoreConfig) -> Result<(), HotColdDBError> { - Self::verify_slots_per_restore_point(config.slots_per_restore_point)?; - Self::verify_epochs_per_blob_prune(config.epochs_per_blob_prune) - } - - /// Check that the restore point frequency is valid. - /// - /// Specifically, check that it is: - /// (1) A divisor of the number of slots per historical root, and - /// (2) Divisible by the number of slots per epoch - /// - /// - /// (1) ensures that we have at least one restore point within range of our state - /// root history when iterating backwards (and allows for more frequent restore points if - /// desired). - /// - /// (2) ensures that restore points align with hot state summaries, making it - /// quick to migrate hot to cold. - fn verify_slots_per_restore_point(slots_per_restore_point: u64) -> Result<(), HotColdDBError> { - let slots_per_historical_root = E::SlotsPerHistoricalRoot::to_u64(); - let slots_per_epoch = E::slots_per_epoch(); - if slots_per_restore_point > 0 - && slots_per_historical_root % slots_per_restore_point == 0 - && slots_per_restore_point % slots_per_epoch == 0 - { - Ok(()) - } else { - Err(HotColdDBError::InvalidSlotsPerRestorePoint { - slots_per_restore_point, - slots_per_historical_root, - slots_per_epoch, - }) - } - } - - // Check that epochs_per_blob_prune is at least 1 epoch to avoid attempting to prune the same - // epochs over and over again. - fn verify_epochs_per_blob_prune(epochs_per_blob_prune: u64) -> Result<(), HotColdDBError> { - if epochs_per_blob_prune > 0 { - Ok(()) - } else { - Err(HotColdDBError::ZeroEpochsPerBlobPrune) - } - } - /// Run a compaction pass to free up space used by deleted states. pub fn compact(&self) -> Result<(), Error> { self.hot_db.compact()?; @@ -2418,12 +2534,12 @@ impl, Cold: ItemStore> HotColdDB block_root: Hash256, ) -> Result, Error> { let mut ops = vec![]; - let mut block_root_writer = - ChunkWriter::::new(&self.cold_db, start_slot.as_usize())?; - for slot in start_slot.as_usize()..end_slot.as_usize() { - block_root_writer.set(slot, block_root, &mut ops)?; + for slot in start_slot.as_u64()..end_slot.as_u64() { + ops.push(KeyValueStoreOp::PutKeyValue( + get_key_for_col(DBColumn::BeaconBlockRoots.into(), &slot.to_be_bytes()), + block_root.as_slice().to_vec(), + )); } - block_root_writer.write(&mut ops)?; Ok(ops) } @@ -2474,7 +2590,7 @@ impl, Cold: ItemStore> HotColdDB "Pruning finalized payloads"; "info" => "you may notice degraded I/O performance while this runs" ); - let anchor_slot = self.get_anchor_info().map(|info| info.anchor_slot); + let anchor_slot = self.get_anchor_info().anchor_slot; let mut ops = vec![]; let mut last_pruned_block_root = None; @@ -2515,7 +2631,7 @@ impl, Cold: ItemStore> HotColdDB ops.push(StoreOp::DeleteExecutionPayload(block_root)); } - if Some(slot) == anchor_slot { + if slot == anchor_slot { info!( self.log, "Payload pruning reached anchor state"; @@ -2622,16 +2738,15 @@ impl, Cold: ItemStore> HotColdDB } // Sanity checks. - if let Some(anchor) = self.get_anchor_info() { - if oldest_blob_slot < anchor.oldest_block_slot { - error!( - self.log, - "Oldest blob is older than oldest block"; - "oldest_blob_slot" => oldest_blob_slot, - "oldest_block_slot" => anchor.oldest_block_slot - ); - return Err(HotColdDBError::BlobPruneLogicError.into()); - } + let anchor = self.get_anchor_info(); + if oldest_blob_slot < anchor.oldest_block_slot { + error!( + self.log, + "Oldest blob is older than oldest block"; + "oldest_blob_slot" => oldest_blob_slot, + "oldest_block_slot" => anchor.oldest_block_slot + ); + return Err(HotColdDBError::BlobPruneLogicError.into()); } // Iterate block roots forwards from the oldest blob slot. @@ -2646,21 +2761,16 @@ impl, Cold: ItemStore> HotColdDB let mut ops = vec![]; let mut last_pruned_block_root = None; - for res in self.forwards_block_roots_iterator_until( - oldest_blob_slot, - end_slot, - || { - let (_, split_state) = self - .get_advanced_hot_state(split.block_root, split.slot, split.state_root)? - .ok_or(HotColdDBError::MissingSplitState( - split.state_root, - split.slot, - ))?; + for res in self.forwards_block_roots_iterator_until(oldest_blob_slot, end_slot, || { + let (_, split_state) = self + .get_advanced_hot_state(split.block_root, split.slot, split.state_root)? + .ok_or(HotColdDBError::MissingSplitState( + split.state_root, + split.slot, + ))?; - Ok((split_state, split.block_root)) - }, - &self.spec, - )? { + Ok((split_state, split.block_root)) + })? { let (block_root, slot) = match res { Ok(tuple) => tuple, Err(e) => { @@ -2724,84 +2834,6 @@ impl, Cold: ItemStore> HotColdDB Ok(()) } - /// This function fills in missing block roots between last restore point slot and split - /// slot, if any. - pub fn heal_freezer_block_roots_at_split(&self) -> Result<(), Error> { - let split = self.get_split_info(); - let last_restore_point_slot = (split.slot - 1) / self.config.slots_per_restore_point - * self.config.slots_per_restore_point; - - // Load split state (which has access to block roots). - let (_, split_state) = self - .get_advanced_hot_state(split.block_root, split.slot, split.state_root)? - .ok_or(HotColdDBError::MissingSplitState( - split.state_root, - split.slot, - ))?; - - let mut batch = vec![]; - let mut chunk_writer = ChunkWriter::::new( - &self.cold_db, - last_restore_point_slot.as_usize(), - )?; - - for slot in (last_restore_point_slot.as_u64()..split.slot.as_u64()).map(Slot::new) { - let block_root = *split_state.get_block_root(slot)?; - chunk_writer.set(slot.as_usize(), block_root, &mut batch)?; - } - chunk_writer.write(&mut batch)?; - self.cold_db.do_atomically(batch)?; - - Ok(()) - } - - pub fn heal_freezer_block_roots_at_genesis(&self) -> Result<(), Error> { - let oldest_block_slot = self.get_oldest_block_slot(); - let split_slot = self.get_split_slot(); - - // Check if backfill has been completed AND the freezer db has data in it - if oldest_block_slot != 0 || split_slot == 0 { - return Ok(()); - } - - let mut block_root_iter = self.forwards_block_roots_iterator_until( - Slot::new(0), - split_slot - 1, - || { - Err(Error::DBError { - message: "Should not require end state".to_string(), - }) - }, - &self.spec, - )?; - - let (genesis_block_root, _) = block_root_iter.next().ok_or_else(|| Error::DBError { - message: "Genesis block root missing".to_string(), - })??; - - let slots_to_fix = itertools::process_results(block_root_iter, |iter| { - iter.take_while(|(block_root, _)| block_root.is_zero()) - .map(|(_, slot)| slot) - .collect::>() - })?; - - let Some(first_slot) = slots_to_fix.first() else { - return Ok(()); - }; - - let mut chunk_writer = - ChunkWriter::::new(&self.cold_db, first_slot.as_usize())?; - let mut ops = vec![]; - for slot in slots_to_fix { - chunk_writer.set(slot.as_usize(), genesis_block_root, &mut ops)?; - } - - chunk_writer.write(&mut ops)?; - self.cold_db.do_atomically(ops)?; - - Ok(()) - } - /// Delete *all* states from the freezer database and update the anchor accordingly. /// /// WARNING: this method deletes the genesis state and replaces it with the provided @@ -2813,46 +2845,48 @@ impl, Cold: ItemStore> HotColdDB genesis_state_root: Hash256, genesis_state: &BeaconState, ) -> Result<(), Error> { - // Make sure there is no missing block roots before pruning - self.heal_freezer_block_roots_at_split()?; - // Update the anchor to use the dummy state upper limit and disable historic state storage. let old_anchor = self.get_anchor_info(); - let new_anchor = if let Some(old_anchor) = old_anchor.clone() { - AnchorInfo { - state_upper_limit: STATE_UPPER_LIMIT_NO_RETAIN, - state_lower_limit: Slot::new(0), - ..old_anchor.clone() - } - } else { - AnchorInfo { - anchor_slot: Slot::new(0), - oldest_block_slot: Slot::new(0), - oldest_block_parent: Hash256::zero(), - state_upper_limit: STATE_UPPER_LIMIT_NO_RETAIN, - state_lower_limit: Slot::new(0), - } + let new_anchor = AnchorInfo { + state_upper_limit: STATE_UPPER_LIMIT_NO_RETAIN, + state_lower_limit: Slot::new(0), + ..old_anchor.clone() }; // Commit the anchor change immediately: if the cold database ops fail they can always be // retried, and we can't do them atomically with this change anyway. - self.compare_and_set_anchor_info_with_write(old_anchor, Some(new_anchor))?; + self.compare_and_set_anchor_info_with_write(old_anchor, new_anchor)?; // Stage freezer data for deletion. Do not bother loading and deserializing values as this // wastes time and is less schema-agnostic. My hope is that this method will be useful for // migrating to the tree-states schema (delete everything in the freezer then start afresh). let mut cold_ops = vec![]; - let columns = [ - DBColumn::BeaconState, - DBColumn::BeaconStateSummary, - DBColumn::BeaconRestorePoint, + let current_schema_columns = vec![ + DBColumn::BeaconColdStateSummary, + DBColumn::BeaconStateSnapshot, + DBColumn::BeaconStateDiff, DBColumn::BeaconStateRoots, + ]; + + // This function is intended to be able to clean up leftover V21 freezer database stuff in + // the case where the V22 schema upgrade failed *after* commiting the version increment but + // *before* cleaning up the freezer DB. + // + // We can remove this once schema V21 has been gone for a while. + let previous_schema_columns = vec![ + DBColumn::BeaconStateSummary, + DBColumn::BeaconBlockRootsChunked, + DBColumn::BeaconStateRootsChunked, + DBColumn::BeaconRestorePoint, DBColumn::BeaconHistoricalRoots, DBColumn::BeaconRandaoMixes, DBColumn::BeaconHistoricalSummaries, ]; + let mut columns = current_schema_columns; + columns.extend(previous_schema_columns); + for column in columns { for res in self.cold_db.iter_column_keys::>(column) { let key = res?; @@ -2862,20 +2896,9 @@ impl, Cold: ItemStore> HotColdDB ))); } } + let delete_ops = cold_ops.len(); - // XXX: We need to commit the mass deletion here *before* re-storing the genesis state, as - // the current schema performs reads as part of `store_cold_state`. This can be deleted - // once the target schema is tree-states. If the process is killed before the genesis state - // is written this can be fixed by re-running. - info!( - self.log, - "Deleting historic states"; - "num_kv" => cold_ops.len(), - ); - self.cold_db.do_atomically(std::mem::take(&mut cold_ops))?; - - // If we just deleted the the genesis state, re-store it using the *current* schema, which - // may be different from the schema of the genesis state we just deleted. + // If we just deleted the genesis state, re-store it using the current* schema. if self.get_split_slot() > 0 { info!( self.log, @@ -2883,9 +2906,15 @@ impl, Cold: ItemStore> HotColdDB "state_root" => ?genesis_state_root, ); self.store_cold_state(&genesis_state_root, genesis_state, &mut cold_ops)?; - self.cold_db.do_atomically(cold_ops)?; } + info!( + self.log, + "Deleting historic states"; + "delete_ops" => delete_ops, + ); + self.cold_db.do_atomically(cold_ops)?; + // In order to reclaim space, we need to compact the freezer DB as well. self.cold_db.compact()?; @@ -2962,7 +2991,6 @@ pub fn migrate_database, Cold: ItemStore>( // boundary (in order for the hot state summary scheme to work). let current_split_slot = store.split.read_recursive().slot; let anchor_info = store.anchor_info.read_recursive().clone(); - let anchor_slot = anchor_info.as_ref().map(|a| a.anchor_slot); if finalized_state.slot() < current_split_slot { return Err(HotColdDBError::FreezeSlotError { @@ -2979,28 +3007,20 @@ pub fn migrate_database, Cold: ItemStore>( } let mut hot_db_ops = vec![]; - let mut cold_db_ops = vec![]; + let mut cold_db_block_ops = vec![]; let mut epoch_boundary_blocks = HashSet::new(); let mut non_checkpoint_block_roots = HashSet::new(); - // Chunk writer for the linear block roots in the freezer DB. - // Start at the new upper limit because we iterate backwards. - let new_frozen_block_root_upper_limit = finalized_state.slot().as_usize().saturating_sub(1); - let mut block_root_writer = - ChunkWriter::::new(&store.cold_db, new_frozen_block_root_upper_limit)?; - - // 1. Copy all of the states between the new finalized state and the split slot, from the hot DB - // to the cold DB. Delete the execution payloads of these now-finalized blocks. - let state_root_iter = RootsIterator::new(&store, finalized_state); - for maybe_tuple in state_root_iter.take_while(|result| match result { - Ok((_, _, slot)) => { - slot >= ¤t_split_slot - && anchor_slot.map_or(true, |anchor_slot| slot >= &anchor_slot) - } - Err(_) => true, - }) { - let (block_root, state_root, slot) = maybe_tuple?; + // Iterate in descending order until the current split slot + let state_roots = RootsIterator::new(&store, finalized_state) + .take_while(|result| match result { + Ok((_, _, slot)) => *slot >= current_split_slot, + Err(_) => true, + }) + .collect::, _>>()?; + // Then, iterate states in slot ascending order, as they are stored wrt previous states. + for (block_root, state_root, slot) in state_roots.into_iter().rev() { // Delete the execution payload if payload pruning is enabled. At a skipped slot we may // delete the payload for the finalized block itself, but that's OK as we only guarantee // that payloads are present for slots >= the split slot. The payload fetching code is also @@ -3009,6 +3029,15 @@ pub fn migrate_database, Cold: ItemStore>( hot_db_ops.push(StoreOp::DeleteExecutionPayload(block_root)); } + // Store the slot to block root mapping. + cold_db_block_ops.push(KeyValueStoreOp::PutKeyValue( + get_key_for_col( + DBColumn::BeaconBlockRoots.into(), + &slot.as_u64().to_be_bytes(), + ), + block_root.as_slice().to_vec(), + )); + // At a missed slot, `state_root_iter` will return the block root // from the previous non-missed slot. This ensures that the block root at an // epoch boundary is always a checkpoint block root. We keep track of block roots @@ -3028,40 +3057,36 @@ pub fn migrate_database, Cold: ItemStore>( // Delete the old summary, and the full state if we lie on an epoch boundary. hot_db_ops.push(StoreOp::DeleteState(state_root, Some(slot))); - // Store the block root for this slot in the linear array of frozen block roots. - block_root_writer.set(slot.as_usize(), block_root, &mut cold_db_ops)?; - // Do not try to store states if a restore point is yet to be stored, or will never be // stored (see `STATE_UPPER_LIMIT_NO_RETAIN`). Make an exception for the genesis state // which always needs to be copied from the hot DB to the freezer and should not be deleted. - if slot != 0 - && anchor_info - .as_ref() - .map_or(false, |anchor| slot < anchor.state_upper_limit) - { + if slot != 0 && slot < anchor_info.state_upper_limit { debug!(store.log, "Pruning finalized state"; "slot" => slot); - continue; } - // Store a pointer from this state root to its slot, so we can later reconstruct states - // from their state root alone. - let cold_state_summary = ColdStateSummary { slot }; - let op = cold_state_summary.as_kv_store_op(state_root); - cold_db_ops.push(op); + let mut cold_db_ops = vec![]; - if slot % store.config.slots_per_restore_point == 0 { - let state: BeaconState = get_full_state(&store.hot_db, &state_root, &store.spec)? + // Only store the cold state if it's on a diff boundary. + // Calling `store_cold_state_summary` instead of `store_cold_state` for those allows us + // to skip loading many hot states. + if matches!( + store.hierarchy.storage_strategy(slot)?, + StorageStrategy::ReplayFrom(..) + ) { + // Store slot -> state_root and state_root -> slot mappings. + store.store_cold_state_summary(&state_root, slot, &mut cold_db_ops)?; + } else { + let state: BeaconState = store + .get_hot_state(&state_root)? .ok_or(HotColdDBError::MissingStateToFreeze(state_root))?; store.store_cold_state(&state_root, &state, &mut cold_db_ops)?; - - // Commit the batch of cold DB ops whenever a full state is written. Each state stored - // may read the linear fields of previous states stored. - store - .cold_db - .do_atomically(std::mem::take(&mut cold_db_ops))?; } + + // Cold states are diffed with respect to each other, so we need to finish writing previous + // states before storing new ones. + store.cold_db.do_atomically(cold_db_ops)?; } // Prune sync committee branch data for all non checkpoint block roots. @@ -3077,10 +3102,6 @@ pub fn migrate_database, Cold: ItemStore>( hot_db_ops.push(StoreOp::DeleteSyncCommitteeBranch(block_root)); }); - // Finish writing the block roots and commit the remaining cold DB ops. - block_root_writer.write(&mut cold_db_ops)?; - store.cold_db.do_atomically(cold_db_ops)?; - // Warning: Critical section. We have to take care not to put any of the two databases in an // inconsistent state if the OS process dies at any point during the freezing // procedure. @@ -3090,8 +3111,7 @@ pub fn migrate_database, Cold: ItemStore>( // at any point below but it may happen that some states won't be deleted from the hot database // and will remain there forever. Since dying in these particular few lines should be an // exceedingly rare event, this should be an acceptable tradeoff. - - // Flush to disk all the states that have just been migrated to the cold store. + store.cold_db.do_atomically(cold_db_block_ops)?; store.cold_db.sync()?; { let mut split_guard = store.split.write(); @@ -3237,27 +3257,7 @@ pub(crate) struct ColdStateSummary { impl StoreItem for ColdStateSummary { fn db_column() -> DBColumn { - DBColumn::BeaconStateSummary - } - - fn as_store_bytes(&self) -> Vec { - self.as_ssz_bytes() - } - - fn from_store_bytes(bytes: &[u8]) -> Result { - Ok(Self::from_ssz_bytes(bytes)?) - } -} - -/// Struct for storing the state root of a restore point in the database. -#[derive(Debug, Clone, Copy, Default, Encode, Decode)] -struct RestorePointHash { - state_root: Hash256, -} - -impl StoreItem for RestorePointHash { - fn db_column() -> DBColumn { - DBColumn::BeaconRestorePoint + DBColumn::BeaconColdStateSummary } fn as_store_bytes(&self) -> Vec { diff --git a/beacon_node/store/src/lib.rs b/beacon_node/store/src/lib.rs index 1d02bfbb3c..0498c7c1e2 100644 --- a/beacon_node/store/src/lib.rs +++ b/beacon_node/store/src/lib.rs @@ -7,7 +7,6 @@ //! //! Provides a simple API for storing/retrieving all types that sometimes needs type-hints. See //! tests for implementation examples. -mod chunk_writer; pub mod chunked_iter; pub mod chunked_vector; pub mod config; @@ -15,25 +14,25 @@ pub mod consensus_context; pub mod errors; mod forwards_iter; mod garbage_collection; +pub mod hdiff; +pub mod historic_state_cache; pub mod hot_cold_store; mod impls; mod leveldb_store; mod memory_store; pub mod metadata; pub mod metrics; -mod partial_beacon_state; +pub mod partial_beacon_state; pub mod reconstruct; pub mod state_cache; pub mod iter; -pub use self::chunk_writer::ChunkWriter; pub use self::config::StoreConfig; pub use self::consensus_context::OnDiskConsensusContext; pub use self::hot_cold_store::{HotColdDB, HotStateSummary, Split}; pub use self::leveldb_store::LevelDB; pub use self::memory_store::MemoryStore; -pub use self::partial_beacon_state::PartialBeaconState; pub use crate::metadata::BlobInfo; pub use errors::Error; pub use impls::beacon_state::StorageContainer as BeaconStateStorageContainer; @@ -251,6 +250,11 @@ pub enum DBColumn { /// For data related to the database itself. #[strum(serialize = "bma")] BeaconMeta, + /// Data related to blocks. + /// + /// - Key: `Hash256` block root. + /// - Value in hot DB: SSZ-encoded blinded block. + /// - Value in cold DB: 8-byte slot of block. #[strum(serialize = "blk")] BeaconBlock, #[strum(serialize = "blb")] @@ -260,9 +264,21 @@ pub enum DBColumn { /// For full `BeaconState`s in the hot database (finalized or fork-boundary states). #[strum(serialize = "ste")] BeaconState, - /// For the mapping from state roots to their slots or summaries. + /// For beacon state snapshots in the freezer DB. + #[strum(serialize = "bsn")] + BeaconStateSnapshot, + /// For compact `BeaconStateDiff`s in the freezer DB. + #[strum(serialize = "bsd")] + BeaconStateDiff, + /// Mapping from state root to `HotStateSummary` in the hot DB. + /// + /// Previously this column also served a role in the freezer DB, mapping state roots to + /// `ColdStateSummary`. However that role is now filled by `BeaconColdStateSummary`. #[strum(serialize = "bss")] BeaconStateSummary, + /// Mapping from state root to `ColdStateSummary` in the cold DB. + #[strum(serialize = "bcs")] + BeaconColdStateSummary, /// For the list of temporary states stored during block import, /// and then made non-temporary by the deletion of their state root from this column. #[strum(serialize = "bst")] @@ -281,15 +297,37 @@ pub enum DBColumn { ForkChoice, #[strum(serialize = "pkc")] PubkeyCache, - /// For the table mapping restore point numbers to state roots. + /// For the legacy table mapping restore point numbers to state roots. + /// + /// DEPRECATED. Can be removed once schema v22 is buried by a hard fork. #[strum(serialize = "brp")] BeaconRestorePoint, - #[strum(serialize = "bbr")] - BeaconBlockRoots, - #[strum(serialize = "bsr")] + /// Mapping from slot to beacon state root in the freezer DB. + /// + /// This new column was created to replace the previous `bsr` column. The replacement was + /// necessary to guarantee atomicity of the upgrade migration. + #[strum(serialize = "bsx")] BeaconStateRoots, + /// DEPRECATED. This is the previous column for beacon state roots stored by "chunk index". + /// + /// Can be removed once schema v22 is buried by a hard fork. + #[strum(serialize = "bsr")] + BeaconStateRootsChunked, + /// Mapping from slot to beacon block root in the freezer DB. + /// + /// This new column was created to replace the previous `bbr` column. The replacement was + /// necessary to guarantee atomicity of the upgrade migration. + #[strum(serialize = "bbx")] + BeaconBlockRoots, + /// DEPRECATED. This is the previous column for beacon block roots stored by "chunk index". + /// + /// Can be removed once schema v22 is buried by a hard fork. + #[strum(serialize = "bbr")] + BeaconBlockRootsChunked, + /// DEPRECATED. Can be removed once schema v22 is buried by a hard fork. #[strum(serialize = "bhr")] BeaconHistoricalRoots, + /// DEPRECATED. Can be removed once schema v22 is buried by a hard fork. #[strum(serialize = "brm")] BeaconRandaoMixes, #[strum(serialize = "dht")] @@ -297,6 +335,7 @@ pub enum DBColumn { /// For Optimistically Imported Merge Transition Blocks #[strum(serialize = "otb")] OptimisticTransitionBlock, + /// DEPRECATED. Can be removed once schema v22 is buried by a hard fork. #[strum(serialize = "bhs")] BeaconHistoricalSummaries, #[strum(serialize = "olc")] @@ -338,6 +377,7 @@ impl DBColumn { | Self::BeaconState | Self::BeaconBlob | Self::BeaconStateSummary + | Self::BeaconColdStateSummary | Self::BeaconStateTemporary | Self::ExecPayload | Self::BeaconChain @@ -349,10 +389,14 @@ impl DBColumn { | Self::DhtEnrs | Self::OptimisticTransitionBlock => 32, Self::BeaconBlockRoots + | Self::BeaconBlockRootsChunked | Self::BeaconStateRoots + | Self::BeaconStateRootsChunked | Self::BeaconHistoricalRoots | Self::BeaconHistoricalSummaries | Self::BeaconRandaoMixes + | Self::BeaconStateSnapshot + | Self::BeaconStateDiff | Self::SyncCommittee | Self::SyncCommitteeBranch | Self::LightClientUpdate => 8, diff --git a/beacon_node/store/src/metadata.rs b/beacon_node/store/src/metadata.rs index 0c93251fe2..3f076a767a 100644 --- a/beacon_node/store/src/metadata.rs +++ b/beacon_node/store/src/metadata.rs @@ -4,7 +4,7 @@ use ssz::{Decode, Encode}; use ssz_derive::{Decode, Encode}; use types::{Checkpoint, Hash256, Slot}; -pub const CURRENT_SCHEMA_VERSION: SchemaVersion = SchemaVersion(21); +pub const CURRENT_SCHEMA_VERSION: SchemaVersion = SchemaVersion(22); // All the keys that get stored under the `BeaconMeta` column. // @@ -21,6 +21,27 @@ 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); +/// The `AnchorInfo` encoding full availability of all historic blocks & states. +pub const ANCHOR_FOR_ARCHIVE_NODE: AnchorInfo = AnchorInfo { + anchor_slot: Slot::new(0), + oldest_block_slot: Slot::new(0), + oldest_block_parent: Hash256::ZERO, + state_upper_limit: Slot::new(0), + state_lower_limit: Slot::new(0), +}; + +/// The `AnchorInfo` encoding an uninitialized anchor. +/// +/// This value should never exist except on initial start-up prior to the anchor being initialised +/// by `init_anchor_info`. +pub const ANCHOR_UNINITIALIZED: AnchorInfo = AnchorInfo { + anchor_slot: Slot::new(u64::MAX), + oldest_block_slot: Slot::new(u64::MAX), + oldest_block_parent: Hash256::ZERO, + state_upper_limit: Slot::new(u64::MAX), + state_lower_limit: Slot::new(0), +}; + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub struct SchemaVersion(pub u64); @@ -88,17 +109,47 @@ impl StoreItem for CompactionTimestamp { /// Database parameters relevant to weak subjectivity sync. #[derive(Debug, PartialEq, Eq, Clone, Encode, Decode, Serialize, Deserialize)] pub struct AnchorInfo { - /// The slot at which the anchor state is present and which we cannot revert. + /// The slot at which the anchor state is present and which we cannot revert. Values on start: + /// - Genesis start: 0 + /// - Checkpoint sync: Slot of the finalized checkpoint block + /// + /// Immutable pub anchor_slot: Slot, - /// The slot from which historical blocks are available (>=). + /// All blocks with slots greater than or equal to this value are available in the database. + /// Additionally, the genesis block is always available. + /// + /// Values on start: + /// - Genesis start: 0 + /// - Checkpoint sync: Slot of the finalized checkpoint block + /// + /// Progressively decreases during backfill sync until reaching 0. pub oldest_block_slot: Slot, /// The block root of the next block that needs to be added to fill in the history. /// /// Zero if we know all blocks back to genesis. pub oldest_block_parent: Hash256, - /// The slot from which historical states are available (>=). + /// All states with slots _greater than or equal to_ `min(split.slot, state_upper_limit)` are + /// available in the database. If `state_upper_limit` is higher than `split.slot`, states are + /// not being written to the freezer database. + /// + /// Values on start if state reconstruction is enabled: + /// - Genesis start: 0 + /// - Checkpoint sync: Slot of the next scheduled snapshot + /// + /// Value on start if state reconstruction is disabled: + /// - 2^64 - 1 representing no historic state storage. + /// + /// Immutable until state reconstruction completes. pub state_upper_limit: Slot, - /// The slot before which historical states are available (<=). + /// All states with slots _less than or equal to_ this value are available in the database. + /// The minimum value is 0, indicating that the genesis state is always available. + /// + /// Values on start: + /// - Genesis start: 0 + /// - Checkpoint sync: 0 + /// + /// When full block backfill completes (`oldest_block_slot == 0`) state reconstruction starts and + /// this value will progressively increase until reaching `state_upper_limit`. pub state_lower_limit: Slot, } @@ -109,6 +160,21 @@ impl AnchorInfo { pub fn block_backfill_complete(&self, target_slot: Slot) -> bool { self.oldest_block_slot <= target_slot } + + /// Return true if all historic states are stored, i.e. if state reconstruction is complete. + pub fn all_historic_states_stored(&self) -> bool { + self.state_lower_limit == self.state_upper_limit + } + + /// Return true if no historic states other than genesis are stored in the database. + pub fn no_historic_states_stored(&self, split_slot: Slot) -> bool { + self.state_lower_limit == 0 && self.state_upper_limit >= split_slot + } + + /// Return true if no historic states other than genesis *will ever be stored*. + pub fn full_state_pruning_enabled(&self) -> bool { + self.state_lower_limit == 0 && self.state_upper_limit == STATE_UPPER_LIMIT_NO_RETAIN + } } impl StoreItem for AnchorInfo { diff --git a/beacon_node/store/src/metrics.rs b/beacon_node/store/src/metrics.rs index 1921b9b327..f0dd061790 100644 --- a/beacon_node/store/src/metrics.rs +++ b/beacon_node/store/src/metrics.rs @@ -73,6 +73,27 @@ pub static DISK_DB_DELETE_COUNT: LazyLock> = LazyLock::new &["col"], ) }); +/* + * Anchor Info + */ +pub static STORE_BEACON_ANCHOR_SLOT: LazyLock> = LazyLock::new(|| { + try_create_int_gauge( + "store_beacon_anchor_slot", + "Current anchor info anchor_slot value", + ) +}); +pub static STORE_BEACON_OLDEST_BLOCK_SLOT: LazyLock> = LazyLock::new(|| { + try_create_int_gauge( + "store_beacon_oldest_block_slot", + "Current anchor info oldest_block_slot value", + ) +}); +pub static STORE_BEACON_STATE_LOWER_LIMIT: LazyLock> = LazyLock::new(|| { + try_create_int_gauge( + "store_beacon_state_lower_limit", + "Current anchor info state_lower_limit value", + ) +}); /* * Beacon State */ @@ -130,6 +151,24 @@ pub static BEACON_STATE_WRITE_BYTES: LazyLock> = LazyLock::ne "Total number of beacon state bytes written to the DB", ) }); +pub static BEACON_HDIFF_READ_TIMES: LazyLock> = LazyLock::new(|| { + try_create_histogram( + "store_hdiff_read_seconds", + "Time required to read the hierarchical diff bytes from the database", + ) +}); +pub static BEACON_HDIFF_DECODE_TIMES: LazyLock> = LazyLock::new(|| { + try_create_histogram( + "store_hdiff_decode_seconds", + "Time required to decode hierarchical diff bytes", + ) +}); +pub static BEACON_HDIFF_BUFFER_CLONE_TIMES: LazyLock> = LazyLock::new(|| { + try_create_histogram( + "store_hdiff_buffer_clone_seconds", + "Time required to clone hierarchical diff buffer bytes", + ) +}); /* * Beacon Block */ @@ -145,12 +184,181 @@ pub static BEACON_BLOCK_CACHE_HIT_COUNT: LazyLock> = LazyLock "Number of hits to the store's block cache", ) }); + +/* + * Caches + */ pub static BEACON_BLOBS_CACHE_HIT_COUNT: LazyLock> = LazyLock::new(|| { try_create_int_counter( "store_beacon_blobs_cache_hit_total", "Number of hits to the store's blob cache", ) }); +pub static STORE_BEACON_BLOCK_CACHE_SIZE: LazyLock> = LazyLock::new(|| { + try_create_int_gauge( + "store_beacon_block_cache_size", + "Current count of items in beacon store block cache", + ) +}); +pub static STORE_BEACON_BLOB_CACHE_SIZE: LazyLock> = LazyLock::new(|| { + try_create_int_gauge( + "store_beacon_blob_cache_size", + "Current count of items in beacon store blob cache", + ) +}); +pub static STORE_BEACON_STATE_CACHE_SIZE: LazyLock> = LazyLock::new(|| { + try_create_int_gauge( + "store_beacon_state_cache_size", + "Current count of items in beacon store state cache", + ) +}); +pub static STORE_BEACON_HISTORIC_STATE_CACHE_SIZE: LazyLock> = + LazyLock::new(|| { + try_create_int_gauge( + "store_beacon_historic_state_cache_size", + "Current count of states in the historic state cache", + ) + }); +pub static STORE_BEACON_HDIFF_BUFFER_CACHE_SIZE: LazyLock> = LazyLock::new(|| { + try_create_int_gauge( + "store_beacon_hdiff_buffer_cache_size", + "Current count of hdiff buffers in the historic state cache", + ) +}); +pub static STORE_BEACON_HDIFF_BUFFER_CACHE_BYTE_SIZE: LazyLock> = + LazyLock::new(|| { + try_create_int_gauge( + "store_beacon_hdiff_buffer_cache_byte_size", + "Memory consumed by hdiff buffers in the historic state cache", + ) + }); +pub static STORE_BEACON_STATE_FREEZER_COMPRESS_TIME: LazyLock> = + LazyLock::new(|| { + try_create_histogram( + "store_beacon_state_compress_seconds", + "Time taken to compress a state snapshot for the freezer DB", + ) + }); +pub static STORE_BEACON_STATE_FREEZER_DECOMPRESS_TIME: LazyLock> = + LazyLock::new(|| { + try_create_histogram( + "store_beacon_state_decompress_seconds", + "Time taken to decompress a state snapshot for the freezer DB", + ) + }); +pub static STORE_BEACON_HDIFF_BUFFER_APPLY_TIME: LazyLock> = + LazyLock::new(|| { + try_create_histogram( + "store_beacon_hdiff_buffer_apply_seconds", + "Time taken to apply hdiff buffer to a state buffer", + ) + }); +pub static STORE_BEACON_HDIFF_BUFFER_COMPUTE_TIME: LazyLock> = + LazyLock::new(|| { + try_create_histogram( + "store_beacon_hdiff_buffer_compute_seconds", + "Time taken to compute hdiff buffer to a state buffer", + ) + }); +pub static STORE_BEACON_HDIFF_BUFFER_LOAD_TIME: LazyLock> = LazyLock::new(|| { + try_create_histogram( + "store_beacon_hdiff_buffer_load_seconds", + "Time taken to load an hdiff buffer", + ) +}); +pub static STORE_BEACON_HDIFF_BUFFER_LOAD_FOR_STORE_TIME: LazyLock> = + LazyLock::new(|| { + try_create_histogram( + "store_beacon_hdiff_buffer_load_for_store_seconds", + "Time taken to load an hdiff buffer to store another hdiff", + ) + }); +pub static STORE_BEACON_HISTORIC_STATE_CACHE_HIT: LazyLock> = + LazyLock::new(|| { + try_create_int_counter( + "store_beacon_historic_state_cache_hit_total", + "Total count of historic state cache hits for full states", + ) + }); +pub static STORE_BEACON_HISTORIC_STATE_CACHE_MISS: LazyLock> = + LazyLock::new(|| { + try_create_int_counter( + "store_beacon_historic_state_cache_miss_total", + "Total count of historic state cache misses for full states", + ) + }); +pub static STORE_BEACON_HDIFF_BUFFER_CACHE_HIT: LazyLock> = + LazyLock::new(|| { + try_create_int_counter( + "store_beacon_hdiff_buffer_cache_hit_total", + "Total count of hdiff buffer cache hits", + ) + }); +pub static STORE_BEACON_HDIFF_BUFFER_CACHE_MISS: LazyLock> = + LazyLock::new(|| { + try_create_int_counter( + "store_beacon_hdiff_buffer_cache_miss_total", + "Total count of hdiff buffer cache miss", + ) + }); +pub static STORE_BEACON_HDIFF_BUFFER_INTO_STATE_TIME: LazyLock> = + LazyLock::new(|| { + try_create_histogram( + "store_beacon_hdiff_buffer_into_state_seconds", + "Time taken to recreate a BeaconState from an hdiff buffer", + ) + }); +pub static STORE_BEACON_HDIFF_BUFFER_FROM_STATE_TIME: LazyLock> = + LazyLock::new(|| { + try_create_histogram( + "store_beacon_hdiff_buffer_from_state_seconds", + "Time taken to create an hdiff buffer from a BeaconState", + ) + }); +pub static STORE_BEACON_REPLAYED_BLOCKS: LazyLock> = LazyLock::new(|| { + try_create_int_counter( + "store_beacon_replayed_blocks_total", + "Total count of replayed blocks", + ) +}); +pub static STORE_BEACON_LOAD_COLD_BLOCKS_TIME: LazyLock> = LazyLock::new(|| { + try_create_histogram( + "store_beacon_load_cold_blocks_time", + "Time spent loading blocks to replay for historic states", + ) +}); +pub static STORE_BEACON_LOAD_HOT_BLOCKS_TIME: LazyLock> = LazyLock::new(|| { + try_create_histogram( + "store_beacon_load_hot_blocks_time", + "Time spent loading blocks to replay for hot states", + ) +}); +pub static STORE_BEACON_REPLAY_COLD_BLOCKS_TIME: LazyLock> = + LazyLock::new(|| { + try_create_histogram( + "store_beacon_replay_cold_blocks_time", + "Time spent replaying blocks for historic states", + ) + }); +pub static STORE_BEACON_COLD_BUILD_BEACON_CACHES_TIME: LazyLock> = + LazyLock::new(|| { + try_create_histogram( + "store_beacon_cold_build_beacon_caches_time", + "Time spent building caches on historic states", + ) + }); +pub static STORE_BEACON_REPLAY_HOT_BLOCKS_TIME: LazyLock> = LazyLock::new(|| { + try_create_histogram( + "store_beacon_replay_hot_blocks_time", + "Time spent replaying blocks for hot states", + ) +}); +pub static STORE_BEACON_RECONSTRUCTION_TIME: LazyLock> = LazyLock::new(|| { + try_create_histogram( + "store_beacon_reconstruction_time_seconds", + "Time taken to run a reconstruct historic states batch", + ) +}); pub static BEACON_DATA_COLUMNS_CACHE_HIT_COUNT: LazyLock> = LazyLock::new(|| { try_create_int_counter( diff --git a/beacon_node/store/src/partial_beacon_state.rs b/beacon_node/store/src/partial_beacon_state.rs index 8a66ec121e..2eb40f47b1 100644 --- a/beacon_node/store/src/partial_beacon_state.rs +++ b/beacon_node/store/src/partial_beacon_state.rs @@ -1,18 +1,20 @@ use crate::chunked_vector::{ - load_variable_list_from_db, load_vector_from_db, BlockRoots, HistoricalRoots, - HistoricalSummaries, RandaoMixes, StateRoots, + load_variable_list_from_db, load_vector_from_db, BlockRootsChunked, HistoricalRoots, + HistoricalSummaries, RandaoMixes, StateRootsChunked, }; -use crate::{get_key_for_col, DBColumn, Error, KeyValueStore, KeyValueStoreOp}; -use ssz::{Decode, DecodeError, Encode}; +use crate::{Error, KeyValueStore}; +use ssz::{Decode, DecodeError}; use ssz_derive::{Decode, Encode}; use std::sync::Arc; use types::historical_summary::HistoricalSummary; use types::superstruct; use types::*; -/// Lightweight variant of the `BeaconState` that is stored in the database. +/// DEPRECATED Lightweight variant of the `BeaconState` that is stored in the database. /// /// Utilises lazy-loading from separate storage for its vector fields. +/// +/// This can be deleted once schema versions prior to V22 are no longer supported. #[superstruct( variants(Base, Altair, Bellatrix, Capella, Deneb, Electra), variant_attributes(derive(Debug, PartialEq, Clone, Encode, Decode)) @@ -142,163 +144,7 @@ where pub pending_consolidations: List, } -/// Implement the conversion function from BeaconState -> PartialBeaconState. -macro_rules! impl_from_state_forgetful { - ($s:ident, $outer:ident, $variant_name:ident, $struct_name:ident, [$($extra_fields:ident),*], [$($extra_fields_opt:ident),*]) => { - PartialBeaconState::$variant_name($struct_name { - // Versioning - genesis_time: $s.genesis_time, - genesis_validators_root: $s.genesis_validators_root, - slot: $s.slot, - fork: $s.fork, - - // History - latest_block_header: $s.latest_block_header.clone(), - block_roots: None, - state_roots: None, - historical_roots: None, - - // Eth1 - eth1_data: $s.eth1_data.clone(), - eth1_data_votes: $s.eth1_data_votes.clone(), - eth1_deposit_index: $s.eth1_deposit_index, - - // Validator registry - validators: $s.validators.clone(), - balances: $s.balances.clone(), - - // Shuffling - latest_randao_value: *$outer - .get_randao_mix($outer.current_epoch()) - .expect("randao at current epoch is OK"), - randao_mixes: None, - - // Slashings - slashings: $s.slashings.clone(), - - // Finality - justification_bits: $s.justification_bits.clone(), - previous_justified_checkpoint: $s.previous_justified_checkpoint, - current_justified_checkpoint: $s.current_justified_checkpoint, - finalized_checkpoint: $s.finalized_checkpoint, - - // Variant-specific fields - $( - $extra_fields: $s.$extra_fields.clone() - ),*, - - // Variant-specific optional - $( - $extra_fields_opt: None - ),* - }) - } -} - impl PartialBeaconState { - /// Convert a `BeaconState` to a `PartialBeaconState`, while dropping the optional fields. - pub fn from_state_forgetful(outer: &BeaconState) -> Self { - match outer { - BeaconState::Base(s) => impl_from_state_forgetful!( - s, - outer, - Base, - PartialBeaconStateBase, - [previous_epoch_attestations, current_epoch_attestations], - [] - ), - BeaconState::Altair(s) => impl_from_state_forgetful!( - s, - outer, - Altair, - PartialBeaconStateAltair, - [ - previous_epoch_participation, - current_epoch_participation, - current_sync_committee, - next_sync_committee, - inactivity_scores - ], - [] - ), - BeaconState::Bellatrix(s) => impl_from_state_forgetful!( - s, - outer, - Bellatrix, - PartialBeaconStateBellatrix, - [ - previous_epoch_participation, - current_epoch_participation, - current_sync_committee, - next_sync_committee, - inactivity_scores, - latest_execution_payload_header - ], - [] - ), - BeaconState::Capella(s) => impl_from_state_forgetful!( - s, - outer, - Capella, - PartialBeaconStateCapella, - [ - previous_epoch_participation, - current_epoch_participation, - current_sync_committee, - next_sync_committee, - inactivity_scores, - latest_execution_payload_header, - next_withdrawal_index, - next_withdrawal_validator_index - ], - [historical_summaries] - ), - BeaconState::Deneb(s) => impl_from_state_forgetful!( - s, - outer, - Deneb, - PartialBeaconStateDeneb, - [ - previous_epoch_participation, - current_epoch_participation, - current_sync_committee, - next_sync_committee, - inactivity_scores, - latest_execution_payload_header, - next_withdrawal_index, - next_withdrawal_validator_index - ], - [historical_summaries] - ), - BeaconState::Electra(s) => impl_from_state_forgetful!( - s, - outer, - Electra, - PartialBeaconStateElectra, - [ - previous_epoch_participation, - current_epoch_participation, - current_sync_committee, - next_sync_committee, - inactivity_scores, - latest_execution_payload_header, - next_withdrawal_index, - next_withdrawal_validator_index, - deposit_requests_start_index, - deposit_balance_to_consume, - exit_balance_to_consume, - earliest_exit_epoch, - consolidation_balance_to_consume, - earliest_consolidation_epoch, - pending_balance_deposits, - pending_partial_withdrawals, - pending_consolidations - ], - [historical_summaries] - ), - } - } - /// SSZ decode. pub fn from_ssz_bytes(bytes: &[u8], spec: &ChainSpec) -> Result { // Slot is after genesis_time (u64) and genesis_validators_root (Hash256). @@ -321,19 +167,13 @@ impl PartialBeaconState { )) } - /// Prepare the partial state for storage in the KV database. - pub fn as_kv_store_op(&self, state_root: Hash256) -> KeyValueStoreOp { - let db_key = get_key_for_col(DBColumn::BeaconState.into(), state_root.as_slice()); - KeyValueStoreOp::PutKeyValue(db_key, self.as_ssz_bytes()) - } - pub fn load_block_roots>( &mut self, store: &S, spec: &ChainSpec, ) -> Result<(), Error> { if self.block_roots().is_none() { - *self.block_roots_mut() = Some(load_vector_from_db::( + *self.block_roots_mut() = Some(load_vector_from_db::( store, self.slot(), spec, @@ -348,7 +188,7 @@ impl PartialBeaconState { spec: &ChainSpec, ) -> Result<(), Error> { if self.state_roots().is_none() { - *self.state_roots_mut() = Some(load_vector_from_db::( + *self.state_roots_mut() = Some(load_vector_from_db::( store, self.slot(), spec, diff --git a/beacon_node/store/src/reconstruct.rs b/beacon_node/store/src/reconstruct.rs index 8ef4886565..9bec83a35c 100644 --- a/beacon_node/store/src/reconstruct.rs +++ b/beacon_node/store/src/reconstruct.rs @@ -1,14 +1,16 @@ //! Implementation of historic state reconstruction (given complete block history). use crate::hot_cold_store::{HotColdDB, HotColdDBError}; +use crate::metadata::ANCHOR_FOR_ARCHIVE_NODE; +use crate::metrics; use crate::{Error, ItemStore}; use itertools::{process_results, Itertools}; -use slog::info; +use slog::{debug, info}; use state_processing::{ per_block_processing, per_slot_processing, BlockSignatureStrategy, ConsensusContext, VerifyBlockRoot, }; use std::sync::Arc; -use types::{EthSpec, Hash256}; +use types::EthSpec; impl HotColdDB where @@ -16,11 +18,16 @@ where Hot: ItemStore, Cold: ItemStore, { - pub fn reconstruct_historic_states(self: &Arc) -> Result<(), Error> { - let Some(mut anchor) = self.get_anchor_info() else { - // Nothing to do, history is complete. + pub fn reconstruct_historic_states( + self: &Arc, + num_blocks: Option, + ) -> Result<(), Error> { + let mut anchor = self.get_anchor_info(); + + // Nothing to do, history is complete. + if anchor.all_historic_states_stored() { return Ok(()); - }; + } // Check that all historic blocks are known. if anchor.oldest_block_slot != 0 { @@ -29,37 +36,30 @@ where }); } - info!( + debug!( self.log, - "Beginning historic state reconstruction"; + "Starting state reconstruction batch"; "start_slot" => anchor.state_lower_limit, ); - let slots_per_restore_point = self.config.slots_per_restore_point; + let _t = metrics::start_timer(&metrics::STORE_BEACON_RECONSTRUCTION_TIME); // Iterate blocks from the state lower limit to the upper limit. - let lower_limit_slot = anchor.state_lower_limit; let split = self.get_split_info(); - let upper_limit_state = self.get_restore_point( - anchor.state_upper_limit.as_u64() / slots_per_restore_point, - &split, - )?; - let upper_limit_slot = upper_limit_state.slot(); + let lower_limit_slot = anchor.state_lower_limit; + let upper_limit_slot = std::cmp::min(split.slot, anchor.state_upper_limit); - // Use a dummy root, as we never read the block for the upper limit state. - let upper_limit_block_root = Hash256::repeat_byte(0xff); - - let block_root_iter = self.forwards_block_roots_iterator( - lower_limit_slot, - upper_limit_state, - upper_limit_block_root, - &self.spec, - )?; + // If `num_blocks` is not specified iterate all blocks. Add 1 so that we end on an epoch + // boundary when `num_blocks` is a multiple of an epoch boundary. We want to be *inclusive* + // of the state at slot `lower_limit_slot + num_blocks`. + let block_root_iter = self + .forwards_block_roots_iterator_until(lower_limit_slot, upper_limit_slot - 1, || { + Err(Error::StateShouldNotBeRequired(upper_limit_slot - 1)) + })? + .take(num_blocks.map_or(usize::MAX, |n| n + 1)); // The state to be advanced. - let mut state = self - .load_cold_state_by_slot(lower_limit_slot)? - .ok_or(HotColdDBError::MissingLowerLimitState(lower_limit_slot))?; + let mut state = self.load_cold_state_by_slot(lower_limit_slot)?; state.build_caches(&self.spec)?; @@ -110,8 +110,19 @@ where // Stage state for storage in freezer DB. self.store_cold_state(&state_root, &state, &mut io_batch)?; - // If the slot lies on an epoch boundary, commit the batch and update the anchor. - if slot % slots_per_restore_point == 0 || slot + 1 == upper_limit_slot { + let batch_complete = + num_blocks.map_or(false, |n_blocks| slot == lower_limit_slot + n_blocks as u64); + let reconstruction_complete = slot + 1 == upper_limit_slot; + + // Commit the I/O batch if: + // + // - The diff/snapshot for this slot is required for future slots, or + // - The reconstruction batch is complete (we are about to return), or + // - Reconstruction is complete. + if self.hierarchy.should_commit_immediately(slot)? + || batch_complete + || reconstruction_complete + { info!( self.log, "State reconstruction in progress"; @@ -122,9 +133,9 @@ where self.cold_db.do_atomically(std::mem::take(&mut io_batch))?; // Update anchor. - let old_anchor = Some(anchor.clone()); + let old_anchor = anchor.clone(); - if slot + 1 == upper_limit_slot { + if reconstruction_complete { // The two limits have met in the middle! We're done! // Perform one last integrity check on the state reached. let computed_state_root = state.update_tree_hash_cache()?; @@ -136,23 +147,36 @@ where }); } - self.compare_and_set_anchor_info_with_write(old_anchor, None)?; + self.compare_and_set_anchor_info_with_write( + old_anchor, + ANCHOR_FOR_ARCHIVE_NODE, + )?; return Ok(()); } else { // The lower limit has been raised, store it. anchor.state_lower_limit = slot; - self.compare_and_set_anchor_info_with_write( - old_anchor, - Some(anchor.clone()), - )?; + self.compare_and_set_anchor_info_with_write(old_anchor, anchor.clone())?; + } + + // If this is the end of the batch, return Ok. The caller will run another + // batch when there is idle capacity. + if batch_complete { + debug!( + self.log, + "Finished state reconstruction batch"; + "start_slot" => lower_limit_slot, + "end_slot" => slot, + ); + return Ok(()); } } } - // Should always reach the `upper_limit_slot` and return early above. - Err(Error::StateReconstructionDidNotComplete) + // Should always reach the `upper_limit_slot` or the end of the batch and return early + // above. + Err(Error::StateReconstructionLogicError) })??; // Check that the split point wasn't mutated during the state reconstruction process. diff --git a/beacon_node/tests/test.rs b/beacon_node/tests/test.rs index 4be6536df9..0738b12ec0 100644 --- a/beacon_node/tests/test.rs +++ b/beacon_node/tests/test.rs @@ -26,7 +26,6 @@ fn build_node(env: &mut Environment) -> LocalBeaconNode { fn http_server_genesis_state() { let mut env = env_builder() .test_logger() - //.async_logger("debug", None) .expect("should build env logger") .multi_threaded_tokio_runtime() .expect("should start tokio runtime") diff --git a/book/src/advanced_database.md b/book/src/advanced_database.md index 345fff6981..d8d6ea61a1 100644 --- a/book/src/advanced_database.md +++ b/book/src/advanced_database.md @@ -7,59 +7,70 @@ the _freezer_ or _cold DB_, and the portion storing recent states as the _hot DB In both the hot and cold DBs, full `BeaconState` data structures are only stored periodically, and intermediate states are reconstructed by quickly replaying blocks on top of the nearest state. For example, to fetch a state at slot 7 the database might fetch a full state from slot 0, and replay -blocks from slots 1-7 while omitting redundant signature checks and Merkle root calculations. The -full states upon which blocks are replayed are referred to as _restore points_ in the case of the +blocks from slots 1-7 while omitting redundant signature checks and Merkle root calculations. In +the freezer DB, Lighthouse also uses hierarchical state diffs to jump larger distances (described in +more detail below). + +The full states upon which blocks are replayed are referred to as _snapshots_ in the case of the freezer DB, and _epoch boundary states_ in the case of the hot DB. The frequency at which the hot database stores full `BeaconState`s is fixed to one-state-per-epoch in order to keep loads of recent states performant. For the freezer DB, the frequency is -configurable via the `--slots-per-restore-point` CLI flag, which is the topic of the next section. +configurable via the `--hierarchy-exponents` CLI flag, which is the topic of the next section. -## Freezer DB Space-time Trade-offs +## Hierarchical State Diffs -Frequent restore points use more disk space but accelerate the loading of historical states. -Conversely, infrequent restore points use much less space, but cause the loading of historical -states to slow down dramatically. A lower _slots per restore point_ value (SPRP) corresponds to more -frequent restore points, while a higher SPRP corresponds to less frequent. The table below shows -some example values. +Since v6.0.0, Lighthouse's freezer database uses _hierarchical state diffs_ or _hdiffs_ for short. +These diffs allow Lighthouse to reconstruct any historic state relatively quickly from a very +compact database. The essence of the hdiffs is that full states (snapshots) are stored only around +once per year. To reconstruct a particular state, Lighthouse fetches the last snapshot prior to that +state, and then applies several _layers_ of diffs. For example, to access a state from November +2022, we might fetch the yearly snapshot for the start of 2022, then apply a monthly diff to jump to +November, and then more granular diffs to reach the particular week, day and epoch desired. +Usually for the last stretch between the start of the epoch and the state requested, some blocks +will be _replayed_. -| Use Case | SPRP | Yearly Disk Usage*| Load Historical State | -|----------------------------|------|-------------------|-----------------------| -| Research | 32 | more than 10 TB | 155 ms | -| Enthusiast (prev. default) | 2048 | hundreds of GB | 10.2 s | -| Validator only (default) | 8192 | tens of GB | 41 s | +The following diagram shows part of the layout of diffs in the default configuration. There is a +full snapshot stored every `2^21` slots. In the next layer there are diffs every `2^18` slots which +approximately correspond to "monthly" diffs. Following this are more granular diffs every `2^16` +slots, every `2^13` slots, and so on down to the per-epoch diffs every `2^5` slots. -*Last update: Dec 2023. +![Tree diagram displaying hierarchical state diffs](./imgs/db-freezer-layout.png) -As we can see, it's a high-stakes trade-off! The relationships to disk usage and historical state -load time are both linear – doubling SPRP halves disk usage and doubles load time. The minimum SPRP -is 32, and the maximum is 8192. +The number of layers and frequency of diffs is configurable via the `--hierarchy-exponents` flag, +which has a default value of `5,9,11,13,16,18,21`. The hierarchy exponents must be provided in order +from smallest to largest. The smallest exponent determines the frequency of the "closest" layer +of diffs, with the default value of 5 corresponding to a diff every `2^5` slots (every epoch). +The largest number determines the frequency of full snapshots, with the default value of 21 +corresponding to a snapshot every `2^21` slots (every 291 days). -The default value is 8192 for databases synced from scratch using Lighthouse v2.2.0 or later, or -2048 for prior versions. Please see the section on [Defaults](#defaults) below. +The number of possible `--hierarchy-exponents` configurations is extremely large and our exploration +of possible configurations is still in its relative infancy. If you experiment with non-default +values of `--hierarchy-exponents` we would be interested to hear how it goes. A few rules of thumb +that we have observed are: -The values shown in the table are approximate, calculated using a simple heuristic: each -`BeaconState` consumes around 145MB of disk space, and each block replayed takes around 5ms. The -**Yearly Disk Usage** column shows the approximate size of the freezer DB _alone_ (hot DB not included), calculated proportionally using the total freezer database disk usage. -The **Load Historical State** time is the worst-case load time for a state in the last slot -before a restore point. +- **More frequent snapshots = more space**. This is quite intuitive - if you store full states more + often then these will take up more space than diffs. However what you lose in space efficiency you + may gain in speed. It would be possible to achieve a configuration similar to Lighthouse's + previous `--slots-per-restore-point 32` using `--hierarchy-exponents 5`, although this would use + _a lot_ of space. It's even possible to push beyond that with `--hierarchy-exponents 0` which + would store a full state every single slot (NOT RECOMMENDED). +- **Less diff layers are not necessarily faster**. One might expect that the fewer diff layers there + are, the less work Lighthouse would have to do to reconstruct any particular state. In practise + this seems to be offset by the increased size of diffs in each layer making the diffs take longer + to apply. We observed no significant performance benefit from `--hierarchy-exponents 5,7,11`, and + a substantial increase in space consumed. -To run a full archival node with fast access to beacon states and a SPRP of 32, the disk usage will be more than 10 TB per year, which is impractical for many users. As such, users may consider running the [tree-states](https://github.com/sigp/lighthouse/releases/tag/v5.0.111-exp) release, which only uses less than 200 GB for a full archival node. The caveat is that it is currently experimental and in alpha release (as of Dec 2023), thus not recommended for running mainnet validators. Nevertheless, it is suitable to be used for analysis purposes, and if you encounter any issues in tree-states, we do appreciate any feedback. We plan to have a stable release of tree-states in 1H 2024. - -### Defaults - -As of Lighthouse v2.2.0, the default slots-per-restore-point value has been increased from 2048 -to 8192 in order to conserve disk space. Existing nodes will continue to use SPRP=2048 unless -re-synced. Note that it is currently not possible to change the SPRP without re-syncing, although -fast re-syncing may be achieved with [Checkpoint Sync](./checkpoint-sync.md). +If in doubt, we recommend running with the default configuration! It takes a long time to +reconstruct states in any given configuration, so it might be some time before the optimal +configuration is determined. ### CLI Configuration -To configure your Lighthouse node's database with a non-default SPRP, run your Beacon Node with -the `--slots-per-restore-point` flag: +To configure your Lighthouse node's database, run your beacon node with the `--hierarchy-exponents` flag: ```bash -lighthouse beacon_node --slots-per-restore-point 32 +lighthouse beacon_node --hierarchy-exponents "5,7,11" ``` ### Historic state cache @@ -72,17 +83,20 @@ The historical state cache size can be specified with the flag `--historic-state lighthouse beacon_node --historic-state-cache-size 4 ``` -> Note: This feature will cause high memory usage. +> Note: Use a large cache limit can lead to high memory usage. ## Glossary -* _Freezer DB_: part of the database storing finalized states. States are stored in a sparser +- _Freezer DB_: part of the database storing finalized states. States are stored in a sparser format, and usually less frequently than in the hot DB. -* _Cold DB_: see _Freezer DB_. -* _Hot DB_: part of the database storing recent states, all blocks, and other runtime data. Full +- _Cold DB_: see _Freezer DB_. +- _HDiff_: hierarchical state diff. +- _Hierarchy Exponents_: configuration for hierarchical state diffs, which determines the density + of stored diffs and snapshots in the freezer DB. +- _Hot DB_: part of the database storing recent states, all blocks, and other runtime data. Full states are stored every epoch. -* _Restore Point_: a full `BeaconState` stored periodically in the freezer DB. -* _Slots Per Restore Point (SPRP)_: the number of slots between restore points in the freezer DB. -* _Split Slot_: the slot at which states are divided between the hot and the cold DBs. All states +- _Snapshot_: a full `BeaconState` stored periodically in the freezer DB. Approximately yearly by + default (every ~291 days). +- _Split Slot_: the slot at which states are divided between the hot and the cold DBs. All states from slots less than the split slot are in the freezer, while all states with slots greater than or equal to the split slot are in the hot DB. diff --git a/book/src/help_bn.md b/book/src/help_bn.md index fa4a473ec0..55815fbdfe 100644 --- a/book/src/help_bn.md +++ b/book/src/help_bn.md @@ -166,9 +166,23 @@ Options: --graffiti Specify your custom graffiti to be included in blocks. Defaults to the current version and commit, truncated to fit in 32 bytes. + --hdiff-buffer-cache-size + Number of hierarchical diff (hdiff) buffers to cache in memory. Each + buffer is around the size of a BeaconState so you should be cautious + about setting this value too high. This flag is irrelevant for most + nodes, which run with state pruning enabled. [default: 16] + --hierarchy-exponents + Specifies the frequency for storing full state snapshots and + hierarchical diffs in the freezer DB. Accepts a comma-separated list + of ascending exponents. Each exponent defines an interval for storing + diffs to the layer above. The last exponent defines the interval for + full snapshots. For example, a config of '4,8,12' would store a full + snapshot every 4096 (2^12) slots, first-level diffs every 256 (2^8) + slots, and second-level diffs every 16 (2^4) slots. Cannot be changed + after initialization. [default: 5,9,11,13,16,18,21] --historic-state-cache-size - Specifies how many states from the freezer database should cache in - memory [default: 1] + Specifies how many states from the freezer database should be cached + in memory [default: 1] --http-address
Set the listen address for the RESTful HTTP API server. --http-allow-origin @@ -364,9 +378,7 @@ Options: --slasher-validator-chunk-size Number of validators per chunk stored on disk. --slots-per-restore-point - Specifies how often a freezer DB restore point should be stored. - Cannot be changed after initialization. [default: 8192 (mainnet) or 64 - (minimal)] + DEPRECATED. This flag has no effect. --state-cache-size Specifies the size of the state cache [default: 128] --suggested-fee-recipient diff --git a/book/src/imgs/db-freezer-layout.png b/book/src/imgs/db-freezer-layout.png new file mode 100644 index 0000000000000000000000000000000000000000..1870eb42674f452335c3bc0de179b9e5e6941402 GIT binary patch literal 159462 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

-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, + pub anchor: AnchorInfo, pub blob_info: BlobInfo, } diff --git a/common/eth2_config/src/lib.rs b/common/eth2_config/src/lib.rs index cd5d7a8bd4..f13e90490e 100644 --- a/common/eth2_config/src/lib.rs +++ b/common/eth2_config/src/lib.rs @@ -32,6 +32,7 @@ const HOLESKY_GENESIS_STATE_SOURCE: GenesisStateSource = GenesisStateSource::Url ], checksum: "0xd750639607c337bbb192b15c27f447732267bf72d1650180a0e44c2d93a80741", genesis_validators_root: "0x9143aa7c615a7f7115e2b6aac319c03529df8242ae705fba9df39b79c59fa8b1", + genesis_state_root: "0x0ea3f6f9515823b59c863454675fefcd1d8b4f2dbe454db166206a41fda060a0", }; const CHIADO_GENESIS_STATE_SOURCE: GenesisStateSource = GenesisStateSource::Url { @@ -39,6 +40,7 @@ const CHIADO_GENESIS_STATE_SOURCE: GenesisStateSource = GenesisStateSource::Url urls: &[], checksum: "0xd4a039454c7429f1dfaa7e11e397ef3d0f50d2d5e4c0e4dc04919d153aa13af1", genesis_validators_root: "0x9d642dac73058fbf39c0ae41ab1e34e4d889043cb199851ded7095bc99eb4c1e", + genesis_state_root: "0xa48419160f8f146ecaa53d12a5d6e1e6af414a328afdc56b60d5002bb472a077", }; /// The core configuration of a Lighthouse beacon node. @@ -100,6 +102,10 @@ pub enum GenesisStateSource { /// /// The format should be 0x-prefixed ASCII bytes. genesis_validators_root: &'static str, + /// The genesis state root. + /// + /// The format should be 0x-prefixed ASCII bytes. + genesis_state_root: &'static str, }, } diff --git a/common/eth2_network_config/src/lib.rs b/common/eth2_network_config/src/lib.rs index 3d0ffc5b9e..5d5a50574b 100644 --- a/common/eth2_network_config/src/lib.rs +++ b/common/eth2_network_config/src/lib.rs @@ -154,6 +154,32 @@ impl Eth2NetworkConfig { } } + /// Get the genesis state root for this network. + /// + /// `Ok(None)` will be returned if the genesis state is not known. No network requests will be + /// made by this function. This function will not error unless the genesis state configuration + /// is corrupted. + pub fn genesis_state_root(&self) -> Result, String> { + match self.genesis_state_source { + GenesisStateSource::Unknown => Ok(None), + GenesisStateSource::Url { + genesis_state_root, .. + } => Hash256::from_str(genesis_state_root) + .map(Option::Some) + .map_err(|e| format!("Unable to parse genesis state root: {:?}", e)), + GenesisStateSource::IncludedBytes => { + self.get_genesis_state_from_bytes::() + .and_then(|mut state| { + Ok(Some( + state + .canonical_root() + .map_err(|e| format!("Hashing error: {e:?}"))?, + )) + }) + } + } + } + /// Construct a consolidated `ChainSpec` from the YAML config. pub fn chain_spec(&self) -> Result { ChainSpec::from_config::(&self.config).ok_or_else(|| { @@ -185,6 +211,7 @@ impl Eth2NetworkConfig { urls: built_in_urls, checksum, genesis_validators_root, + .. } => { let checksum = Hash256::from_str(checksum).map_err(|e| { format!("Unable to parse genesis state bytes checksum: {:?}", e) @@ -507,6 +534,7 @@ mod tests { urls, checksum, genesis_validators_root, + .. } = net.genesis_state_source { Hash256::from_str(checksum).expect("the checksum must be a valid 32-byte value"); diff --git a/common/metrics/src/lib.rs b/common/metrics/src/lib.rs index 1f2ac71aea..22513af8bc 100644 --- a/common/metrics/src/lib.rs +++ b/common/metrics/src/lib.rs @@ -283,6 +283,14 @@ pub fn stop_timer(timer: Option) { } } +/// Stops a timer created with `start_timer(..)`. +/// +/// Return the duration that the timer was running for, or 0.0 if it was `None` due to incorrect +/// initialisation. +pub fn stop_timer_with_duration(timer: Option) -> Duration { + Duration::from_secs_f64(timer.map_or(0.0, |t| t.stop_and_record())) +} + pub fn observe_vec(vec: &Result, name: &[&str], value: f64) { if let Some(h) = get_histogram(vec, name) { h.observe(value) diff --git a/consensus/state_processing/src/common/update_progressive_balances_cache.rs b/consensus/state_processing/src/common/update_progressive_balances_cache.rs index 101e861683..1fdfe802c4 100644 --- a/consensus/state_processing/src/common/update_progressive_balances_cache.rs +++ b/consensus/state_processing/src/common/update_progressive_balances_cache.rs @@ -1,6 +1,6 @@ /// A collection of all functions that mutates the `ProgressiveBalancesCache`. use crate::metrics::{ - PARTICIPATION_CURR_EPOCH_TARGET_ATTESTING_GWEI_PROGRESSIVE_TOTAL, + self, PARTICIPATION_CURR_EPOCH_TARGET_ATTESTING_GWEI_PROGRESSIVE_TOTAL, PARTICIPATION_PREV_EPOCH_TARGET_ATTESTING_GWEI_PROGRESSIVE_TOTAL, }; use crate::{BlockProcessingError, EpochProcessingError}; @@ -21,6 +21,8 @@ pub fn initialize_progressive_balances_cache( return Ok(()); } + let _timer = metrics::start_timer(&metrics::BUILD_PROGRESSIVE_BALANCES_CACHE_TIME); + // Calculate the total flag balances for previous & current epoch in a single iteration. // This calculates `get_total_balance(unslashed_participating_indices(..))` for each flag in // the current and previous epoch. diff --git a/consensus/state_processing/src/epoch_cache.rs b/consensus/state_processing/src/epoch_cache.rs index 5af5e639fd..dc1d79709e 100644 --- a/consensus/state_processing/src/epoch_cache.rs +++ b/consensus/state_processing/src/epoch_cache.rs @@ -1,6 +1,7 @@ use crate::common::altair::BaseRewardPerIncrement; use crate::common::base::SqrtTotalActiveBalance; use crate::common::{altair, base}; +use crate::metrics; use safe_arith::SafeArith; use types::epoch_cache::{EpochCache, EpochCacheError, EpochCacheKey}; use types::{ @@ -138,6 +139,8 @@ pub fn initialize_epoch_cache( return Ok(()); } + let _timer = metrics::start_timer(&metrics::BUILD_EPOCH_CACHE_TIME); + let current_epoch = state.current_epoch(); let next_epoch = state.next_epoch().map_err(EpochCacheError::BeaconState)?; let decision_block_root = state diff --git a/consensus/state_processing/src/metrics.rs b/consensus/state_processing/src/metrics.rs index b53dee96d9..8772dbd4f8 100644 --- a/consensus/state_processing/src/metrics.rs +++ b/consensus/state_processing/src/metrics.rs @@ -41,6 +41,20 @@ pub static PROCESS_EPOCH_TIME: LazyLock> = LazyLock::new(|| { "Time required for process_epoch", ) }); +pub static BUILD_EPOCH_CACHE_TIME: LazyLock> = LazyLock::new(|| { + try_create_histogram( + "beacon_state_processing_epoch_cache", + "Time required to build the epoch cache", + ) +}); +pub static BUILD_PROGRESSIVE_BALANCES_CACHE_TIME: LazyLock> = + LazyLock::new(|| { + try_create_histogram( + "beacon_state_processing_progressive_balances_cache", + "Time required to build the progressive balances cache", + ) + }); + /* * Participation Metrics (progressive balances) */ diff --git a/consensus/types/src/beacon_state.rs b/consensus/types/src/beacon_state.rs index f214991d51..833231dca3 100644 --- a/consensus/types/src/beacon_state.rs +++ b/consensus/types/src/beacon_state.rs @@ -155,7 +155,6 @@ pub enum Error { current_fork: ForkName, }, TotalActiveBalanceDiffUninitialized, - MissingImmutableValidator(usize), IndexNotSupported(usize), InvalidFlagIndex(usize), MerkleTreeError(merkle_proof::MerkleTreeError), diff --git a/consensus/types/src/historical_summary.rs b/consensus/types/src/historical_summary.rs index 76bb111ea2..8c82d52b81 100644 --- a/consensus/types/src/historical_summary.rs +++ b/consensus/types/src/historical_summary.rs @@ -15,6 +15,7 @@ use tree_hash_derive::TreeHash; #[derive( Debug, PartialEq, + Eq, Serialize, Deserialize, Encode, diff --git a/consensus/types/src/validator.rs b/consensus/types/src/validator.rs index 8cf118eea5..275101ddbe 100644 --- a/consensus/types/src/validator.rs +++ b/consensus/types/src/validator.rs @@ -15,6 +15,7 @@ use tree_hash_derive::TreeHash; Debug, Clone, PartialEq, + Eq, Serialize, Deserialize, Encode, diff --git a/database_manager/src/cli.rs b/database_manager/src/cli.rs index 5521b97805..4246a51f89 100644 --- a/database_manager/src/cli.rs +++ b/database_manager/src/cli.rs @@ -3,6 +3,7 @@ use clap_utils::get_color_style; use clap_utils::FLAG_HEADER; use serde::{Deserialize, Serialize}; use std::path::PathBuf; +use store::hdiff::HierarchyConfig; use crate::InspectTarget; @@ -21,13 +22,14 @@ use crate::InspectTarget; pub struct DatabaseManager { #[clap( long, - value_name = "SLOT_COUNT", - help = "Specifies how often a freezer DB restore point should be stored. \ - Cannot be changed after initialization. \ - [default: 2048 (mainnet) or 64 (minimal)]", + global = true, + value_name = "N0,N1,N2,...", + help = "Specifies the frequency for storing full state snapshots and hierarchical \ + diffs in the freezer DB.", + default_value_t = HierarchyConfig::default(), display_order = 0 )] - pub slots_per_restore_point: Option, + pub hierarchy_exponents: HierarchyConfig, #[clap( long, diff --git a/database_manager/src/lib.rs b/database_manager/src/lib.rs index 3d55631848..fc15e98616 100644 --- a/database_manager/src/lib.rs +++ b/database_manager/src/lib.rs @@ -6,7 +6,7 @@ use beacon_chain::{ builder::Witness, eth1_chain::CachingEth1Backend, schema_change::migrate_schema, slot_clock::SystemTimeSlotClock, }; -use beacon_node::{get_data_dir, get_slots_per_restore_point, ClientConfig}; +use beacon_node::{get_data_dir, ClientConfig}; use clap::ArgMatches; use clap::ValueEnum; use cli::{Compact, Inspect}; @@ -16,7 +16,6 @@ use slog::{info, warn, Logger}; use std::fs; use std::io::Write; use std::path::PathBuf; -use store::metadata::STATE_UPPER_LIMIT_NO_RETAIN; use store::{ errors::Error, metadata::{SchemaVersion, CURRENT_SCHEMA_VERSION}, @@ -39,13 +38,8 @@ fn parse_client_config( client_config .blobs_db_path .clone_from(&database_manager_config.blobs_dir); - - let (sprp, sprp_explicit) = - get_slots_per_restore_point::(database_manager_config.slots_per_restore_point)?; - - client_config.store.slots_per_restore_point = sprp; - client_config.store.slots_per_restore_point_set_explicitly = sprp_explicit; client_config.store.blob_prune_margin_epochs = database_manager_config.blob_prune_margin_epochs; + client_config.store.hierarchy_config = database_manager_config.hierarchy_exponents.clone(); Ok(client_config) } @@ -298,6 +292,7 @@ fn parse_migrate_config(migrate_config: &Migrate) -> Result( migrate_config: MigrateConfig, client_config: ClientConfig, + mut genesis_state: BeaconState, runtime_context: &RuntimeContext, log: Logger, ) -> Result<(), Error> { @@ -328,13 +323,13 @@ pub fn migrate_db( "to" => to.as_u64(), ); + let genesis_state_root = genesis_state.canonical_root()?; migrate_schema::, _, _, _>>( db, - client_config.eth1.deposit_contract_deploy_block, + Some(genesis_state_root), from, to, log, - &spec, ) } @@ -426,8 +421,7 @@ pub fn prune_states( // correct network, and that we don't end up storing the wrong genesis state. let genesis_from_db = db .load_cold_state_by_slot(Slot::new(0)) - .map_err(|e| format!("Error reading genesis state: {e:?}"))? - .ok_or("Error: genesis state missing from database. Check schema version.")?; + .map_err(|e| format!("Error reading genesis state: {e:?}"))?; if genesis_from_db.genesis_validators_root() != genesis_state.genesis_validators_root() { return Err(format!( @@ -438,18 +432,12 @@ pub fn prune_states( // Check that the user has confirmed they want to proceed. if !prune_config.confirm { - match db.get_anchor_info() { - Some(anchor_info) - if anchor_info.state_lower_limit == 0 - && anchor_info.state_upper_limit == STATE_UPPER_LIMIT_NO_RETAIN => - { - info!(log, "States have already been pruned"); - return Ok(()); - } - _ => { - info!(log, "Ready to prune states"); - } + if db.get_anchor_info().full_state_pruning_enabled() { + info!(log, "States have already been pruned"); + return Ok(()); } + + info!(log, "Ready to prune states"); warn!( log, "Pruning states is irreversible"; @@ -484,10 +472,33 @@ pub fn run( let log = context.log().clone(); let format_err = |e| format!("Fatal error: {:?}", e); + let get_genesis_state = || { + let executor = env.core_context().executor; + let network_config = context + .eth2_network_config + .clone() + .ok_or("Missing network config")?; + + executor + .block_on_dangerous( + network_config.genesis_state::( + client_config.genesis_state_url.as_deref(), + client_config.genesis_state_url_timeout, + &log, + ), + "get_genesis_state", + ) + .ok_or("Shutting down")? + .map_err(|e| format!("Error getting genesis state: {e}"))? + .ok_or("Genesis state missing".to_string()) + }; + match &db_manager_config.subcommand { cli::DatabaseManagerSubcommand::Migrate(migrate_config) => { let migrate_config = parse_migrate_config(migrate_config)?; - migrate_db(migrate_config, client_config, &context, log).map_err(format_err) + let genesis_state = get_genesis_state()?; + migrate_db(migrate_config, client_config, genesis_state, &context, log) + .map_err(format_err) } cli::DatabaseManagerSubcommand::Inspect(inspect_config) => { let inspect_config = parse_inspect_config(inspect_config)?; @@ -503,27 +514,8 @@ pub fn run( prune_blobs(client_config, &context, log).map_err(format_err) } cli::DatabaseManagerSubcommand::PruneStates(prune_states_config) => { - let executor = env.core_context().executor; - let network_config = context - .eth2_network_config - .clone() - .ok_or("Missing network config")?; - - let genesis_state = executor - .block_on_dangerous( - network_config.genesis_state::( - client_config.genesis_state_url.as_deref(), - client_config.genesis_state_url_timeout, - &log, - ), - "get_genesis_state", - ) - .ok_or("Shutting down")? - .map_err(|e| format!("Error getting genesis state: {e}"))? - .ok_or("Genesis state missing")?; - let prune_config = parse_prune_states_config(prune_states_config)?; - + let genesis_state = get_genesis_state()?; prune_states(client_config, prune_config, genesis_state, &context, log) } cli::DatabaseManagerSubcommand::Compact(compact_config) => { diff --git a/lighthouse/tests/beacon_node.rs b/lighthouse/tests/beacon_node.rs index 100d12cba0..6e730c007f 100644 --- a/lighthouse/tests/beacon_node.rs +++ b/lighthouse/tests/beacon_node.rs @@ -1819,45 +1819,12 @@ fn validator_monitor_metrics_threshold_custom() { } // Tests for Store flags. +// DEPRECATED but should still be accepted. #[test] fn slots_per_restore_point_flag() { CommandLineTest::new() .flag("slots-per-restore-point", Some("64")) - .run_with_zero_port() - .with_config(|config| assert_eq!(config.store.slots_per_restore_point, 64)); -} -#[test] -fn slots_per_restore_point_update_prev_default() { - use beacon_node::beacon_chain::store::config::{ - DEFAULT_SLOTS_PER_RESTORE_POINT, PREV_DEFAULT_SLOTS_PER_RESTORE_POINT, - }; - - CommandLineTest::new() - .flag("slots-per-restore-point", Some("2048")) - .run_with_zero_port() - .with_config_and_dir(|config, dir| { - // Check that 2048 is the previous default. - assert_eq!( - config.store.slots_per_restore_point, - PREV_DEFAULT_SLOTS_PER_RESTORE_POINT - ); - - // Restart the BN with the same datadir and the new default SPRP. It should - // allow this. - CommandLineTest::new() - .flag("datadir", Some(&dir.path().display().to_string())) - .flag("zero-ports", None) - .run_with_no_datadir() - .with_config(|config| { - // The dumped config will have the new default 8192 value, but the fact that - // the BN started and ran (with the same datadir) means that the override - // was successful. - assert_eq!( - config.store.slots_per_restore_point, - DEFAULT_SLOTS_PER_RESTORE_POINT - ); - }); - }) + .run_with_zero_port(); } #[test] @@ -1905,6 +1872,27 @@ fn historic_state_cache_size_default() { }); } #[test] +fn hdiff_buffer_cache_size_flag() { + CommandLineTest::new() + .flag("hdiff-buffer-cache-size", Some("1")) + .run_with_zero_port() + .with_config(|config| { + assert_eq!(config.store.hdiff_buffer_cache_size.get(), 1); + }); +} +#[test] +fn hdiff_buffer_cache_size_default() { + use beacon_node::beacon_chain::store::config::DEFAULT_HDIFF_BUFFER_CACHE_SIZE; + CommandLineTest::new() + .run_with_zero_port() + .with_config(|config| { + assert_eq!( + config.store.hdiff_buffer_cache_size, + DEFAULT_HDIFF_BUFFER_CACHE_SIZE + ); + }); +} +#[test] fn auto_compact_db_flag() { CommandLineTest::new() .flag("auto-compact-db", Some("false")) diff --git a/watch/README.md b/watch/README.md index 34519e52e5..877cddf234 100644 --- a/watch/README.md +++ b/watch/README.md @@ -39,8 +39,6 @@ diesel database reset --database-url postgres://postgres:postgres@localhost/dev 1. Ensure a synced Lighthouse beacon node with historical states is available at `localhost:5052`. -The smaller the value of `--slots-per-restore-point` the faster beacon.watch -will be able to sync to the beacon node. 1. Run the updater daemon: ``` From c5007eaa1cfee839f9efafda6aed44fe18cbf4a8 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Mon, 18 Nov 2024 19:36:01 +1100 Subject: [PATCH 011/254] Deprecate `eth1` and `dummy-eth1` flags (#6566) * Deprecate eth1/dummy-eth1 flags * Update book * Simplify + make staking conflict with disable-deposit-contract --- beacon_node/client/src/config.rs | 7 +------ beacon_node/src/cli.rs | 12 ++++++------ beacon_node/src/config.rs | 22 ++-------------------- beacon_node/src/lib.rs | 9 +-------- book/src/help_bn.md | 10 +--------- lighthouse/tests/beacon_node.rs | 20 ++++++++++++++++++-- testing/node_test_rig/src/lib.rs | 2 -- 7 files changed, 29 insertions(+), 53 deletions(-) diff --git a/beacon_node/client/src/config.rs b/beacon_node/client/src/config.rs index a25216ff3e..becc781ed3 100644 --- a/beacon_node/client/src/config.rs +++ b/beacon_node/client/src/config.rs @@ -59,10 +59,6 @@ pub struct Config { /// Path where the blobs database will be located if blobs should be in a separate database. pub blobs_db_path: Option, pub log_file: PathBuf, - /// If true, the node will use co-ordinated junk for eth1 values. - /// - /// This is the method used for the 2019 client interop in Canada. - pub dummy_eth1_backend: bool, pub sync_eth1_chain: bool, /// Graffiti to be inserted everytime we create a block if the validator doesn't specify. pub beacon_graffiti: GraffitiOrigin, @@ -103,8 +99,7 @@ impl Default for Config { store: <_>::default(), network: NetworkConfig::default(), chain: <_>::default(), - dummy_eth1_backend: false, - sync_eth1_chain: false, + sync_eth1_chain: true, eth1: <_>::default(), execution_layer: None, trusted_setup, diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index 87c6e84ba7..cecfcee868 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -693,8 +693,7 @@ pub fn cli_app() -> Command { Arg::new("staking") .long("staking") .help("Standard option for a staking beacon node. This will enable the HTTP 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") + on localhost:5052 and import deposit logs from the execution node.") .action(ArgAction::SetTrue) .help_heading(FLAG_HEADER) .display_order(0) @@ -706,21 +705,21 @@ pub fn cli_app() -> Command { .arg( Arg::new("eth1") .long("eth1") - .help("If present the node will connect to an eth1 node. This is required for \ - block production, you must use this flag if you wish to serve a validator.") + .help("DEPRECATED") .action(ArgAction::SetTrue) .help_heading(FLAG_HEADER) .display_order(0) + .hide(true) ) .arg( Arg::new("dummy-eth1") .long("dummy-eth1") + .help("DEPRECATED") .action(ArgAction::SetTrue) .help_heading(FLAG_HEADER) .conflicts_with("eth1") - .help("If present, uses an eth1 backend that generates static dummy data.\ - Identical to the method used at the 2019 Canada interop.") .display_order(0) + .hide(true) ) .arg( Arg::new("eth1-purge-cache") @@ -1489,6 +1488,7 @@ pub fn cli_app() -> Command { Useful if you intend to run a non-validating beacon node.") .action(ArgAction::SetTrue) .help_heading(FLAG_HEADER) + .conflicts_with("staking") .display_order(0) ) .arg( diff --git a/beacon_node/src/config.rs b/beacon_node/src/config.rs index adcb591aed..8d8a44a6fd 100644 --- a/beacon_node/src/config.rs +++ b/beacon_node/src/config.rs @@ -121,7 +121,6 @@ pub fn get_config( if cli_args.get_flag("staking") { client_config.http_api.enabled = true; - client_config.sync_eth1_chain = true; } /* @@ -263,18 +262,12 @@ pub fn get_config( * Eth1 */ - // When present, use an eth1 backend that generates deterministic junk. - // - // Useful for running testnets without the overhead of a deposit contract. if cli_args.get_flag("dummy-eth1") { - client_config.dummy_eth1_backend = true; + warn!(log, "The --dummy-eth1 flag is deprecated"); } - // When present, attempt to sync to an eth1 node. - // - // Required for block production. if cli_args.get_flag("eth1") { - client_config.sync_eth1_chain = true; + warn!(log, "The --eth1 flag is deprecated"); } if let Some(val) = cli_args.get_one::("eth1-blocks-per-log-query") { @@ -297,17 +290,6 @@ pub fn get_config( let endpoints: String = clap_utils::parse_required(cli_args, "execution-endpoint")?; let mut el_config = execution_layer::Config::default(); - // Always follow the deposit contract when there is an execution endpoint. - // - // This is wasteful for non-staking nodes as they have no need to process deposit contract - // logs and build an "eth1" cache. The alternative is to explicitly require the `--eth1` or - // `--staking` flags, however that poses a risk to stakers since they cannot produce blocks - // without "eth1". - // - // The waste for non-staking nodes is relatively small so we err on the side of safety for - // stakers. The merge is already complicated enough. - client_config.sync_eth1_chain = true; - // Parse a single execution endpoint, logging warnings if multiple endpoints are supplied. let execution_endpoint = parse_only_one_value( endpoints.as_str(), diff --git a/beacon_node/src/lib.rs b/beacon_node/src/lib.rs index 9413eb3924..cca617d8c6 100644 --- a/beacon_node/src/lib.rs +++ b/beacon_node/src/lib.rs @@ -140,7 +140,7 @@ impl ProductionBeaconNode { let builder = builder .beacon_chain_builder(client_genesis, client_config.clone()) .await?; - let builder = if client_config.sync_eth1_chain && !client_config.dummy_eth1_backend { + let builder = if client_config.sync_eth1_chain { info!( log, "Block production enabled"; @@ -150,13 +150,6 @@ impl ProductionBeaconNode { builder .caching_eth1_backend(client_config.eth1.clone()) .await? - } else if client_config.dummy_eth1_backend { - warn!( - log, - "Block production impaired"; - "reason" => "dummy eth1 backend is enabled" - ); - builder.dummy_eth1_backend()? } else { info!( log, diff --git a/book/src/help_bn.md b/book/src/help_bn.md index 55815fbdfe..a4ab44748c 100644 --- a/book/src/help_bn.md +++ b/book/src/help_bn.md @@ -480,9 +480,6 @@ Flags: --disable-upnp Disables UPnP support. Setting this will prevent Lighthouse from attempting to automatically establish external port mappings. - --dummy-eth1 - If present, uses an eth1 backend that generates static dummy - data.Identical to the method used at the 2019 Canada interop. -e, --enr-match Sets the local ENR IP address and port to match those set for lighthouse. Specifically, the IP address will be the value of @@ -490,10 +487,6 @@ Flags: --enable-private-discovery Lighthouse by default does not discover private IP addresses. Set this flag to enable connection attempts to local addresses. - --eth1 - If present the node will connect to an eth1 node. This is required for - block production, you must use this flag if you wish to serve a - validator. --eth1-purge-cache Purges the eth1 block and deposit caches --genesis-backfill @@ -561,8 +554,7 @@ Flags: --staking Standard option for a staking beacon node. This will enable the HTTP 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 + node. --stdin-inputs If present, read all user inputs from stdin instead of tty. --subscribe-all-subnets diff --git a/lighthouse/tests/beacon_node.rs b/lighthouse/tests/beacon_node.rs index 6e730c007f..80986653c1 100644 --- a/lighthouse/tests/beacon_node.rs +++ b/lighthouse/tests/beacon_node.rs @@ -396,13 +396,14 @@ fn genesis_backfill_with_historic_flag() { } // Tests for Eth1 flags. +// DEPRECATED but should not crash #[test] fn dummy_eth1_flag() { CommandLineTest::new() .flag("dummy-eth1", None) - .run_with_zero_port() - .with_config(|config| assert!(config.dummy_eth1_backend)); + .run_with_zero_port(); } +// DEPRECATED but should not crash #[test] fn eth1_flag() { CommandLineTest::new() @@ -2483,6 +2484,21 @@ fn sync_eth1_chain_disable_deposit_contract_sync_flag() { .with_config(|config| assert_eq!(config.sync_eth1_chain, false)); } +#[test] +#[should_panic] +fn disable_deposit_contract_sync_conflicts_with_staking() { + let dir = TempDir::new().expect("Unable to create temporary directory"); + CommandLineTest::new_with_no_execution_endpoint() + .flag("disable-deposit-contract-sync", None) + .flag("staking", None) + .flag("execution-endpoints", Some("http://localhost:8551/")) + .flag( + "execution-jwt", + dir.path().join("jwt-file").as_os_str().to_str(), + ) + .run_with_zero_port(); +} + #[test] fn light_client_server_default() { CommandLineTest::new() diff --git a/testing/node_test_rig/src/lib.rs b/testing/node_test_rig/src/lib.rs index 6b453a8cbc..ac01c84b9d 100644 --- a/testing/node_test_rig/src/lib.rs +++ b/testing/node_test_rig/src/lib.rs @@ -104,8 +104,6 @@ pub fn testing_client_config() -> ClientConfig { client_config.http_api.enabled = true; client_config.http_api.listen_port = 0; - client_config.dummy_eth1_backend = true; - let now = SystemTime::now() .duration_since(UNIX_EPOCH) .expect("should get system time") From 8cebc87d956707a385aa21aa75ef748e6391a586 Mon Sep 17 00:00:00 2001 From: Age Manning Date: Tue, 19 Nov 2024 09:52:23 +1100 Subject: [PATCH 012/254] Update to latest discovery (#6486) * Upgrade discv5 to v0.8 * Rename some logs * Improve the NAT reporting with new discv5 metrics * Merge branch 'unstable' into discv5-v8 * Limited Cargo.lock update * Update yanked futures-* crates --- Cargo.lock | 125 ++++++++++-------- Cargo.toml | 2 +- beacon_node/lighthouse_network/src/config.rs | 4 +- .../lighthouse_network/src/discovery/mod.rs | 4 - beacon_node/lighthouse_network/src/metrics.rs | 3 + .../src/peer_manager/network_behaviour.rs | 14 +- boot_node/src/server.rs | 4 +- common/system_health/src/lib.rs | 50 ++++++- 8 files changed, 133 insertions(+), 73 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a2014728e9..b7ba237ac7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -230,9 +230,9 @@ dependencies = [ [[package]] name = "alloy-rlp" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26154390b1d205a4a7ac7352aa2eb4f81f391399d4e2f546fb81a2f8bb383f62" +checksum = "da0822426598f95e45dd1ea32a738dac057529a709ee645fcc516ffa4cbde08f" dependencies = [ "alloy-rlp-derive", "arrayvec", @@ -241,9 +241,9 @@ dependencies = [ [[package]] name = "alloy-rlp-derive" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d0f2d905ebd295e7effec65e5f6868d153936130ae718352771de3e7d03c75c" +checksum = "2b09cae092c27b6f1bde952653a22708691802e57bfef4a2973b80bea21efd3f" dependencies = [ "proc-macro2", "quote", @@ -1952,6 +1952,17 @@ dependencies = [ "tokio-util", ] +[[package]] +name = "delay_map" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df941644b671f05f59433e481ba0d31ac10e3667de725236a4c0d587c496fba1" +dependencies = [ + "futures", + "tokio", + "tokio-util", +] + [[package]] name = "deposit_contract" version = "0.2.0" @@ -2186,20 +2197,20 @@ dependencies = [ [[package]] name = "discv5" -version = "0.7.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f569b8c367554666c8652305621e8bae3634a2ff5c6378081d5bd8c399c99f23" +checksum = "898d136ecb64116ec68aecf14d889bd30f8b1fe0c19e262953f7388dbe77052e" dependencies = [ "aes 0.8.4", "aes-gcm", "alloy-rlp", "arrayvec", "ctr 0.9.2", - "delay_map", + "delay_map 0.4.0", "enr", "fnv", "futures", - "hashlink 0.8.4", + "hashlink 0.9.1", "hex", "hkdf", "lazy_static", @@ -2207,13 +2218,13 @@ dependencies = [ "lru", "more-asserts", "multiaddr", - "parking_lot 0.11.2", + "parking_lot 0.12.3", "rand", "smallvec", - "socket2 0.4.10", + "socket2", "tokio", "tracing", - "uint", + "uint 0.10.0", "zeroize", ] @@ -2410,12 +2421,12 @@ dependencies = [ [[package]] name = "enr" -version = "0.12.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "972070166c68827e64bd1ebc8159dd8e32d9bc2da7ebe8f20b61308f7974ad30" +checksum = "851bd664a3d3a3c175cff92b2f0df02df3c541b4895d0ae307611827aae46152" dependencies = [ "alloy-rlp", - "base64 0.21.7", + "base64 0.22.1", "bytes", "ed25519-dalek", "hex", @@ -2708,7 +2719,7 @@ dependencies = [ "serde_json", "sha3 0.9.1", "thiserror", - "uint", + "uint 0.9.5", ] [[package]] @@ -2725,7 +2736,7 @@ dependencies = [ "serde_json", "sha3 0.10.8", "thiserror", - "uint", + "uint 0.9.5", ] [[package]] @@ -2767,7 +2778,7 @@ dependencies = [ "impl-rlp", "impl-serde 0.3.2", "primitive-types 0.10.1", - "uint", + "uint 0.9.5", ] [[package]] @@ -2783,7 +2794,7 @@ dependencies = [ "impl-serde 0.4.0", "primitive-types 0.12.2", "scale-info", - "uint", + "uint 0.9.5", ] [[package]] @@ -3297,9 +3308,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -3307,9 +3318,9 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" @@ -3325,9 +3336,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" @@ -3341,9 +3352,9 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", @@ -3363,15 +3374,15 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-ticker" @@ -3392,9 +3403,9 @@ checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", @@ -3753,7 +3764,7 @@ dependencies = [ "ipnet", "once_cell", "rand", - "socket2 0.5.7", + "socket2", "thiserror", "tinyvec", "tokio", @@ -4009,7 +4020,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2 0.5.7", + "socket2", "tokio", "tower-service", "tracing", @@ -4339,7 +4350,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" dependencies = [ - "socket2 0.5.7", + "socket2", "widestring 1.1.0", "windows-sys 0.48.0", "winreg", @@ -4805,7 +4816,7 @@ dependencies = [ "libp2p-swarm", "rand", "smallvec", - "socket2 0.5.7", + "socket2", "tokio", "tracing", "void", @@ -4906,7 +4917,7 @@ dependencies = [ "rand", "ring 0.17.8", "rustls 0.23.13", - "socket2 0.5.7", + "socket2", "thiserror", "tokio", "tracing", @@ -4960,7 +4971,7 @@ dependencies = [ "libc", "libp2p-core", "libp2p-identity", - "socket2 0.5.7", + "socket2", "tokio", "tracing", ] @@ -5146,7 +5157,7 @@ dependencies = [ "alloy-rlp", "async-channel", "bytes", - "delay_map", + "delay_map 0.3.0", "directory", "dirs", "discv5", @@ -5715,7 +5726,7 @@ dependencies = [ "beacon_chain", "beacon_processor", "bls", - "delay_map", + "delay_map 0.3.0", "derivative", "error-chain", "eth2", @@ -6502,7 +6513,7 @@ dependencies = [ "impl-codec 0.5.1", "impl-rlp", "impl-serde 0.3.2", - "uint", + "uint 0.9.5", ] [[package]] @@ -6516,7 +6527,7 @@ dependencies = [ "impl-rlp", "impl-serde 0.4.0", "scale-info", - "uint", + "uint 0.9.5", ] [[package]] @@ -6731,7 +6742,7 @@ dependencies = [ "quinn-udp", "rustc-hash 2.0.0", "rustls 0.23.13", - "socket2 0.5.7", + "socket2", "thiserror", "tokio", "tracing", @@ -6762,7 +6773,7 @@ checksum = "4fe68c2e9e1a1234e218683dbdf9f9dfcb094113c5ac2b938dfcb9bab4c4140b" dependencies = [ "libc", "once_cell", - "socket2 0.5.7", + "socket2", "tracing", "windows-sys 0.59.0", ] @@ -8080,16 +8091,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "socket2" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "socket2" version = "0.5.7" @@ -8692,7 +8693,7 @@ dependencies = [ "mio", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.7", + "socket2", "tokio-macros", "windows-sys 0.52.0", ] @@ -8748,7 +8749,7 @@ dependencies = [ "postgres-protocol", "postgres-types", "rand", - "socket2 0.5.7", + "socket2", "tokio", "tokio-util", "whoami", @@ -9128,6 +9129,18 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "uint" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "909988d098b2f738727b161a106cfc7cab00c539c2687a8836f8e565976fb53e" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + [[package]] name = "unarray" version = "0.1.4" diff --git a/Cargo.toml b/Cargo.toml index eedb8a0591..8cf4abb33e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -127,7 +127,7 @@ derivative = "2" dirs = "3" either = "1.9" rust_eth_kzg = "0.5.1" -discv5 = { version = "0.7", features = ["libp2p"] } +discv5 = { version = "0.9", features = ["libp2p"] } env_logger = "0.9" error-chain = "0.12" ethereum_hashing = "0.7.0" diff --git a/beacon_node/lighthouse_network/src/config.rs b/beacon_node/lighthouse_network/src/config.rs index d70e50b1da..21f3dc830f 100644 --- a/beacon_node/lighthouse_network/src/config.rs +++ b/beacon_node/lighthouse_network/src/config.rs @@ -305,12 +305,12 @@ impl Default for Config { let discv5_config = discv5::ConfigBuilder::new(discv5_listen_config) .enable_packet_filter() .session_cache_capacity(5000) - .request_timeout(Duration::from_secs(1)) + .request_timeout(Duration::from_secs(2)) .query_peer_timeout(Duration::from_secs(2)) .query_timeout(Duration::from_secs(30)) .request_retries(1) .enr_peer_update_min(10) - .query_parallelism(5) + .query_parallelism(8) .disable_report_discovered_peers() .ip_limit() // limits /24 IP's in buckets. .incoming_bucket_limit(8) // half the bucket size diff --git a/beacon_node/lighthouse_network/src/discovery/mod.rs b/beacon_node/lighthouse_network/src/discovery/mod.rs index d57c67bacb..b91ad40916 100644 --- a/beacon_node/lighthouse_network/src/discovery/mod.rs +++ b/beacon_node/lighthouse_network/src/discovery/mod.rs @@ -1052,10 +1052,6 @@ impl NetworkBehaviour for Discovery { discv5::Event::SocketUpdated(socket_addr) => { info!(self.log, "Address updated"; "ip" => %socket_addr.ip(), "udp_port" => %socket_addr.port()); metrics::inc_counter(&metrics::ADDRESS_UPDATE_COUNT); - // We have SOCKET_UPDATED messages. This occurs when discovery has a majority of - // users reporting an external port and our ENR gets updated. - // Which means we are able to do NAT traversal. - metrics::set_gauge_vec(&metrics::NAT_OPEN, &["discv5"], 1); // Discv5 will have updated our local ENR. We save the updated version // to disk. diff --git a/beacon_node/lighthouse_network/src/metrics.rs b/beacon_node/lighthouse_network/src/metrics.rs index 15445c7d64..cb9c007b91 100644 --- a/beacon_node/lighthouse_network/src/metrics.rs +++ b/beacon_node/lighthouse_network/src/metrics.rs @@ -8,6 +8,7 @@ pub static NAT_OPEN: LazyLock> = LazyLock::new(|| { &["protocol"], ) }); + pub static ADDRESS_UPDATE_COUNT: LazyLock> = LazyLock::new(|| { try_create_int_counter( "libp2p_address_update_total", @@ -212,4 +213,6 @@ pub fn scrape_discovery_metrics() { set_gauge(&DISCOVERY_SESSIONS, metrics.active_sessions as i64); set_gauge_vec(&DISCOVERY_BYTES, &["inbound"], metrics.bytes_recv as i64); set_gauge_vec(&DISCOVERY_BYTES, &["outbound"], metrics.bytes_sent as i64); + set_gauge_vec(&NAT_OPEN, &["discv5_ipv4"], metrics.ipv4_contactable as i64); + set_gauge_vec(&NAT_OPEN, &["discv5_ipv6"], metrics.ipv6_contactable as i64); } diff --git a/beacon_node/lighthouse_network/src/peer_manager/network_behaviour.rs b/beacon_node/lighthouse_network/src/peer_manager/network_behaviour.rs index c40f78b4b0..11676f9a01 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/network_behaviour.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/network_behaviour.rs @@ -7,10 +7,12 @@ use futures::StreamExt; use libp2p::core::transport::PortUse; use libp2p::core::ConnectedPoint; use libp2p::identity::PeerId; +use libp2p::multiaddr::Protocol; use libp2p::swarm::behaviour::{ConnectionClosed, ConnectionEstablished, DialFailure, FromSwarm}; use libp2p::swarm::dial_opts::{DialOpts, PeerCondition}; use libp2p::swarm::dummy::ConnectionHandler; use libp2p::swarm::{ConnectionDenied, ConnectionId, NetworkBehaviour, ToSwarm}; +pub use metrics::{set_gauge_vec, NAT_OPEN}; use slog::{debug, error, trace}; use types::EthSpec; @@ -160,8 +162,8 @@ impl NetworkBehaviour for PeerManager { ) -> Result<(), ConnectionDenied> { // get the IP address to verify it's not banned. let ip = match remote_addr.iter().next() { - Some(libp2p::multiaddr::Protocol::Ip6(ip)) => IpAddr::V6(ip), - Some(libp2p::multiaddr::Protocol::Ip4(ip)) => IpAddr::V4(ip), + Some(Protocol::Ip6(ip)) => IpAddr::V6(ip), + Some(Protocol::Ip4(ip)) => IpAddr::V4(ip), _ => { return Err(ConnectionDenied::new(format!( "Connection to peer rejected: invalid multiaddr: {remote_addr}" @@ -207,6 +209,14 @@ impl NetworkBehaviour for PeerManager { )); } + // We have an inbound connection, this is indicative of having our libp2p NAT ports open. We + // distinguish between ipv4 and ipv6 here: + match remote_addr.iter().next() { + Some(Protocol::Ip4(_)) => set_gauge_vec(&NAT_OPEN, &["libp2p_ipv4"], 1), + Some(Protocol::Ip6(_)) => set_gauge_vec(&NAT_OPEN, &["libp2p_ipv6"], 1), + _ => {} + } + Ok(ConnectionHandler) } diff --git a/boot_node/src/server.rs b/boot_node/src/server.rs index 00738462e0..96032dddcc 100644 --- a/boot_node/src/server.rs +++ b/boot_node/src/server.rs @@ -136,8 +136,8 @@ pub async fn run( "active_sessions" => metrics.active_sessions, "requests/s" => format_args!("{:.2}", metrics.unsolicited_requests_per_second), "ipv4_nodes" => ipv4_only_reachable, - "ipv6_nodes" => ipv6_only_reachable, - "ipv6_and_ipv4_nodes" => ipv4_ipv6_reachable, + "ipv6_only_nodes" => ipv6_only_reachable, + "dual_stack_nodes" => ipv4_ipv6_reachable, "unreachable_nodes" => unreachable_nodes, ); diff --git a/common/system_health/src/lib.rs b/common/system_health/src/lib.rs index feec08af84..3431189842 100644 --- a/common/system_health/src/lib.rs +++ b/common/system_health/src/lib.rs @@ -198,23 +198,61 @@ pub fn observe_system_health_vc( } } +/// The current state of Lighthouse NAT/connectivity. +#[derive(Serialize, Deserialize)] +pub struct NatState { + /// Contactable on discovery ipv4. + discv5_ipv4: bool, + /// Contactable on discovery ipv6. + discv5_ipv6: bool, + /// Contactable on libp2p ipv4. + libp2p_ipv4: bool, + /// Contactable on libp2p ipv6. + libp2p_ipv6: bool, +} + +impl NatState { + pub fn is_anything_open(&self) -> bool { + self.discv5_ipv4 || self.discv5_ipv6 || self.libp2p_ipv4 || self.libp2p_ipv6 + } +} + /// Observes if NAT traversal is possible. -pub fn observe_nat() -> bool { - let discv5_nat = lighthouse_network::metrics::get_int_gauge( +pub fn observe_nat() -> NatState { + let discv5_ipv4 = lighthouse_network::metrics::get_int_gauge( &lighthouse_network::metrics::NAT_OPEN, - &["discv5"], + &["discv5_ipv4"], ) .map(|g| g.get() == 1) .unwrap_or_default(); - let libp2p_nat = lighthouse_network::metrics::get_int_gauge( + let discv5_ipv6 = lighthouse_network::metrics::get_int_gauge( + &lighthouse_network::metrics::NAT_OPEN, + &["discv5_ipv6"], + ) + .map(|g| g.get() == 1) + .unwrap_or_default(); + + let libp2p_ipv4 = lighthouse_network::metrics::get_int_gauge( &lighthouse_network::metrics::NAT_OPEN, &["libp2p"], ) .map(|g| g.get() == 1) .unwrap_or_default(); - discv5_nat || libp2p_nat + let libp2p_ipv6 = lighthouse_network::metrics::get_int_gauge( + &lighthouse_network::metrics::NAT_OPEN, + &["libp2p"], + ) + .map(|g| g.get() == 1) + .unwrap_or_default(); + + NatState { + discv5_ipv4, + discv5_ipv6, + libp2p_ipv4, + libp2p_ipv6, + } } /// Observes the Beacon Node system health. @@ -242,7 +280,7 @@ pub fn observe_system_health_bn( .unwrap_or_else(|| (String::from("None"), 0, 0)); // Determine if the NAT is open or not. - let nat_open = observe_nat(); + let nat_open = observe_nat().is_anything_open(); SystemHealthBN { system_health, From b1e9f694608efe97cdc0e253757434ba64238120 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Wed, 20 Nov 2024 09:43:18 +1100 Subject: [PATCH 013/254] Fix v22 schema upgrade (#6591) * Fix v22 schema upgrade * Ownership --- .../src/schema_change/migration_schema_v22.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/beacon_node/beacon_chain/src/schema_change/migration_schema_v22.rs b/beacon_node/beacon_chain/src/schema_change/migration_schema_v22.rs index fcb78ab801..f532c0e672 100644 --- a/beacon_node/beacon_chain/src/schema_change/migration_schema_v22.rs +++ b/beacon_node/beacon_chain/src/schema_change/migration_schema_v22.rs @@ -45,13 +45,15 @@ pub fn upgrade_to_v22( ) -> Result<(), Error> { info!(log, "Upgrading from v21 to v22"); - let mut old_anchor = db.get_anchor_info(); + let old_anchor = db.get_anchor_info(); // If the anchor was uninitialized in the old schema (`None`), this represents a full archive // node. - if old_anchor == ANCHOR_UNINITIALIZED { - old_anchor = ANCHOR_FOR_ARCHIVE_NODE; - } + let effective_anchor = if old_anchor == ANCHOR_UNINITIALIZED { + ANCHOR_FOR_ARCHIVE_NODE + } else { + old_anchor.clone() + }; let split_slot = db.get_split_slot(); let genesis_state_root = genesis_state_root.ok_or(Error::GenesisStateUnknown)?; @@ -79,7 +81,7 @@ pub fn upgrade_to_v22( // Write the block roots in the new format in a new column. Similar to above, we do this // separately from deleting the old format block roots so that this is crash safe. - let oldest_block_slot = old_anchor.oldest_block_slot; + let oldest_block_slot = effective_anchor.oldest_block_slot; write_new_schema_block_roots::( &db, genesis_block_root, @@ -100,7 +102,7 @@ pub fn upgrade_to_v22( let new_anchor = AnchorInfo { state_upper_limit: STATE_UPPER_LIMIT_NO_RETAIN, state_lower_limit: Slot::new(0), - ..old_anchor.clone() + ..effective_anchor.clone() }; let hot_ops = vec![db.compare_and_set_anchor_info(old_anchor, new_anchor)?]; db.store_schema_version_atomically(SchemaVersion(22), hot_ops)?; From 94311c65163e3d0a8d17d95201c04b646794f1f4 Mon Sep 17 00:00:00 2001 From: Pawan Dhananjay Date: Thu, 21 Nov 2024 03:57:13 +0530 Subject: [PATCH 014/254] Add additional metrics for idontwant (#6578) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add additional metrics for idontwant * Resolve issues from review * Fix tests * Don't exceed capacity * Apply suggestions from code review Co-authored-by: João Oliveira * Return early on failure * Add comment --- .../gossipsub/src/behaviour.rs | 47 +++++++++++++++---- .../gossipsub/src/behaviour/tests.rs | 17 ++++--- .../gossipsub/src/metrics.rs | 34 ++++++++++++++ .../lighthouse_network/gossipsub/src/types.rs | 6 ++- 4 files changed, 86 insertions(+), 18 deletions(-) diff --git a/beacon_node/lighthouse_network/gossipsub/src/behaviour.rs b/beacon_node/lighthouse_network/gossipsub/src/behaviour.rs index 88fe48c441..5ead0c06a0 100644 --- a/beacon_node/lighthouse_network/gossipsub/src/behaviour.rs +++ b/beacon_node/lighthouse_network/gossipsub/src/behaviour.rs @@ -1385,7 +1385,7 @@ where "IWANT: Peer has asked for message too many times; ignoring request" ); } else if let Some(peer) = &mut self.connected_peers.get_mut(peer_id) { - if peer.dont_send.get(&id).is_some() { + if peer.dont_send_received.get(&id).is_some() { tracing::debug!(%peer_id, message=%id, "Peer already sent IDONTWANT for this message"); continue; } @@ -1817,6 +1817,15 @@ where // Calculate the message id on the transformed data. let msg_id = self.config.message_id(&message); + if let Some(metrics) = self.metrics.as_mut() { + if let Some(peer) = self.connected_peers.get_mut(propagation_source) { + // Record if we received a message that we already sent a IDONTWANT for to the peer + if peer.dont_send_sent.contains_key(&msg_id) { + metrics.register_idontwant_messages_ignored_per_topic(&raw_message.topic); + } + } + } + // Check the validity of the message // Peers get penalized if this message is invalid. We don't add it to the duplicate cache // and instead continually penalize peers that repeatedly send this message. @@ -2512,11 +2521,19 @@ where // Flush stale IDONTWANTs. for peer in self.connected_peers.values_mut() { - while let Some((_front, instant)) = peer.dont_send.front() { + while let Some((_front, instant)) = peer.dont_send_received.front() { if (*instant + IDONTWANT_TIMEOUT) >= Instant::now() { break; } else { - peer.dont_send.pop_front(); + peer.dont_send_received.pop_front(); + } + } + // If metrics are not enabled, this queue would be empty. + while let Some((_front, instant)) = peer.dont_send_sent.front() { + if (*instant + IDONTWANT_TIMEOUT) >= Instant::now() { + break; + } else { + peer.dont_send_sent.pop_front(); } } } @@ -2751,6 +2768,16 @@ where .entry(*peer_id) .or_default() .non_priority += 1; + return; + } + // IDONTWANT sent successfully. + if let Some(metrics) = self.metrics.as_mut() { + peer.dont_send_sent.insert(msg_id.clone(), Instant::now()); + // Don't exceed capacity. + if peer.dont_send_sent.len() > IDONTWANT_CAP { + peer.dont_send_sent.pop_front(); + } + metrics.register_idontwant_messages_sent_per_topic(&message.topic); } } } @@ -2808,7 +2835,7 @@ where if !recipient_peers.is_empty() { for peer_id in recipient_peers.iter() { if let Some(peer) = self.connected_peers.get_mut(peer_id) { - if peer.dont_send.get(msg_id).is_some() { + if peer.dont_send_received.get(msg_id).is_some() { tracing::debug!(%peer_id, message=%msg_id, "Peer doesn't want message"); continue; } @@ -3162,7 +3189,8 @@ where connections: vec![], sender: RpcSender::new(self.config.connection_handler_queue_len()), topics: Default::default(), - dont_send: LinkedHashMap::new(), + dont_send_received: LinkedHashMap::new(), + dont_send_sent: LinkedHashMap::new(), }); // Add the new connection connected_peer.connections.push(connection_id); @@ -3194,7 +3222,8 @@ where connections: vec![], sender: RpcSender::new(self.config.connection_handler_queue_len()), topics: Default::default(), - dont_send: LinkedHashMap::new(), + dont_send_received: LinkedHashMap::new(), + dont_send_sent: LinkedHashMap::new(), }); // Add the new connection connected_peer.connections.push(connection_id); @@ -3366,10 +3395,10 @@ where metrics.register_idontwant_bytes(idontwant_size); } for message_id in message_ids { - peer.dont_send.insert(message_id, Instant::now()); + peer.dont_send_received.insert(message_id, Instant::now()); // Don't exceed capacity. - if peer.dont_send.len() > IDONTWANT_CAP { - peer.dont_send.pop_front(); + if peer.dont_send_received.len() > IDONTWANT_CAP { + peer.dont_send_received.pop_front(); } } } diff --git a/beacon_node/lighthouse_network/gossipsub/src/behaviour/tests.rs b/beacon_node/lighthouse_network/gossipsub/src/behaviour/tests.rs index 62f026b568..713fe1f266 100644 --- a/beacon_node/lighthouse_network/gossipsub/src/behaviour/tests.rs +++ b/beacon_node/lighthouse_network/gossipsub/src/behaviour/tests.rs @@ -238,7 +238,8 @@ where kind: kind.clone().unwrap_or(PeerKind::Floodsub), connections: vec![connection_id], topics: Default::default(), - dont_send: LinkedHashMap::new(), + dont_send_received: LinkedHashMap::new(), + dont_send_sent: LinkedHashMap::new(), sender, }, ); @@ -626,7 +627,8 @@ fn test_join() { kind: PeerKind::Floodsub, connections: vec![connection_id], topics: Default::default(), - dont_send: LinkedHashMap::new(), + dont_send_received: LinkedHashMap::new(), + dont_send_sent: LinkedHashMap::new(), sender, }, ); @@ -1023,7 +1025,8 @@ fn test_get_random_peers() { connections: vec![ConnectionId::new_unchecked(0)], topics: topics.clone(), sender: RpcSender::new(gs.config.connection_handler_queue_len()), - dont_send: LinkedHashMap::new(), + dont_send_sent: LinkedHashMap::new(), + dont_send_received: LinkedHashMap::new(), }, ); } @@ -5408,7 +5411,7 @@ fn doesnt_forward_idontwant() { .unwrap(); let message_id = gs.config.message_id(&message); let peer = gs.connected_peers.get_mut(&peers[2]).unwrap(); - peer.dont_send.insert(message_id, Instant::now()); + peer.dont_send_received.insert(message_id, Instant::now()); gs.handle_received_message(raw_message.clone(), &local_id); assert_eq!( @@ -5457,7 +5460,7 @@ fn parses_idontwant() { }, ); let peer = gs.connected_peers.get_mut(&peers[1]).unwrap(); - assert!(peer.dont_send.get(&message_id).is_some()); + assert!(peer.dont_send_received.get(&message_id).is_some()); } /// Test that a node clears stale IDONTWANT messages. @@ -5473,10 +5476,10 @@ fn clear_stale_idontwant() { .create_network(); let peer = gs.connected_peers.get_mut(&peers[2]).unwrap(); - peer.dont_send + peer.dont_send_received .insert(MessageId::new(&[1, 2, 3, 4]), Instant::now()); std::thread::sleep(Duration::from_secs(3)); gs.heartbeat(); let peer = gs.connected_peers.get_mut(&peers[2]).unwrap(); - assert!(peer.dont_send.is_empty()); + assert!(peer.dont_send_received.is_empty()); } diff --git a/beacon_node/lighthouse_network/gossipsub/src/metrics.rs b/beacon_node/lighthouse_network/gossipsub/src/metrics.rs index a4ac389a74..d3ca6c299e 100644 --- a/beacon_node/lighthouse_network/gossipsub/src/metrics.rs +++ b/beacon_node/lighthouse_network/gossipsub/src/metrics.rs @@ -188,6 +188,12 @@ pub(crate) struct Metrics { /// The number of bytes we have received in every IDONTWANT control message. idontwant_bytes: Counter, + /// Number of IDONTWANT messages sent per topic. + idontwant_messages_sent_per_topic: Family, + + /// Number of full messages we received that we previously sent a IDONTWANT for. + idontwant_messages_ignored_per_topic: Family, + /// The size of the priority queue. priority_queue_size: Histogram, /// The size of the non-priority queue. @@ -341,6 +347,18 @@ impl Metrics { metric }; + // IDONTWANT messages sent per topic + let idontwant_messages_sent_per_topic = register_family!( + "idonttwant_messages_sent_per_topic", + "Number of IDONTWANT messages sent per topic" + ); + + // IDONTWANTs which were ignored, and we still received the message per topic + let idontwant_messages_ignored_per_topic = register_family!( + "idontwant_messages_ignored_per_topic", + "IDONTWANT messages that were sent but we received the full message regardless" + ); + let idontwant_bytes = { let metric = Counter::default(); registry.register( @@ -405,6 +423,8 @@ impl Metrics { idontwant_msgs, idontwant_bytes, idontwant_msgs_ids, + idontwant_messages_sent_per_topic, + idontwant_messages_ignored_per_topic, priority_queue_size, non_priority_queue_size, } @@ -608,6 +628,20 @@ impl Metrics { self.idontwant_bytes.inc_by(bytes as u64); } + /// Register receiving an IDONTWANT control message for a given topic. + pub(crate) fn register_idontwant_messages_sent_per_topic(&mut self, topic: &TopicHash) { + self.idontwant_messages_sent_per_topic + .get_or_create(topic) + .inc(); + } + + /// Register receiving a message for an already sent IDONTWANT. + pub(crate) fn register_idontwant_messages_ignored_per_topic(&mut self, topic: &TopicHash) { + self.idontwant_messages_ignored_per_topic + .get_or_create(topic) + .inc(); + } + /// Register receiving an IDONTWANT msg for this topic. pub(crate) fn register_idontwant(&mut self, msgs: usize) { self.idontwant_msgs.inc(); diff --git a/beacon_node/lighthouse_network/gossipsub/src/types.rs b/beacon_node/lighthouse_network/gossipsub/src/types.rs index d14a929374..f5dac380e3 100644 --- a/beacon_node/lighthouse_network/gossipsub/src/types.rs +++ b/beacon_node/lighthouse_network/gossipsub/src/types.rs @@ -123,8 +123,10 @@ pub(crate) struct PeerConnections { pub(crate) sender: RpcSender, /// Subscribed topics. pub(crate) topics: BTreeSet, - /// Don't send messages. - pub(crate) dont_send: LinkedHashMap, + /// IDONTWANT messages received from the peer. + pub(crate) dont_send_received: LinkedHashMap, + /// IDONTWANT messages we sent to the peer. + pub(crate) dont_send_sent: LinkedHashMap, } /// Describes the types of peers that can exist in the gossipsub context. From 6e1945fc5d2b2025d8ec7eb9b3f2b1eed94967ad Mon Sep 17 00:00:00 2001 From: Pawan Dhananjay Date: Thu, 21 Nov 2024 04:34:45 +0530 Subject: [PATCH 015/254] Avoid computing rpc_blob_limits multiple times (#6595) * Compute blob rpc limits in static block * Fix min size * Use MainnetEthSpec in rpc tests * Revert MainnetEthSpec; add another constant for blob size minimal --- .../lighthouse_network/src/rpc/protocol.rs | 45 ++++++++++++------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/beacon_node/lighthouse_network/src/rpc/protocol.rs b/beacon_node/lighthouse_network/src/rpc/protocol.rs index d0dbffe932..57c2795b04 100644 --- a/beacon_node/lighthouse_network/src/rpc/protocol.rs +++ b/beacon_node/lighthouse_network/src/rpc/protocol.rs @@ -18,11 +18,11 @@ use tokio_util::{ }; use types::{ BeaconBlock, BeaconBlockAltair, BeaconBlockBase, BeaconBlockCapella, BeaconBlockElectra, - BlobSidecar, ChainSpec, DataColumnSidecar, EmptyBlock, EthSpec, ForkContext, ForkName, - LightClientBootstrap, LightClientBootstrapAltair, LightClientFinalityUpdate, + BlobSidecar, ChainSpec, DataColumnSidecar, EmptyBlock, EthSpec, EthSpecId, ForkContext, + ForkName, LightClientBootstrap, LightClientBootstrapAltair, LightClientFinalityUpdate, LightClientFinalityUpdateAltair, LightClientOptimisticUpdate, - LightClientOptimisticUpdateAltair, LightClientUpdate, MainnetEthSpec, Signature, - SignedBeaconBlock, + LightClientOptimisticUpdateAltair, LightClientUpdate, MainnetEthSpec, MinimalEthSpec, + Signature, SignedBeaconBlock, }; // Note: Hardcoding the `EthSpec` type for `SignedBeaconBlock` as min/max values is @@ -105,6 +105,20 @@ pub static SIGNED_BEACON_BLOCK_ELECTRA_MAX: LazyLock = LazyLock::new(|| { + ssz::BYTES_PER_LENGTH_OFFSET }); // Length offset for the blob commitments field. +pub static BLOB_SIDECAR_SIZE: LazyLock = + LazyLock::new(BlobSidecar::::max_size); + +pub static BLOB_SIDECAR_SIZE_MINIMAL: LazyLock = + LazyLock::new(BlobSidecar::::max_size); + +pub static DATA_COLUMNS_SIDECAR_MIN: LazyLock = LazyLock::new(|| { + DataColumnSidecar::::empty() + .as_ssz_bytes() + .len() +}); +pub static DATA_COLUMNS_SIDECAR_MAX: LazyLock = + LazyLock::new(DataColumnSidecar::::max_size); + pub static ERROR_TYPE_MIN: LazyLock = LazyLock::new(|| { VariableList::::from(Vec::::new()) .as_ssz_bytes() @@ -597,8 +611,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 => rpc_data_column_limits::(), - Protocol::DataColumnsByRange => rpc_data_column_limits::(), + Protocol::DataColumnsByRoot => rpc_data_column_limits(), + Protocol::DataColumnsByRange => rpc_data_column_limits(), Protocol::Ping => RpcLimits::new( ::ssz_fixed_len(), ::ssz_fixed_len(), @@ -668,17 +682,18 @@ impl ProtocolId { } pub fn rpc_blob_limits() -> RpcLimits { - RpcLimits::new( - BlobSidecar::::empty().as_ssz_bytes().len(), - BlobSidecar::::max_size(), - ) + match E::spec_name() { + EthSpecId::Minimal => { + RpcLimits::new(*BLOB_SIDECAR_SIZE_MINIMAL, *BLOB_SIDECAR_SIZE_MINIMAL) + } + EthSpecId::Mainnet | EthSpecId::Gnosis => { + RpcLimits::new(*BLOB_SIDECAR_SIZE, *BLOB_SIDECAR_SIZE) + } + } } -pub fn rpc_data_column_limits() -> RpcLimits { - RpcLimits::new( - DataColumnSidecar::::empty().as_ssz_bytes().len(), - DataColumnSidecar::::max_size(), - ) +pub fn rpc_data_column_limits() -> RpcLimits { + RpcLimits::new(*DATA_COLUMNS_SIDECAR_MIN, *DATA_COLUMNS_SIDECAR_MAX) } /* Inbound upgrade */ From 6329042628ea0afcbcbce3874284c78ba9aa41a7 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Fri, 22 Nov 2024 11:30:57 +1100 Subject: [PATCH 016/254] Add filecoin address to `FUNDING.json` (#6602) * Add filecoin address to drips FUNDING.json * Add newline --- FUNDING.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/FUNDING.json b/FUNDING.json index b2fe1aed41..3164f351be 100644 --- a/FUNDING.json +++ b/FUNDING.json @@ -2,9 +2,13 @@ "drips": { "ethereum": { "ownedBy": "0x25c4a76E7d118705e7Ea2e9b7d8C59930d8aCD3b" + }, + "filecoin": { + "ownedBy": "0x25c4a76E7d118705e7Ea2e9b7d8C59930d8aCD3b" } }, "opRetro": { "projectId": "0x04b1cd5a7c59117474ce414b309fa48e985bdaab4b0dab72045f74d04ebd8cff" } } + From 08e8b92e5032044f253a7357aaf0a7c74f9f8b80 Mon Sep 17 00:00:00 2001 From: Age Manning Date: Tue, 26 Nov 2024 12:48:07 +1100 Subject: [PATCH 017/254] Simple Subnet Management (#6146) * Initial temp commit * Merge latest unstable * First draft without tests * Update tests for new version * Correct comments and reviewers comments * Merge latest unstable * Fix errors * Missed a comment, corrected it * Fix lints * Merge latest unstable * Fix tests * Merge latest unstable * Reviewers comments * Remove sync subnets from ENR on unsubscribe * Merge branch 'unstable' into simple-peer-mapping * Merge branch 'unstable' into simple-peer-mapping * Merge branch 'unstable' into simple-peer-mapping * Merge latest unstable * Prevent clash with pin of rust_eth_kzg --- Cargo.lock | 1158 +++++++++++------ Cargo.toml | 2 +- beacon_node/network/src/service.rs | 90 +- beacon_node/network/src/service/tests.rs | 9 +- .../src/subnet_service/attestation_subnets.rs | 687 ---------- beacon_node/network/src/subnet_service/mod.rs | 660 +++++++++- .../src/subnet_service/sync_subnets.rs | 359 ----- .../network/src/subnet_service/tests/mod.rs | 412 +++--- consensus/types/src/chain_spec.rs | 57 +- consensus/types/src/subnet_id.rs | 106 +- 10 files changed, 1606 insertions(+), 1934 deletions(-) delete mode 100644 beacon_node/network/src/subnet_service/attestation_subnets.rs delete mode 100644 beacon_node/network/src/subnet_service/sync_subnets.rs diff --git a/Cargo.lock b/Cargo.lock index b7ba237ac7..d7ce7b9f6c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -59,9 +59,9 @@ dependencies = [ [[package]] name = "addr2line" -version = "0.24.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5fb1d8e4442bd405fdfd1dacb42792696b0cf9cb15882e5d097b742a676d375" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] @@ -149,9 +149,9 @@ dependencies = [ [[package]] name = "allocator-api2" -version = "0.2.18" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +checksum = "45862d1c77f2228b9e10bc609d5bc203d86ebc9b87ad8d5d5167a6c9abf739d9" [[package]] name = "alloy-consensus" @@ -177,9 +177,9 @@ dependencies = [ [[package]] name = "alloy-eip7702" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37d319bb544ca6caeab58c39cea8921c55d924d4f68f2c60f24f914673f9a74a" +checksum = "ea59dc42102bc9a1905dc57901edc6dd48b9f38115df86c7d252acba70d71d04" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -204,9 +204,9 @@ dependencies = [ [[package]] name = "alloy-primitives" -version = "0.8.3" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "411aff151f2a73124ee473708e82ed51b2535f68928b6a1caa8bc1246ae6f7cd" +checksum = "9fce5dbd6a4f118eecc4719eaa9c7ffc31c315e6c5ccde3642db927802312425" dependencies = [ "alloy-rlp", "arbitrary", @@ -215,16 +215,22 @@ dependencies = [ "const-hex", "derive_arbitrary", "derive_more 1.0.0", + "foldhash", "getrandom", + "hashbrown 0.15.1", "hex-literal", + "indexmap 2.6.0", "itoa", "k256 0.13.4", "keccak-asm", + "paste", "proptest", "proptest-derive", "rand", "ruint", + "rustc-hash 2.0.0", "serde", + "sha3 0.10.8", "tiny-keccak", ] @@ -247,7 +253,7 @@ checksum = "2b09cae092c27b6f1bde952653a22708691802e57bfef4a2973b80bea21efd3f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -273,9 +279,9 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstream" -version = "0.6.15" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ "anstyle", "anstyle-parse", @@ -288,49 +294,49 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.8" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "anstyle-parse" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.4" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" dependencies = [ "anstyle", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "anyhow" -version = "1.0.89" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" +checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" [[package]] name = "arbitrary" -version = "1.3.2" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" dependencies = [ "derive_arbitrary", ] @@ -504,7 +510,7 @@ dependencies = [ "nom", "num-traits", "rusticata-macros", - "thiserror", + "thiserror 1.0.69", "time", ] @@ -516,7 +522,7 @@ checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", "synstructure", ] @@ -528,7 +534,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -550,9 +556,9 @@ dependencies = [ [[package]] name = "async-io" -version = "2.3.4" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "444b0228950ee6501b3568d3c93bf1176a1fdbc3b758dcd9475046d30f4dc7e8" +checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059" dependencies = [ "async-lock", "cfg-if", @@ -561,7 +567,7 @@ dependencies = [ "futures-lite", "parking", "polling", - "rustix 0.38.37", + "rustix 0.38.41", "slab", "tracing", "windows-sys 0.59.0", @@ -580,13 +586,13 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.82" +version = "0.1.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -643,20 +649,20 @@ checksum = "3c87f3f15e7794432337fc718554eaa4dc8f04c9677a950ffe366f20a162ae42" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] name = "autocfg" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "axum" -version = "0.7.6" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f43644eed690f5374f1af436ecd6aea01cd201f6fbdf0178adaf6907afb2cec" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" dependencies = [ "async-trait", "axum-core", @@ -665,7 +671,7 @@ dependencies = [ "http 1.1.0", "http-body 1.0.1", "http-body-util", - "hyper 1.4.1", + "hyper 1.5.1", "hyper-util", "itoa", "matchit", @@ -678,9 +684,9 @@ dependencies = [ "serde_json", "serde_path_to_error", "serde_urlencoded", - "sync_wrapper 1.0.1", + "sync_wrapper 1.0.2", "tokio", - "tower 0.5.1", + "tower", "tower-layer", "tower-service", "tracing", @@ -688,9 +694,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6b8ba012a258d63c9adfa28b9ddcf66149da6f986c5b5452e629d5ee64bf00" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" dependencies = [ "async-trait", "bytes", @@ -701,7 +707,7 @@ dependencies = [ "mime", "pin-project-lite", "rustversion", - "sync_wrapper 1.0.1", + "sync_wrapper 1.0.2", "tower-layer", "tower-service", "tracing", @@ -842,7 +848,7 @@ dependencies = [ "genesis", "hex", "http_api", - "hyper 1.4.1", + "hyper 1.5.1", "lighthouse_network", "monitoring_api", "node_test_rig", @@ -907,9 +913,9 @@ dependencies = [ [[package]] name = "bindgen" -version = "0.69.4" +version = "0.69.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" dependencies = [ "bitflags 2.6.0", "cexpr", @@ -924,7 +930,7 @@ dependencies = [ "regex", "rustc-hash 1.1.0", "shlex", - "syn 2.0.77", + "syn 2.0.89", "which", ] @@ -1139,9 +1145,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.7.2" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" +checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" dependencies = [ "serde", ] @@ -1211,7 +1217,7 @@ dependencies = [ "semver 1.0.23", "serde", "serde_json", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -1222,9 +1228,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.1.21" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07b1695e2c7e8fc85310cde85aeaab7e3097f593c91d209d3f9df76c928100f0" +checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47" dependencies = [ "jobserver", "libc", @@ -1348,9 +1354,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.18" +version = "4.5.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0956a43b323ac1afaffc053ed5c4b7c1f1800bacd1683c353aabbb752515dd3" +checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f" dependencies = [ "clap_builder", "clap_derive", @@ -1358,9 +1364,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.18" +version = "4.5.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d72166dd41634086d5803a47eb71ae740e61d84709c36f3c34110173db3961b" +checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec" dependencies = [ "anstream", "anstyle", @@ -1378,14 +1384,14 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] name = "clap_lex" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" +checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7" [[package]] name = "clap_utils" @@ -1456,9 +1462,9 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "compare_fields" @@ -1487,9 +1493,9 @@ dependencies = [ [[package]] name = "const-hex" -version = "1.12.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8a24a26d37e1ffd45343323dc9fe6654ceea44c12f2fcb3d7ac29e610bc6" +checksum = "487981fa1af147182687064d0a2c336586d337a606595ced9ffb0c685c250c73" dependencies = [ "cfg-if", "cpufeatures", @@ -1543,9 +1549,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.14" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" dependencies = [ "libc", ] @@ -1794,7 +1800,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -1842,7 +1848,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -1864,7 +1870,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core 0.20.10", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -1889,9 +1895,9 @@ dependencies = [ [[package]] name = "dary_heap" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7762d17f1241643615821a8455a0b2c3e803784b058693d990b11f2dce25a0ca" +checksum = "04d2cd9c18b9f454ed67da600630b021a8a80bf33f8c95896ab33aaf1c26b728" [[package]] name = "data-encoding" @@ -1942,16 +1948,6 @@ version = "0.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b72465f46d518f6015d9cf07f7f3013a95dd6b9c2747c3d65ae0cce43929d14f" -[[package]] -name = "delay_map" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4355c25cbf99edcb6b4a0e906f6bdc6956eda149e84455bea49696429b2f8e8" -dependencies = [ - "futures", - "tokio-util", -] - [[package]] name = "delay_map" version = "0.4.0" @@ -2034,13 +2030,13 @@ dependencies = [ [[package]] name = "derive_arbitrary" -version = "1.3.2" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" +checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -2053,7 +2049,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version 0.4.1", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -2073,15 +2069,15 @@ checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", "unicode-xid", ] [[package]] name = "diesel" -version = "2.2.4" +version = "2.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "158fe8e2e68695bd615d7e4f3227c0727b151330d3e253b525086c348d055d5e" +checksum = "cbf9649c05e0a9dbd6d0b0b8301db5182b972d0fd02f0a7c6736cf632d7c0fd5" dependencies = [ "bitflags 2.6.0", "byteorder", @@ -2101,7 +2097,7 @@ dependencies = [ "dsl_auto_type", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -2121,7 +2117,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "209c735641a413bc68c4923a9d6ad4bcb3ca306b794edaa7eb0b3228a99ffb25" dependencies = [ - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -2206,7 +2202,7 @@ dependencies = [ "alloy-rlp", "arrayvec", "ctr 0.9.2", - "delay_map 0.4.0", + "delay_map", "enr", "fnv", "futures", @@ -2236,7 +2232,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -2267,7 +2263,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -2412,9 +2408,9 @@ dependencies = [ [[package]] name = "encoding_rs" -version = "0.8.34" +version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ "cfg-if", ] @@ -2447,7 +2443,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -2718,7 +2714,7 @@ dependencies = [ "serde", "serde_json", "sha3 0.9.1", - "thiserror", + "thiserror 1.0.69", "uint 0.9.5", ] @@ -2735,7 +2731,7 @@ dependencies = [ "serde", "serde_json", "sha3 0.10.8", - "thiserror", + "thiserror 1.0.69", "uint 0.9.5", ] @@ -2841,7 +2837,7 @@ dependencies = [ "darling 0.20.10", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -2860,7 +2856,7 @@ dependencies = [ "pin-project", "serde", "serde_json", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -2927,7 +2923,7 @@ dependencies = [ "serde_json", "strum", "syn 1.0.109", - "thiserror", + "thiserror 1.0.69", "tiny-keccak", "unicode-xid", ] @@ -2955,7 +2951,7 @@ dependencies = [ "reqwest", "serde", "serde_json", - "thiserror", + "thiserror 1.0.69", "tokio", "tracing", "tracing-futures", @@ -3094,9 +3090,9 @@ checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" [[package]] name = "fastrand" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" +checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" [[package]] name = "fastrlp" @@ -3116,7 +3112,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e182f7dbc2ef73d9ef67351c5fbbea084729c48362d3ce9dd44c28e32e277fe5" dependencies = [ "libc", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -3204,9 +3200,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.33" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253" +checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" dependencies = [ "crc32fast", "libz-sys", @@ -3219,6 +3215,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" + [[package]] name = "foreign-types" version = "0.3.2" @@ -3283,9 +3285,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", @@ -3324,9 +3326,9 @@ checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", @@ -3342,9 +3344,9 @@ checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" -version = "2.3.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" +checksum = "cef40d21ae2c515b51041df9ed313ed21e572df340ea58a922a0aefe7e8891a1" dependencies = [ "futures-core", "pin-project-lite", @@ -3358,7 +3360,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -3368,7 +3370,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f2f12607f92c69b12ed746fabf9ca4f5c482cba46679c1a75b874ed7c26adb" dependencies = [ "futures-io", - "rustls 0.23.13", + "rustls 0.23.18", "rustls-pki-types", ] @@ -3485,9 +3487,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.31.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "git-version" @@ -3506,7 +3508,7 @@ checksum = "53010ccb100b96a67bc32c0175f0ed1426b31b655d562898e57325f81c023ac0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -3594,7 +3596,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.5.0", + "indexmap 2.6.0", "slab", "tokio", "tokio-util", @@ -3642,6 +3644,18 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "hashbrown" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", + "serde", +] + [[package]] name = "hashers" version = "1.0.1" @@ -3765,7 +3779,7 @@ dependencies = [ "once_cell", "rand", "socket2", - "thiserror", + "thiserror 1.0.69", "tinyvec", "tokio", "tracing", @@ -3788,7 +3802,7 @@ dependencies = [ "rand", "resolv-conf", "smallvec", - "thiserror", + "thiserror 1.0.69", "tokio", "tracing", ] @@ -3987,9 +4001,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.9.4" +version = "1.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" [[package]] name = "httpdate" @@ -4005,9 +4019,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.30" +version = "0.14.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9" +checksum = "8c08302e8fa335b151b788c775ff56e7a03ae64ff85c548ee820fecb70356e85" dependencies = [ "bytes", "futures-channel", @@ -4029,9 +4043,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.4.1" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" +checksum = "97818827ef4f364230e16705d4706e2897df2bb60617d6ca15d598025a3c481f" dependencies = [ "bytes", "futures-channel", @@ -4054,7 +4068,7 @@ checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" dependencies = [ "futures-util", "http 0.2.12", - "hyper 0.14.30", + "hyper 0.14.31", "rustls 0.21.12", "tokio", "tokio-rustls 0.24.1", @@ -4067,7 +4081,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ "bytes", - "hyper 0.14.30", + "hyper 0.14.31", "native-tls", "tokio", "tokio-native-tls", @@ -4075,18 +4089,17 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.8" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da62f120a8a37763efb0cf8fdf264b884c7b8b9ac8660b900c8661030c00e6ba" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" dependencies = [ "bytes", "futures-util", "http 1.1.0", "http-body 1.0.1", - "hyper 1.4.1", + "hyper 1.5.1", "pin-project-lite", "tokio", - "tower 0.4.13", "tower-service", ] @@ -4113,6 +4126,124 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.89", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -4131,12 +4262,23 @@ dependencies = [ [[package]] name = "idna" -version = "0.5.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", ] [[package]] @@ -4151,9 +4293,9 @@ dependencies = [ [[package]] name = "if-watch" -version = "3.2.0" +version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6b0422c86d7ce0e97169cc42e04ae643caf278874a7a3c87b8150a220dc7e1e" +checksum = "cdf9d64cfcf380606e64f9a0bcf493616b65331199f984151a6fa11a7b3cde38" dependencies = [ "async-io", "core-foundation", @@ -4162,8 +4304,12 @@ dependencies = [ "if-addrs", "ipnet", "log", + "netlink-packet-core", + "netlink-packet-route", + "netlink-proto", + "netlink-sys", "rtnetlink", - "system-configuration", + "system-configuration 0.6.1", "tokio", "windows", ] @@ -4179,7 +4325,7 @@ dependencies = [ "bytes", "futures", "http 0.2.12", - "hyper 0.14.30", + "hyper 0.14.31", "log", "rand", "tokio", @@ -4202,7 +4348,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f" dependencies = [ - "parity-scale-codec 3.6.12", + "parity-scale-codec 3.7.0", ] [[package]] @@ -4234,13 +4380,13 @@ dependencies = [ [[package]] name = "impl-trait-for-tuples" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11d7a9f6330b71fea57921c9b61c47ee6e84f72d394754eff6163ae67e7395eb" +checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.89", ] [[package]] @@ -4261,12 +4407,14 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ + "arbitrary", "equivalent", - "hashbrown 0.14.5", + "hashbrown 0.15.1", + "serde", ] [[package]] @@ -4358,9 +4506,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.10.0" +version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "187674a687eed5fe42285b40c6291f9a01517d415fad1c3cbc6a9f778af7fcd4" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" [[package]] name = "is-terminal" @@ -4408,9 +4556,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "540654e97a3f4470a492cd30ff187bc95d89557a903a2bbf112e2fae98104ef2" [[package]] name = "jobserver" @@ -4423,9 +4571,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.70" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" dependencies = [ "wasm-bindgen", ] @@ -4596,9 +4744,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.158" +version = "0.2.164" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" +checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f" [[package]] name = "libflate" @@ -4636,9 +4784,9 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.8" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" [[package]] name = "libmdbx" @@ -4652,7 +4800,7 @@ dependencies = [ "libc", "mdbx-sys", "parking_lot 0.12.3", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -4684,7 +4832,7 @@ dependencies = [ "multiaddr", "pin-project", "rw-stream-sink", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -4732,7 +4880,7 @@ dependencies = [ "rand", "rw-stream-sink", "smallvec", - "thiserror", + "thiserror 1.0.69", "tracing", "unsigned-varint 0.8.0", "void", @@ -4773,16 +4921,16 @@ dependencies = [ "quick-protobuf", "quick-protobuf-codec", "smallvec", - "thiserror", + "thiserror 1.0.69", "tracing", "void", ] [[package]] name = "libp2p-identity" -version = "0.2.9" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cca1eb2bc1fd29f099f3daaab7effd01e1a54b7c577d0ed082521034d912e8" +checksum = "257b5621d159b32282eac446bed6670c39c7dc68a200a992d8f056afa0066f6d" dependencies = [ "asn1_der", "bs58 0.5.1", @@ -4795,9 +4943,8 @@ dependencies = [ "rand", "sec1 0.7.3", "sha2 0.10.8", - "thiserror", + "thiserror 1.0.69", "tracing", - "void", "zeroize", ] @@ -4877,7 +5024,7 @@ dependencies = [ "sha2 0.10.8", "snow", "static_assertions", - "thiserror", + "thiserror 1.0.69", "tracing", "x25519-dalek", "zeroize", @@ -4916,9 +5063,9 @@ dependencies = [ "quinn", "rand", "ring 0.17.8", - "rustls 0.23.13", + "rustls 0.23.18", "socket2", - "thiserror", + "thiserror 1.0.69", "tokio", "tracing", ] @@ -4956,7 +5103,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -4988,9 +5135,9 @@ dependencies = [ "libp2p-identity", "rcgen", "ring 0.17.8", - "rustls 0.23.13", + "rustls 0.23.18", "rustls-webpki 0.101.7", - "thiserror", + "thiserror 1.0.69", "x509-parser", "yasna", ] @@ -5020,10 +5167,10 @@ dependencies = [ "either", "futures", "libp2p-core", - "thiserror", + "thiserror 1.0.69", "tracing", "yamux 0.12.1", - "yamux 0.13.3", + "yamux 0.13.4", ] [[package]] @@ -5157,7 +5304,7 @@ dependencies = [ "alloy-rlp", "async-channel", "bytes", - "delay_map 0.3.0", + "delay_map", "directory", "dirs", "discv5", @@ -5232,6 +5379,12 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "litemap" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" + [[package]] name = "lmdb-rkv" version = "0.14.0" @@ -5300,11 +5453,11 @@ dependencies = [ [[package]] name = "lru" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37ee39891760e7d94734f6f63fedc29a2e4a152f836120753a72503f09fcf904" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" dependencies = [ - "hashbrown 0.14.5", + "hashbrown 0.15.1", ] [[package]] @@ -5433,18 +5586,18 @@ dependencies = [ [[package]] name = "metastruct" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f00a5ba4a0f3453c31c397b214e1675d95b697c33763aa58add57ea833424384" +checksum = "d74f54f231f9a18d77393ecc5cc7ab96709b2a61ee326c2b2b291009b0cc5a07" dependencies = [ "metastruct_macro", ] [[package]] name = "metastruct_macro" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c3a991d4536c933306e52f0e8ab303757185ec13a09d1f3e1cbde5a0d8410bf" +checksum = "985e7225f3a4dfbec47a0c6a730a874185fda840d365d7bbd6ba199dd81796d5" dependencies = [ "darling 0.13.4", "itertools 0.10.5", @@ -5580,9 +5733,9 @@ checksum = "1fafa6961cabd9c63bcd77a45d7e3b7f3b552b70417831fb0f56db717e72407e" [[package]] name = "multiaddr" -version = "0.18.1" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b852bc02a2da5feed68cd14fa50d0774b92790a5bdbfa932a813926c8472070" +checksum = "fe6351f60b488e04c1d21bc69e56b89cb3f5e8f5d22557d6e8031bdfd79b6961" dependencies = [ "arrayref", "byteorder", @@ -5593,7 +5746,7 @@ dependencies = [ "percent-encoding", "serde", "static_assertions", - "unsigned-varint 0.7.2", + "unsigned-varint 0.8.0", "url", ] @@ -5610,12 +5763,12 @@ dependencies = [ [[package]] name = "multihash" -version = "0.19.1" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "076d548d76a0e2a0d4ab471d0b1c36c577786dfc4471242035d97a12a735c492" +checksum = "cc41f430805af9d1cf4adae4ed2149c759b877b01d909a1f40256188d09345d2" dependencies = [ "core2", - "unsigned-varint 0.7.2", + "unsigned-varint 0.8.0", ] [[package]] @@ -5651,21 +5804,20 @@ dependencies = [ [[package]] name = "netlink-packet-core" -version = "0.4.2" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "345b8ab5bd4e71a2986663e88c56856699d060e78e152e6e9d7966fcd5491297" +checksum = "72724faf704479d67b388da142b186f916188505e7e0b26719019c525882eda4" dependencies = [ "anyhow", "byteorder", - "libc", "netlink-packet-utils", ] [[package]] name = "netlink-packet-route" -version = "0.12.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9ea4302b9759a7a88242299225ea3688e63c85ea136371bb6cf94fd674efaab" +checksum = "053998cea5a306971f88580d0829e90f270f940befd7cf928da179d4187a5a66" dependencies = [ "anyhow", "bitflags 1.3.2", @@ -5684,21 +5836,21 @@ dependencies = [ "anyhow", "byteorder", "paste", - "thiserror", + "thiserror 1.0.69", ] [[package]] name = "netlink-proto" -version = "0.10.0" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65b4b14489ab424703c092062176d52ba55485a89c076b4f9db05092b7223aa6" +checksum = "86b33524dc0968bfad349684447bfce6db937a9ac3332a1fe60c0c5a5ce63f21" dependencies = [ "bytes", "futures", "log", "netlink-packet-core", "netlink-sys", - "thiserror", + "thiserror 1.0.69", "tokio", ] @@ -5726,7 +5878,7 @@ dependencies = [ "beacon_chain", "beacon_processor", "bls", - "delay_map 0.3.0", + "delay_map", "derivative", "error-chain", "eth2", @@ -5776,6 +5928,17 @@ dependencies = [ "libc", ] +[[package]] +name = "nix" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", +] + [[package]] name = "nix" version = "0.29.0" @@ -5916,9 +6079,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.4" +version = "0.36.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" dependencies = [ "memchr", ] @@ -5934,9 +6097,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.19.0" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "oneshot_broadcast" @@ -5984,9 +6147,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.66" +version = "0.10.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" +checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" dependencies = [ "bitflags 2.6.0", "cfg-if", @@ -6005,7 +6168,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -6016,18 +6179,18 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" -version = "300.3.2+3.3.2" +version = "300.4.1+3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a211a18d945ef7e648cc6e0058f4c548ee46aab922ea203e0d30e966ea23647b" +checksum = "faa4eac4138c62414b5622d1b31c5c304f34b406b013c079c2bbc652fdd6678c" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.103" +version = "0.9.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" dependencies = [ "cc", "libc", @@ -6101,15 +6264,16 @@ dependencies = [ [[package]] name = "parity-scale-codec" -version = "3.6.12" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "306800abfa29c7f16596b5970a588435e3d5b3149683d00c12b699cc19f895ee" +checksum = "8be4817d39f3272f69c59fe05d0535ae6456c2dc2fa1ba02910296c7e0a5c590" dependencies = [ "arrayvec", "bitvec 1.0.1", "byte-slice-cast", "impl-trait-for-tuples", - "parity-scale-codec-derive 3.6.12", + "parity-scale-codec-derive 3.7.0", + "rustversion", "serde", ] @@ -6127,14 +6291,14 @@ dependencies = [ [[package]] name = "parity-scale-codec-derive" -version = "3.6.12" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d830939c76d294956402033aee57a6da7b438f2294eb94864c37b0569053a42c" +checksum = "8781a75c6205af67215f382092b6e0a4ff3734798523e69073d4bcd294ec767b" dependencies = [ "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.89", ] [[package]] @@ -6186,7 +6350,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.4", + "redox_syscall 0.5.7", "smallvec", "windows-targets 0.52.6", ] @@ -6256,12 +6420,12 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.7.13" +version = "2.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdbef9d1d47087a895abd220ed25eb4ad973a5e26f6a4367b038c25e28dfc2d9" +checksum = "879952a81a83930934cbf1786752d6dedc3b1f29e8f8fb2ad1d0a36f377cf442" dependencies = [ "memchr", - "thiserror", + "thiserror 1.0.69", "ucd-trie", ] @@ -6295,29 +6459,29 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.5" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +checksum = "be57f64e946e500c8ee36ef6331845d40a93055567ec57e8fae13efd33759b95" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.5" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] name = "pin-project-lite" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" [[package]] name = "pin-utils" @@ -6347,9 +6511,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "platforms" @@ -6387,15 +6551,15 @@ dependencies = [ [[package]] name = "polling" -version = "3.7.3" +version = "3.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511" +checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" dependencies = [ "cfg-if", "concurrent-queue", "hermit-abi 0.4.0", "pin-project-lite", - "rustix 0.38.37", + "rustix 0.38.41", "tracing", "windows-sys 0.59.0", ] @@ -6486,12 +6650,12 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.20" +version = "0.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" +checksum = "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033" dependencies = [ "proc-macro2", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -6546,14 +6710,14 @@ version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" dependencies = [ - "toml_edit 0.22.21", + "toml_edit 0.22.22", ] [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] @@ -6585,7 +6749,7 @@ dependencies = [ "memchr", "parking_lot 0.12.3", "protobuf", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -6608,7 +6772,7 @@ checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -6625,7 +6789,7 @@ dependencies = [ "rand", "rand_chacha", "rand_xorshift", - "regex-syntax 0.8.4", + "regex-syntax 0.8.5", "rusty-fork", "tempfile", "unarray", @@ -6639,7 +6803,7 @@ checksum = "6ff7ff745a347b87471d859a377a9a404361e7efc2a971d73424a6d183c0fc77" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -6676,7 +6840,7 @@ dependencies = [ "num_cpus", "once_cell", "platforms", - "thiserror", + "thiserror 1.0.69", "unescape", ] @@ -6703,7 +6867,7 @@ dependencies = [ "asynchronous-codec", "bytes", "quick-protobuf", - "thiserror", + "thiserror 1.0.69", "unsigned-varint 0.8.0", ] @@ -6731,9 +6895,9 @@ dependencies = [ [[package]] name = "quinn" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c7c5fdde3cdae7203427dc4f0a68fe0ed09833edc525a03456b153b79828684" +checksum = "62e96808277ec6f97351a2380e6c25114bc9e67037775464979f3037c92d05ef" dependencies = [ "bytes", "futures-io", @@ -6741,36 +6905,40 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash 2.0.0", - "rustls 0.23.13", + "rustls 0.23.18", "socket2", - "thiserror", + "thiserror 2.0.3", "tokio", "tracing", ] [[package]] name = "quinn-proto" -version = "0.11.8" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fadfaed2cd7f389d0161bb73eeb07b7b78f8691047a6f3e73caaeae55310a4a6" +checksum = "a2fe5ef3495d7d2e377ff17b1a8ce2ee2ec2a18cde8b6ad6619d65d0701c135d" dependencies = [ "bytes", + "getrandom", "rand", "ring 0.17.8", "rustc-hash 2.0.0", - "rustls 0.23.13", + "rustls 0.23.18", + "rustls-pki-types", "slab", - "thiserror", + "thiserror 2.0.3", "tinyvec", "tracing", + "web-time", ] [[package]] name = "quinn-udp" -version = "0.5.5" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fe68c2e9e1a1234e218683dbdf9f9dfcb094113c5ac2b938dfcb9bab4c4140b" +checksum = "7d5a626c6807713b15cac82a6acaccd6043c9a5408c24baae07611fec3f243da" dependencies = [ + "cfg_aliases", "libc", "once_cell", "socket2", @@ -6829,6 +6997,7 @@ dependencies = [ "libc", "rand_chacha", "rand_core", + "serde", ] [[package]] @@ -6893,9 +7062,9 @@ dependencies = [ [[package]] name = "redb" -version = "2.1.4" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "074373f3e7e5d27d8741d19512232adb47be8622d3daef3a45bcae72050c3d2a" +checksum = "84b1de48a7cf7ba193e81e078d17ee2b786236eed1d3f7c60f8a09545efc4925" dependencies = [ "libc", ] @@ -6911,9 +7080,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.4" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0884ad60e090bf1345b93da0a5de8923c93884cd03f40dfcfddd3b4bee661853" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" dependencies = [ "bitflags 2.6.0", ] @@ -6926,19 +7095,19 @@ checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ "getrandom", "libredox", - "thiserror", + "thiserror 1.0.69", ] [[package]] name = "regex" -version = "1.10.6" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.7", - "regex-syntax 0.8.4", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", ] [[package]] @@ -6952,13 +7121,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.4", + "regex-syntax 0.8.5", ] [[package]] @@ -6969,9 +7138,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "reqwest" @@ -6987,7 +7156,7 @@ dependencies = [ "h2", "http 0.2.12", "http-body 0.4.6", - "hyper 0.14.30", + "hyper 0.14.31", "hyper-rustls", "hyper-tls", "ipnet", @@ -7004,7 +7173,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "sync_wrapper 0.1.2", - "system-configuration", + "system-configuration 0.5.1", "tokio", "tokio-native-tls", "tokio-rustls 0.24.1", @@ -7128,16 +7297,19 @@ dependencies = [ [[package]] name = "rtnetlink" -version = "0.10.1" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322c53fd76a18698f1c27381d58091de3a043d356aa5bd0d510608b565f469a0" +checksum = "7a552eb82d19f38c3beed3f786bd23aa434ceb9ac43ab44419ca6d67a7e186c0" dependencies = [ "futures", "log", + "netlink-packet-core", "netlink-packet-route", + "netlink-packet-utils", "netlink-proto", - "nix 0.24.3", - "thiserror", + "netlink-sys", + "nix 0.26.4", + "thiserror 1.0.69", "tokio", ] @@ -7155,7 +7327,7 @@ dependencies = [ "fastrlp", "num-bigint", "num-traits", - "parity-scale-codec 3.6.12", + "parity-scale-codec 3.7.0", "primitive-types 0.12.2", "proptest", "rand", @@ -7267,9 +7439,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.37" +version = "0.38.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" +checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6" dependencies = [ "bitflags 2.6.0", "errno", @@ -7306,9 +7478,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.13" +version = "0.23.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2dabaac7466917e566adb06783a81ca48944c6898a1b08b9374106dd671f4c8" +checksum = "9c9cc1d47e243d655ace55ed38201c19ae02c148ae56412ab8750e8f0166ab7f" dependencies = [ "once_cell", "ring 0.17.8", @@ -7329,19 +7501,21 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "2.1.3" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" dependencies = [ - "base64 0.22.1", "rustls-pki-types", ] [[package]] name = "rustls-pki-types" -version = "1.8.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" +checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" +dependencies = [ + "web-time", +] [[package]] name = "rustls-webpki" @@ -7366,9 +7540,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" +checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" [[package]] name = "rusty-fork" @@ -7423,33 +7597,33 @@ dependencies = [ [[package]] name = "scale-info" -version = "2.11.3" +version = "2.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca070c12893629e2cc820a9761bedf6ce1dcddc9852984d1dc734b8bd9bd024" +checksum = "346a3b32eba2640d17a9cb5927056b08f3de90f65b72fe09402c2ad07d684d0b" dependencies = [ "cfg-if", - "derive_more 0.99.18", - "parity-scale-codec 3.6.12", + "derive_more 1.0.0", + "parity-scale-codec 3.7.0", "scale-info-derive", ] [[package]] name = "scale-info-derive" -version = "2.11.3" +version = "2.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d35494501194174bda522a32605929eefc9ecf7e0a326c26db1fdd85881eb62" +checksum = "c6630024bf739e2179b91fb424b28898baf819414262c5d376677dbff1fe7ebf" dependencies = [ "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.89", ] [[package]] name = "schannel" -version = "0.1.24" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9aaafd5a2b6e3d657ff009d82fbd630b6bd54dd4eb06f21693925cdf80f9b8b" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" dependencies = [ "windows-sys 0.59.0", ] @@ -7540,9 +7714,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.12.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6" +checksum = "fa39c7303dc58b5543c94d22c1766b0d31f2ee58306363ea622b10bbc075eaa2" dependencies = [ "core-foundation-sys", "libc", @@ -7568,9 +7742,9 @@ dependencies = [ [[package]] name = "semver-parser" -version = "0.10.2" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7" +checksum = "9900206b54a3527fdc7b8a938bffd94a568bac4f4aa8113b209df75a09c0dec2" dependencies = [ "pest", ] @@ -7591,9 +7765,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.210" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" dependencies = [ "serde_derive", ] @@ -7610,20 +7784,20 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.210" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] name = "serde_json" -version = "1.0.128" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" dependencies = [ "itoa", "memchr", @@ -7649,14 +7823,14 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] name = "serde_spanned" -version = "0.6.7" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" dependencies = [ "serde", ] @@ -7701,7 +7875,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.5.0", + "indexmap 2.6.0", "itoa", "ryu", "serde", @@ -7843,7 +8017,7 @@ checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" dependencies = [ "num-bigint", "num-traits", - "thiserror", + "thiserror 1.0.69", "time", ] @@ -8231,7 +8405,7 @@ dependencies = [ "tempfile", "types", "xdelta3", - "zstd 0.13.1", + "zstd 0.13.2", ] [[package]] @@ -8322,9 +8496,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.77" +version = "2.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e" dependencies = [ "proc-macro2", "quote", @@ -8339,9 +8513,9 @@ checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" [[package]] name = "sync_wrapper" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" [[package]] name = "synstructure" @@ -8351,7 +8525,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -8377,7 +8551,18 @@ checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags 1.3.2", "core-foundation", - "system-configuration-sys", + "system-configuration-sys 0.5.0", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.6.0", + "core-foundation", + "system-configuration-sys 0.6.0", ] [[package]] @@ -8390,6 +8575,16 @@ dependencies = [ "libc", ] +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "system_health" version = "0.1.0" @@ -8442,14 +8637,14 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.12.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" +checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" dependencies = [ "cfg-if", "fastrand", "once_cell", - "rustix 0.38.37", + "rustix 0.38.41", "windows-sys 0.59.0", ] @@ -8475,12 +8670,12 @@ dependencies = [ [[package]] name = "terminal_size" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" +checksum = "4f599bd7ca042cfdf8f4512b277c02ba102247820f9d9d4a9f521f496751a6ef" dependencies = [ - "rustix 0.38.37", - "windows-sys 0.48.0", + "rustix 0.38.41", + "windows-sys 0.59.0", ] [[package]] @@ -8518,22 +8713,42 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.64" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c006c85c7651b3cf2ada4584faa36773bd07bac24acfb39f3c431b36d7e667aa" +dependencies = [ + "thiserror-impl 2.0.3", ] [[package]] name = "thiserror-impl" -version = "1.0.64" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.89", ] [[package]] @@ -8641,7 +8856,7 @@ dependencies = [ "rand", "rustc-hash 1.1.0", "sha2 0.10.8", - "thiserror", + "thiserror 1.0.69", "unicode-normalization", "wasm-bindgen", "zeroize", @@ -8656,6 +8871,16 @@ dependencies = [ "crunchy", ] +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tinytemplate" version = "1.2.1" @@ -8683,9 +8908,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.40.0" +version = "1.41.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" dependencies = [ "backtrace", "bytes", @@ -8716,7 +8941,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -8821,7 +9046,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.21", + "toml_edit 0.22.22", ] [[package]] @@ -8839,37 +9064,22 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.5.0", + "indexmap 2.6.0", "toml_datetime", "winnow 0.5.40", ] [[package]] name = "toml_edit" -version = "0.22.21" +version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b072cee73c449a636ffd6f32bd8de3a9f7119139aff882f44943ce2986dc5cf" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ - "indexmap 2.5.0", + "indexmap 2.6.0", "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.18", -] - -[[package]] -name = "tower" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" -dependencies = [ - "futures-core", - "futures-util", - "pin-project", - "pin-project-lite", - "tokio", - "tower-layer", - "tower-service", + "winnow 0.6.20", ] [[package]] @@ -8919,7 +9129,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" dependencies = [ "crossbeam-channel", - "thiserror", + "thiserror 1.0.69", "time", "tracing-subscriber", ] @@ -8932,7 +9142,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -9023,7 +9233,7 @@ dependencies = [ "darling 0.20.10", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -9038,9 +9248,9 @@ dependencies = [ [[package]] name = "triomphe" -version = "0.1.13" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6631e42e10b40c0690bf92f404ebcfe6e1fdb480391d15f17cc8e96eeed5369" +checksum = "ef8f7726da4807b58ea5c96fdc122f80702030edc33b35aff9190a51148ccc85" dependencies = [ "serde", "stable_deref_trait", @@ -9113,9 +9323,9 @@ dependencies = [ [[package]] name = "ucd-trie" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" [[package]] name = "uint" @@ -9155,24 +9365,21 @@ checksum = "ccb97dac3243214f8d8507998906ca3e2e0b900bf9bf4870477f125b82e68f6e" [[package]] name = "unicase" -version = "2.7.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" -dependencies = [ - "version_check", -] +checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" [[package]] name = "unicode-bidi" -version = "0.3.15" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" +checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" [[package]] name = "unicode-ident" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] name = "unicode-normalization" @@ -9185,9 +9392,9 @@ dependencies = [ [[package]] name = "unicode-properties" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ea75f83c0137a9b98608359a5f1af8144876eb67bcb1ce837368e906a9f524" +checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" [[package]] name = "unicode-xid" @@ -9250,15 +9457,27 @@ dependencies = [ [[package]] name = "url" -version = "2.5.2" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", - "idna 0.5.0", + "idna 1.0.3", "percent-encoding", ] +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -9290,7 +9509,7 @@ dependencies = [ "eth2", "fdlimit", "graffiti_file", - "hyper 1.4.1", + "hyper 1.5.1", "initialized_validators", "metrics", "monitoring_api", @@ -9529,13 +9748,13 @@ dependencies = [ "futures-util", "headers", "http 0.2.12", - "hyper 0.14.30", + "hyper 0.14.31", "log", "mime", "mime_guess", "percent-encoding", "pin-project", - "rustls-pemfile 2.1.3", + "rustls-pemfile 2.2.0", "scoped-tls", "serde", "serde_json", @@ -9580,9 +9799,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" dependencies = [ "cfg-if", "once_cell", @@ -9591,24 +9810,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.43" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" +checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" dependencies = [ "cfg-if", "js-sys", @@ -9618,9 +9837,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -9628,28 +9847,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" [[package]] name = "wasm-streams" -version = "0.4.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" dependencies = [ "futures-util", "js-sys", @@ -9688,7 +9907,7 @@ dependencies = [ "env_logger 0.9.3", "eth2", "http_api", - "hyper 1.4.1", + "hyper 1.5.1", "log", "logging", "network", @@ -9709,9 +9928,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.70" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" +checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" dependencies = [ "js-sys", "wasm-bindgen", @@ -9770,7 +9989,7 @@ dependencies = [ "either", "home", "once_cell", - "rustix 0.38.37", + "rustix 0.38.41", ] [[package]] @@ -9779,7 +9998,7 @@ version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" dependencies = [ - "redox_syscall 0.5.4", + "redox_syscall 0.5.7", "wasite", "web-sys", ] @@ -9829,12 +10048,12 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" -version = "0.51.1" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca229916c5ee38c2f2bc1e9d8f04df975b4bd93f9955dc69fabb5d91270045c9" +checksum = "efc5cf48f83140dcaab716eeaea345f9e93d0018fb81162753a3f76c3397b538" dependencies = [ - "windows-core 0.51.1", - "windows-targets 0.48.5", + "windows-core 0.53.0", + "windows-targets 0.52.6", ] [[package]] @@ -9851,18 +10070,28 @@ dependencies = [ [[package]] name = "windows-core" -version = "0.51.1" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] name = "windows-core" -version = "0.52.0" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +checksum = "9dcc5b895a6377f1ab9fa55acedab1fd5ac0db66ad1e6c7f47e28a22e446a5dd" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" dependencies = [ "windows-targets 0.52.6", ] @@ -10092,9 +10321,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.18" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" dependencies = [ "memchr", ] @@ -10109,6 +10338,18 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + [[package]] name = "ws_stream_wasm" version = "0.7.4" @@ -10122,7 +10363,7 @@ dependencies = [ "pharos", "rustc_version 0.4.1", "send_wrapper", - "thiserror", + "thiserror 1.0.69", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -10168,7 +10409,7 @@ dependencies = [ "nom", "oid-registry", "rusticata-macros", - "thiserror", + "thiserror 1.0.69", "time", ] @@ -10188,9 +10429,9 @@ dependencies = [ [[package]] name = "xml-rs" -version = "0.8.22" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af4e2e2f7cba5a093896c1e150fbfe177d1883e7448200efb81d40b9d339ef26" +checksum = "af310deaae937e48a26602b730250b4949e125f468f11e6990be3e5304ddd96f" [[package]] name = "xmltree" @@ -10229,9 +10470,9 @@ dependencies = [ [[package]] name = "yamux" -version = "0.13.3" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31b5e376a8b012bee9c423acdbb835fc34d45001cfa3106236a624e4b738028" +checksum = "17610762a1207ee816c6fadc29220904753648aba0a9ed61c7b8336e80a559c4" dependencies = [ "futures", "log", @@ -10252,6 +10493,30 @@ dependencies = [ "time", ] +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.89", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.7.35" @@ -10270,7 +10535,28 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", +] + +[[package]] +name = "zerofrom" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.89", + "synstructure", ] [[package]] @@ -10290,7 +10576,29 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", +] + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.89", ] [[package]] @@ -10324,11 +10632,11 @@ dependencies = [ [[package]] name = "zstd" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d789b1514203a1120ad2429eae43a7bd32b90976a7bb8a05f7ec02fa88cc23a" +checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" dependencies = [ - "zstd-safe 7.1.0", + "zstd-safe 7.2.1", ] [[package]] @@ -10343,9 +10651,9 @@ dependencies = [ [[package]] name = "zstd-safe" -version = "7.1.0" +version = "7.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd99b45c6bc03a018c8b8a86025678c87e55526064e38f9df301989dce7ec0a" +checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059" dependencies = [ "zstd-sys", ] diff --git a/Cargo.toml b/Cargo.toml index 8cf4abb33e..e11f7505ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -122,7 +122,7 @@ clap = { version = "4.5.4", features = ["derive", "cargo", "wrap_help"] } c-kzg = { version = "1", default-features = false } compare_fields_derive = { path = "common/compare_fields_derive" } criterion = "0.5" -delay_map = "0.3" +delay_map = "0.4" derivative = "2" dirs = "3" either = "1.9" diff --git a/beacon_node/network/src/service.rs b/beacon_node/network/src/service.rs index 5a66cb7f30..37dc4a8384 100644 --- a/beacon_node/network/src/service.rs +++ b/beacon_node/network/src/service.rs @@ -2,12 +2,9 @@ use crate::nat; use crate::network_beacon_processor::InvalidBlockStorage; use crate::persisted_dht::{clear_dht, load_dht, persist_dht}; use crate::router::{Router, RouterMessage}; -use crate::subnet_service::SyncCommitteeService; +use crate::subnet_service::{SubnetService, SubnetServiceMessage, Subscription}; +use crate::NetworkConfig; use crate::{error, metrics}; -use crate::{ - subnet_service::{AttestationService, SubnetServiceMessage}, - NetworkConfig, -}; use beacon_chain::{BeaconChain, BeaconChainTypes}; use beacon_processor::{work_reprocessing_queue::ReprocessQueueMessage, BeaconProcessorSend}; use futures::channel::mpsc::Sender; @@ -165,10 +162,8 @@ pub struct NetworkService { beacon_chain: Arc>, /// The underlying libp2p service that drives all the network interactions. libp2p: Network, - /// An attestation and subnet manager service. - attestation_service: AttestationService, - /// A sync committeee subnet manager service. - sync_committee_service: SyncCommitteeService, + /// An attestation and sync committee subnet manager service. + subnet_service: SubnetService, /// The receiver channel for lighthouse to communicate with the network service. network_recv: mpsc::UnboundedReceiver>, /// The receiver channel for lighthouse to send validator subscription requests. @@ -317,16 +312,13 @@ impl NetworkService { network_log.clone(), )?; - // attestation subnet service - let attestation_service = AttestationService::new( + // attestation and sync committee subnet service + let subnet_service = SubnetService::new( beacon_chain.clone(), network_globals.local_enr().node_id(), &config, &network_log, ); - // sync committee subnet service - let sync_committee_service = - SyncCommitteeService::new(beacon_chain.clone(), &config, &network_log); // create a timer for updating network metrics let metrics_update = tokio::time::interval(Duration::from_secs(METRIC_UPDATE_INTERVAL)); @@ -344,8 +336,7 @@ impl NetworkService { let network_service = NetworkService { beacon_chain, libp2p, - attestation_service, - sync_committee_service, + subnet_service, network_recv, validator_subscription_recv, router_send, @@ -460,11 +451,8 @@ impl NetworkService { // handle a message from a validator requesting a subscription to a subnet Some(msg) = self.validator_subscription_recv.recv() => self.on_validator_subscription_msg(msg).await, - // process any attestation service events - Some(msg) = self.attestation_service.next() => self.on_attestation_service_msg(msg), - - // process any sync committee service events - Some(msg) = self.sync_committee_service.next() => self.on_sync_committee_service_message(msg), + // process any subnet service events + Some(msg) = self.subnet_service.next() => self.on_subnet_service_msg(msg), event = self.libp2p.next_event() => self.on_libp2p_event(event, &mut shutdown_sender).await, @@ -552,13 +540,14 @@ impl NetworkService { match message { // attestation information gets processed in the attestation service PubsubMessage::Attestation(ref subnet_and_attestation) => { - let subnet = subnet_and_attestation.0; + let subnet_id = subnet_and_attestation.0; let attestation = &subnet_and_attestation.1; // checks if we have an aggregator for the slot. If so, we should process // the attestation, else we just just propagate the Attestation. - let should_process = self - .attestation_service - .should_process_attestation(subnet, attestation); + let should_process = self.subnet_service.should_process_attestation( + Subnet::Attestation(subnet_id), + attestation, + ); self.send_to_router(RouterMessage::PubsubMessage( id, source, @@ -832,20 +821,12 @@ impl NetworkService { async fn on_validator_subscription_msg(&mut self, msg: ValidatorSubscriptionMessage) { match msg { ValidatorSubscriptionMessage::AttestationSubscribe { subscriptions } => { - if let Err(e) = self - .attestation_service - .validator_subscriptions(subscriptions.into_iter()) - { - warn!(self.log, "Attestation validator subscription failed"; "error" => e); - } + let subscriptions = subscriptions.into_iter().map(Subscription::Attestation); + self.subnet_service.validator_subscriptions(subscriptions) } ValidatorSubscriptionMessage::SyncCommitteeSubscribe { subscriptions } => { - if let Err(e) = self - .sync_committee_service - .validator_subscriptions(subscriptions) - { - warn!(self.log, "Sync committee calidator subscription failed"; "error" => e); - } + let subscriptions = subscriptions.into_iter().map(Subscription::SyncCommittee); + self.subnet_service.validator_subscriptions(subscriptions) } } } @@ -881,7 +862,7 @@ impl NetworkService { } } - fn on_attestation_service_msg(&mut self, msg: SubnetServiceMessage) { + fn on_subnet_service_msg(&mut self, msg: SubnetServiceMessage) { match msg { SubnetServiceMessage::Subscribe(subnet) => { for fork_digest in self.required_gossip_fork_digests() { @@ -900,36 +881,9 @@ impl NetworkService { SubnetServiceMessage::EnrAdd(subnet) => { self.libp2p.update_enr_subnet(subnet, true); } - SubnetServiceMessage::EnrRemove(subnet) => { - self.libp2p.update_enr_subnet(subnet, false); - } - SubnetServiceMessage::DiscoverPeers(subnets_to_discover) => { - self.libp2p.discover_subnet_peers(subnets_to_discover); - } - } - } - - fn on_sync_committee_service_message(&mut self, msg: SubnetServiceMessage) { - match msg { - SubnetServiceMessage::Subscribe(subnet) => { - for fork_digest in self.required_gossip_fork_digests() { - let topic = - GossipTopic::new(subnet.into(), GossipEncoding::default(), fork_digest); - self.libp2p.subscribe(topic); - } - } - SubnetServiceMessage::Unsubscribe(subnet) => { - for fork_digest in self.required_gossip_fork_digests() { - let topic = - GossipTopic::new(subnet.into(), GossipEncoding::default(), fork_digest); - self.libp2p.unsubscribe(topic); - } - } - SubnetServiceMessage::EnrAdd(subnet) => { - self.libp2p.update_enr_subnet(subnet, true); - } - SubnetServiceMessage::EnrRemove(subnet) => { - self.libp2p.update_enr_subnet(subnet, false); + SubnetServiceMessage::EnrRemove(sync_subnet_id) => { + self.libp2p + .update_enr_subnet(Subnet::SyncCommittee(sync_subnet_id), false); } SubnetServiceMessage::DiscoverPeers(subnets_to_discover) => { self.libp2p.discover_subnet_peers(subnets_to_discover); diff --git a/beacon_node/network/src/service/tests.rs b/beacon_node/network/src/service/tests.rs index b55992c624..c46e46e0fa 100644 --- a/beacon_node/network/src/service/tests.rs +++ b/beacon_node/network/src/service/tests.rs @@ -169,21 +169,18 @@ mod tests { // Subscribe to the topics. runtime.block_on(async { while network_globals.gossipsub_subscriptions.read().len() < 2 { - if let Some(msg) = network_service.attestation_service.next().await { - network_service.on_attestation_service_msg(msg); + if let Some(msg) = network_service.subnet_service.next().await { + network_service.on_subnet_service_msg(msg); } } }); // Make sure the service is subscribed to the topics. let (old_topic1, old_topic2) = { - let mut subnets = SubnetId::compute_subnets_for_epoch::( + let mut subnets = SubnetId::compute_attestation_subnets( network_globals.local_enr().node_id().raw(), - beacon_chain.epoch().unwrap(), &spec, ) - .unwrap() - .0 .collect::>(); assert_eq!(2, subnets.len()); diff --git a/beacon_node/network/src/subnet_service/attestation_subnets.rs b/beacon_node/network/src/subnet_service/attestation_subnets.rs deleted file mode 100644 index 432a2b7fb7..0000000000 --- a/beacon_node/network/src/subnet_service/attestation_subnets.rs +++ /dev/null @@ -1,687 +0,0 @@ -//! This service keeps track of which shard subnet the beacon node should be subscribed to at any -//! given time. It schedules subscriptions to shard subnets, requests peer discoveries and -//! determines whether attestations should be aggregated and/or passed to the beacon node. - -use super::SubnetServiceMessage; -use std::collections::HashSet; -use std::collections::{HashMap, VecDeque}; -use std::pin::Pin; -use std::sync::Arc; -use std::task::{Context, Poll}; -use std::time::Duration; - -use beacon_chain::{BeaconChain, BeaconChainTypes}; -use delay_map::{HashMapDelay, HashSetDelay}; -use futures::prelude::*; -use lighthouse_network::{discv5::enr::NodeId, NetworkConfig, Subnet, SubnetDiscovery}; -use slog::{debug, error, info, o, trace, warn}; -use slot_clock::SlotClock; -use types::{Attestation, EthSpec, Slot, SubnetId, ValidatorSubscription}; - -use crate::metrics; - -/// The minimum number of slots ahead that we attempt to discover peers for a subscription. If the -/// slot is less than this number, skip the peer discovery process. -/// Subnet discovery query takes at most 30 secs, 2 slots take 24s. -pub(crate) const MIN_PEER_DISCOVERY_SLOT_LOOK_AHEAD: u64 = 2; -/// The fraction of a slot that we subscribe to a subnet before the required slot. -/// -/// Currently a whole slot ahead. -const ADVANCE_SUBSCRIBE_SLOT_FRACTION: u32 = 1; - -/// The number of slots after an aggregator duty where we remove the entry from -/// `aggregate_validators_on_subnet` delay map. -const UNSUBSCRIBE_AFTER_AGGREGATOR_DUTY: u32 = 2; - -#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] -pub(crate) enum SubscriptionKind { - /// Long lived subscriptions. - /// - /// These have a longer duration and are advertised in our ENR. - LongLived, - /// Short lived subscriptions. - /// - /// Subscribing to these subnets has a short duration and we don't advertise it in our ENR. - ShortLived, -} - -/// A particular subnet at a given slot. -#[derive(PartialEq, Eq, Hash, Clone, Debug, Copy)] -pub struct ExactSubnet { - /// The `SubnetId` associated with this subnet. - pub subnet_id: SubnetId, - /// The `Slot` associated with this subnet. - pub slot: Slot, -} - -pub struct AttestationService { - /// Queued events to return to the driving service. - events: VecDeque, - - /// A reference to the beacon chain to process received attestations. - pub(crate) beacon_chain: Arc>, - - /// Subnets we are currently subscribed to as short lived subscriptions. - /// - /// Once they expire, we unsubscribe from these. - /// We subscribe to subnets when we are an aggregator for an exact subnet. - short_lived_subscriptions: HashMapDelay, - - /// Subnets we are currently subscribed to as long lived subscriptions. - /// - /// We advertise these in our ENR. When these expire, the subnet is removed from our ENR. - /// These are required of all beacon nodes. The exact number is determined by the chain - /// specification. - long_lived_subscriptions: HashSet, - - /// Short lived subscriptions that need to be executed in the future. - scheduled_short_lived_subscriptions: HashSetDelay, - - /// A collection timeouts to track the existence of aggregate validator subscriptions at an - /// `ExactSubnet`. - aggregate_validators_on_subnet: Option>, - - /// The waker for the current thread. - waker: Option, - - /// The discovery mechanism of lighthouse is disabled. - discovery_disabled: bool, - - /// We are always subscribed to all subnets. - subscribe_all_subnets: bool, - - /// Our Discv5 node_id. - node_id: NodeId, - - /// Future used to manage subscribing and unsubscribing from long lived subnets. - next_long_lived_subscription_event: Pin>, - - /// Whether this node is a block proposer-only node. - proposer_only: bool, - - /// The logger for the attestation service. - log: slog::Logger, -} - -impl AttestationService { - /* Public functions */ - - /// Establish the service based on the passed configuration. - pub fn new( - beacon_chain: Arc>, - node_id: NodeId, - config: &NetworkConfig, - log: &slog::Logger, - ) -> Self { - let log = log.new(o!("service" => "attestation_service")); - - let slot_duration = beacon_chain.slot_clock.slot_duration(); - - if config.subscribe_all_subnets { - slog::info!(log, "Subscribing to all subnets"); - } else { - slog::info!(log, "Deterministic long lived subnets enabled"; "subnets_per_node" => beacon_chain.spec.subnets_per_node, "subscription_duration_in_epochs" => beacon_chain.spec.epochs_per_subnet_subscription); - } - - let track_validators = !config.import_all_attestations; - let aggregate_validators_on_subnet = - track_validators.then(|| HashSetDelay::new(slot_duration)); - let mut service = AttestationService { - events: VecDeque::with_capacity(10), - beacon_chain, - short_lived_subscriptions: HashMapDelay::new(slot_duration), - long_lived_subscriptions: HashSet::default(), - scheduled_short_lived_subscriptions: HashSetDelay::default(), - aggregate_validators_on_subnet, - waker: None, - discovery_disabled: config.disable_discovery, - subscribe_all_subnets: config.subscribe_all_subnets, - node_id, - next_long_lived_subscription_event: { - // Set a dummy sleep. Calculating the current subnet subscriptions will update this - // value with a smarter timing - Box::pin(tokio::time::sleep(Duration::from_secs(1))) - }, - proposer_only: config.proposer_only, - log, - }; - - // If we are not subscribed to all subnets, handle the deterministic set of subnets - if !config.subscribe_all_subnets { - service.recompute_long_lived_subnets(); - } - - service - } - - /// Return count of all currently subscribed subnets (long-lived **and** short-lived). - #[cfg(test)] - pub fn subscription_count(&self) -> usize { - if self.subscribe_all_subnets { - self.beacon_chain.spec.attestation_subnet_count as usize - } else { - let count = self - .short_lived_subscriptions - .keys() - .chain(self.long_lived_subscriptions.iter()) - .collect::>() - .len(); - count - } - } - - /// Returns whether we are subscribed to a subnet for testing purposes. - #[cfg(test)] - pub(crate) fn is_subscribed( - &self, - subnet_id: &SubnetId, - subscription_kind: SubscriptionKind, - ) -> bool { - match subscription_kind { - SubscriptionKind::LongLived => self.long_lived_subscriptions.contains(subnet_id), - SubscriptionKind::ShortLived => self.short_lived_subscriptions.contains_key(subnet_id), - } - } - - #[cfg(test)] - pub(crate) fn long_lived_subscriptions(&self) -> &HashSet { - &self.long_lived_subscriptions - } - - /// Processes a list of validator subscriptions. - /// - /// This will: - /// - Register new validators as being known. - /// - Search for peers for required subnets. - /// - Request subscriptions for subnets on specific slots when required. - /// - Build the timeouts for each of these events. - /// - /// This returns a result simply for the ergonomics of using ?. The result can be - /// safely dropped. - pub fn validator_subscriptions( - &mut self, - subscriptions: impl Iterator, - ) -> Result<(), String> { - // If the node is in a proposer-only state, we ignore all subnet subscriptions. - if self.proposer_only { - return Ok(()); - } - - // Maps each subnet_id subscription to it's highest slot - let mut subnets_to_discover: HashMap = HashMap::new(); - - // Registers the validator with the attestation service. - for subscription in subscriptions { - metrics::inc_counter(&metrics::SUBNET_SUBSCRIPTION_REQUESTS); - - trace!(self.log, - "Validator subscription"; - "subscription" => ?subscription, - ); - - // Compute the subnet that is associated with this subscription - let subnet_id = match SubnetId::compute_subnet::( - subscription.slot, - subscription.attestation_committee_index, - subscription.committee_count_at_slot, - &self.beacon_chain.spec, - ) { - Ok(subnet_id) => subnet_id, - Err(e) => { - warn!(self.log, - "Failed to compute subnet id for validator subscription"; - "error" => ?e, - ); - continue; - } - }; - // Ensure each subnet_id inserted into the map has the highest slot as it's value. - // Higher slot corresponds to higher min_ttl in the `SubnetDiscovery` entry. - if let Some(slot) = subnets_to_discover.get(&subnet_id) { - if subscription.slot > *slot { - subnets_to_discover.insert(subnet_id, subscription.slot); - } - } else if !self.discovery_disabled { - subnets_to_discover.insert(subnet_id, subscription.slot); - } - - let exact_subnet = ExactSubnet { - subnet_id, - slot: subscription.slot, - }; - - // Determine if the validator is an aggregator. If so, we subscribe to the subnet and - // if successful add the validator to a mapping of known aggregators for that exact - // subnet. - - if subscription.is_aggregator { - metrics::inc_counter(&metrics::SUBNET_SUBSCRIPTION_AGGREGATOR_REQUESTS); - if let Err(e) = self.subscribe_to_short_lived_subnet(exact_subnet) { - warn!(self.log, - "Subscription to subnet error"; - "error" => e, - ); - } else { - trace!(self.log, - "Subscribed to subnet for aggregator duties"; - "exact_subnet" => ?exact_subnet, - ); - } - } - } - - // If the discovery mechanism isn't disabled, attempt to set up a peer discovery for the - // required subnets. - if !self.discovery_disabled { - if let Err(e) = self.discover_peers_request( - subnets_to_discover - .into_iter() - .map(|(subnet_id, slot)| ExactSubnet { subnet_id, slot }), - ) { - warn!(self.log, "Discovery lookup request error"; "error" => e); - }; - } - - Ok(()) - } - - fn recompute_long_lived_subnets(&mut self) { - // Ensure the next computation is scheduled even if assigning subnets fails. - let next_subscription_event = self - .recompute_long_lived_subnets_inner() - .unwrap_or_else(|_| self.beacon_chain.slot_clock.slot_duration()); - - debug!(self.log, "Recomputing deterministic long lived subnets"); - self.next_long_lived_subscription_event = - Box::pin(tokio::time::sleep(next_subscription_event)); - - if let Some(waker) = self.waker.as_ref() { - waker.wake_by_ref(); - } - } - - /// Gets the long lived subnets the node should be subscribed to during the current epoch and - /// the remaining duration for which they remain valid. - fn recompute_long_lived_subnets_inner(&mut self) -> Result { - let current_epoch = self.beacon_chain.epoch().map_err(|e| { - if !self - .beacon_chain - .slot_clock - .is_prior_to_genesis() - .unwrap_or(false) - { - error!(self.log, "Failed to get the current epoch from clock"; "err" => ?e) - } - })?; - - let (subnets, next_subscription_epoch) = SubnetId::compute_subnets_for_epoch::( - self.node_id.raw(), - current_epoch, - &self.beacon_chain.spec, - ) - .map_err(|e| error!(self.log, "Could not compute subnets for current epoch"; "err" => e))?; - - let next_subscription_slot = - next_subscription_epoch.start_slot(T::EthSpec::slots_per_epoch()); - let next_subscription_event = self - .beacon_chain - .slot_clock - .duration_to_slot(next_subscription_slot) - .ok_or_else(|| { - error!( - self.log, - "Failed to compute duration to next to long lived subscription event" - ) - })?; - - self.update_long_lived_subnets(subnets.collect()); - - Ok(next_subscription_event) - } - - /// Updates the long lived subnets. - /// - /// New subnets are registered as subscribed, removed subnets as unsubscribed and the Enr - /// updated accordingly. - fn update_long_lived_subnets(&mut self, mut subnets: HashSet) { - info!(self.log, "Subscribing to long-lived subnets"; "subnets" => ?subnets.iter().collect::>()); - for subnet in &subnets { - // Add the events for those subnets that are new as long lived subscriptions. - if !self.long_lived_subscriptions.contains(subnet) { - // Check if this subnet is new and send the subscription event if needed. - if !self.short_lived_subscriptions.contains_key(subnet) { - debug!(self.log, "Subscribing to subnet"; - "subnet" => ?subnet, - "subscription_kind" => ?SubscriptionKind::LongLived, - ); - self.queue_event(SubnetServiceMessage::Subscribe(Subnet::Attestation( - *subnet, - ))); - } - self.queue_event(SubnetServiceMessage::EnrAdd(Subnet::Attestation(*subnet))); - if !self.discovery_disabled { - self.queue_event(SubnetServiceMessage::DiscoverPeers(vec![SubnetDiscovery { - subnet: Subnet::Attestation(*subnet), - min_ttl: None, - }])) - } - } - } - - // Update the long_lived_subnets set and check for subnets that are being removed - std::mem::swap(&mut self.long_lived_subscriptions, &mut subnets); - for subnet in subnets { - if !self.long_lived_subscriptions.contains(&subnet) { - self.handle_removed_subnet(subnet, SubscriptionKind::LongLived); - } - } - } - - /// Checks if we have subscribed aggregate validators for the subnet. If not, checks the gossip - /// verification, re-propagates and returns false. - pub fn should_process_attestation( - &self, - subnet: SubnetId, - attestation: &Attestation, - ) -> bool { - // Proposer-only mode does not need to process attestations - if self.proposer_only { - return false; - } - self.aggregate_validators_on_subnet - .as_ref() - .map(|tracked_vals| { - tracked_vals.contains_key(&ExactSubnet { - subnet_id: subnet, - slot: attestation.data().slot, - }) - }) - .unwrap_or(true) - } - - /* Internal private functions */ - - /// Adds an event to the event queue and notifies that this service is ready to be polled - /// again. - fn queue_event(&mut self, ev: SubnetServiceMessage) { - self.events.push_back(ev); - if let Some(waker) = &self.waker { - waker.wake_by_ref() - } - } - /// Checks if there are currently queued discovery requests and the time required to make the - /// request. - /// - /// If there is sufficient time, queues a peer discovery request for all the required subnets. - fn discover_peers_request( - &mut self, - exact_subnets: impl Iterator, - ) -> Result<(), &'static str> { - let current_slot = self - .beacon_chain - .slot_clock - .now() - .ok_or("Could not get the current slot")?; - - let discovery_subnets: Vec = exact_subnets - .filter_map(|exact_subnet| { - // Check if there is enough time to perform a discovery lookup. - if exact_subnet.slot - >= current_slot.saturating_add(MIN_PEER_DISCOVERY_SLOT_LOOK_AHEAD) - { - // Send out an event to start looking for peers. - // Require the peer for an additional slot to ensure we keep the peer for the - // duration of the subscription. - let min_ttl = self - .beacon_chain - .slot_clock - .duration_to_slot(exact_subnet.slot + 1) - .map(|duration| std::time::Instant::now() + duration); - Some(SubnetDiscovery { - subnet: Subnet::Attestation(exact_subnet.subnet_id), - min_ttl, - }) - } else { - // We may want to check the global PeerInfo to see estimated timeouts for each - // peer before they can be removed. - warn!(self.log, - "Not enough time for a discovery search"; - "subnet_id" => ?exact_subnet - ); - None - } - }) - .collect(); - - if !discovery_subnets.is_empty() { - self.queue_event(SubnetServiceMessage::DiscoverPeers(discovery_subnets)); - } - Ok(()) - } - - // Subscribes to the subnet if it should be done immediately, or schedules it if required. - fn subscribe_to_short_lived_subnet( - &mut self, - ExactSubnet { subnet_id, slot }: ExactSubnet, - ) -> Result<(), &'static str> { - let slot_duration = self.beacon_chain.slot_clock.slot_duration(); - - // The short time we schedule the subscription before it's actually required. This - // ensures we are subscribed on time, and allows consecutive subscriptions to the same - // subnet to overlap, reducing subnet churn. - let advance_subscription_duration = slot_duration / ADVANCE_SUBSCRIBE_SLOT_FRACTION; - // The time to the required slot. - let time_to_subscription_slot = self - .beacon_chain - .slot_clock - .duration_to_slot(slot) - .unwrap_or_default(); // If this is a past slot we will just get a 0 duration. - - // Calculate how long before we need to subscribe to the subnet. - let time_to_subscription_start = - time_to_subscription_slot.saturating_sub(advance_subscription_duration); - - // The time after a duty slot where we no longer need it in the `aggregate_validators_on_subnet` - // delay map. - let time_to_unsubscribe = - time_to_subscription_slot + UNSUBSCRIBE_AFTER_AGGREGATOR_DUTY * slot_duration; - if let Some(tracked_vals) = self.aggregate_validators_on_subnet.as_mut() { - tracked_vals.insert_at(ExactSubnet { subnet_id, slot }, time_to_unsubscribe); - } - - // If the subscription should be done in the future, schedule it. Otherwise subscribe - // immediately. - if time_to_subscription_start.is_zero() { - // This is a current or past slot, we subscribe immediately. - self.subscribe_to_short_lived_subnet_immediately(subnet_id, slot + 1)?; - } else { - // This is a future slot, schedule subscribing. - trace!(self.log, "Scheduling subnet subscription"; "subnet" => ?subnet_id, "time_to_subscription_start" => ?time_to_subscription_start); - self.scheduled_short_lived_subscriptions - .insert_at(ExactSubnet { subnet_id, slot }, time_to_subscription_start); - } - - Ok(()) - } - - /* A collection of functions that handle the various timeouts */ - - /// Registers a subnet as subscribed. - /// - /// Checks that the time in which the subscription would end is not in the past. If we are - /// already subscribed, extends the timeout if necessary. If this is a new subscription, we send - /// out the appropriate events. - /// - /// On determinist long lived subnets, this is only used for short lived subscriptions. - fn subscribe_to_short_lived_subnet_immediately( - &mut self, - subnet_id: SubnetId, - end_slot: Slot, - ) -> Result<(), &'static str> { - if self.subscribe_all_subnets { - // Case not handled by this service. - return Ok(()); - } - - let time_to_subscription_end = self - .beacon_chain - .slot_clock - .duration_to_slot(end_slot) - .unwrap_or_default(); - - // First check this is worth doing. - if time_to_subscription_end.is_zero() { - return Err("Time when subscription would end has already passed."); - } - - let subscription_kind = SubscriptionKind::ShortLived; - - // We need to check and add a subscription for the right kind, regardless of the presence - // of the subnet as a subscription of the other kind. This is mainly since long lived - // subscriptions can be removed at any time when a validator goes offline. - - let (subscriptions, already_subscribed_as_other_kind) = ( - &mut self.short_lived_subscriptions, - self.long_lived_subscriptions.contains(&subnet_id), - ); - - match subscriptions.get(&subnet_id) { - Some(current_end_slot) => { - // We are already subscribed. Check if we need to extend the subscription. - if &end_slot > current_end_slot { - trace!(self.log, "Extending subscription to subnet"; - "subnet" => ?subnet_id, - "prev_end_slot" => current_end_slot, - "new_end_slot" => end_slot, - "subscription_kind" => ?subscription_kind, - ); - subscriptions.insert_at(subnet_id, end_slot, time_to_subscription_end); - } - } - None => { - // This is a new subscription. Add with the corresponding timeout and send the - // notification. - subscriptions.insert_at(subnet_id, end_slot, time_to_subscription_end); - - // Inform of the subscription. - if !already_subscribed_as_other_kind { - debug!(self.log, "Subscribing to subnet"; - "subnet" => ?subnet_id, - "end_slot" => end_slot, - "subscription_kind" => ?subscription_kind, - ); - self.queue_event(SubnetServiceMessage::Subscribe(Subnet::Attestation( - subnet_id, - ))); - } - } - } - - Ok(()) - } - - // Unsubscribes from a subnet that was removed if it does not continue to exist as a - // subscription of the other kind. For long lived subscriptions, it also removes the - // advertisement from our ENR. - fn handle_removed_subnet(&mut self, subnet_id: SubnetId, subscription_kind: SubscriptionKind) { - let exists_in_other_subscriptions = match subscription_kind { - SubscriptionKind::LongLived => self.short_lived_subscriptions.contains_key(&subnet_id), - SubscriptionKind::ShortLived => self.long_lived_subscriptions.contains(&subnet_id), - }; - - if !exists_in_other_subscriptions { - // Subscription no longer exists as short lived or long lived. - debug!(self.log, "Unsubscribing from subnet"; "subnet" => ?subnet_id, "subscription_kind" => ?subscription_kind); - self.queue_event(SubnetServiceMessage::Unsubscribe(Subnet::Attestation( - subnet_id, - ))); - } - - if subscription_kind == SubscriptionKind::LongLived { - // Remove from our ENR even if we remain subscribed in other way. - self.queue_event(SubnetServiceMessage::EnrRemove(Subnet::Attestation( - subnet_id, - ))); - } - } -} - -impl Stream for AttestationService { - type Item = SubnetServiceMessage; - - fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - // Update the waker if needed. - if let Some(waker) = &self.waker { - if waker.will_wake(cx.waker()) { - self.waker = Some(cx.waker().clone()); - } - } else { - self.waker = Some(cx.waker().clone()); - } - - // Send out any generated events. - if let Some(event) = self.events.pop_front() { - return Poll::Ready(Some(event)); - } - - // If we aren't subscribed to all subnets, handle the deterministic long-lived subnets - if !self.subscribe_all_subnets { - match self.next_long_lived_subscription_event.as_mut().poll(cx) { - Poll::Ready(_) => { - self.recompute_long_lived_subnets(); - // We re-wake the task as there could be other subscriptions to process - self.waker - .as_ref() - .expect("Waker has been set") - .wake_by_ref(); - } - Poll::Pending => {} - } - } - - // Process scheduled subscriptions that might be ready, since those can extend a soon to - // expire subscription. - match self.scheduled_short_lived_subscriptions.poll_next_unpin(cx) { - Poll::Ready(Some(Ok(ExactSubnet { subnet_id, slot }))) => { - if let Err(e) = - self.subscribe_to_short_lived_subnet_immediately(subnet_id, slot + 1) - { - debug!(self.log, "Failed to subscribe to short lived subnet"; "subnet" => ?subnet_id, "err" => e); - } - self.waker - .as_ref() - .expect("Waker has been set") - .wake_by_ref(); - } - Poll::Ready(Some(Err(e))) => { - error!(self.log, "Failed to check for scheduled subnet subscriptions"; "error"=> e); - } - Poll::Ready(None) | Poll::Pending => {} - } - - // Finally process any expired subscriptions. - match self.short_lived_subscriptions.poll_next_unpin(cx) { - Poll::Ready(Some(Ok((subnet_id, _end_slot)))) => { - self.handle_removed_subnet(subnet_id, SubscriptionKind::ShortLived); - // We re-wake the task as there could be other subscriptions to process - self.waker - .as_ref() - .expect("Waker has been set") - .wake_by_ref(); - } - Poll::Ready(Some(Err(e))) => { - error!(self.log, "Failed to check for subnet unsubscription times"; "error"=> e); - } - Poll::Ready(None) | Poll::Pending => {} - } - - // Poll to remove entries on expiration, no need to act on expiration events. - if let Some(tracked_vals) = self.aggregate_validators_on_subnet.as_mut() { - if let Poll::Ready(Some(Err(e))) = tracked_vals.poll_next_unpin(cx) { - error!(self.log, "Failed to check for aggregate validator on subnet expirations"; "error"=> e); - } - } - - Poll::Pending - } -} diff --git a/beacon_node/network/src/subnet_service/mod.rs b/beacon_node/network/src/subnet_service/mod.rs index 6450fc72ee..ab73b6ad9c 100644 --- a/beacon_node/network/src/subnet_service/mod.rs +++ b/beacon_node/network/src/subnet_service/mod.rs @@ -1,10 +1,25 @@ -pub mod attestation_subnets; -pub mod sync_subnets; +//! This service keeps track of which shard subnet the beacon node should be subscribed to at any +//! given time. It schedules subscriptions to shard subnets, requests peer discoveries and +//! determines whether attestations should be aggregated and/or passed to the beacon node. -use lighthouse_network::{Subnet, SubnetDiscovery}; +use std::collections::HashSet; +use std::collections::{HashMap, VecDeque}; +use std::pin::Pin; +use std::sync::Arc; +use std::task::{Context, Poll}; +use std::time::Duration; +use tokio::time::Instant; -pub use attestation_subnets::AttestationService; -pub use sync_subnets::SyncCommitteeService; +use beacon_chain::{BeaconChain, BeaconChainTypes}; +use delay_map::HashSetDelay; +use futures::prelude::*; +use lighthouse_network::{discv5::enr::NodeId, NetworkConfig, Subnet, SubnetDiscovery}; +use slog::{debug, error, o, warn}; +use slot_clock::SlotClock; +use types::{ + Attestation, EthSpec, Slot, SubnetId, SyncCommitteeSubscription, SyncSubnetId, + ValidatorSubscription, +}; #[cfg(test)] mod tests; @@ -17,12 +32,642 @@ pub enum SubnetServiceMessage { Unsubscribe(Subnet), /// Add the `SubnetId` to the ENR bitfield. EnrAdd(Subnet), - /// Remove the `SubnetId` from the ENR bitfield. - EnrRemove(Subnet), + /// Remove a sync committee subnet from the ENR. + EnrRemove(SyncSubnetId), /// Discover peers for a list of `SubnetDiscovery`. DiscoverPeers(Vec), } +use crate::metrics; + +/// The minimum number of slots ahead that we attempt to discover peers for a subscription. If the +/// slot is less than this number, skip the peer discovery process. +/// Subnet discovery query takes at most 30 secs, 2 slots take 24s. +pub(crate) const MIN_PEER_DISCOVERY_SLOT_LOOK_AHEAD: u64 = 2; +/// The fraction of a slot that we subscribe to a subnet before the required slot. +/// +/// Currently a whole slot ahead. +const ADVANCE_SUBSCRIBE_SLOT_FRACTION: u32 = 1; + +/// The number of slots after an aggregator duty where we remove the entry from +/// `aggregate_validators_on_subnet` delay map. +const UNSUBSCRIBE_AFTER_AGGREGATOR_DUTY: u32 = 2; + +/// A particular subnet at a given slot. This is used for Attestation subnets and not for sync +/// committee subnets because the logic for handling subscriptions between these types is different. +#[derive(PartialEq, Eq, Hash, Clone, Debug, Copy)] +pub struct ExactSubnet { + /// The `SubnetId` associated with this subnet. + pub subnet: Subnet, + /// For Attestations, this slot represents the start time at which we need to subscribe to the + /// slot. + pub slot: Slot, +} + +/// The enum used to group all kinds of validator subscriptions +#[derive(Debug, Clone, PartialEq)] +pub enum Subscription { + Attestation(ValidatorSubscription), + SyncCommittee(SyncCommitteeSubscription), +} + +pub struct SubnetService { + /// Queued events to return to the driving service. + events: VecDeque, + + /// A reference to the beacon chain to process received attestations. + pub(crate) beacon_chain: Arc>, + + /// Subnets we are currently subscribed to as short lived subscriptions. + /// + /// Once they expire, we unsubscribe from these. + /// We subscribe to subnets when we are an aggregator for an exact subnet. + // NOTE: When setup the default timeout is set for sync committee subscriptions. + subscriptions: HashSetDelay, + + /// Subscriptions that need to be executed in the future. + scheduled_subscriptions: HashSetDelay, + + /// A list of permanent subnets that this node is subscribed to. + // TODO: Shift this to a dynamic bitfield + permanent_attestation_subscriptions: HashSet, + + /// A collection timeouts to track the existence of aggregate validator subscriptions at an + /// `ExactSubnet`. + aggregate_validators_on_subnet: Option>, + + /// The waker for the current thread. + waker: Option, + + /// The discovery mechanism of lighthouse is disabled. + discovery_disabled: bool, + + /// We are always subscribed to all subnets. + subscribe_all_subnets: bool, + + /// Whether this node is a block proposer-only node. + proposer_only: bool, + + /// The logger for the attestation service. + log: slog::Logger, +} + +impl SubnetService { + /* Public functions */ + + /// Establish the service based on the passed configuration. + pub fn new( + beacon_chain: Arc>, + node_id: NodeId, + config: &NetworkConfig, + log: &slog::Logger, + ) -> Self { + let log = log.new(o!("service" => "subnet_service")); + + let slot_duration = beacon_chain.slot_clock.slot_duration(); + + if config.subscribe_all_subnets { + slog::info!(log, "Subscribing to all subnets"); + } + + // Build the list of known permanent subscriptions, so that we know not to subscribe or + // discover them. + let mut permanent_attestation_subscriptions = HashSet::default(); + if config.subscribe_all_subnets { + // We are subscribed to all subnets, set all the bits to true. + for index in 0..beacon_chain.spec.attestation_subnet_count { + permanent_attestation_subscriptions + .insert(Subnet::Attestation(SubnetId::from(index))); + } + } else { + // Not subscribed to all subnets, so just calculate the required subnets from the node + // id. + for subnet_id in + SubnetId::compute_attestation_subnets(node_id.raw(), &beacon_chain.spec) + { + permanent_attestation_subscriptions.insert(Subnet::Attestation(subnet_id)); + } + } + + // Set up the sync committee subscriptions + let spec = &beacon_chain.spec; + let epoch_duration_secs = + beacon_chain.slot_clock.slot_duration().as_secs() * T::EthSpec::slots_per_epoch(); + let default_sync_committee_duration = Duration::from_secs( + epoch_duration_secs.saturating_mul(spec.epochs_per_sync_committee_period.as_u64()), + ); + + let track_validators = !config.import_all_attestations; + let aggregate_validators_on_subnet = + track_validators.then(|| HashSetDelay::new(slot_duration)); + + let mut events = VecDeque::with_capacity(10); + + // Queue discovery queries for the permanent attestation subnets + if !config.disable_discovery { + events.push_back(SubnetServiceMessage::DiscoverPeers( + permanent_attestation_subscriptions + .iter() + .cloned() + .map(|subnet| SubnetDiscovery { + subnet, + min_ttl: None, + }) + .collect(), + )); + } + + // Pre-populate the events with permanent subscriptions + for subnet in permanent_attestation_subscriptions.iter() { + events.push_back(SubnetServiceMessage::Subscribe(*subnet)); + events.push_back(SubnetServiceMessage::EnrAdd(*subnet)); + } + + SubnetService { + events, + beacon_chain, + subscriptions: HashSetDelay::new(default_sync_committee_duration), + permanent_attestation_subscriptions, + scheduled_subscriptions: HashSetDelay::default(), + aggregate_validators_on_subnet, + waker: None, + discovery_disabled: config.disable_discovery, + subscribe_all_subnets: config.subscribe_all_subnets, + proposer_only: config.proposer_only, + log, + } + } + + /// Return count of all currently subscribed short-lived subnets. + #[cfg(test)] + pub fn subscriptions(&self) -> impl Iterator { + self.subscriptions.iter() + } + + #[cfg(test)] + pub fn permanent_subscriptions(&self) -> impl Iterator { + self.permanent_attestation_subscriptions.iter() + } + + /// Returns whether we are subscribed to a subnet for testing purposes. + #[cfg(test)] + pub(crate) fn is_subscribed(&self, subnet: &Subnet) -> bool { + self.subscriptions.contains_key(subnet) + } + + /// Processes a list of validator subscriptions. + /// + /// This is fundamentally called form the HTTP API when a validator requests duties from us + /// This will: + /// - Register new validators as being known. + /// - Search for peers for required subnets. + /// - Request subscriptions for subnets on specific slots when required. + /// - Build the timeouts for each of these events. + /// + /// This returns a result simply for the ergonomics of using ?. The result can be + /// safely dropped. + pub fn validator_subscriptions(&mut self, subscriptions: impl Iterator) { + // If the node is in a proposer-only state, we ignore all subnet subscriptions. + if self.proposer_only { + return; + } + + // Maps each subnet subscription to it's highest slot + let mut subnets_to_discover: HashMap = HashMap::new(); + + // Registers the validator with the attestation service. + for general_subscription in subscriptions { + match general_subscription { + Subscription::Attestation(subscription) => { + metrics::inc_counter(&metrics::SUBNET_SUBSCRIPTION_REQUESTS); + + // Compute the subnet that is associated with this subscription + let subnet = match SubnetId::compute_subnet::( + subscription.slot, + subscription.attestation_committee_index, + subscription.committee_count_at_slot, + &self.beacon_chain.spec, + ) { + Ok(subnet_id) => Subnet::Attestation(subnet_id), + Err(e) => { + warn!(self.log, + "Failed to compute subnet id for validator subscription"; + "error" => ?e, + ); + continue; + } + }; + + // Ensure each subnet_id inserted into the map has the highest slot as it's value. + // Higher slot corresponds to higher min_ttl in the `SubnetDiscovery` entry. + if let Some(slot) = subnets_to_discover.get(&subnet) { + if subscription.slot > *slot { + subnets_to_discover.insert(subnet, subscription.slot); + } + } else if !self.discovery_disabled { + subnets_to_discover.insert(subnet, subscription.slot); + } + + let exact_subnet = ExactSubnet { + subnet, + slot: subscription.slot, + }; + + // Determine if the validator is an aggregator. If so, we subscribe to the subnet and + // if successful add the validator to a mapping of known aggregators for that exact + // subnet. + + if subscription.is_aggregator { + metrics::inc_counter(&metrics::SUBNET_SUBSCRIPTION_AGGREGATOR_REQUESTS); + if let Err(e) = self.subscribe_to_subnet(exact_subnet) { + warn!(self.log, + "Subscription to subnet error"; + "error" => e, + ); + } + } + } + Subscription::SyncCommittee(subscription) => { + metrics::inc_counter(&metrics::SYNC_COMMITTEE_SUBSCRIPTION_REQUESTS); + // NOTE: We assume all subscriptions have been verified before reaching this service + + // Registers the validator with the subnet service. + let subnet_ids = + match SyncSubnetId::compute_subnets_for_sync_committee::( + &subscription.sync_committee_indices, + ) { + Ok(subnet_ids) => subnet_ids, + Err(e) => { + warn!(self.log, + "Failed to compute subnet id for sync committee subscription"; + "error" => ?e, + "validator_index" => subscription.validator_index + ); + continue; + } + }; + + for subnet_id in subnet_ids { + let subnet = Subnet::SyncCommittee(subnet_id); + let slot_required_until = subscription + .until_epoch + .start_slot(T::EthSpec::slots_per_epoch()); + subnets_to_discover.insert(subnet, slot_required_until); + + let Some(duration_to_unsubscribe) = self + .beacon_chain + .slot_clock + .duration_to_slot(slot_required_until) + else { + warn!(self.log, "Subscription to sync subnet error"; "error" => "Unable to determine duration to unsubscription slot", "validator_index" => subscription.validator_index); + continue; + }; + + if duration_to_unsubscribe == Duration::from_secs(0) { + let current_slot = self + .beacon_chain + .slot_clock + .now() + .unwrap_or(Slot::from(0u64)); + warn!( + self.log, + "Sync committee subscription is past expiration"; + "subnet" => ?subnet, + "current_slot" => ?current_slot, + "unsubscribe_slot" => ?slot_required_until, ); + continue; + } + + self.subscribe_to_sync_subnet( + subnet, + duration_to_unsubscribe, + slot_required_until, + ); + } + } + } + } + + // If the discovery mechanism isn't disabled, attempt to set up a peer discovery for the + // required subnets. + if !self.discovery_disabled { + if let Err(e) = self.discover_peers_request(subnets_to_discover.into_iter()) { + warn!(self.log, "Discovery lookup request error"; "error" => e); + }; + } + } + + /// Checks if we have subscribed aggregate validators for the subnet. If not, checks the gossip + /// verification, re-propagates and returns false. + pub fn should_process_attestation( + &self, + subnet: Subnet, + attestation: &Attestation, + ) -> bool { + // Proposer-only mode does not need to process attestations + if self.proposer_only { + return false; + } + self.aggregate_validators_on_subnet + .as_ref() + .map(|tracked_vals| { + tracked_vals.contains_key(&ExactSubnet { + subnet, + slot: attestation.data().slot, + }) + }) + .unwrap_or(true) + } + + /* Internal private functions */ + + /// Adds an event to the event queue and notifies that this service is ready to be polled + /// again. + fn queue_event(&mut self, ev: SubnetServiceMessage) { + self.events.push_back(ev); + if let Some(waker) = &self.waker { + waker.wake_by_ref() + } + } + /// Checks if there are currently queued discovery requests and the time required to make the + /// request. + /// + /// If there is sufficient time, queues a peer discovery request for all the required subnets. + // NOTE: Sending early subscriptions results in early searching for peers on subnets. + fn discover_peers_request( + &mut self, + subnets_to_discover: impl Iterator, + ) -> Result<(), &'static str> { + let current_slot = self + .beacon_chain + .slot_clock + .now() + .ok_or("Could not get the current slot")?; + + let discovery_subnets: Vec = subnets_to_discover + .filter_map(|(subnet, relevant_slot)| { + // We generate discovery requests for all subnets (even one's we are permenantly + // subscribed to) in order to ensure our peer counts are satisfactory to perform the + // necessary duties. + + // Check if there is enough time to perform a discovery lookup. + if relevant_slot >= current_slot.saturating_add(MIN_PEER_DISCOVERY_SLOT_LOOK_AHEAD) + { + // Send out an event to start looking for peers. + // Require the peer for an additional slot to ensure we keep the peer for the + // duration of the subscription. + let min_ttl = self + .beacon_chain + .slot_clock + .duration_to_slot(relevant_slot + 1) + .map(|duration| std::time::Instant::now() + duration); + Some(SubnetDiscovery { subnet, min_ttl }) + } else { + // We may want to check the global PeerInfo to see estimated timeouts for each + // peer before they can be removed. + warn!(self.log, + "Not enough time for a discovery search"; + "subnet_id" => ?subnet, + ); + None + } + }) + .collect(); + + if !discovery_subnets.is_empty() { + self.queue_event(SubnetServiceMessage::DiscoverPeers(discovery_subnets)); + } + Ok(()) + } + + // Subscribes to the subnet if it should be done immediately, or schedules it if required. + fn subscribe_to_subnet( + &mut self, + ExactSubnet { subnet, slot }: ExactSubnet, + ) -> Result<(), &'static str> { + // If the subnet is one of our permanent subnets, we do not need to subscribe. + if self.subscribe_all_subnets || self.permanent_attestation_subscriptions.contains(&subnet) + { + return Ok(()); + } + + let slot_duration = self.beacon_chain.slot_clock.slot_duration(); + + // The short time we schedule the subscription before it's actually required. This + // ensures we are subscribed on time, and allows consecutive subscriptions to the same + // subnet to overlap, reducing subnet churn. + let advance_subscription_duration = slot_duration / ADVANCE_SUBSCRIBE_SLOT_FRACTION; + // The time to the required slot. + let time_to_subscription_slot = self + .beacon_chain + .slot_clock + .duration_to_slot(slot) + .unwrap_or_default(); // If this is a past slot we will just get a 0 duration. + + // Calculate how long before we need to subscribe to the subnet. + let time_to_subscription_start = + time_to_subscription_slot.saturating_sub(advance_subscription_duration); + + // The time after a duty slot where we no longer need it in the `aggregate_validators_on_subnet` + // delay map. + let time_to_unsubscribe = + time_to_subscription_slot + UNSUBSCRIBE_AFTER_AGGREGATOR_DUTY * slot_duration; + if let Some(tracked_vals) = self.aggregate_validators_on_subnet.as_mut() { + tracked_vals.insert_at(ExactSubnet { subnet, slot }, time_to_unsubscribe); + } + + // If the subscription should be done in the future, schedule it. Otherwise subscribe + // immediately. + if time_to_subscription_start.is_zero() { + // This is a current or past slot, we subscribe immediately. + self.subscribe_to_subnet_immediately(subnet, slot + 1)?; + } else { + // This is a future slot, schedule subscribing. + self.scheduled_subscriptions + .insert_at(subnet, time_to_subscription_start); + } + + Ok(()) + } + + /// Adds a subscription event to the sync subnet. + fn subscribe_to_sync_subnet( + &mut self, + subnet: Subnet, + duration_to_unsubscribe: Duration, + slot_required_until: Slot, + ) { + // Return if we have subscribed to all subnets + if self.subscribe_all_subnets { + return; + } + + // Update the unsubscription duration if we already have a subscription for the subnet + if let Some(current_instant_to_unsubscribe) = self.subscriptions.deadline(&subnet) { + // The extra 500ms in the comparison accounts of the inaccuracy of the underlying + // DelayQueue inside the delaymap struct. + let current_duration_to_unsubscribe = (current_instant_to_unsubscribe + + Duration::from_millis(500)) + .checked_duration_since(Instant::now()) + .unwrap_or(Duration::from_secs(0)); + + if duration_to_unsubscribe > current_duration_to_unsubscribe { + self.subscriptions + .update_timeout(&subnet, duration_to_unsubscribe); + } + } else { + // We have not subscribed before, so subscribe + self.subscriptions + .insert_at(subnet, duration_to_unsubscribe); + // We are not currently subscribed and have no waiting subscription, create one + debug!(self.log, "Subscribing to subnet"; "subnet" => ?subnet, "until" => ?slot_required_until); + self.events + .push_back(SubnetServiceMessage::Subscribe(subnet)); + + // add the sync subnet to the ENR bitfield + self.events.push_back(SubnetServiceMessage::EnrAdd(subnet)); + } + } + + /* A collection of functions that handle the various timeouts */ + + /// Registers a subnet as subscribed. + /// + /// Checks that the time in which the subscription would end is not in the past. If we are + /// already subscribed, extends the timeout if necessary. If this is a new subscription, we send + /// out the appropriate events. + fn subscribe_to_subnet_immediately( + &mut self, + subnet: Subnet, + end_slot: Slot, + ) -> Result<(), &'static str> { + if self.subscribe_all_subnets { + // Case not handled by this service. + return Ok(()); + } + + let time_to_subscription_end = self + .beacon_chain + .slot_clock + .duration_to_slot(end_slot) + .unwrap_or_default(); + + // First check this is worth doing. + if time_to_subscription_end.is_zero() { + return Err("Time when subscription would end has already passed."); + } + + // Check if we already have this subscription. If we do, optionally update the timeout of + // when we need the subscription, otherwise leave as is. + // If this is a new subscription simply add it to our mapping and subscribe. + match self.subscriptions.deadline(&subnet) { + Some(current_end_slot_time) => { + // We are already subscribed. Check if we need to extend the subscription. + if current_end_slot_time + .checked_duration_since(Instant::now()) + .unwrap_or(Duration::from_secs(0)) + < time_to_subscription_end + { + self.subscriptions + .update_timeout(&subnet, time_to_subscription_end); + } + } + None => { + // This is a new subscription. Add with the corresponding timeout and send the + // notification. + self.subscriptions + .insert_at(subnet, time_to_subscription_end); + + // Inform of the subscription. + debug!(self.log, "Subscribing to subnet"; + "subnet" => ?subnet, + "end_slot" => end_slot, + ); + self.queue_event(SubnetServiceMessage::Subscribe(subnet)); + } + } + Ok(()) + } + + // Unsubscribes from a subnet that was removed. + fn handle_removed_subnet(&mut self, subnet: Subnet) { + if !self.subscriptions.contains_key(&subnet) { + // Subscription no longer exists as short lived subnet + debug!(self.log, "Unsubscribing from subnet"; "subnet" => ?subnet); + self.queue_event(SubnetServiceMessage::Unsubscribe(subnet)); + + // If this is a sync subnet, we need to remove it from our ENR. + if let Subnet::SyncCommittee(sync_subnet_id) = subnet { + self.queue_event(SubnetServiceMessage::EnrRemove(sync_subnet_id)); + } + } + } +} + +impl Stream for SubnetService { + type Item = SubnetServiceMessage; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + // Update the waker if needed. + if let Some(waker) = &self.waker { + if waker.will_wake(cx.waker()) { + self.waker = Some(cx.waker().clone()); + } + } else { + self.waker = Some(cx.waker().clone()); + } + + // Send out any generated events. + if let Some(event) = self.events.pop_front() { + return Poll::Ready(Some(event)); + } + + // Process scheduled subscriptions that might be ready, since those can extend a soon to + // expire subscription. + match self.scheduled_subscriptions.poll_next_unpin(cx) { + Poll::Ready(Some(Ok(subnet))) => { + let current_slot = self.beacon_chain.slot_clock.now().unwrap_or_default(); + if let Err(e) = self.subscribe_to_subnet_immediately(subnet, current_slot + 1) { + debug!(self.log, "Failed to subscribe to short lived subnet"; "subnet" => ?subnet, "err" => e); + } + self.waker + .as_ref() + .expect("Waker has been set") + .wake_by_ref(); + } + Poll::Ready(Some(Err(e))) => { + error!(self.log, "Failed to check for scheduled subnet subscriptions"; "error"=> e); + } + Poll::Ready(None) | Poll::Pending => {} + } + + // Process any expired subscriptions. + match self.subscriptions.poll_next_unpin(cx) { + Poll::Ready(Some(Ok(subnet))) => { + self.handle_removed_subnet(subnet); + // We re-wake the task as there could be other subscriptions to process + self.waker + .as_ref() + .expect("Waker has been set") + .wake_by_ref(); + } + Poll::Ready(Some(Err(e))) => { + error!(self.log, "Failed to check for subnet unsubscription times"; "error"=> e); + } + Poll::Ready(None) | Poll::Pending => {} + } + + // Poll to remove entries on expiration, no need to act on expiration events. + if let Some(tracked_vals) = self.aggregate_validators_on_subnet.as_mut() { + if let Poll::Ready(Some(Err(e))) = tracked_vals.poll_next_unpin(cx) { + error!(self.log, "Failed to check for aggregate validator on subnet expirations"; "error"=> e); + } + } + + Poll::Pending + } +} + /// Note: This `PartialEq` impl is for use only in tests. /// The `DiscoverPeers` comparison is good enough for testing only. #[cfg(test)] @@ -32,7 +677,6 @@ impl PartialEq for SubnetServiceMessage { (SubnetServiceMessage::Subscribe(a), SubnetServiceMessage::Subscribe(b)) => a == b, (SubnetServiceMessage::Unsubscribe(a), SubnetServiceMessage::Unsubscribe(b)) => a == b, (SubnetServiceMessage::EnrAdd(a), SubnetServiceMessage::EnrAdd(b)) => a == b, - (SubnetServiceMessage::EnrRemove(a), SubnetServiceMessage::EnrRemove(b)) => a == b, (SubnetServiceMessage::DiscoverPeers(a), SubnetServiceMessage::DiscoverPeers(b)) => { if a.len() != b.len() { return false; diff --git a/beacon_node/network/src/subnet_service/sync_subnets.rs b/beacon_node/network/src/subnet_service/sync_subnets.rs deleted file mode 100644 index eda7ce8efb..0000000000 --- a/beacon_node/network/src/subnet_service/sync_subnets.rs +++ /dev/null @@ -1,359 +0,0 @@ -//! This service keeps track of which sync committee subnet the beacon node should be subscribed to at any -//! given time. It schedules subscriptions to sync committee subnets and requests peer discoveries. - -use std::collections::{hash_map::Entry, HashMap, VecDeque}; -use std::pin::Pin; -use std::sync::Arc; -use std::task::{Context, Poll}; -use std::time::Duration; - -use futures::prelude::*; -use slog::{debug, error, o, trace, warn}; - -use super::SubnetServiceMessage; -use beacon_chain::{BeaconChain, BeaconChainTypes}; -use delay_map::HashSetDelay; -use lighthouse_network::{NetworkConfig, Subnet, SubnetDiscovery}; -use slot_clock::SlotClock; -use types::{Epoch, EthSpec, SyncCommitteeSubscription, SyncSubnetId}; - -use crate::metrics; - -/// The minimum number of slots ahead that we attempt to discover peers for a subscription. If the -/// slot is less than this number, skip the peer discovery process. -/// Subnet discovery query takes at most 30 secs, 2 slots take 24s. -const MIN_PEER_DISCOVERY_SLOT_LOOK_AHEAD: u64 = 2; - -/// A particular subnet at a given slot. -#[derive(PartialEq, Eq, Hash, Clone, Debug)] -pub struct ExactSubnet { - /// The `SyncSubnetId` associated with this subnet. - pub subnet_id: SyncSubnetId, - /// The epoch until which we need to stay subscribed to the subnet. - pub until_epoch: Epoch, -} -pub struct SyncCommitteeService { - /// Queued events to return to the driving service. - events: VecDeque, - - /// A reference to the beacon chain to process received attestations. - pub(crate) beacon_chain: Arc>, - - /// The collection of all currently subscribed subnets. - subscriptions: HashMap, - - /// A collection of timeouts for when to unsubscribe from a subnet. - unsubscriptions: HashSetDelay, - - /// The waker for the current thread. - waker: Option, - - /// The discovery mechanism of lighthouse is disabled. - discovery_disabled: bool, - - /// We are always subscribed to all subnets. - subscribe_all_subnets: bool, - - /// Whether this node is a block proposer-only node. - proposer_only: bool, - - /// The logger for the attestation service. - log: slog::Logger, -} - -impl SyncCommitteeService { - /* Public functions */ - - pub fn new( - beacon_chain: Arc>, - config: &NetworkConfig, - log: &slog::Logger, - ) -> Self { - let log = log.new(o!("service" => "sync_committee_service")); - - let spec = &beacon_chain.spec; - let epoch_duration_secs = - beacon_chain.slot_clock.slot_duration().as_secs() * T::EthSpec::slots_per_epoch(); - let default_timeout = - epoch_duration_secs.saturating_mul(spec.epochs_per_sync_committee_period.as_u64()); - - SyncCommitteeService { - events: VecDeque::with_capacity(10), - beacon_chain, - subscriptions: HashMap::new(), - unsubscriptions: HashSetDelay::new(Duration::from_secs(default_timeout)), - waker: None, - subscribe_all_subnets: config.subscribe_all_subnets, - discovery_disabled: config.disable_discovery, - proposer_only: config.proposer_only, - log, - } - } - - /// Return count of all currently subscribed subnets. - #[cfg(test)] - pub fn subscription_count(&self) -> usize { - use types::consts::altair::SYNC_COMMITTEE_SUBNET_COUNT; - if self.subscribe_all_subnets { - SYNC_COMMITTEE_SUBNET_COUNT as usize - } else { - self.subscriptions.len() - } - } - - /// Processes a list of sync committee subscriptions. - /// - /// This will: - /// - Search for peers for required subnets. - /// - Request subscriptions required subnets. - /// - Build the timeouts for each of these events. - /// - /// This returns a result simply for the ergonomics of using ?. The result can be - /// safely dropped. - pub fn validator_subscriptions( - &mut self, - subscriptions: Vec, - ) -> Result<(), String> { - // A proposer-only node does not subscribe to any sync-committees - if self.proposer_only { - return Ok(()); - } - - let mut subnets_to_discover = Vec::new(); - for subscription in subscriptions { - metrics::inc_counter(&metrics::SYNC_COMMITTEE_SUBSCRIPTION_REQUESTS); - //NOTE: We assume all subscriptions have been verified before reaching this service - - // Registers the validator with the subnet service. - // This will subscribe to long-lived random subnets if required. - trace!(self.log, - "Sync committee subscription"; - "subscription" => ?subscription, - ); - - let subnet_ids = match SyncSubnetId::compute_subnets_for_sync_committee::( - &subscription.sync_committee_indices, - ) { - Ok(subnet_ids) => subnet_ids, - Err(e) => { - warn!(self.log, - "Failed to compute subnet id for sync committee subscription"; - "error" => ?e, - "validator_index" => subscription.validator_index - ); - continue; - } - }; - - for subnet_id in subnet_ids { - let exact_subnet = ExactSubnet { - subnet_id, - until_epoch: subscription.until_epoch, - }; - subnets_to_discover.push(exact_subnet.clone()); - if let Err(e) = self.subscribe_to_subnet(exact_subnet.clone()) { - warn!(self.log, - "Subscription to sync subnet error"; - "error" => e, - "validator_index" => subscription.validator_index, - ); - } else { - trace!(self.log, - "Subscribed to subnet for sync committee duties"; - "exact_subnet" => ?exact_subnet, - "validator_index" => subscription.validator_index - ); - } - } - } - // If the discovery mechanism isn't disabled, attempt to set up a peer discovery for the - // required subnets. - if !self.discovery_disabled { - if let Err(e) = self.discover_peers_request(subnets_to_discover.iter()) { - warn!(self.log, "Discovery lookup request error"; "error" => e); - }; - } - - // pre-emptively wake the thread to check for new events - if let Some(waker) = &self.waker { - waker.wake_by_ref(); - } - Ok(()) - } - - /* Internal private functions */ - - /// Checks if there are currently queued discovery requests and the time required to make the - /// request. - /// - /// If there is sufficient time, queues a peer discovery request for all the required subnets. - fn discover_peers_request<'a>( - &mut self, - exact_subnets: impl Iterator, - ) -> Result<(), &'static str> { - let current_slot = self - .beacon_chain - .slot_clock - .now() - .ok_or("Could not get the current slot")?; - - let slots_per_epoch = T::EthSpec::slots_per_epoch(); - - let discovery_subnets: Vec = exact_subnets - .filter_map(|exact_subnet| { - let until_slot = exact_subnet.until_epoch.end_slot(slots_per_epoch); - // check if there is enough time to perform a discovery lookup - if until_slot >= current_slot.saturating_add(MIN_PEER_DISCOVERY_SLOT_LOOK_AHEAD) { - // if the slot is more than epoch away, add an event to start looking for peers - // add one slot to ensure we keep the peer for the subscription slot - let min_ttl = self - .beacon_chain - .slot_clock - .duration_to_slot(until_slot + 1) - .map(|duration| std::time::Instant::now() + duration); - Some(SubnetDiscovery { - subnet: Subnet::SyncCommittee(exact_subnet.subnet_id), - min_ttl, - }) - } else { - // We may want to check the global PeerInfo to see estimated timeouts for each - // peer before they can be removed. - warn!(self.log, - "Not enough time for a discovery search"; - "subnet_id" => ?exact_subnet - ); - None - } - }) - .collect(); - - if !discovery_subnets.is_empty() { - self.events - .push_back(SubnetServiceMessage::DiscoverPeers(discovery_subnets)); - } - Ok(()) - } - - /// Adds a subscription event and an associated unsubscription event if required. - fn subscribe_to_subnet(&mut self, exact_subnet: ExactSubnet) -> Result<(), &'static str> { - // Return if we have subscribed to all subnets - if self.subscribe_all_subnets { - return Ok(()); - } - - // Return if we already have a subscription for exact_subnet - if self.subscriptions.get(&exact_subnet.subnet_id) == Some(&exact_subnet.until_epoch) { - return Ok(()); - } - - // Return if we already have subscription set to expire later than the current request. - if let Some(until_epoch) = self.subscriptions.get(&exact_subnet.subnet_id) { - if *until_epoch >= exact_subnet.until_epoch { - return Ok(()); - } - } - - // initialise timing variables - let current_slot = self - .beacon_chain - .slot_clock - .now() - .ok_or("Could not get the current slot")?; - - let slots_per_epoch = T::EthSpec::slots_per_epoch(); - let until_slot = exact_subnet.until_epoch.end_slot(slots_per_epoch); - // Calculate the duration to the unsubscription event. - let expected_end_subscription_duration = if current_slot >= until_slot { - warn!( - self.log, - "Sync committee subscription is past expiration"; - "current_slot" => current_slot, - "exact_subnet" => ?exact_subnet, - ); - return Ok(()); - } else { - let slot_duration = self.beacon_chain.slot_clock.slot_duration(); - - // the duration until we no longer need this subscription. We assume a single slot is - // sufficient. - self.beacon_chain - .slot_clock - .duration_to_slot(until_slot) - .ok_or("Unable to determine duration to unsubscription slot")? - + slot_duration - }; - - if let Entry::Vacant(e) = self.subscriptions.entry(exact_subnet.subnet_id) { - // We are not currently subscribed and have no waiting subscription, create one - debug!(self.log, "Subscribing to subnet"; "subnet" => *exact_subnet.subnet_id, "until_epoch" => ?exact_subnet.until_epoch); - e.insert(exact_subnet.until_epoch); - self.events - .push_back(SubnetServiceMessage::Subscribe(Subnet::SyncCommittee( - exact_subnet.subnet_id, - ))); - - // add the subnet to the ENR bitfield - self.events - .push_back(SubnetServiceMessage::EnrAdd(Subnet::SyncCommittee( - exact_subnet.subnet_id, - ))); - - // add an unsubscription event to remove ourselves from the subnet once completed - self.unsubscriptions - .insert_at(exact_subnet.subnet_id, expected_end_subscription_duration); - } else { - // We are already subscribed, extend the unsubscription duration - self.unsubscriptions - .update_timeout(&exact_subnet.subnet_id, expected_end_subscription_duration); - } - - Ok(()) - } - - /// A queued unsubscription is ready. - fn handle_unsubscriptions(&mut self, subnet_id: SyncSubnetId) { - debug!(self.log, "Unsubscribing from subnet"; "subnet" => *subnet_id); - - self.subscriptions.remove(&subnet_id); - self.events - .push_back(SubnetServiceMessage::Unsubscribe(Subnet::SyncCommittee( - subnet_id, - ))); - - self.events - .push_back(SubnetServiceMessage::EnrRemove(Subnet::SyncCommittee( - subnet_id, - ))); - } -} - -impl Stream for SyncCommitteeService { - type Item = SubnetServiceMessage; - - fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - // update the waker if needed - if let Some(waker) = &self.waker { - if waker.will_wake(cx.waker()) { - self.waker = Some(cx.waker().clone()); - } - } else { - self.waker = Some(cx.waker().clone()); - } - - // process any un-subscription events - match self.unsubscriptions.poll_next_unpin(cx) { - Poll::Ready(Some(Ok(exact_subnet))) => self.handle_unsubscriptions(exact_subnet), - Poll::Ready(Some(Err(e))) => { - error!(self.log, "Failed to check for subnet unsubscription times"; "error"=> e); - } - Poll::Ready(None) | Poll::Pending => {} - } - - // process any generated events - if let Some(event) = self.events.pop_front() { - return Poll::Ready(Some(event)); - } - - Poll::Pending - } -} diff --git a/beacon_node/network/src/subnet_service/tests/mod.rs b/beacon_node/network/src/subnet_service/tests/mod.rs index a784b05ea7..c56079b9ac 100644 --- a/beacon_node/network/src/subnet_service/tests/mod.rs +++ b/beacon_node/network/src/subnet_service/tests/mod.rs @@ -5,9 +5,9 @@ use beacon_chain::{ test_utils::get_kzg, BeaconChain, }; -use futures::prelude::*; use genesis::{generate_deterministic_keypairs, interop_genesis_state, DEFAULT_ETH1_BLOCK_HASH}; use lighthouse_network::NetworkConfig; +use logging::test_logger; use slog::{o, Drain, Logger}; use sloggers::{null::NullLoggerBuilder, Build}; use slot_clock::{SlotClock, SystemTimeSlotClock}; @@ -21,6 +21,10 @@ use types::{ SyncCommitteeSubscription, SyncSubnetId, ValidatorSubscription, }; +// Set to enable/disable logging +// const TEST_LOG_LEVEL: Option = Some(slog::Level::Debug); +const TEST_LOG_LEVEL: Option = None; + const SLOT_DURATION_MILLIS: u64 = 400; type TestBeaconChainType = Witness< @@ -42,7 +46,7 @@ impl TestBeaconChain { let keypairs = generate_deterministic_keypairs(1); - let log = get_logger(None); + let log = get_logger(TEST_LOG_LEVEL); let store = HotColdDB::open_ephemeral(StoreConfig::default(), spec.clone(), log.clone()).unwrap(); @@ -114,15 +118,13 @@ fn get_logger(log_level: Option) -> Logger { static CHAIN: LazyLock = LazyLock::new(TestBeaconChain::new_with_system_clock); -fn get_attestation_service( - log_level: Option, -) -> AttestationService { - let log = get_logger(log_level); +fn get_subnet_service() -> SubnetService { + let log = test_logger(); let config = NetworkConfig::default(); let beacon_chain = CHAIN.chain.clone(); - AttestationService::new( + SubnetService::new( beacon_chain, lighthouse_network::discv5::enr::NodeId::random(), &config, @@ -130,15 +132,6 @@ fn get_attestation_service( ) } -fn get_sync_committee_service() -> SyncCommitteeService { - let log = get_logger(None); - let config = NetworkConfig::default(); - - let beacon_chain = CHAIN.chain.clone(); - - SyncCommitteeService::new(beacon_chain, &config, &log) -} - // gets a number of events from the subscription service, or returns none if it times out after a number // of slots async fn get_events + Unpin>( @@ -172,10 +165,10 @@ async fn get_events + Unpin>( events } -mod attestation_service { +mod test { #[cfg(not(windows))] - use crate::subnet_service::attestation_subnets::MIN_PEER_DISCOVERY_SLOT_LOOK_AHEAD; + use crate::subnet_service::MIN_PEER_DISCOVERY_SLOT_LOOK_AHEAD; use super::*; @@ -184,13 +177,13 @@ mod attestation_service { slot: Slot, committee_count_at_slot: u64, is_aggregator: bool, - ) -> ValidatorSubscription { - ValidatorSubscription { + ) -> Subscription { + Subscription::Attestation(ValidatorSubscription { attestation_committee_index, slot, committee_count_at_slot, is_aggregator, - } + }) } fn get_subscriptions( @@ -198,7 +191,7 @@ mod attestation_service { slot: Slot, committee_count_at_slot: u64, is_aggregator: bool, - ) -> Vec { + ) -> Vec { (0..validator_count) .map(|validator_index| { get_subscription( @@ -215,72 +208,77 @@ mod attestation_service { async fn subscribe_current_slot_wait_for_unsubscribe() { // subscription config let committee_index = 1; - // Keep a low subscription slot so that there are no additional subnet discovery events. - let subscription_slot = 0; - let committee_count = 1; let subnets_per_node = MainnetEthSpec::default_spec().subnets_per_node as usize; // create the attestation service and subscriptions - let mut attestation_service = get_attestation_service(None); - let current_slot = attestation_service + let mut subnet_service = get_subnet_service(); + let _events = get_events(&mut subnet_service, None, 1).await; + + let current_slot = subnet_service .beacon_chain .slot_clock .now() .expect("Could not get current slot"); + // Generate a subnet that isn't in our permanent subnet collection + let subscription_slot = current_slot + 1; + let mut committee_count = 1; + let mut subnet = Subnet::Attestation( + SubnetId::compute_subnet::( + current_slot, + committee_index, + committee_count, + &subnet_service.beacon_chain.spec, + ) + .unwrap(), + ); + while subnet_service + .permanent_subscriptions() + .any(|x| *x == subnet) + { + committee_count += 1; + subnet = Subnet::Attestation( + SubnetId::compute_subnet::( + subscription_slot, + committee_index, + committee_count, + &subnet_service.beacon_chain.spec, + ) + .unwrap(), + ); + } + let subscriptions = vec![get_subscription( committee_index, - current_slot + Slot::new(subscription_slot), + current_slot, committee_count, true, )]; // submit the subscriptions - attestation_service - .validator_subscriptions(subscriptions.into_iter()) - .unwrap(); + subnet_service.validator_subscriptions(subscriptions.into_iter()); // not enough time for peer discovery, just subscribe, unsubscribe - let subnet_id = SubnetId::compute_subnet::( - current_slot + Slot::new(subscription_slot), - committee_index, - committee_count, - &attestation_service.beacon_chain.spec, - ) - .unwrap(); let expected = [ - SubnetServiceMessage::Subscribe(Subnet::Attestation(subnet_id)), - SubnetServiceMessage::Unsubscribe(Subnet::Attestation(subnet_id)), + SubnetServiceMessage::Subscribe(subnet), + SubnetServiceMessage::Unsubscribe(subnet), ]; // Wait for 1 slot duration to get the unsubscription event let events = get_events( - &mut attestation_service, - Some(subnets_per_node * 3 + 2), - (MainnetEthSpec::slots_per_epoch() * 3) as u32, + &mut subnet_service, + Some(2), + (MainnetEthSpec::slots_per_epoch()) as u32, ) .await; - matches::assert_matches!( - events[..6], - [ - SubnetServiceMessage::Subscribe(_any1), - SubnetServiceMessage::EnrAdd(_any3), - SubnetServiceMessage::DiscoverPeers(_), - SubnetServiceMessage::Subscribe(_), - SubnetServiceMessage::EnrAdd(_), - SubnetServiceMessage::DiscoverPeers(_), - ] - ); + assert_eq!(events, expected); - // If the long lived and short lived subnets are the same, there should be no more events - // as we don't resubscribe already subscribed subnets. - if !attestation_service - .is_subscribed(&subnet_id, attestation_subnets::SubscriptionKind::LongLived) - { - assert_eq!(expected[..], events[subnets_per_node * 3..]); - } - // Should be subscribed to only subnets_per_node long lived subnet after unsubscription. - assert_eq!(attestation_service.subscription_count(), subnets_per_node); + // Should be subscribed to only subnets_per_node permananet subnet after unsubscription. + assert_eq!( + subnet_service.permanent_subscriptions().count(), + subnets_per_node + ); + assert_eq!(subnet_service.subscriptions().count(), 0); } /// Test to verify that we are not unsubscribing to a subnet before a required subscription. @@ -289,7 +287,6 @@ mod attestation_service { async fn test_same_subnet_unsubscription() { // subscription config let committee_count = 1; - let subnets_per_node = MainnetEthSpec::default_spec().subnets_per_node as usize; // Makes 2 validator subscriptions to the same subnet but at different slots. // There should be just 1 unsubscription event for the later slot subscription (subscription_slot2). @@ -298,9 +295,10 @@ mod attestation_service { let com1 = 1; let com2 = 0; - // create the attestation service and subscriptions - let mut attestation_service = get_attestation_service(None); - let current_slot = attestation_service + // create the subnet service and subscriptions + let mut subnet_service = get_subnet_service(); + let _events = get_events(&mut subnet_service, None, 0).await; + let current_slot = subnet_service .beacon_chain .slot_clock .now() @@ -324,7 +322,7 @@ mod attestation_service { current_slot + Slot::new(subscription_slot1), com1, committee_count, - &attestation_service.beacon_chain.spec, + &subnet_service.beacon_chain.spec, ) .unwrap(); @@ -332,7 +330,7 @@ mod attestation_service { current_slot + Slot::new(subscription_slot2), com2, committee_count, - &attestation_service.beacon_chain.spec, + &subnet_service.beacon_chain.spec, ) .unwrap(); @@ -341,110 +339,80 @@ mod attestation_service { assert_eq!(subnet_id1, subnet_id2); // submit the subscriptions - attestation_service - .validator_subscriptions(vec![sub1, sub2].into_iter()) - .unwrap(); + subnet_service.validator_subscriptions(vec![sub1, sub2].into_iter()); // Unsubscription event should happen at slot 2 (since subnet id's are the same, unsubscription event should be at higher slot + 1) - // Get all events for 1 slot duration (unsubscription event should happen after 2 slot durations). - let events = get_events(&mut attestation_service, None, 1).await; - matches::assert_matches!( - events[..3], - [ - SubnetServiceMessage::Subscribe(_any1), - SubnetServiceMessage::EnrAdd(_any3), - SubnetServiceMessage::DiscoverPeers(_), - ] - ); - let expected = SubnetServiceMessage::Subscribe(Subnet::Attestation(subnet_id1)); - // Should be still subscribed to 2 long lived and up to 1 short lived subnet if both are - // different. - if !attestation_service.is_subscribed( - &subnet_id1, - attestation_subnets::SubscriptionKind::LongLived, - ) { - // The index is 3*subnets_per_node (because we subscribe + discover + enr per long lived - // subnet) + 1 - let index = 3 * subnets_per_node; - assert_eq!(expected, events[index]); - assert_eq!( - attestation_service.subscription_count(), - subnets_per_node + 1 - ); + if subnet_service.is_subscribed(&Subnet::Attestation(subnet_id1)) { + // If we are permanently subscribed to this subnet, we won't see a subscribe message + let _ = get_events(&mut subnet_service, None, 1).await; } else { - assert!(attestation_service.subscription_count() == subnets_per_node); + let subscription = get_events(&mut subnet_service, None, 1).await; + assert_eq!(subscription, [expected]); } // Get event for 1 more slot duration, we should get the unsubscribe event now. - let unsubscribe_event = get_events(&mut attestation_service, None, 1).await; + let unsubscribe_event = get_events(&mut subnet_service, None, 1).await; // If the long lived and short lived subnets are different, we should get an unsubscription // event. - if !attestation_service.is_subscribed( - &subnet_id1, - attestation_subnets::SubscriptionKind::LongLived, - ) { - assert_eq!( - [SubnetServiceMessage::Unsubscribe(Subnet::Attestation( - subnet_id1 - ))], - unsubscribe_event[..] - ); + let expected = SubnetServiceMessage::Unsubscribe(Subnet::Attestation(subnet_id1)); + if !subnet_service.is_subscribed(&Subnet::Attestation(subnet_id1)) { + assert_eq!([expected], unsubscribe_event[..]); } - // Should be subscribed 2 long lived subnet after unsubscription. - assert_eq!(attestation_service.subscription_count(), subnets_per_node); + // Should no longer be subscribed to any short lived subnets after unsubscription. + assert_eq!(subnet_service.subscriptions().count(), 0); } #[tokio::test] async fn subscribe_all_subnets() { let attestation_subnet_count = MainnetEthSpec::default_spec().attestation_subnet_count; let subscription_slot = 3; - let subscription_count = attestation_subnet_count; + let subscriptions_count = attestation_subnet_count; let committee_count = 1; let subnets_per_node = MainnetEthSpec::default_spec().subnets_per_node as usize; // create the attestation service and subscriptions - let mut attestation_service = get_attestation_service(None); - let current_slot = attestation_service + let mut subnet_service = get_subnet_service(); + let current_slot = subnet_service .beacon_chain .slot_clock .now() .expect("Could not get current slot"); let subscriptions = get_subscriptions( - subscription_count, + subscriptions_count, current_slot + subscription_slot, committee_count, true, ); // submit the subscriptions - attestation_service - .validator_subscriptions(subscriptions.into_iter()) - .unwrap(); + subnet_service.validator_subscriptions(subscriptions.into_iter()); - let events = get_events(&mut attestation_service, Some(131), 10).await; + let events = get_events(&mut subnet_service, Some(130), 10).await; let mut discover_peer_count = 0; let mut enr_add_count = 0; - let mut unexpected_msg_count = 0; let mut unsubscribe_event_count = 0; + let mut subscription_event_count = 0; for event in &events { match event { SubnetServiceMessage::DiscoverPeers(_) => discover_peer_count += 1, - SubnetServiceMessage::Subscribe(_any_subnet) => {} + SubnetServiceMessage::Subscribe(_any_subnet) => subscription_event_count += 1, SubnetServiceMessage::EnrAdd(_any_subnet) => enr_add_count += 1, SubnetServiceMessage::Unsubscribe(_) => unsubscribe_event_count += 1, - _ => unexpected_msg_count += 1, + SubnetServiceMessage::EnrRemove(_) => {} } } - // There should be a Subscribe Event, and Enr Add event and a DiscoverPeers event for each - // long-lived subnet initially. The next event should be a bulk discovery event. - let bulk_discovery_index = 3 * subnets_per_node; + // There should be a Subscribe Event, an Enr Add event for each + // permanent subnet initially. There is a single discovery event for the permanent + // subnets. + // The next event should be a bulk discovery event. + let bulk_discovery_index = subnets_per_node * 2 + 1; // The bulk discovery request length should be equal to validator_count let bulk_discovery_event = &events[bulk_discovery_index]; if let SubnetServiceMessage::DiscoverPeers(d) = bulk_discovery_event { @@ -455,14 +423,13 @@ mod attestation_service { // 64 `DiscoverPeer` requests of length 1 corresponding to deterministic subnets // and 1 `DiscoverPeer` request corresponding to bulk subnet discovery. - assert_eq!(discover_peer_count, subnets_per_node + 1); - assert_eq!(attestation_service.subscription_count(), subnets_per_node); + assert_eq!(discover_peer_count, 1 + 1); + assert_eq!(subscription_event_count, attestation_subnet_count); assert_eq!(enr_add_count, subnets_per_node); assert_eq!( unsubscribe_event_count, attestation_subnet_count - subnets_per_node as u64 ); - assert_eq!(unexpected_msg_count, 0); // test completed successfully } @@ -473,30 +440,28 @@ mod attestation_service { let subnets_per_node = MainnetEthSpec::default_spec().subnets_per_node as usize; // the 65th subscription should result in no more messages than the previous scenario - let subscription_count = attestation_subnet_count + 1; + let subscriptions_count = attestation_subnet_count + 1; let committee_count = 1; // create the attestation service and subscriptions - let mut attestation_service = get_attestation_service(None); - let current_slot = attestation_service + let mut subnet_service = get_subnet_service(); + let current_slot = subnet_service .beacon_chain .slot_clock .now() .expect("Could not get current slot"); let subscriptions = get_subscriptions( - subscription_count, + subscriptions_count, current_slot + subscription_slot, committee_count, true, ); // submit the subscriptions - attestation_service - .validator_subscriptions(subscriptions.into_iter()) - .unwrap(); + subnet_service.validator_subscriptions(subscriptions.into_iter()); - let events = get_events(&mut attestation_service, None, 3).await; + let events = get_events(&mut subnet_service, None, 3).await; let mut discover_peer_count = 0; let mut enr_add_count = 0; let mut unexpected_msg_count = 0; @@ -506,7 +471,10 @@ mod attestation_service { SubnetServiceMessage::DiscoverPeers(_) => discover_peer_count += 1, SubnetServiceMessage::Subscribe(_any_subnet) => {} SubnetServiceMessage::EnrAdd(_any_subnet) => enr_add_count += 1, - _ => unexpected_msg_count += 1, + _ => { + unexpected_msg_count += 1; + println!("{:?}", event); + } } } @@ -520,8 +488,8 @@ mod attestation_service { // subnets_per_node `DiscoverPeer` requests of length 1 corresponding to long-lived subnets // and 1 `DiscoverPeer` request corresponding to the bulk subnet discovery. - assert_eq!(discover_peer_count, subnets_per_node + 1); - assert_eq!(attestation_service.subscription_count(), subnets_per_node); + assert_eq!(discover_peer_count, 1 + 1); // Generates a single discovery for permanent + // subscriptions and 1 for the subscription assert_eq!(enr_add_count, subnets_per_node); assert_eq!(unexpected_msg_count, 0); } @@ -531,7 +499,6 @@ mod attestation_service { async fn test_subscribe_same_subnet_several_slots_apart() { // subscription config let committee_count = 1; - let subnets_per_node = MainnetEthSpec::default_spec().subnets_per_node as usize; // Makes 2 validator subscriptions to the same subnet but at different slots. // There should be just 1 unsubscription event for the later slot subscription (subscription_slot2). @@ -541,8 +508,11 @@ mod attestation_service { let com2 = 0; // create the attestation service and subscriptions - let mut attestation_service = get_attestation_service(None); - let current_slot = attestation_service + let mut subnet_service = get_subnet_service(); + // Remove permanent events + let _events = get_events(&mut subnet_service, None, 0).await; + + let current_slot = subnet_service .beacon_chain .slot_clock .now() @@ -566,7 +536,7 @@ mod attestation_service { current_slot + Slot::new(subscription_slot1), com1, committee_count, - &attestation_service.beacon_chain.spec, + &subnet_service.beacon_chain.spec, ) .unwrap(); @@ -574,7 +544,7 @@ mod attestation_service { current_slot + Slot::new(subscription_slot2), com2, committee_count, - &attestation_service.beacon_chain.spec, + &subnet_service.beacon_chain.spec, ) .unwrap(); @@ -583,39 +553,26 @@ mod attestation_service { assert_eq!(subnet_id1, subnet_id2); // submit the subscriptions - attestation_service - .validator_subscriptions(vec![sub1, sub2].into_iter()) - .unwrap(); + subnet_service.validator_subscriptions(vec![sub1, sub2].into_iter()); // Unsubscription event should happen at the end of the slot. - let events = get_events(&mut attestation_service, None, 1).await; - matches::assert_matches!( - events[..3], - [ - SubnetServiceMessage::Subscribe(_any1), - SubnetServiceMessage::EnrAdd(_any3), - SubnetServiceMessage::DiscoverPeers(_), - ] - ); + let events = get_events(&mut subnet_service, None, 1).await; let expected_subscription = SubnetServiceMessage::Subscribe(Subnet::Attestation(subnet_id1)); let expected_unsubscription = SubnetServiceMessage::Unsubscribe(Subnet::Attestation(subnet_id1)); - if !attestation_service.is_subscribed( - &subnet_id1, - attestation_subnets::SubscriptionKind::LongLived, - ) { - assert_eq!(expected_subscription, events[subnets_per_node * 3]); - assert_eq!(expected_unsubscription, events[subnets_per_node * 3 + 2]); + if !subnet_service.is_subscribed(&Subnet::Attestation(subnet_id1)) { + assert_eq!(expected_subscription, events[0]); + assert_eq!(expected_unsubscription, events[2]); } - assert_eq!(attestation_service.subscription_count(), 2); + assert_eq!(subnet_service.subscriptions().count(), 0); println!("{events:?}"); let subscription_slot = current_slot + subscription_slot2 - 1; // one less do to the // advance subscription time - let wait_slots = attestation_service + let wait_slots = subnet_service .beacon_chain .slot_clock .duration_to_slot(subscription_slot) @@ -623,90 +580,42 @@ mod attestation_service { .as_millis() as u64 / SLOT_DURATION_MILLIS; - let no_events = dbg!(get_events(&mut attestation_service, None, wait_slots as u32).await); + let no_events = dbg!(get_events(&mut subnet_service, None, wait_slots as u32).await); assert_eq!(no_events, []); - let second_subscribe_event = get_events(&mut attestation_service, None, 2).await; - // If the long lived and short lived subnets are different, we should get an unsubscription event. - if !attestation_service.is_subscribed( - &subnet_id1, - attestation_subnets::SubscriptionKind::LongLived, - ) { + let second_subscribe_event = get_events(&mut subnet_service, None, 2).await; + // If the permanent and short lived subnets are different, we should get an unsubscription event. + if !subnet_service.is_subscribed(&Subnet::Attestation(subnet_id1)) { assert_eq!( - [SubnetServiceMessage::Subscribe(Subnet::Attestation( - subnet_id1 - ))], + [expected_subscription, expected_unsubscription], second_subscribe_event[..] ); } } #[tokio::test] - async fn test_update_deterministic_long_lived_subnets() { - let mut attestation_service = get_attestation_service(None); - let subnets_per_node = MainnetEthSpec::default_spec().subnets_per_node as usize; - - let current_slot = attestation_service - .beacon_chain - .slot_clock - .now() - .expect("Could not get current slot"); - - let subscriptions = get_subscriptions(20, current_slot, 30, false); - - // submit the subscriptions - attestation_service - .validator_subscriptions(subscriptions.into_iter()) - .unwrap(); - - // There should only be the same subscriptions as there are in the specification, - // regardless of subscriptions - assert_eq!( - attestation_service.long_lived_subscriptions().len(), - subnets_per_node - ); - - let events = get_events(&mut attestation_service, None, 4).await; - - // Check that we attempt to subscribe and register ENRs - matches::assert_matches!( - events[..6], - [ - SubnetServiceMessage::Subscribe(_), - SubnetServiceMessage::EnrAdd(_), - SubnetServiceMessage::DiscoverPeers(_), - SubnetServiceMessage::Subscribe(_), - SubnetServiceMessage::EnrAdd(_), - SubnetServiceMessage::DiscoverPeers(_), - ] - ); - } -} - -mod sync_committee_service { - use super::*; - - #[tokio::test] - async fn subscribe_and_unsubscribe() { + async fn subscribe_and_unsubscribe_sync_committee() { // subscription config let validator_index = 1; let until_epoch = Epoch::new(1); let sync_committee_indices = vec![1]; // create the attestation service and subscriptions - let mut sync_committee_service = get_sync_committee_service(); + let mut subnet_service = get_subnet_service(); + let _events = get_events(&mut subnet_service, None, 0).await; - let subscriptions = vec![SyncCommitteeSubscription { - validator_index, - sync_committee_indices: sync_committee_indices.clone(), - until_epoch, - }]; + let subscriptions = + std::iter::once(Subscription::SyncCommittee(SyncCommitteeSubscription { + validator_index, + sync_committee_indices: sync_committee_indices.clone(), + until_epoch, + })); // submit the subscriptions - sync_committee_service - .validator_subscriptions(subscriptions) - .unwrap(); + subnet_service.validator_subscriptions(subscriptions); + + // Remove permanent subscription events let subnet_ids = SyncSubnetId::compute_subnets_for_sync_committee::( &sync_committee_indices, @@ -716,7 +625,7 @@ mod sync_committee_service { // Note: the unsubscription event takes 2 epochs (8 * 2 * 0.4 secs = 3.2 secs) let events = get_events( - &mut sync_committee_service, + &mut subnet_service, Some(5), (MainnetEthSpec::slots_per_epoch() * 3) as u32, // Have some buffer time before getting 5 events ) @@ -738,7 +647,7 @@ mod sync_committee_service { ); // Should be unsubscribed at the end. - assert_eq!(sync_committee_service.subscription_count(), 0); + assert_eq!(subnet_service.subscriptions().count(), 0); } #[tokio::test] @@ -749,21 +658,22 @@ mod sync_committee_service { let sync_committee_indices = vec![1]; // create the attestation service and subscriptions - let mut sync_committee_service = get_sync_committee_service(); + let mut subnet_service = get_subnet_service(); + // Get the initial events from permanent subnet subscriptions + let _events = get_events(&mut subnet_service, None, 1).await; - let subscriptions = vec![SyncCommitteeSubscription { - validator_index, - sync_committee_indices: sync_committee_indices.clone(), - until_epoch, - }]; + let subscriptions = + std::iter::once(Subscription::SyncCommittee(SyncCommitteeSubscription { + validator_index, + sync_committee_indices: sync_committee_indices.clone(), + until_epoch, + })); // submit the subscriptions - sync_committee_service - .validator_subscriptions(subscriptions) - .unwrap(); + subnet_service.validator_subscriptions(subscriptions); // Get all immediate events (won't include unsubscriptions) - let events = get_events(&mut sync_committee_service, None, 1).await; + let events = get_events(&mut subnet_service, None, 1).await; matches::assert_matches!( events[..], [ @@ -777,28 +687,30 @@ mod sync_committee_service { // Event 1 is a duplicate of an existing subscription // Event 2 is the same subscription with lower `until_epoch` than the existing subscription let subscriptions = vec![ - SyncCommitteeSubscription { + Subscription::SyncCommittee(SyncCommitteeSubscription { validator_index, sync_committee_indices: sync_committee_indices.clone(), until_epoch, - }, - SyncCommitteeSubscription { + }), + Subscription::SyncCommittee(SyncCommitteeSubscription { validator_index, sync_committee_indices: sync_committee_indices.clone(), until_epoch: until_epoch - 1, - }, + }), ]; // submit the subscriptions - sync_committee_service - .validator_subscriptions(subscriptions) - .unwrap(); + subnet_service.validator_subscriptions(subscriptions.into_iter()); // Get all immediate events (won't include unsubscriptions) - let events = get_events(&mut sync_committee_service, None, 1).await; + let events = get_events(&mut subnet_service, None, 1).await; matches::assert_matches!(events[..], [SubnetServiceMessage::DiscoverPeers(_),]); // Should be unsubscribed at the end. - assert_eq!(sync_committee_service.subscription_count(), 1); + let sync_committee_subscriptions = subnet_service + .subscriptions() + .filter(|s| matches!(s, Subnet::SyncCommittee(_))) + .count(); + assert_eq!(sync_committee_subscriptions, 1); } } diff --git a/consensus/types/src/chain_spec.rs b/consensus/types/src/chain_spec.rs index 1c4effb4ae..79dcc65ea3 100644 --- a/consensus/types/src/chain_spec.rs +++ b/consensus/types/src/chain_spec.rs @@ -204,7 +204,6 @@ pub struct ChainSpec { pub target_aggregators_per_committee: u64, pub gossip_max_size: u64, pub max_request_blocks: u64, - pub epochs_per_subnet_subscription: u64, pub min_epochs_for_block_requests: u64, pub max_chunk_size: u64, pub ttfb_timeout: u64, @@ -215,9 +214,7 @@ pub struct ChainSpec { pub message_domain_valid_snappy: [u8; 4], pub subnets_per_node: u8, pub attestation_subnet_count: u64, - pub attestation_subnet_extra_bits: u8, pub attestation_subnet_prefix_bits: u8, - pub attestation_subnet_shuffling_prefix_bits: u8, /* * Networking Deneb @@ -816,7 +813,6 @@ impl ChainSpec { subnets_per_node: 2, maximum_gossip_clock_disparity_millis: default_maximum_gossip_clock_disparity_millis(), target_aggregators_per_committee: 16, - epochs_per_subnet_subscription: default_epochs_per_subnet_subscription(), gossip_max_size: default_gossip_max_size(), min_epochs_for_block_requests: default_min_epochs_for_block_requests(), max_chunk_size: default_max_chunk_size(), @@ -824,10 +820,7 @@ impl ChainSpec { resp_timeout: default_resp_timeout(), message_domain_invalid_snappy: default_message_domain_invalid_snappy(), message_domain_valid_snappy: default_message_domain_valid_snappy(), - attestation_subnet_extra_bits: default_attestation_subnet_extra_bits(), attestation_subnet_prefix_bits: default_attestation_subnet_prefix_bits(), - attestation_subnet_shuffling_prefix_bits: - default_attestation_subnet_shuffling_prefix_bits(), max_request_blocks: default_max_request_blocks(), /* @@ -1133,7 +1126,6 @@ impl ChainSpec { subnets_per_node: 4, // Make this larger than usual to avoid network damage maximum_gossip_clock_disparity_millis: default_maximum_gossip_clock_disparity_millis(), target_aggregators_per_committee: 16, - epochs_per_subnet_subscription: default_epochs_per_subnet_subscription(), gossip_max_size: default_gossip_max_size(), min_epochs_for_block_requests: 33024, max_chunk_size: default_max_chunk_size(), @@ -1141,11 +1133,8 @@ impl ChainSpec { resp_timeout: default_resp_timeout(), message_domain_invalid_snappy: default_message_domain_invalid_snappy(), message_domain_valid_snappy: default_message_domain_valid_snappy(), - attestation_subnet_extra_bits: default_attestation_subnet_extra_bits(), - attestation_subnet_prefix_bits: default_attestation_subnet_prefix_bits(), - attestation_subnet_shuffling_prefix_bits: - default_attestation_subnet_shuffling_prefix_bits(), max_request_blocks: default_max_request_blocks(), + attestation_subnet_prefix_bits: default_attestation_subnet_prefix_bits(), /* * Networking Deneb Specific @@ -1302,9 +1291,6 @@ pub struct Config { #[serde(default = "default_max_request_blocks")] #[serde(with = "serde_utils::quoted_u64")] max_request_blocks: u64, - #[serde(default = "default_epochs_per_subnet_subscription")] - #[serde(with = "serde_utils::quoted_u64")] - epochs_per_subnet_subscription: u64, #[serde(default = "default_min_epochs_for_block_requests")] #[serde(with = "serde_utils::quoted_u64")] min_epochs_for_block_requests: u64, @@ -1329,15 +1315,9 @@ pub struct Config { #[serde(default = "default_message_domain_valid_snappy")] #[serde(with = "serde_utils::bytes_4_hex")] message_domain_valid_snappy: [u8; 4], - #[serde(default = "default_attestation_subnet_extra_bits")] - #[serde(with = "serde_utils::quoted_u8")] - attestation_subnet_extra_bits: u8, #[serde(default = "default_attestation_subnet_prefix_bits")] #[serde(with = "serde_utils::quoted_u8")] attestation_subnet_prefix_bits: u8, - #[serde(default = "default_attestation_subnet_shuffling_prefix_bits")] - #[serde(with = "serde_utils::quoted_u8")] - attestation_subnet_shuffling_prefix_bits: u8, #[serde(default = "default_max_request_blocks_deneb")] #[serde(with = "serde_utils::quoted_u64")] max_request_blocks_deneb: u64, @@ -1419,6 +1399,10 @@ fn default_subnets_per_node() -> u8 { 2u8 } +fn default_attestation_subnet_prefix_bits() -> u8 { + 6 +} + const fn default_max_per_epoch_activation_churn_limit() -> u64 { 8 } @@ -1451,18 +1435,6 @@ const fn default_message_domain_valid_snappy() -> [u8; 4] { [1, 0, 0, 0] } -const fn default_attestation_subnet_extra_bits() -> u8 { - 0 -} - -const fn default_attestation_subnet_prefix_bits() -> u8 { - 6 -} - -const fn default_attestation_subnet_shuffling_prefix_bits() -> u8 { - 3 -} - const fn default_max_request_blocks() -> u64 { 1024 } @@ -1495,10 +1467,6 @@ const fn default_max_per_epoch_activation_exit_churn_limit() -> u64 { 256_000_000_000 } -const fn default_epochs_per_subnet_subscription() -> u64 { - 256 -} - const fn default_attestation_propagation_slot_range() -> u64 { 32 } @@ -1676,6 +1644,7 @@ impl Config { shard_committee_period: spec.shard_committee_period, eth1_follow_distance: spec.eth1_follow_distance, subnets_per_node: spec.subnets_per_node, + attestation_subnet_prefix_bits: spec.attestation_subnet_prefix_bits, inactivity_score_bias: spec.inactivity_score_bias, inactivity_score_recovery_rate: spec.inactivity_score_recovery_rate, @@ -1692,7 +1661,6 @@ impl Config { gossip_max_size: spec.gossip_max_size, max_request_blocks: spec.max_request_blocks, - epochs_per_subnet_subscription: spec.epochs_per_subnet_subscription, min_epochs_for_block_requests: spec.min_epochs_for_block_requests, max_chunk_size: spec.max_chunk_size, ttfb_timeout: spec.ttfb_timeout, @@ -1701,9 +1669,6 @@ impl Config { maximum_gossip_clock_disparity_millis: spec.maximum_gossip_clock_disparity_millis, message_domain_invalid_snappy: spec.message_domain_invalid_snappy, message_domain_valid_snappy: spec.message_domain_valid_snappy, - attestation_subnet_extra_bits: spec.attestation_subnet_extra_bits, - attestation_subnet_prefix_bits: spec.attestation_subnet_prefix_bits, - 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, @@ -1757,6 +1722,7 @@ impl Config { shard_committee_period, eth1_follow_distance, subnets_per_node, + attestation_subnet_prefix_bits, inactivity_score_bias, inactivity_score_recovery_rate, ejection_balance, @@ -1774,11 +1740,7 @@ impl Config { resp_timeout, message_domain_invalid_snappy, message_domain_valid_snappy, - attestation_subnet_extra_bits, - attestation_subnet_prefix_bits, - attestation_subnet_shuffling_prefix_bits, max_request_blocks, - epochs_per_subnet_subscription, attestation_propagation_slot_range, maximum_gossip_clock_disparity_millis, max_request_blocks_deneb, @@ -1842,11 +1804,8 @@ impl Config { resp_timeout, message_domain_invalid_snappy, message_domain_valid_snappy, - attestation_subnet_extra_bits, attestation_subnet_prefix_bits, - attestation_subnet_shuffling_prefix_bits, max_request_blocks, - epochs_per_subnet_subscription, attestation_propagation_slot_range, maximum_gossip_clock_disparity_millis, max_request_blocks_deneb, @@ -2142,9 +2101,7 @@ mod yaml_tests { check_default!(resp_timeout); check_default!(message_domain_invalid_snappy); check_default!(message_domain_valid_snappy); - check_default!(attestation_subnet_extra_bits); check_default!(attestation_subnet_prefix_bits); - check_default!(attestation_subnet_shuffling_prefix_bits); assert_eq!(chain_spec.bellatrix_fork_epoch, None); } diff --git a/consensus/types/src/subnet_id.rs b/consensus/types/src/subnet_id.rs index 9bfe6fb261..187b070d29 100644 --- a/consensus/types/src/subnet_id.rs +++ b/consensus/types/src/subnet_id.rs @@ -1,14 +1,17 @@ //! Identifies each shard by an integer identifier. -use crate::{AttestationRef, ChainSpec, CommitteeIndex, Epoch, EthSpec, Slot}; +use crate::{AttestationRef, ChainSpec, CommitteeIndex, EthSpec, Slot}; use alloy_primitives::{bytes::Buf, U256}; use safe_arith::{ArithError, SafeArith}; use serde::{Deserialize, Serialize}; use std::ops::{Deref, DerefMut}; use std::sync::LazyLock; -use swap_or_not_shuffle::compute_shuffled_index; const MAX_SUBNET_ID: usize = 64; +/// The number of bits in a Discovery `NodeId`. This is used for binary operations on the node-id +/// data. +const NODE_ID_BITS: u64 = 256; + static SUBNET_ID_TO_STRING: LazyLock> = LazyLock::new(|| { let mut v = Vec::with_capacity(MAX_SUBNET_ID); @@ -74,52 +77,22 @@ impl SubnetId { .into()) } - /// Computes the set of subnets the node should be subscribed to during the current epoch, - /// along with the first epoch in which these subscriptions are no longer valid. + /// Computes the set of subnets the node should be subscribed to. We subscribe to these subnets + /// for the duration of the node's runtime. #[allow(clippy::arithmetic_side_effects)] - pub fn compute_subnets_for_epoch( + pub fn compute_attestation_subnets( raw_node_id: [u8; 32], - epoch: Epoch, spec: &ChainSpec, - ) -> Result<(impl Iterator, Epoch), &'static str> { - // simplify variable naming - let subscription_duration = spec.epochs_per_subnet_subscription; + ) -> impl Iterator { + // The bits of the node-id we are using to define the subnets. let prefix_bits = spec.attestation_subnet_prefix_bits as u64; - let shuffling_prefix_bits = spec.attestation_subnet_shuffling_prefix_bits as u64; - let node_id = U256::from_be_slice(&raw_node_id); + let node_id = U256::from_be_slice(&raw_node_id); // calculate the prefixes used to compute the subnet and shuffling - let node_id_prefix = (node_id >> (256 - prefix_bits)).as_le_slice().get_u64_le(); - let shuffling_prefix = (node_id >> (256 - (prefix_bits + shuffling_prefix_bits))) + let node_id_prefix = (node_id >> (NODE_ID_BITS - prefix_bits)) .as_le_slice() .get_u64_le(); - // number of groups the shuffling creates - let shuffling_groups = 1 << shuffling_prefix_bits; - // shuffling group for this node - let shuffling_bits = shuffling_prefix % shuffling_groups; - let epoch_transition = (node_id_prefix - + (shuffling_bits * (subscription_duration >> shuffling_prefix_bits))) - % subscription_duration; - - // Calculate at which epoch this node needs to re-evaluate - let valid_until_epoch = epoch.as_u64() - + subscription_duration - .saturating_sub((epoch.as_u64() + epoch_transition) % subscription_duration); - - let subscription_event_idx = (epoch.as_u64() + epoch_transition) / subscription_duration; - let permutation_seed = - ethereum_hashing::hash(&int_to_bytes::int_to_bytes8(subscription_event_idx)); - - let num_subnets = 1 << spec.attestation_subnet_prefix_bits; - let permutated_prefix = compute_shuffled_index( - node_id_prefix as usize, - num_subnets, - &permutation_seed, - spec.shuffle_round_count, - ) - .ok_or("Unable to shuffle")? as u64; - // Get the constants we need to avoid holding a reference to the spec let &ChainSpec { subnets_per_node, @@ -127,10 +100,8 @@ impl SubnetId { .. } = spec; - let subnet_set_generator = (0..subnets_per_node).map(move |idx| { - SubnetId::new((permutated_prefix + idx as u64) % attestation_subnet_count) - }); - Ok((subnet_set_generator, valid_until_epoch.into())) + (0..subnets_per_node) + .map(move |idx| SubnetId::new((node_id_prefix + idx as u64) % attestation_subnet_count)) } } @@ -180,7 +151,7 @@ mod tests { /// A set of tests compared to the python specification #[test] - fn compute_subnets_for_epoch_unit_test() { + fn compute_attestation_subnets_test() { // Randomized variables used generated with the python specification let node_ids = [ "0", @@ -189,59 +160,34 @@ mod tests { "27726842142488109545414954493849224833670205008410190955613662332153332462900", "39755236029158558527862903296867805548949739810920318269566095185775868999998", "31899136003441886988955119620035330314647133604576220223892254902004850516297", - "58579998103852084482416614330746509727562027284701078483890722833654510444626", - "28248042035542126088870192155378394518950310811868093527036637864276176517397", - "60930578857433095740782970114409273483106482059893286066493409689627770333527", - "103822458477361691467064888613019442068586830412598673713899771287914656699997", ] .map(|v| Uint256::from_str_radix(v, 10).unwrap().to_be_bytes::<32>()); - let epochs = [ - 54321u64, 1017090249, 1827566880, 846255942, 766597383, 1204990115, 1616209495, - 1774367616, 1484598751, 3525502229, - ] - .map(Epoch::from); + let expected_subnets = [ + vec![0, 1], + vec![49u64, 50u64], + vec![10, 11], + vec![15, 16], + vec![21, 22], + vec![17, 18], + ]; // Test mainnet let spec = ChainSpec::mainnet(); - // Calculated by hand - let expected_valid_time = [ - 54528u64, 1017090255, 1827567030, 846256049, 766597387, 1204990287, 1616209536, - 1774367857, 1484598847, 3525502311, - ]; - - // Calculated from pyspec - let expected_subnets = [ - vec![4u64, 5u64], - vec![31, 32], - vec![39, 40], - vec![38, 39], - vec![53, 54], - vec![57, 58], - vec![48, 49], - vec![1, 2], - vec![34, 35], - vec![37, 38], - ]; - for x in 0..node_ids.len() { println!("Test: {}", x); println!( - "NodeId: {:?}\n Epoch: {}\n, expected_update_time: {}\n, expected_subnets: {:?}", - node_ids[x], epochs[x], expected_valid_time[x], expected_subnets[x] + "NodeId: {:?}\nExpected_subnets: {:?}", + node_ids[x], expected_subnets[x] ); - let (computed_subnets, valid_time) = SubnetId::compute_subnets_for_epoch::< - crate::MainnetEthSpec, - >(node_ids[x], epochs[x], &spec) - .unwrap(); + let computed_subnets = SubnetId::compute_attestation_subnets(node_ids[x], &spec); assert_eq!( expected_subnets[x], computed_subnets.map(SubnetId::into).collect::>() ); - assert_eq!(Epoch::from(expected_valid_time[x]), valid_time); } } } From 79de61b62405d506c21cb1c9f88d96c187d64dba Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Tue, 26 Nov 2024 16:08:24 +1100 Subject: [PATCH 018/254] Update DB migrations in book (#6611) * Update DB migrations in book * Merge branch 'unstable' into db-migrations-v22 --- book/src/database-migrations.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/book/src/database-migrations.md b/book/src/database-migrations.md index 6d75b90100..a9bfb00ccd 100644 --- a/book/src/database-migrations.md +++ b/book/src/database-migrations.md @@ -16,6 +16,7 @@ validator client or the slasher**. | Lighthouse version | Release date | Schema version | Downgrade available? | |--------------------|--------------|----------------|----------------------| +| v6.0.0 | Nov 2024 | v22 | no | | v5.3.0 | Aug 2024 | v21 | yes | | v5.2.0 | Jun 2024 | v19 | no | | v5.1.0 | Mar 2024 | v19 | no | @@ -208,6 +209,7 @@ Here are the steps to prune historic states: | Lighthouse version | Release date | Schema version | Downgrade available? | |--------------------|--------------|----------------|-------------------------------------| +| v6.0.0 | Nov 2024 | v22 | no | | v5.3.0 | Aug 2024 | v21 | yes | | v5.2.0 | Jun 2024 | v19 | yes before Deneb using <= v5.2.1 | | v5.1.0 | Mar 2024 | v19 | yes before Deneb using <= v5.2.1 | From 720f59602100664455ac88556606899d7a4836c3 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Tue, 26 Nov 2024 18:17:18 +1100 Subject: [PATCH 019/254] Pin rust_eth_kzg to 0.5.1 (#6608) * Pin rust_eth_kzg to 0.5.1 * Pin crate_crypto transitive deps --- Cargo.toml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index e11f7505ee..fbeb616a14 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -126,7 +126,14 @@ delay_map = "0.4" derivative = "2" dirs = "3" either = "1.9" -rust_eth_kzg = "0.5.1" + # TODO: rust_eth_kzg is pinned for now while a perf regression is investigated + # The crate_crypto_* dependencies can be removed from this file completely once we update +rust_eth_kzg = "=0.5.1" +crate_crypto_internal_eth_kzg_bls12_381 = "=0.5.1" +crate_crypto_internal_eth_kzg_erasure_codes = "=0.5.1" +crate_crypto_internal_eth_kzg_maybe_rayon = "=0.5.1" +crate_crypto_internal_eth_kzg_polynomial = "=0.5.1" +crate_crypto_kzg_multi_open_fk20 = "=0.5.1" discv5 = { version = "0.9", features = ["libp2p"] } env_logger = "0.9" error-chain = "0.12" From 38f5f665e17deec06ba03bd4dc7244b038e72f9b Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Thu, 28 Nov 2024 09:39:50 +0700 Subject: [PATCH 020/254] Remove `error-chain` dependency (#6628) * remove error-chain dependency * rerun CI * rerun CI --- Cargo.lock | 13 ------------- Cargo.toml | 1 - beacon_node/client/Cargo.toml | 1 - beacon_node/client/src/error.rs | 7 ------- beacon_node/client/src/lib.rs | 1 - beacon_node/lighthouse_network/Cargo.toml | 1 - .../lighthouse_network/src/discovery/mod.rs | 4 ++-- beacon_node/lighthouse_network/src/lib.rs | 2 +- .../lighthouse_network/src/peer_manager/mod.rs | 4 ++-- .../src/service/gossipsub_scoring_parameters.rs | 8 ++++---- .../lighthouse_network/src/service/mod.rs | 8 ++++---- .../lighthouse_network/src/service/utils.rs | 12 +++++------- .../lighthouse_network/src/types/error.rs | 5 ----- beacon_node/lighthouse_network/src/types/mod.rs | 1 - beacon_node/network/Cargo.toml | 1 - beacon_node/network/src/error.rs | 8 -------- beacon_node/network/src/lib.rs | 1 - beacon_node/network/src/router.rs | 3 +-- beacon_node/network/src/service.rs | 17 ++++++++++------- 19 files changed, 29 insertions(+), 69 deletions(-) delete mode 100644 beacon_node/client/src/error.rs delete mode 100644 beacon_node/lighthouse_network/src/types/error.rs delete mode 100644 beacon_node/network/src/error.rs diff --git a/Cargo.lock b/Cargo.lock index d7ce7b9f6c..8f8ff45b4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1418,7 +1418,6 @@ dependencies = [ "directory", "dirs", "environment", - "error-chain", "eth1", "eth2", "eth2_config", @@ -2515,16 +2514,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "error-chain" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" -dependencies = [ - "backtrace", - "version_check", -] - [[package]] name = "eth1" version = "0.2.0" @@ -5309,7 +5298,6 @@ dependencies = [ "dirs", "discv5", "either", - "error-chain", "ethereum_ssz", "ethereum_ssz_derive", "fnv", @@ -5880,7 +5868,6 @@ dependencies = [ "bls", "delay_map", "derivative", - "error-chain", "eth2", "eth2_network_config", "ethereum_ssz", diff --git a/Cargo.toml b/Cargo.toml index fbeb616a14..0be462754e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -136,7 +136,6 @@ crate_crypto_internal_eth_kzg_polynomial = "=0.5.1" crate_crypto_kzg_multi_open_fk20 = "=0.5.1" discv5 = { version = "0.9", features = ["libp2p"] } env_logger = "0.9" -error-chain = "0.12" ethereum_hashing = "0.7.0" ethereum_serde_utils = "0.7" ethereum_ssz = "0.7" diff --git a/beacon_node/client/Cargo.toml b/beacon_node/client/Cargo.toml index 21a6e42cc5..4df13eb3d4 100644 --- a/beacon_node/client/Cargo.toml +++ b/beacon_node/client/Cargo.toml @@ -21,7 +21,6 @@ eth2_config = { workspace = true } slot_clock = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } -error-chain = { workspace = true } slog = { workspace = true } tokio = { workspace = true } futures = { workspace = true } diff --git a/beacon_node/client/src/error.rs b/beacon_node/client/src/error.rs deleted file mode 100644 index 20cf6f9877..0000000000 --- a/beacon_node/client/src/error.rs +++ /dev/null @@ -1,7 +0,0 @@ -use error_chain::error_chain; - -error_chain! { - links { - Network(network::error::Error, network::error::ErrorKind); - } -} diff --git a/beacon_node/client/src/lib.rs b/beacon_node/client/src/lib.rs index e6042103e1..0b6550c208 100644 --- a/beacon_node/client/src/lib.rs +++ b/beacon_node/client/src/lib.rs @@ -4,7 +4,6 @@ mod metrics; mod notifier; pub mod builder; -pub mod error; use beacon_chain::BeaconChain; use lighthouse_network::{Enr, Multiaddr, NetworkGlobals}; diff --git a/beacon_node/lighthouse_network/Cargo.toml b/beacon_node/lighthouse_network/Cargo.toml index c4fad99702..eccc244d59 100644 --- a/beacon_node/lighthouse_network/Cargo.toml +++ b/beacon_node/lighthouse_network/Cargo.toml @@ -18,7 +18,6 @@ slog = { workspace = true } lighthouse_version = { workspace = true } tokio = { workspace = true } futures = { workspace = true } -error-chain = { workspace = true } dirs = { workspace = true } fnv = { workspace = true } metrics = { workspace = true } diff --git a/beacon_node/lighthouse_network/src/discovery/mod.rs b/beacon_node/lighthouse_network/src/discovery/mod.rs index b91ad40916..578bb52b51 100644 --- a/beacon_node/lighthouse_network/src/discovery/mod.rs +++ b/beacon_node/lighthouse_network/src/discovery/mod.rs @@ -8,8 +8,8 @@ pub mod enr_ext; // Allow external use of the lighthouse ENR builder use crate::service::TARGET_SUBNET_PEERS; -use crate::{error, Enr, NetworkConfig, NetworkGlobals, Subnet, SubnetDiscovery}; use crate::{metrics, ClearDialError}; +use crate::{Enr, NetworkConfig, NetworkGlobals, Subnet, SubnetDiscovery}; use discv5::{enr::NodeId, Discv5}; pub use enr::{build_enr, load_enr_from_disk, use_or_load_enr, CombinedKey, Eth2Enr}; pub use enr_ext::{peer_id_to_node_id, CombinedKeyExt, EnrExt}; @@ -205,7 +205,7 @@ impl Discovery { network_globals: Arc>, log: &slog::Logger, spec: &ChainSpec, - ) -> error::Result { + ) -> Result { let log = log.clone(); let enr_dir = match config.network_dir.to_str() { diff --git a/beacon_node/lighthouse_network/src/lib.rs b/beacon_node/lighthouse_network/src/lib.rs index ced803add8..f186547d31 100644 --- a/beacon_node/lighthouse_network/src/lib.rs +++ b/beacon_node/lighthouse_network/src/lib.rs @@ -101,7 +101,7 @@ impl<'a> std::fmt::Display for ClearDialError<'a> { } pub use crate::types::{ - error, Enr, EnrSyncCommitteeBitfield, GossipTopic, NetworkGlobals, PubsubMessage, Subnet, + Enr, EnrSyncCommitteeBitfield, GossipTopic, NetworkGlobals, PubsubMessage, Subnet, SubnetDiscovery, }; diff --git a/beacon_node/lighthouse_network/src/peer_manager/mod.rs b/beacon_node/lighthouse_network/src/peer_manager/mod.rs index c1e72d250f..4df2566dac 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/mod.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/mod.rs @@ -4,7 +4,7 @@ use crate::discovery::enr_ext::EnrExt; use crate::discovery::peer_id_to_node_id; use crate::rpc::{GoodbyeReason, MetaData, Protocol, RPCError, RpcErrorResponse}; use crate::service::TARGET_SUBNET_PEERS; -use crate::{error, metrics, Gossipsub, NetworkGlobals, PeerId, Subnet, SubnetDiscovery}; +use crate::{metrics, Gossipsub, NetworkGlobals, PeerId, Subnet, SubnetDiscovery}; use delay_map::HashSetDelay; use discv5::Enr; use libp2p::identify::Info as IdentifyInfo; @@ -144,7 +144,7 @@ impl PeerManager { cfg: config::Config, network_globals: Arc>, log: &slog::Logger, - ) -> error::Result { + ) -> Result { let config::Config { discovery_enabled, metrics_enabled, diff --git a/beacon_node/lighthouse_network/src/service/gossipsub_scoring_parameters.rs b/beacon_node/lighthouse_network/src/service/gossipsub_scoring_parameters.rs index c6a764bb0e..6fffd649f5 100644 --- a/beacon_node/lighthouse_network/src/service/gossipsub_scoring_parameters.rs +++ b/beacon_node/lighthouse_network/src/service/gossipsub_scoring_parameters.rs @@ -1,5 +1,5 @@ use crate::types::{GossipEncoding, GossipKind, GossipTopic}; -use crate::{error, TopicHash}; +use crate::TopicHash; use gossipsub::{IdentTopic as Topic, PeerScoreParams, PeerScoreThresholds, TopicScoreParams}; use std::cmp::max; use std::collections::HashMap; @@ -84,7 +84,7 @@ impl PeerScoreSettings { thresholds: &PeerScoreThresholds, enr_fork_id: &EnrForkId, current_slot: Slot, - ) -> error::Result { + ) -> Result { let mut params = PeerScoreParams { decay_interval: self.decay_interval, decay_to_zero: self.decay_to_zero, @@ -175,7 +175,7 @@ impl PeerScoreSettings { &self, active_validators: usize, current_slot: Slot, - ) -> error::Result<(TopicScoreParams, TopicScoreParams, TopicScoreParams)> { + ) -> Result<(TopicScoreParams, TopicScoreParams, TopicScoreParams), String> { let (aggregators_per_slot, committees_per_slot) = self.expected_aggregator_count_per_slot(active_validators)?; let multiple_bursts_per_subnet_per_epoch = @@ -256,7 +256,7 @@ impl PeerScoreSettings { fn expected_aggregator_count_per_slot( &self, active_validators: usize, - ) -> error::Result<(f64, usize)> { + ) -> Result<(f64, usize), String> { let committees_per_slot = E::get_committee_count_per_slot_with( active_validators, self.max_committees_per_slot, diff --git a/beacon_node/lighthouse_network/src/service/mod.rs b/beacon_node/lighthouse_network/src/service/mod.rs index b23e417adb..ff7707e98d 100644 --- a/beacon_node/lighthouse_network/src/service/mod.rs +++ b/beacon_node/lighthouse_network/src/service/mod.rs @@ -20,7 +20,7 @@ use crate::types::{ }; use crate::EnrExt; use crate::Eth2Enr; -use crate::{error, metrics, Enr, NetworkGlobals, PubsubMessage, TopicHash}; +use crate::{metrics, Enr, NetworkGlobals, PubsubMessage, TopicHash}; use api_types::{AppRequestId, PeerRequestId, RequestId, Response}; use futures::stream::StreamExt; use gossipsub::{ @@ -170,7 +170,7 @@ impl Network { executor: task_executor::TaskExecutor, mut ctx: ServiceContext<'_>, log: &slog::Logger, - ) -> error::Result<(Self, Arc>)> { + ) -> Result<(Self, Arc>), String> { let log = log.new(o!("service"=> "libp2p")); let config = ctx.config.clone(); @@ -515,7 +515,7 @@ impl Network { /// - Starts listening in the given ports. /// - Dials boot-nodes and libp2p peers. /// - Subscribes to starting gossipsub topics. - async fn start(&mut self, config: &crate::NetworkConfig) -> error::Result<()> { + async fn start(&mut self, config: &crate::NetworkConfig) -> Result<(), String> { let enr = self.network_globals.local_enr(); info!(self.log, "Libp2p Starting"; "peer_id" => %enr.peer_id(), "bandwidth_config" => format!("{}-{}", config.network_load, NetworkLoad::from(config.network_load).name)); debug!(self.log, "Attempting to open listening ports"; config.listen_addrs(), "discovery_enabled" => !config.disable_discovery, "quic_enabled" => !config.disable_quic_support); @@ -920,7 +920,7 @@ impl Network { &mut self, active_validators: usize, current_slot: Slot, - ) -> error::Result<()> { + ) -> Result<(), String> { let (beacon_block_params, beacon_aggregate_proof_params, beacon_attestation_subnet_params) = self.score_settings .get_dynamic_topic_params(active_validators, current_slot)?; diff --git a/beacon_node/lighthouse_network/src/service/utils.rs b/beacon_node/lighthouse_network/src/service/utils.rs index f4988e68cd..490928c08c 100644 --- a/beacon_node/lighthouse_network/src/service/utils.rs +++ b/beacon_node/lighthouse_network/src/service/utils.rs @@ -1,9 +1,7 @@ use crate::multiaddr::Protocol; use crate::rpc::methods::MetaDataV3; use crate::rpc::{MetaData, MetaDataV1, MetaDataV2}; -use crate::types::{ - error, EnrAttestationBitfield, EnrSyncCommitteeBitfield, GossipEncoding, GossipKind, -}; +use crate::types::{EnrAttestationBitfield, EnrSyncCommitteeBitfield, GossipEncoding, GossipKind}; use crate::{GossipTopic, NetworkConfig}; use futures::future::Either; use gossipsub; @@ -83,7 +81,7 @@ pub fn build_transport( // Useful helper functions for debugging. Currently not used in the client. #[allow(dead_code)] -fn keypair_from_hex(hex_bytes: &str) -> error::Result { +fn keypair_from_hex(hex_bytes: &str) -> Result { let hex_bytes = if let Some(stripped) = hex_bytes.strip_prefix("0x") { stripped.to_string() } else { @@ -91,18 +89,18 @@ fn keypair_from_hex(hex_bytes: &str) -> error::Result { }; hex::decode(hex_bytes) - .map_err(|e| format!("Failed to parse p2p secret key bytes: {:?}", e).into()) + .map_err(|e| format!("Failed to parse p2p secret key bytes: {:?}", e)) .and_then(keypair_from_bytes) } #[allow(dead_code)] -fn keypair_from_bytes(mut bytes: Vec) -> error::Result { +fn keypair_from_bytes(mut bytes: Vec) -> Result { secp256k1::SecretKey::try_from_bytes(&mut bytes) .map(|secret| { let keypair: secp256k1::Keypair = secret.into(); keypair.into() }) - .map_err(|e| format!("Unable to parse p2p secret key: {:?}", e).into()) + .map_err(|e| format!("Unable to parse p2p secret key: {:?}", e)) } /// Loads a private key from disk. If this fails, a new key is diff --git a/beacon_node/lighthouse_network/src/types/error.rs b/beacon_node/lighthouse_network/src/types/error.rs deleted file mode 100644 index a291e8fec5..0000000000 --- a/beacon_node/lighthouse_network/src/types/error.rs +++ /dev/null @@ -1,5 +0,0 @@ -// generates error types - -use error_chain::error_chain; - -error_chain! {} diff --git a/beacon_node/lighthouse_network/src/types/mod.rs b/beacon_node/lighthouse_network/src/types/mod.rs index 82558f6c97..6f266fd2ba 100644 --- a/beacon_node/lighthouse_network/src/types/mod.rs +++ b/beacon_node/lighthouse_network/src/types/mod.rs @@ -1,4 +1,3 @@ -pub mod error; mod globals; mod pubsub; mod subnet; diff --git a/beacon_node/network/Cargo.toml b/beacon_node/network/Cargo.toml index 500cd23fae..6fc818e9c9 100644 --- a/beacon_node/network/Cargo.toml +++ b/beacon_node/network/Cargo.toml @@ -31,7 +31,6 @@ hex = { workspace = true } ethereum_ssz = { workspace = true } ssz_types = { workspace = true } futures = { workspace = true } -error-chain = { workspace = true } tokio = { workspace = true } tokio-stream = { workspace = true } smallvec = { workspace = true } diff --git a/beacon_node/network/src/error.rs b/beacon_node/network/src/error.rs deleted file mode 100644 index 1a964235e9..0000000000 --- a/beacon_node/network/src/error.rs +++ /dev/null @@ -1,8 +0,0 @@ -// generates error types -use error_chain::error_chain; - -error_chain! { - links { - Libp2p(lighthouse_network::error::Error, lighthouse_network::error::ErrorKind); - } -} diff --git a/beacon_node/network/src/lib.rs b/beacon_node/network/src/lib.rs index 13a2569b75..2a7fedb53e 100644 --- a/beacon_node/network/src/lib.rs +++ b/beacon_node/network/src/lib.rs @@ -1,5 +1,4 @@ /// This crate provides the network server for Lighthouse. -pub mod error; pub mod service; mod metrics; diff --git a/beacon_node/network/src/router.rs b/beacon_node/network/src/router.rs index e1badfda9d..0a99b6af0c 100644 --- a/beacon_node/network/src/router.rs +++ b/beacon_node/network/src/router.rs @@ -5,7 +5,6 @@ //! syncing-related responses to the Sync manager. #![allow(clippy::unit_arg)] -use crate::error; use crate::network_beacon_processor::{InvalidBlockStorage, NetworkBeaconProcessor}; use crate::service::NetworkMessage; use crate::status::status_message; @@ -92,7 +91,7 @@ impl Router { beacon_processor_send: BeaconProcessorSend, beacon_processor_reprocess_tx: mpsc::Sender, log: slog::Logger, - ) -> error::Result>> { + ) -> Result>, String> { let message_handler_log = log.new(o!("service"=> "router")); trace!(message_handler_log, "Service starting"); diff --git a/beacon_node/network/src/service.rs b/beacon_node/network/src/service.rs index 37dc4a8384..7826807e03 100644 --- a/beacon_node/network/src/service.rs +++ b/beacon_node/network/src/service.rs @@ -1,10 +1,10 @@ +use crate::metrics; use crate::nat; use crate::network_beacon_processor::InvalidBlockStorage; use crate::persisted_dht::{clear_dht, load_dht, persist_dht}; use crate::router::{Router, RouterMessage}; use crate::subnet_service::{SubnetService, SubnetServiceMessage, Subscription}; use crate::NetworkConfig; -use crate::{error, metrics}; use beacon_chain::{BeaconChain, BeaconChainTypes}; use beacon_processor::{work_reprocessing_queue::ReprocessQueueMessage, BeaconProcessorSend}; use futures::channel::mpsc::Sender; @@ -208,11 +208,14 @@ impl NetworkService { libp2p_registry: Option<&'_ mut Registry>, beacon_processor_send: BeaconProcessorSend, beacon_processor_reprocess_tx: mpsc::Sender, - ) -> error::Result<( - NetworkService, - Arc>, - NetworkSenders, - )> { + ) -> Result< + ( + NetworkService, + Arc>, + NetworkSenders, + ), + String, + > { let network_log = executor.log().clone(); // build the channels for external comms let (network_senders, network_receivers) = NetworkSenders::new(); @@ -367,7 +370,7 @@ impl NetworkService { libp2p_registry: Option<&'_ mut Registry>, beacon_processor_send: BeaconProcessorSend, beacon_processor_reprocess_tx: mpsc::Sender, - ) -> error::Result<(Arc>, NetworkSenders)> { + ) -> Result<(Arc>, NetworkSenders), String> { let (network_service, network_globals, network_senders) = Self::build( beacon_chain, config, From fa6c4c02a38b998e2bae35bd768a1af241faff4a Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Fri, 29 Nov 2024 13:23:54 +1100 Subject: [PATCH 021/254] Fix Rust 1.83 Clippy lints (#6629) * Fix Rust 1.83 Clippy lints * Cargo fmt --- .../src/attestation_verification.rs | 10 +++--- beacon_node/beacon_chain/src/beacon_chain.rs | 2 ++ .../beacon_chain/src/block_verification.rs | 1 + beacon_node/beacon_chain/src/eth1_chain.rs | 3 +- .../beacon_chain/src/observed_aggregates.rs | 4 +-- .../gossipsub/src/backoff.rs | 3 +- beacon_node/lighthouse_network/src/lib.rs | 4 +-- .../operation_pool/src/attestation_storage.rs | 2 +- beacon_node/store/src/chunked_iter.rs | 2 +- beacon_node/store/src/forwards_iter.rs | 8 ++--- beacon_node/store/src/iter.rs | 32 +++++++++---------- common/eth2/src/lighthouse.rs | 6 ++-- common/eth2_config/src/lib.rs | 2 +- common/logging/src/lib.rs | 4 +-- common/validator_dir/src/insecure_keys.rs | 2 +- .../state_processing/src/block_replayer.rs | 2 +- consensus/types/src/aggregate_and_proof.rs | 2 +- consensus/types/src/attestation.rs | 4 +-- consensus/types/src/beacon_block.rs | 5 +-- consensus/types/src/beacon_block_body.rs | 2 +- consensus/types/src/beacon_committee.rs | 2 +- consensus/types/src/beacon_state/iter.rs | 2 +- .../types/src/execution_payload_header.rs | 2 +- consensus/types/src/indexed_attestation.rs | 2 +- consensus/types/src/light_client_header.rs | 4 +-- consensus/types/src/light_client_update.rs | 2 +- consensus/types/src/payload.rs | 2 +- consensus/types/src/slot_epoch.rs | 2 +- crypto/bls/src/macros.rs | 2 +- crypto/kzg/src/trusted_setup.rs | 4 +-- lighthouse/src/main.rs | 2 +- slasher/src/database/interface.rs | 2 +- testing/ef_tests/src/cases/fork_choice.rs | 2 +- validator_client/signing_method/src/lib.rs | 2 +- watch/src/database/mod.rs | 18 +++++------ 35 files changed, 73 insertions(+), 77 deletions(-) diff --git a/beacon_node/beacon_chain/src/attestation_verification.rs b/beacon_node/beacon_chain/src/attestation_verification.rs index 9ee0b01df3..c3dea3dbb4 100644 --- a/beacon_node/beacon_chain/src/attestation_verification.rs +++ b/beacon_node/beacon_chain/src/attestation_verification.rs @@ -306,7 +306,7 @@ pub struct VerifiedAggregatedAttestation<'a, T: BeaconChainTypes> { indexed_attestation: IndexedAttestation, } -impl<'a, T: BeaconChainTypes> VerifiedAggregatedAttestation<'a, T> { +impl VerifiedAggregatedAttestation<'_, T> { pub fn into_indexed_attestation(self) -> IndexedAttestation { self.indexed_attestation } @@ -319,7 +319,7 @@ pub struct VerifiedUnaggregatedAttestation<'a, T: BeaconChainTypes> { subnet_id: SubnetId, } -impl<'a, T: BeaconChainTypes> VerifiedUnaggregatedAttestation<'a, T> { +impl VerifiedUnaggregatedAttestation<'_, T> { pub fn into_indexed_attestation(self) -> IndexedAttestation { self.indexed_attestation } @@ -327,7 +327,7 @@ impl<'a, T: BeaconChainTypes> VerifiedUnaggregatedAttestation<'a, T> { /// Custom `Clone` implementation is to avoid the restrictive trait bounds applied by the usual derive /// macro. -impl<'a, T: BeaconChainTypes> Clone for IndexedUnaggregatedAttestation<'a, T> { +impl Clone for IndexedUnaggregatedAttestation<'_, T> { fn clone(&self) -> Self { Self { attestation: self.attestation, @@ -353,7 +353,7 @@ pub trait VerifiedAttestation: Sized { } } -impl<'a, T: BeaconChainTypes> VerifiedAttestation for VerifiedAggregatedAttestation<'a, T> { +impl VerifiedAttestation for VerifiedAggregatedAttestation<'_, T> { fn attestation(&self) -> AttestationRef { self.attestation() } @@ -363,7 +363,7 @@ impl<'a, T: BeaconChainTypes> VerifiedAttestation for VerifiedAggregatedAttes } } -impl<'a, T: BeaconChainTypes> VerifiedAttestation for VerifiedUnaggregatedAttestation<'a, T> { +impl VerifiedAttestation for VerifiedUnaggregatedAttestation<'_, T> { fn attestation(&self) -> AttestationRef { self.attestation } diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index a78ae266e5..80766d57b3 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -1112,6 +1112,7 @@ impl BeaconChain { /// ## Errors /// /// May return a database error. + #[allow(clippy::type_complexity)] pub fn get_blocks_checking_caches( self: &Arc, block_roots: Vec, @@ -1127,6 +1128,7 @@ impl BeaconChain { Ok(BeaconBlockStreamer::::new(self, CheckCaches::Yes)?.launch_stream(block_roots)) } + #[allow(clippy::type_complexity)] pub fn get_blocks( self: &Arc, block_roots: Vec, diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 3ae19430aa..4c5f53248f 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -2072,6 +2072,7 @@ pub fn get_validator_pubkey_cache( /// /// The signature verifier is empty because it does not yet have any of this block's signatures /// added to it. Use `Self::apply_to_signature_verifier` to apply the signatures. +#[allow(clippy::type_complexity)] fn get_signature_verifier<'a, T: BeaconChainTypes>( state: &'a BeaconState, validator_pubkey_cache: &'a ValidatorPubkeyCache, diff --git a/beacon_node/beacon_chain/src/eth1_chain.rs b/beacon_node/beacon_chain/src/eth1_chain.rs index 276262085e..cb6e4c34f3 100644 --- a/beacon_node/beacon_chain/src/eth1_chain.rs +++ b/beacon_node/beacon_chain/src/eth1_chain.rs @@ -107,8 +107,7 @@ fn get_sync_status( // Determine how many voting periods are contained in distance between // now and genesis, rounding up. - let voting_periods_past = - (seconds_till_genesis + voting_period_duration - 1) / voting_period_duration; + let voting_periods_past = seconds_till_genesis.div_ceil(voting_period_duration); // Return the start time of the current voting period*. // diff --git a/beacon_node/beacon_chain/src/observed_aggregates.rs b/beacon_node/beacon_chain/src/observed_aggregates.rs index 038edfe27f..dec012fb92 100644 --- a/beacon_node/beacon_chain/src/observed_aggregates.rs +++ b/beacon_node/beacon_chain/src/observed_aggregates.rs @@ -113,7 +113,7 @@ pub trait SubsetItem { fn root(&self) -> Result; } -impl<'a, E: EthSpec> SubsetItem for AttestationRef<'a, E> { +impl SubsetItem for AttestationRef<'_, E> { type Item = BitList; fn is_subset(&self, other: &Self::Item) -> bool { match self { @@ -159,7 +159,7 @@ impl<'a, E: EthSpec> SubsetItem for AttestationRef<'a, E> { } } -impl<'a, E: EthSpec> SubsetItem for &'a SyncCommitteeContribution { +impl SubsetItem for &SyncCommitteeContribution { type Item = BitVector; fn is_subset(&self, other: &Self::Item) -> bool { self.aggregation_bits.is_subset(other) diff --git a/beacon_node/lighthouse_network/gossipsub/src/backoff.rs b/beacon_node/lighthouse_network/gossipsub/src/backoff.rs index f83a24baaf..537d2319c2 100644 --- a/beacon_node/lighthouse_network/gossipsub/src/backoff.rs +++ b/beacon_node/lighthouse_network/gossipsub/src/backoff.rs @@ -48,8 +48,7 @@ pub(crate) struct BackoffStorage { impl BackoffStorage { fn heartbeats(d: &Duration, heartbeat_interval: &Duration) -> usize { - ((d.as_nanos() + heartbeat_interval.as_nanos() - 1) / heartbeat_interval.as_nanos()) - as usize + d.as_nanos().div_ceil(heartbeat_interval.as_nanos()) as usize } pub(crate) fn new( diff --git a/beacon_node/lighthouse_network/src/lib.rs b/beacon_node/lighthouse_network/src/lib.rs index f186547d31..2f8fd82c51 100644 --- a/beacon_node/lighthouse_network/src/lib.rs +++ b/beacon_node/lighthouse_network/src/lib.rs @@ -63,7 +63,7 @@ impl<'de> Deserialize<'de> for PeerIdSerialized { // A wrapper struct that prints a dial error nicely. struct ClearDialError<'a>(&'a DialError); -impl<'a> ClearDialError<'a> { +impl ClearDialError<'_> { fn most_inner_error(err: &(dyn std::error::Error)) -> &(dyn std::error::Error) { let mut current = err; while let Some(source) = current.source() { @@ -73,7 +73,7 @@ impl<'a> ClearDialError<'a> { } } -impl<'a> std::fmt::Display for ClearDialError<'a> { +impl std::fmt::Display for ClearDialError<'_> { fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { match &self.0 { DialError::Transport(errors) => { diff --git a/beacon_node/operation_pool/src/attestation_storage.rs b/beacon_node/operation_pool/src/attestation_storage.rs index 4de9d351f3..083c1170f0 100644 --- a/beacon_node/operation_pool/src/attestation_storage.rs +++ b/beacon_node/operation_pool/src/attestation_storage.rs @@ -105,7 +105,7 @@ impl SplitAttestation { } } -impl<'a, E: EthSpec> CompactAttestationRef<'a, E> { +impl CompactAttestationRef<'_, E> { pub fn attestation_data(&self) -> AttestationData { AttestationData { slot: self.data.slot, diff --git a/beacon_node/store/src/chunked_iter.rs b/beacon_node/store/src/chunked_iter.rs index b3322b5225..8f6682e758 100644 --- a/beacon_node/store/src/chunked_iter.rs +++ b/beacon_node/store/src/chunked_iter.rs @@ -56,7 +56,7 @@ where } } -impl<'a, F, E, Hot, Cold> Iterator for ChunkedVectorIter<'a, F, E, Hot, Cold> +impl Iterator for ChunkedVectorIter<'_, F, E, Hot, Cold> where F: Field, E: EthSpec, diff --git a/beacon_node/store/src/forwards_iter.rs b/beacon_node/store/src/forwards_iter.rs index e0f44f3aff..27769a310a 100644 --- a/beacon_node/store/src/forwards_iter.rs +++ b/beacon_node/store/src/forwards_iter.rs @@ -149,8 +149,8 @@ impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> } } -impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> Iterator - for FrozenForwardsIterator<'a, E, Hot, Cold> +impl, Cold: ItemStore> Iterator + for FrozenForwardsIterator<'_, E, Hot, Cold> { type Item = Result<(Hash256, Slot)>; @@ -349,8 +349,8 @@ impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> } } -impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> Iterator - for HybridForwardsIterator<'a, E, Hot, Cold> +impl, Cold: ItemStore> Iterator + for HybridForwardsIterator<'_, E, Hot, Cold> { type Item = Result<(Hash256, Slot)>; diff --git a/beacon_node/store/src/iter.rs b/beacon_node/store/src/iter.rs index 71dc96d99e..97a88c01c8 100644 --- a/beacon_node/store/src/iter.rs +++ b/beacon_node/store/src/iter.rs @@ -53,8 +53,8 @@ pub struct StateRootsIterator<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore inner: RootsIterator<'a, E, Hot, Cold>, } -impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> Clone - for StateRootsIterator<'a, E, Hot, Cold> +impl, Cold: ItemStore> Clone + for StateRootsIterator<'_, E, Hot, Cold> { fn clone(&self) -> Self { Self { @@ -77,8 +77,8 @@ impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> StateRootsIterator<' } } -impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> Iterator - for StateRootsIterator<'a, E, Hot, Cold> +impl, Cold: ItemStore> Iterator + for StateRootsIterator<'_, E, Hot, Cold> { type Item = Result<(Hash256, Slot), Error>; @@ -101,8 +101,8 @@ pub struct BlockRootsIterator<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore inner: RootsIterator<'a, E, Hot, Cold>, } -impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> Clone - for BlockRootsIterator<'a, E, Hot, Cold> +impl, Cold: ItemStore> Clone + for BlockRootsIterator<'_, E, Hot, Cold> { fn clone(&self) -> Self { Self { @@ -136,8 +136,8 @@ impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> BlockRootsIterator<' } } -impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> Iterator - for BlockRootsIterator<'a, E, Hot, Cold> +impl, Cold: ItemStore> Iterator + for BlockRootsIterator<'_, E, Hot, Cold> { type Item = Result<(Hash256, Slot), Error>; @@ -155,9 +155,7 @@ pub struct RootsIterator<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> slot: Slot, } -impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> Clone - for RootsIterator<'a, E, Hot, Cold> -{ +impl, Cold: ItemStore> Clone for RootsIterator<'_, E, Hot, Cold> { fn clone(&self) -> Self { Self { store: self.store, @@ -232,8 +230,8 @@ impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> RootsIterator<'a, E, } } -impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> Iterator - for RootsIterator<'a, E, Hot, Cold> +impl, Cold: ItemStore> Iterator + for RootsIterator<'_, E, Hot, Cold> { /// (block_root, state_root, slot) type Item = Result<(Hash256, Hash256, Slot), Error>; @@ -295,8 +293,8 @@ impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> } } -impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> Iterator - for ParentRootBlockIterator<'a, E, Hot, Cold> +impl, Cold: ItemStore> Iterator + for ParentRootBlockIterator<'_, E, Hot, Cold> { type Item = Result<(Hash256, SignedBeaconBlock>), Error>; @@ -336,8 +334,8 @@ impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> BlockIterator<'a, E, } } -impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> Iterator - for BlockIterator<'a, E, Hot, Cold> +impl, Cold: ItemStore> Iterator + for BlockIterator<'_, E, Hot, Cold> { type Item = Result>, Error>; diff --git a/common/eth2/src/lighthouse.rs b/common/eth2/src/lighthouse.rs index 309d8228aa..66dd5d779b 100644 --- a/common/eth2/src/lighthouse.rs +++ b/common/eth2/src/lighthouse.rs @@ -528,9 +528,9 @@ impl BeaconNodeHttpClient { self.post_with_response(path, &()).await } - /// - /// Analysis endpoints. - /// + /* + Analysis endpoints. + */ /// `GET` lighthouse/analysis/block_rewards?start_slot,end_slot pub async fn get_lighthouse_analysis_block_rewards( diff --git a/common/eth2_config/src/lib.rs b/common/eth2_config/src/lib.rs index f13e90490e..50386feb8a 100644 --- a/common/eth2_config/src/lib.rs +++ b/common/eth2_config/src/lib.rs @@ -120,7 +120,7 @@ pub struct Eth2NetArchiveAndDirectory<'a> { pub genesis_state_source: GenesisStateSource, } -impl<'a> Eth2NetArchiveAndDirectory<'a> { +impl Eth2NetArchiveAndDirectory<'_> { /// The directory that should be used to store files downloaded for this net. pub fn dir(&self) -> PathBuf { env::var("CARGO_MANIFEST_DIR") diff --git a/common/logging/src/lib.rs b/common/logging/src/lib.rs index 4bb3739298..7fe7f79506 100644 --- a/common/logging/src/lib.rs +++ b/common/logging/src/lib.rs @@ -105,7 +105,7 @@ impl<'a> AlignedRecordDecorator<'a> { } } -impl<'a> Write for AlignedRecordDecorator<'a> { +impl Write for AlignedRecordDecorator<'_> { fn write(&mut self, buf: &[u8]) -> Result { if buf.iter().any(u8::is_ascii_control) { let filtered = buf @@ -124,7 +124,7 @@ impl<'a> Write for AlignedRecordDecorator<'a> { } } -impl<'a> slog_term::RecordDecorator for AlignedRecordDecorator<'a> { +impl slog_term::RecordDecorator for AlignedRecordDecorator<'_> { fn reset(&mut self) -> Result<()> { self.message_active = false; self.message_count = 0; diff --git a/common/validator_dir/src/insecure_keys.rs b/common/validator_dir/src/insecure_keys.rs index f8cc51da63..83720bb58c 100644 --- a/common/validator_dir/src/insecure_keys.rs +++ b/common/validator_dir/src/insecure_keys.rs @@ -15,7 +15,7 @@ use types::test_utils::generate_deterministic_keypair; /// A very weak password with which to encrypt the keystores. pub const INSECURE_PASSWORD: &[u8] = &[50; 51]; -impl<'a> Builder<'a> { +impl Builder<'_> { /// Generate the voting keystore using a deterministic, well-known, **unsafe** keypair. /// /// **NEVER** use these keys in production! diff --git a/consensus/state_processing/src/block_replayer.rs b/consensus/state_processing/src/block_replayer.rs index d7621ebf18..0cdb2a2bed 100644 --- a/consensus/state_processing/src/block_replayer.rs +++ b/consensus/state_processing/src/block_replayer.rs @@ -303,7 +303,7 @@ where } } -impl<'a, E, Error> BlockReplayer<'a, E, Error, StateRootIterDefault> +impl BlockReplayer<'_, E, Error, StateRootIterDefault> where E: EthSpec, Error: From, diff --git a/consensus/types/src/aggregate_and_proof.rs b/consensus/types/src/aggregate_and_proof.rs index 223b12e768..6edd8d3892 100644 --- a/consensus/types/src/aggregate_and_proof.rs +++ b/consensus/types/src/aggregate_and_proof.rs @@ -146,4 +146,4 @@ impl AggregateAndProof { } impl SignedRoot for AggregateAndProof {} -impl<'a, E: EthSpec> SignedRoot for AggregateAndProofRef<'a, E> {} +impl SignedRoot for AggregateAndProofRef<'_, E> {} diff --git a/consensus/types/src/attestation.rs b/consensus/types/src/attestation.rs index 3801a2b5d2..190964736f 100644 --- a/consensus/types/src/attestation.rs +++ b/consensus/types/src/attestation.rs @@ -233,7 +233,7 @@ impl Attestation { } } -impl<'a, E: EthSpec> AttestationRef<'a, E> { +impl AttestationRef<'_, E> { pub fn clone_as_attestation(self) -> Attestation { match self { Self::Base(att) => Attestation::Base(att.clone()), @@ -422,7 +422,7 @@ impl SlotData for Attestation { } } -impl<'a, E: EthSpec> SlotData for AttestationRef<'a, E> { +impl SlotData for AttestationRef<'_, E> { fn get_slot(&self) -> Slot { self.data().slot } diff --git a/consensus/types/src/beacon_block.rs b/consensus/types/src/beacon_block.rs index a298303513..801b7dd1c7 100644 --- a/consensus/types/src/beacon_block.rs +++ b/consensus/types/src/beacon_block.rs @@ -80,10 +80,7 @@ pub struct BeaconBlock = FullPayload pub type BlindedBeaconBlock = BeaconBlock>; impl> SignedRoot for BeaconBlock {} -impl<'a, E: EthSpec, Payload: AbstractExecPayload> SignedRoot - for BeaconBlockRef<'a, E, Payload> -{ -} +impl> SignedRoot for BeaconBlockRef<'_, E, Payload> {} /// Empty block trait for each block variant to implement. pub trait EmptyBlock { diff --git a/consensus/types/src/beacon_block_body.rs b/consensus/types/src/beacon_block_body.rs index 1090b2cc03..b896dc4693 100644 --- a/consensus/types/src/beacon_block_body.rs +++ b/consensus/types/src/beacon_block_body.rs @@ -380,7 +380,7 @@ impl<'a, E: EthSpec, Payload: AbstractExecPayload> BeaconBlockBodyRefMut<'a, } } -impl<'a, E: EthSpec, Payload: AbstractExecPayload> BeaconBlockBodyRef<'a, E, Payload> { +impl> BeaconBlockBodyRef<'_, E, Payload> { /// Get the fork_name of this object pub fn fork_name(self) -> ForkName { match self { diff --git a/consensus/types/src/beacon_committee.rs b/consensus/types/src/beacon_committee.rs index ad293c3a3b..bdb91cd6e6 100644 --- a/consensus/types/src/beacon_committee.rs +++ b/consensus/types/src/beacon_committee.rs @@ -7,7 +7,7 @@ pub struct BeaconCommittee<'a> { pub committee: &'a [usize], } -impl<'a> BeaconCommittee<'a> { +impl BeaconCommittee<'_> { pub fn into_owned(self) -> OwnedBeaconCommittee { OwnedBeaconCommittee { slot: self.slot, diff --git a/consensus/types/src/beacon_state/iter.rs b/consensus/types/src/beacon_state/iter.rs index 2caa0365e0..d99c769e40 100644 --- a/consensus/types/src/beacon_state/iter.rs +++ b/consensus/types/src/beacon_state/iter.rs @@ -27,7 +27,7 @@ impl<'a, E: EthSpec> BlockRootsIter<'a, E> { } } -impl<'a, E: EthSpec> Iterator for BlockRootsIter<'a, E> { +impl Iterator for BlockRootsIter<'_, E> { type Item = Result<(Slot, Hash256), Error>; fn next(&mut self) -> Option { diff --git a/consensus/types/src/execution_payload_header.rs b/consensus/types/src/execution_payload_header.rs index e9690435f1..4bfbfee9bf 100644 --- a/consensus/types/src/execution_payload_header.rs +++ b/consensus/types/src/execution_payload_header.rs @@ -371,7 +371,7 @@ impl TryFrom> for ExecutionPayloadHeaderDe } } -impl<'a, E: EthSpec> ExecutionPayloadHeaderRefMut<'a, E> { +impl ExecutionPayloadHeaderRefMut<'_, E> { /// Mutate through pub fn replace(self, header: ExecutionPayloadHeader) -> Result<(), BeaconStateError> { match self { diff --git a/consensus/types/src/indexed_attestation.rs b/consensus/types/src/indexed_attestation.rs index 9274600ed2..f3243a9f05 100644 --- a/consensus/types/src/indexed_attestation.rs +++ b/consensus/types/src/indexed_attestation.rs @@ -134,7 +134,7 @@ impl IndexedAttestation { } } -impl<'a, E: EthSpec> IndexedAttestationRef<'a, E> { +impl IndexedAttestationRef<'_, E> { pub fn is_double_vote(&self, other: Self) -> bool { self.data().target.epoch == other.data().target.epoch && self.data() != other.data() } diff --git a/consensus/types/src/light_client_header.rs b/consensus/types/src/light_client_header.rs index 52800f18ac..6655e0a093 100644 --- a/consensus/types/src/light_client_header.rs +++ b/consensus/types/src/light_client_header.rs @@ -179,12 +179,12 @@ impl LightClientHeaderCapella { .to_ref() .block_body_merkle_proof(EXECUTION_PAYLOAD_INDEX)?; - return Ok(LightClientHeaderCapella { + Ok(LightClientHeaderCapella { beacon: block.message().block_header(), execution: header, execution_branch: FixedVector::new(execution_branch)?, _phantom_data: PhantomData, - }); + }) } } diff --git a/consensus/types/src/light_client_update.rs b/consensus/types/src/light_client_update.rs index a7ddf8eb31..c3a50e71c1 100644 --- a/consensus/types/src/light_client_update.rs +++ b/consensus/types/src/light_client_update.rs @@ -418,7 +418,7 @@ impl LightClientUpdate { return Ok(new_attested_header_slot < prev_attested_header_slot); } - return Ok(new.signature_slot() < self.signature_slot()); + Ok(new.signature_slot() < self.signature_slot()) } fn is_next_sync_committee_branch_empty<'a>(&'a self) -> bool { diff --git a/consensus/types/src/payload.rs b/consensus/types/src/payload.rs index 80a70c171f..b82a897da5 100644 --- a/consensus/types/src/payload.rs +++ b/consensus/types/src/payload.rs @@ -317,7 +317,7 @@ impl<'a, E: EthSpec> FullPayloadRef<'a, E> { } } -impl<'b, E: EthSpec> ExecPayload for FullPayloadRef<'b, E> { +impl ExecPayload for FullPayloadRef<'_, E> { fn block_type() -> BlockType { BlockType::Full } diff --git a/consensus/types/src/slot_epoch.rs b/consensus/types/src/slot_epoch.rs index 8c8f2d073d..0391756047 100644 --- a/consensus/types/src/slot_epoch.rs +++ b/consensus/types/src/slot_epoch.rs @@ -133,7 +133,7 @@ pub struct SlotIter<'a> { slots_per_epoch: u64, } -impl<'a> Iterator for SlotIter<'a> { +impl Iterator for SlotIter<'_> { type Item = Slot; fn next(&mut self) -> Option { diff --git a/crypto/bls/src/macros.rs b/crypto/bls/src/macros.rs index f3a7374ba7..58b1ec7d6c 100644 --- a/crypto/bls/src/macros.rs +++ b/crypto/bls/src/macros.rs @@ -20,7 +20,7 @@ macro_rules! impl_tree_hash { // but benchmarks have show that to be at least 15% slower because of the // unnecessary copying and allocation (one Vec per byte) let values_per_chunk = tree_hash::BYTES_PER_CHUNK; - let minimum_chunk_count = ($byte_size + values_per_chunk - 1) / values_per_chunk; + let minimum_chunk_count = $byte_size.div_ceil(values_per_chunk); tree_hash::merkle_root(&self.serialize(), minimum_chunk_count) } }; diff --git a/crypto/kzg/src/trusted_setup.rs b/crypto/kzg/src/trusted_setup.rs index f788be265a..7aaa1d9919 100644 --- a/crypto/kzg/src/trusted_setup.rs +++ b/crypto/kzg/src/trusted_setup.rs @@ -99,7 +99,7 @@ impl<'de> Deserialize<'de> for G1Point { { struct G1PointVisitor; - impl<'de> Visitor<'de> for G1PointVisitor { + impl Visitor<'_> for G1PointVisitor { type Value = G1Point; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { formatter.write_str("A 48 byte hex encoded string") @@ -135,7 +135,7 @@ impl<'de> Deserialize<'de> for G2Point { { struct G2PointVisitor; - impl<'de> Visitor<'de> for G2PointVisitor { + impl Visitor<'_> for G2PointVisitor { type Value = G2Point; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { formatter.write_str("A 96 byte hex encoded string") diff --git a/lighthouse/src/main.rs b/lighthouse/src/main.rs index e33e4cb9b8..43c5e1107c 100644 --- a/lighthouse/src/main.rs +++ b/lighthouse/src/main.rs @@ -81,7 +81,7 @@ fn build_profile_name() -> String { std::env!("OUT_DIR") .split(std::path::MAIN_SEPARATOR) .nth_back(3) - .unwrap_or_else(|| "unknown") + .unwrap_or("unknown") .to_string() } diff --git a/slasher/src/database/interface.rs b/slasher/src/database/interface.rs index 46cf9a4a0c..af72006caa 100644 --- a/slasher/src/database/interface.rs +++ b/slasher/src/database/interface.rs @@ -192,7 +192,7 @@ impl<'env> RwTransaction<'env> { } } -impl<'env> Cursor<'env> { +impl Cursor<'_> { /// Return the first key in the current database while advancing the cursor's position. pub fn first_key(&mut self) -> Result, Error> { match self { diff --git a/testing/ef_tests/src/cases/fork_choice.rs b/testing/ef_tests/src/cases/fork_choice.rs index 33ae132e8a..7d4d229fef 100644 --- a/testing/ef_tests/src/cases/fork_choice.rs +++ b/testing/ef_tests/src/cases/fork_choice.rs @@ -871,7 +871,7 @@ pub struct ManuallyVerifiedAttestation<'a, T: BeaconChainTypes> { indexed_attestation: IndexedAttestation, } -impl<'a, T: BeaconChainTypes> VerifiedAttestation for ManuallyVerifiedAttestation<'a, T> { +impl VerifiedAttestation for ManuallyVerifiedAttestation<'_, T> { fn attestation(&self) -> AttestationRef { self.attestation.to_ref() } diff --git a/validator_client/signing_method/src/lib.rs b/validator_client/signing_method/src/lib.rs index 2fe4af39d3..f3b62c9500 100644 --- a/validator_client/signing_method/src/lib.rs +++ b/validator_client/signing_method/src/lib.rs @@ -49,7 +49,7 @@ pub enum SignableMessage<'a, E: EthSpec, Payload: AbstractExecPayload = FullP VoluntaryExit(&'a VoluntaryExit), } -impl<'a, E: EthSpec, Payload: AbstractExecPayload> SignableMessage<'a, E, Payload> { +impl> SignableMessage<'_, E, Payload> { /// Returns the `SignedRoot` for the contained message. /// /// The actual `SignedRoot` trait is not used since it also requires a `TreeHash` impl, which is diff --git a/watch/src/database/mod.rs b/watch/src/database/mod.rs index b31583c629..7193b0744a 100644 --- a/watch/src/database/mod.rs +++ b/watch/src/database/mod.rs @@ -109,9 +109,9 @@ pub fn get_active_config(conn: &mut PgConn) -> Result, Err .optional()?) } -/// -/// INSERT statements -/// +/* + * INSERT statements + */ /// Inserts a single row into the `canonical_slots` table. /// If `new_slot.beacon_block` is `None`, the value in the row will be `null`. @@ -245,9 +245,9 @@ pub fn insert_batch_validators( Ok(()) } -/// -/// SELECT statements -/// +/* + * SELECT statements + */ /// Selects a single row of the `canonical_slots` table corresponding to a given `slot_query`. pub fn get_canonical_slot( @@ -746,9 +746,9 @@ pub fn count_validators_activated_before_slot( .map_err(Error::Database) } -/// -/// DELETE statements. -/// +/* + * DELETE statements. + */ /// Deletes all rows of the `canonical_slots` table which have `slot` greater than `slot_query`. /// From 1c8161f92b036d72765c4fdfea2a3cf8180a04ff Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Fri, 29 Nov 2024 13:58:19 +1100 Subject: [PATCH 022/254] Fetch blobs from EL prior to block verification (#6600) * Fetch blobs from EL prior to block verification * Run fetch blobs in parallel with block import * Merge branch 'unstable' into fetch-blobs-earlier * Merge branch 'unstable' into fetch-blobs-earlier --- .../gossip_methods.rs | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 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 e92f450476..317bfb104b 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -1444,6 +1444,20 @@ impl NetworkBeaconProcessor { } } + // Block is gossip valid. Attempt to fetch blobs from the EL using versioned hashes derived + // from kzg commitments, without having to wait for all blobs to be sent from the peers. + let publish_blobs = true; + let self_clone = self.clone(); + let block_clone = block.clone(); + self.executor.spawn( + async move { + self_clone + .fetch_engine_blobs_and_publish(block_clone, block_root, publish_blobs) + .await + }, + "fetch_blobs_gossip", + ); + let result = self .chain .process_block_with_early_caching( @@ -1494,13 +1508,6 @@ impl NetworkBeaconProcessor { "slot" => slot, "block_root" => %block_root, ); - - // Block is valid, we can now attempt fetching blobs from EL using version hashes - // derived from kzg commitments from the block, without having to wait for all blobs - // to be sent from the peers if we already have them. - let publish_blobs = true; - self.fetch_engine_blobs_and_publish(block.clone(), *block_root, publish_blobs) - .await; } Err(BlockError::ParentUnknown { .. }) => { // This should not occur. It should be checked by `should_forward_block`. From f8e31f62726de375cca7087b6d2ef6b9283a749f Mon Sep 17 00:00:00 2001 From: Pawan Dhananjay Date: Mon, 2 Dec 2024 05:01:10 +0530 Subject: [PATCH 023/254] Increase rpc rate limits (#6626) * Increase rate limits for byrange requests * Merge branch 'unstable' into reduce-blob-limits * Update limits --- beacon_node/lighthouse_network/src/rpc/config.rs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/beacon_node/lighthouse_network/src/rpc/config.rs b/beacon_node/lighthouse_network/src/rpc/config.rs index 42ece6dc4f..7b3a59eac7 100644 --- a/beacon_node/lighthouse_network/src/rpc/config.rs +++ b/beacon_node/lighthouse_network/src/rpc/config.rs @@ -104,15 +104,14 @@ impl RateLimiterConfig { pub const DEFAULT_META_DATA_QUOTA: Quota = Quota::n_every(2, 5); pub const DEFAULT_STATUS_QUOTA: Quota = Quota::n_every(5, 15); pub const DEFAULT_GOODBYE_QUOTA: Quota = Quota::one_every(10); - pub const DEFAULT_BLOCKS_BY_RANGE_QUOTA: Quota = Quota::n_every(1024, 10); + // The number is chosen to balance between upload bandwidth required to serve + // blocks and a decent syncing rate for honest nodes. Malicious nodes would need to + // spread out their requests over the time window to max out bandwidth on the server. + pub const DEFAULT_BLOCKS_BY_RANGE_QUOTA: Quota = Quota::n_every(128, 10); pub const DEFAULT_BLOCKS_BY_ROOT_QUOTA: Quota = Quota::n_every(128, 10); - // `BlocksByRange` and `BlobsByRange` are sent together during range sync. - // It makes sense for blocks and blobs quotas to be equivalent in terms of the number of blocks: - // 1024 blocks * 6 max blobs per block. - // This doesn't necessarily mean that we are sending this many blobs, because the quotas are - // measured against the maximum request size. - pub const DEFAULT_BLOBS_BY_RANGE_QUOTA: Quota = Quota::n_every(6144, 10); - pub const DEFAULT_BLOBS_BY_ROOT_QUOTA: Quota = Quota::n_every(768, 10); + // `DEFAULT_BLOCKS_BY_RANGE_QUOTA` * (target + 1) to account for high usage + pub const DEFAULT_BLOBS_BY_RANGE_QUOTA: Quota = Quota::n_every(512, 10); + pub const DEFAULT_BLOBS_BY_ROOT_QUOTA: Quota = Quota::n_every(512, 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); From 770d677a4e25df54f87789b0f6269c4be1149f96 Mon Sep 17 00:00:00 2001 From: Age Manning Date: Mon, 2 Dec 2024 12:44:58 +1100 Subject: [PATCH 024/254] Increase idle connection timeout (#6604) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Increase idle connection timeout * Update beacon_node/lighthouse_network/src/service/mod.rs Co-authored-by: João Oliveira --- beacon_node/lighthouse_network/src/service/mod.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/beacon_node/lighthouse_network/src/service/mod.rs b/beacon_node/lighthouse_network/src/service/mod.rs index ff7707e98d..afcbfce173 100644 --- a/beacon_node/lighthouse_network/src/service/mod.rs +++ b/beacon_node/lighthouse_network/src/service/mod.rs @@ -38,6 +38,7 @@ use std::num::{NonZeroU8, NonZeroUsize}; use std::path::PathBuf; use std::pin::Pin; use std::sync::Arc; +use std::time::Duration; use types::{ consts::altair::SYNC_COMMITTEE_SUBNET_COUNT, EnrForkId, EthSpec, ForkContext, Slot, SubnetId, }; @@ -466,6 +467,8 @@ impl Network { let config = libp2p::swarm::Config::with_executor(Executor(executor)) .with_notify_handler_buffer_size(NonZeroUsize::new(7).expect("Not zero")) .with_per_connection_event_buffer_size(4) + .with_idle_connection_timeout(Duration::from_secs(10)) // Other clients can timeout + // during negotiation .with_dial_concurrency_factor(NonZeroU8::new(1).unwrap()); let builder = SwarmBuilder::with_existing_identity(local_keypair) From c042dc14d74352512b7632e0ee6ec07f1aa26b3a Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Mon, 2 Dec 2024 15:00:51 +1100 Subject: [PATCH 025/254] Release v6.0.0 (#6605) * Release v6.0.0 --- Cargo.lock | 8 ++++---- beacon_node/Cargo.toml | 2 +- boot_node/Cargo.toml | 2 +- common/lighthouse_version/src/lib.rs | 4 ++-- lcli/Cargo.toml | 2 +- lighthouse/Cargo.toml | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8f8ff45b4d..1ddeecf711 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -833,7 +833,7 @@ dependencies = [ [[package]] name = "beacon_node" -version = "5.3.0" +version = "6.0.0" dependencies = [ "account_utils", "beacon_chain", @@ -1078,7 +1078,7 @@ dependencies = [ [[package]] name = "boot_node" -version = "5.3.0" +version = "6.0.0" dependencies = [ "beacon_node", "bytes", @@ -4674,7 +4674,7 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "lcli" -version = "5.3.0" +version = "6.0.0" dependencies = [ "account_utils", "beacon_chain", @@ -5244,7 +5244,7 @@ dependencies = [ [[package]] name = "lighthouse" -version = "5.3.0" +version = "6.0.0" dependencies = [ "account_manager", "account_utils", diff --git a/beacon_node/Cargo.toml b/beacon_node/Cargo.toml index bb946e3c5a..fd4f0f6d4a 100644 --- a/beacon_node/Cargo.toml +++ b/beacon_node/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "beacon_node" -version = "5.3.0" +version = "6.0.0" authors = [ "Paul Hauner ", "Age Manning "] edition = { workspace = true } diff --git a/common/lighthouse_version/src/lib.rs b/common/lighthouse_version/src/lib.rs index f988dd86b1..07e51597e3 100644 --- a/common/lighthouse_version/src/lib.rs +++ b/common/lighthouse_version/src/lib.rs @@ -17,8 +17,8 @@ pub const VERSION: &str = git_version!( // NOTE: using --match instead of --exclude for compatibility with old Git "--match=thiswillnevermatchlol" ], - prefix = "Lighthouse/v5.3.0-", - fallback = "Lighthouse/v5.3.0" + prefix = "Lighthouse/v6.0.0-", + fallback = "Lighthouse/v6.0.0" ); /// Returns the first eight characters of the latest commit hash for this build. diff --git a/lcli/Cargo.toml b/lcli/Cargo.toml index 77d122efb7..88daddd8aa 100644 --- a/lcli/Cargo.toml +++ b/lcli/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "lcli" description = "Lighthouse CLI (modeled after zcli)" -version = "5.3.0" +version = "6.0.0" authors = ["Paul Hauner "] edition = { workspace = true } diff --git a/lighthouse/Cargo.toml b/lighthouse/Cargo.toml index dd1cb68f06..329519fb54 100644 --- a/lighthouse/Cargo.toml +++ b/lighthouse/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lighthouse" -version = "5.3.0" +version = "6.0.0" authors = ["Sigma Prime "] edition = { workspace = true } autotests = false From 1fd86f8b595c6115bb235767ab72262a8746c984 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Tue, 3 Dec 2024 11:08:53 +1100 Subject: [PATCH 026/254] Add a security section to the book (#6581) * Add a security section to the book * Update book/src/security.md Co-authored-by: chonghe <44791194+chong-he@users.noreply.github.com> * Update book/src/security.md Co-authored-by: chonghe <44791194+chong-he@users.noreply.github.com> --- book/src/SUMMARY.md | 1 + book/src/resources/2020-lh-trail-of-bits.pdf | Bin 0 -> 501738 bytes book/src/security.md | 12 ++++++++++++ 3 files changed, 13 insertions(+) create mode 100644 book/src/resources/2020-lh-trail-of-bits.pdf create mode 100644 book/src/security.md diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index c38ee58e3b..02683a1172 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -66,3 +66,4 @@ * [Development Environment](./setup.md) * [FAQs](./faq.md) * [Protocol Developers](./developers.md) +* [Security Researchers](./security.md) diff --git a/book/src/resources/2020-lh-trail-of-bits.pdf b/book/src/resources/2020-lh-trail-of-bits.pdf new file mode 100644 index 0000000000000000000000000000000000000000..162bef53f0550c677a8919b4d8f3efd5b3f90948 GIT binary patch literal 501738 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

>6`JW~lI3g!Uca`eoG^gP6FY3;pW09yaF3!V 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 OuePz#+u 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 z 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≪gw~}Q$GQsQY$bfB1vDYv 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�}*{csU^ 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 ukr5Hm&ImSJZ;``GtR{ye-PTMcDfJo;` Date: Tue, 3 Dec 2024 03:56:43 -0500 Subject: [PATCH 027/254] Deprecate futures ticker (#6630) * deprecate futures-ticker * Merge branch 'unstable' of github.com:sigp/lighthouse into deprecate-futures-timer * Merge branch 'unstable' into deprecate-futures-timer * making the linter happy * remove unrequired #[allow(unused_imports)] * fixing minor issues * merge commit * minor fix * clippy changes --- Cargo.lock | 36 ++++++++++++------- .../lighthouse_network/gossipsub/Cargo.toml | 3 +- .../gossipsub/src/behaviour.rs | 25 +++++++------ .../gossipsub/src/behaviour/tests.rs | 1 + .../gossipsub/src/peer_score.rs | 2 +- 5 files changed, 38 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1ddeecf711..5cea2d2ec5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3375,22 +3375,15 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" -[[package]] -name = "futures-ticker" -version = "0.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9763058047f713632a52e916cc7f6a4b3fc6e9fc1ff8c5b1dc49e5a89041682e" -dependencies = [ - "futures", - "futures-timer", - "instant", -] - [[package]] name = "futures-timer" version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" +dependencies = [ + "gloo-timers", + "send_wrapper 0.4.0", +] [[package]] name = "futures-util" @@ -3506,6 +3499,18 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[package]] +name = "gloo-timers" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "gossipsub" version = "0.5.0" @@ -3518,7 +3523,6 @@ dependencies = [ "either", "fnv", "futures", - "futures-ticker", "futures-timer", "getrandom", "hashlink 0.9.1", @@ -7736,6 +7740,12 @@ dependencies = [ "pest", ] +[[package]] +name = "send_wrapper" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f638d531eccd6e23b980caf34876660d38e265409d8e99b397ab71eb3612fad0" + [[package]] name = "send_wrapper" version = "0.6.0" @@ -10349,7 +10359,7 @@ dependencies = [ "log", "pharos", "rustc_version 0.4.1", - "send_wrapper", + "send_wrapper 0.6.0", "thiserror 1.0.69", "wasm-bindgen", "wasm-bindgen-futures", diff --git a/beacon_node/lighthouse_network/gossipsub/Cargo.toml b/beacon_node/lighthouse_network/gossipsub/Cargo.toml index a01d60dae9..6cbe6d3a1c 100644 --- a/beacon_node/lighthouse_network/gossipsub/Cargo.toml +++ b/beacon_node/lighthouse_network/gossipsub/Cargo.toml @@ -10,7 +10,7 @@ keywords = ["peer-to-peer", "libp2p", "networking"] categories = ["network-programming", "asynchronous"] [features] -wasm-bindgen = ["getrandom/js"] +wasm-bindgen = ["getrandom/js", "futures-timer/wasm-bindgen"] rsa = [] [dependencies] @@ -22,7 +22,6 @@ bytes = "1.5" either = "1.9" fnv = "1.0.7" futures = "0.3.30" -futures-ticker = "0.0.3" futures-timer = "3.0.2" getrandom = "0.2.12" hashlink.workspace = true diff --git a/beacon_node/lighthouse_network/gossipsub/src/behaviour.rs b/beacon_node/lighthouse_network/gossipsub/src/behaviour.rs index 5ead0c06a0..aafd869bee 100644 --- a/beacon_node/lighthouse_network/gossipsub/src/behaviour.rs +++ b/beacon_node/lighthouse_network/gossipsub/src/behaviour.rs @@ -29,8 +29,7 @@ use std::{ time::Duration, }; -use futures::StreamExt; -use futures_ticker::Ticker; +use futures::FutureExt; use hashlink::LinkedHashMap; use prometheus_client::registry::Registry; use rand::{seq::SliceRandom, thread_rng}; @@ -74,6 +73,7 @@ use super::{ types::RpcOut, }; use super::{PublishError, SubscriptionError, TopicScoreParams, ValidationError}; +use futures_timer::Delay; use quick_protobuf::{MessageWrite, Writer}; use std::{cmp::Ordering::Equal, fmt::Debug}; @@ -301,7 +301,7 @@ pub struct Behaviour { mcache: MessageCache, /// Heartbeat interval stream. - heartbeat: Ticker, + heartbeat: Delay, /// Number of heartbeats since the beginning of time; this allows us to amortize some resource /// clean up -- eg backoff clean up. @@ -318,7 +318,7 @@ pub struct Behaviour { outbound_peers: HashSet, /// Stores optional peer score data together with thresholds and decay interval. - peer_score: Option<(PeerScore, PeerScoreThresholds, Ticker)>, + peer_score: Option<(PeerScore, PeerScoreThresholds, Delay)>, /// Counts the number of `IHAVE` received from each peer since the last heartbeat. count_received_ihave: HashMap, @@ -466,10 +466,7 @@ where config.backoff_slack(), ), mcache: MessageCache::new(config.history_gossip(), config.history_length()), - heartbeat: Ticker::new_with_next( - config.heartbeat_interval(), - config.heartbeat_initial_delay(), - ), + heartbeat: Delay::new(config.heartbeat_interval() + config.heartbeat_initial_delay()), heartbeat_ticks: 0, px_peers: HashSet::new(), outbound_peers: HashSet::new(), @@ -938,7 +935,7 @@ where return Err("Peer score set twice".into()); } - let interval = Ticker::new(params.decay_interval); + let interval = Delay::new(params.decay_interval); let peer_score = PeerScore::new_with_message_delivery_time_callback(params, callback); self.peer_score = Some((peer_score, threshold, interval)); Ok(()) @@ -1208,7 +1205,7 @@ where } fn score_below_threshold_from_scores( - peer_score: &Option<(PeerScore, PeerScoreThresholds, Ticker)>, + peer_score: &Option<(PeerScore, PeerScoreThresholds, Delay)>, peer_id: &PeerId, threshold: impl Fn(&PeerScoreThresholds) -> f64, ) -> (bool, f64) { @@ -3427,14 +3424,16 @@ where } // update scores - if let Some((peer_score, _, interval)) = &mut self.peer_score { - while let Poll::Ready(Some(_)) = interval.poll_next_unpin(cx) { + if let Some((peer_score, _, delay)) = &mut self.peer_score { + if delay.poll_unpin(cx).is_ready() { peer_score.refresh_scores(); + delay.reset(peer_score.params.decay_interval); } } - while let Poll::Ready(Some(_)) = self.heartbeat.poll_next_unpin(cx) { + if self.heartbeat.poll_unpin(cx).is_ready() { self.heartbeat(); + self.heartbeat.reset(self.config.heartbeat_interval()); } Poll::Pending diff --git a/beacon_node/lighthouse_network/gossipsub/src/behaviour/tests.rs b/beacon_node/lighthouse_network/gossipsub/src/behaviour/tests.rs index 713fe1f266..90b8fe43fb 100644 --- a/beacon_node/lighthouse_network/gossipsub/src/behaviour/tests.rs +++ b/beacon_node/lighthouse_network/gossipsub/src/behaviour/tests.rs @@ -25,6 +25,7 @@ use crate::subscription_filter::WhitelistSubscriptionFilter; use crate::types::RpcReceiver; use crate::{config::ConfigBuilder, types::Rpc, IdentTopic as Topic}; use byteorder::{BigEndian, ByteOrder}; +use futures::StreamExt; use libp2p::core::ConnectedPoint; use rand::Rng; use std::net::Ipv4Addr; diff --git a/beacon_node/lighthouse_network/gossipsub/src/peer_score.rs b/beacon_node/lighthouse_network/gossipsub/src/peer_score.rs index fa02f06f69..ec6fe7bdb6 100644 --- a/beacon_node/lighthouse_network/gossipsub/src/peer_score.rs +++ b/beacon_node/lighthouse_network/gossipsub/src/peer_score.rs @@ -44,7 +44,7 @@ mod tests; const TIME_CACHE_DURATION: u64 = 120; pub(crate) struct PeerScore { - params: PeerScoreParams, + pub(crate) params: PeerScoreParams, /// The score parameters. peer_stats: HashMap, /// Tracking peers per IP. From e9ec67e78a062de9a90d76e77b4a8a9ecd1f9156 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Tue, 10 Dec 2024 13:14:13 +1100 Subject: [PATCH 028/254] Fix Kurtosis, web3signer and cargo-audit for CI (#6671) * Update kurtosis-cli * Fix name of Kurtosis artefact used in doppelganger tests * Ignore idna vuln * Set Java Version to 21 (required since Web3Signer 24.12.0). --- .github/workflows/local-testnet.yml | 6 +++--- .github/workflows/test-suite.yml | 5 +++++ Makefile | 2 +- scripts/tests/doppelganger_protection.sh | 4 ++-- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/.github/workflows/local-testnet.yml b/.github/workflows/local-testnet.yml index d496cc6348..1cd2f24548 100644 --- a/.github/workflows/local-testnet.yml +++ b/.github/workflows/local-testnet.yml @@ -40,7 +40,7 @@ jobs: run: | echo "deb [trusted=yes] https://apt.fury.io/kurtosis-tech/ /" | sudo tee /etc/apt/sources.list.d/kurtosis.list sudo apt update - sudo apt install -y kurtosis-cli=1.3.1 + sudo apt install -y kurtosis-cli kurtosis analytics disable - name: Download Docker image artifact @@ -86,7 +86,7 @@ jobs: run: | echo "deb [trusted=yes] https://apt.fury.io/kurtosis-tech/ /" | sudo tee /etc/apt/sources.list.d/kurtosis.list sudo apt update - sudo apt install -y kurtosis-cli=1.3.1 + sudo apt install -y kurtosis-cli kurtosis analytics disable - name: Download Docker image artifact @@ -121,7 +121,7 @@ jobs: run: | echo "deb [trusted=yes] https://apt.fury.io/kurtosis-tech/ /" | sudo tee /etc/apt/sources.list.d/kurtosis.list sudo apt update - sudo apt install -y kurtosis-cli=1.3.1 + sudo apt install -y kurtosis-cli kurtosis analytics disable - name: Download Docker image artifact diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index d6ef180934..8da46ed8ee 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -83,6 +83,11 @@ jobs: runs-on: ${{ github.repository == 'sigp/lighthouse' && fromJson('["self-hosted", "linux", "CI", "large"]') || 'ubuntu-latest' }} steps: - uses: actions/checkout@v4 + # Set Java version to 21. (required since Web3Signer 24.12.0). + - uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' - name: Get latest version of stable Rust if: env.SELF_HOSTED_RUNNERS == 'false' uses: moonrepo/setup-rust@v1 diff --git a/Makefile b/Makefile index fd7d45f26a..ab239c94d3 100644 --- a/Makefile +++ b/Makefile @@ -240,7 +240,7 @@ install-audit: cargo install --force cargo-audit audit-CI: - cargo audit + cargo audit --ignore RUSTSEC-2024-0421 # Runs `cargo vendor` to make sure dependencies can be vendored for packaging, reproducibility and archival purpose. vendor: diff --git a/scripts/tests/doppelganger_protection.sh b/scripts/tests/doppelganger_protection.sh index 441e2a6357..5be5c13dee 100755 --- a/scripts/tests/doppelganger_protection.sh +++ b/scripts/tests/doppelganger_protection.sh @@ -71,7 +71,7 @@ if [[ "$BEHAVIOR" == "failure" ]]; then # This process should not last longer than 2 epochs vc_1_range_start=0 vc_1_range_end=$(($KEYS_PER_NODE - 1)) - vc_1_keys_artifact_id="1-lighthouse-geth-$vc_1_range_start-$vc_1_range_end-0" + vc_1_keys_artifact_id="1-lighthouse-geth-$vc_1_range_start-$vc_1_range_end" service_name=vc-1-doppelganger kurtosis service add \ @@ -107,7 +107,7 @@ if [[ "$BEHAVIOR" == "success" ]]; then vc_4_range_start=$(($KEYS_PER_NODE * 3)) vc_4_range_end=$(($KEYS_PER_NODE * 4 - 1)) - vc_4_keys_artifact_id="4-lighthouse-geth-$vc_4_range_start-$vc_4_range_end-0" + vc_4_keys_artifact_id="4-lighthouse-geth-$vc_4_range_start-$vc_4_range_end" service_name=vc-4 kurtosis service add \ From 3b8254a8ecbe78eae659f1b3cfdfca78b02a5ad0 Mon Sep 17 00:00:00 2001 From: Age Manning Date: Tue, 10 Dec 2024 15:24:55 +1100 Subject: [PATCH 029/254] Correct flakey CI tests (#6646) * Correct flakey CI tests * Correct clippy * Extend timeout for events --- beacon_node/network/src/subnet_service/mod.rs | 1 + beacon_node/network/src/subnet_service/tests/mod.rs | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/beacon_node/network/src/subnet_service/mod.rs b/beacon_node/network/src/subnet_service/mod.rs index ab73b6ad9c..ec6f3b10a3 100644 --- a/beacon_node/network/src/subnet_service/mod.rs +++ b/beacon_node/network/src/subnet_service/mod.rs @@ -213,6 +213,7 @@ impl SubnetService { #[cfg(test)] pub(crate) fn is_subscribed(&self, subnet: &Subnet) -> bool { self.subscriptions.contains_key(subnet) + || self.permanent_attestation_subscriptions.contains(subnet) } /// Processes a list of validator subscriptions. diff --git a/beacon_node/network/src/subnet_service/tests/mod.rs b/beacon_node/network/src/subnet_service/tests/mod.rs index c56079b9ac..91e4841b26 100644 --- a/beacon_node/network/src/subnet_service/tests/mod.rs +++ b/beacon_node/network/src/subnet_service/tests/mod.rs @@ -225,7 +225,7 @@ mod test { let mut committee_count = 1; let mut subnet = Subnet::Attestation( SubnetId::compute_subnet::( - current_slot, + subscription_slot, committee_index, committee_count, &subnet_service.beacon_chain.spec, @@ -250,7 +250,7 @@ mod test { let subscriptions = vec![get_subscription( committee_index, - current_slot, + subscription_slot, committee_count, true, )]; @@ -556,7 +556,8 @@ mod test { subnet_service.validator_subscriptions(vec![sub1, sub2].into_iter()); // Unsubscription event should happen at the end of the slot. - let events = get_events(&mut subnet_service, None, 1).await; + // We wait for 2 slots, to avoid timeout issues + let events = get_events(&mut subnet_service, None, 2).await; let expected_subscription = SubnetServiceMessage::Subscribe(Subnet::Attestation(subnet_id1)); @@ -567,6 +568,7 @@ mod test { assert_eq!(expected_subscription, events[0]); assert_eq!(expected_unsubscription, events[2]); } + // Check that there are no more subscriptions assert_eq!(subnet_service.subscriptions().count(), 0); println!("{events:?}"); From b2590bcb37784f7f3540aa10a9cca123e9c66777 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Thu, 12 Dec 2024 09:51:46 +1100 Subject: [PATCH 030/254] Tweak reconstruction batch size (#6668) * Tweak reconstruction batch size * Merge branch 'release-v6.0.1' into reconstruction-batch-size --- beacon_node/beacon_chain/src/migrate.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/beacon_node/beacon_chain/src/migrate.rs b/beacon_node/beacon_chain/src/migrate.rs index 37a2e8917b..bc4b8e1ed8 100644 --- a/beacon_node/beacon_chain/src/migrate.rs +++ b/beacon_node/beacon_chain/src/migrate.rs @@ -26,8 +26,10 @@ const MIN_COMPACTION_PERIOD_SECONDS: u64 = 7200; const COMPACTION_FINALITY_DISTANCE: u64 = 1024; /// Maximum number of blocks applied in each reconstruction burst. /// -/// This limits the amount of time that the finalization migration is paused for. -const BLOCKS_PER_RECONSTRUCTION: usize = 8192 * 4; +/// This limits the amount of time that the finalization migration is paused for. We set this +/// conservatively because pausing the finalization migration for too long can cause hot state +/// cache misses and excessive disk use. +const BLOCKS_PER_RECONSTRUCTION: usize = 1024; /// Default number of epochs to wait between finalization migrations. pub const DEFAULT_EPOCHS_PER_MIGRATION: u64 = 1; From a2b00090fd8c6c5d6b4ffc88f8d0f937d9165c58 Mon Sep 17 00:00:00 2001 From: Daniel Knopik <107140945+dknopik@users.noreply.github.com> Date: Thu, 12 Dec 2024 00:51:20 +0100 Subject: [PATCH 031/254] Remove `ZeroizeString` in favour of `Zeroizing` (#6661) * Remove ZeroizeString in favour of Zeroizing * cargo fmt * remove unrelated line that slipped in * Update beacon_node/store/Cargo.toml thanks michael! Co-authored-by: Michael Sproul * Merge branch 'unstable' into remove-zeroizedstring --- Cargo.lock | 11 +- Cargo.toml | 2 +- account_manager/Cargo.toml | 1 + account_manager/src/validator/create.rs | 2 +- account_manager/src/validator/import.rs | 16 ++- account_manager/src/wallet/create.rs | 4 +- common/account_utils/src/lib.rs | 108 ++---------------- .../src/validator_definitions.rs | 11 +- common/eth2/Cargo.toml | 5 +- common/eth2/src/lighthouse_vc/http_client.rs | 12 +- common/eth2/src/lighthouse_vc/std_types.rs | 4 +- common/eth2/src/lighthouse_vc/types.rs | 17 ++- crypto/eth2_keystore/src/keystore.rs | 46 +------- lighthouse/Cargo.toml | 1 + lighthouse/tests/account_manager.rs | 9 +- validator_client/http_api/Cargo.toml | 1 + .../http_api/src/create_validator.rs | 7 +- validator_client/http_api/src/keystores.rs | 5 +- validator_client/http_api/src/test_utils.rs | 4 +- validator_client/http_api/src/tests.rs | 5 +- .../http_api/src/tests/keystores.rs | 3 +- .../initialized_validators/Cargo.toml | 1 + .../initialized_validators/src/lib.rs | 16 +-- validator_manager/Cargo.toml | 1 + validator_manager/src/common.rs | 5 +- validator_manager/src/import_validators.rs | 14 +-- validator_manager/src/move_validators.rs | 5 +- 27 files changed, 99 insertions(+), 217 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5cea2d2ec5..00aeaa9af4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -36,6 +36,7 @@ dependencies = [ "tokio", "types", "validator_dir", + "zeroize", ] [[package]] @@ -2561,8 +2562,6 @@ dependencies = [ name = "eth2" version = "0.1.0" dependencies = [ - "account_utils", - "bytes", "derivative", "eth2_keystore", "ethereum_serde_utils", @@ -2570,7 +2569,6 @@ dependencies = [ "ethereum_ssz_derive", "futures", "futures-util", - "libsecp256k1", "lighthouse_network", "mediatype", "pretty_reqwest_error", @@ -2578,7 +2576,6 @@ dependencies = [ "proto_array", "psutil", "reqwest", - "ring 0.16.20", "sensitive_url", "serde", "serde_json", @@ -2587,6 +2584,7 @@ dependencies = [ "store", "tokio", "types", + "zeroize", ] [[package]] @@ -4433,6 +4431,7 @@ dependencies = [ "url", "validator_dir", "validator_metrics", + "zeroize", ] [[package]] @@ -5287,6 +5286,7 @@ dependencies = [ "validator_client", "validator_dir", "validator_manager", + "zeroize", ] [[package]] @@ -9584,6 +9584,7 @@ dependencies = [ "validator_store", "warp", "warp_utils", + "zeroize", ] [[package]] @@ -9627,6 +9628,7 @@ dependencies = [ "tree_hash", "types", "validator_http_api", + "zeroize", ] [[package]] @@ -10562,6 +10564,7 @@ version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" dependencies = [ + "serde", "zeroize_derive", ] diff --git a/Cargo.toml b/Cargo.toml index 0be462754e..9e921190b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -201,7 +201,7 @@ tree_hash_derive = "0.8" url = "2" uuid = { version = "0.8", features = ["serde", "v4"] } warp = { version = "0.3.7", default-features = false, features = ["tls"] } -zeroize = { version = "1", features = ["zeroize_derive"] } +zeroize = { version = "1", features = ["zeroize_derive", "serde"] } zip = "0.6" # Local crates. diff --git a/account_manager/Cargo.toml b/account_manager/Cargo.toml index 7f2fa05a88..48230bb281 100644 --- a/account_manager/Cargo.toml +++ b/account_manager/Cargo.toml @@ -27,6 +27,7 @@ safe_arith = { workspace = true } slot_clock = { workspace = true } filesystem = { workspace = true } sensitive_url = { workspace = true } +zeroize = { workspace = true } [dev-dependencies] tempfile = { workspace = true } diff --git a/account_manager/src/validator/create.rs b/account_manager/src/validator/create.rs index ec5af1e2ec..73e0ad54d4 100644 --- a/account_manager/src/validator/create.rs +++ b/account_manager/src/validator/create.rs @@ -294,7 +294,7 @@ pub fn read_wallet_password_from_cli( eprintln!(); eprintln!("{}", WALLET_PASSWORD_PROMPT); let password = - PlainText::from(read_password_from_user(stdin_inputs)?.as_ref().to_vec()); + PlainText::from(read_password_from_user(stdin_inputs)?.as_bytes().to_vec()); Ok(password) } } diff --git a/account_manager/src/validator/import.rs b/account_manager/src/validator/import.rs index 19ab5ad60a..4d2353b553 100644 --- a/account_manager/src/validator/import.rs +++ b/account_manager/src/validator/import.rs @@ -7,7 +7,7 @@ use account_utils::{ recursively_find_voting_keystores, PasswordStorage, ValidatorDefinition, ValidatorDefinitions, CONFIG_FILENAME, }, - ZeroizeString, STDIN_INPUTS_FLAG, + STDIN_INPUTS_FLAG, }; use clap::{Arg, ArgAction, ArgMatches, Command}; use clap_utils::FLAG_HEADER; @@ -16,6 +16,7 @@ use std::fs; use std::path::PathBuf; use std::thread::sleep; use std::time::Duration; +use zeroize::Zeroizing; pub const CMD: &str = "import"; pub const KEYSTORE_FLAG: &str = "keystore"; @@ -148,7 +149,7 @@ pub fn cli_run(matches: &ArgMatches, validator_dir: PathBuf) -> Result<(), Strin // Skip keystores that already exist, but exit early if any operation fails. // Reuses the same password for all keystores if the `REUSE_PASSWORD_FLAG` flag is set. let mut num_imported_keystores = 0; - let mut previous_password: Option = None; + let mut previous_password: Option> = None; for src_keystore in &keystore_paths { let keystore = Keystore::from_json_file(src_keystore) @@ -182,14 +183,17 @@ pub fn cli_run(matches: &ArgMatches, validator_dir: PathBuf) -> Result<(), Strin let password = match keystore_password_path.as_ref() { Some(path) => { - let password_from_file: ZeroizeString = fs::read_to_string(path) + let password_from_file: Zeroizing = fs::read_to_string(path) .map_err(|e| format!("Unable to read {:?}: {:?}", path, e))? .into(); - password_from_file.without_newlines() + password_from_file + .trim_end_matches(['\r', '\n']) + .to_string() + .into() } None => { let password_from_user = read_password_from_user(stdin_inputs)?; - if password_from_user.as_ref().is_empty() { + if password_from_user.is_empty() { eprintln!("Continuing without password."); sleep(Duration::from_secs(1)); // Provides nicer UX. break None; @@ -314,7 +318,7 @@ pub fn cli_run(matches: &ArgMatches, validator_dir: PathBuf) -> Result<(), Strin /// Otherwise, returns the keystore error. fn check_password_on_keystore( keystore: &Keystore, - password: &ZeroizeString, + password: &Zeroizing, ) -> Result { match keystore.decrypt_keypair(password.as_ref()) { Ok(_) => { diff --git a/account_manager/src/wallet/create.rs b/account_manager/src/wallet/create.rs index b22007050f..6369646929 100644 --- a/account_manager/src/wallet/create.rs +++ b/account_manager/src/wallet/create.rs @@ -226,14 +226,14 @@ pub fn read_new_wallet_password_from_cli( eprintln!(); eprintln!("{}", NEW_WALLET_PASSWORD_PROMPT); let password = - PlainText::from(read_password_from_user(stdin_inputs)?.as_ref().to_vec()); + PlainText::from(read_password_from_user(stdin_inputs)?.as_bytes().to_vec()); // Ensure the password meets the minimum requirements. match is_password_sufficiently_complex(password.as_bytes()) { Ok(_) => { eprintln!("{}", RETYPE_PASSWORD_PROMPT); let retyped_password = - PlainText::from(read_password_from_user(stdin_inputs)?.as_ref().to_vec()); + PlainText::from(read_password_from_user(stdin_inputs)?.as_bytes().to_vec()); if retyped_password == password { break Ok(password); } else { diff --git a/common/account_utils/src/lib.rs b/common/account_utils/src/lib.rs index c1fa621abb..0f576efb3a 100644 --- a/common/account_utils/src/lib.rs +++ b/common/account_utils/src/lib.rs @@ -8,18 +8,14 @@ use eth2_wallet::{ }; use filesystem::{create_with_600_perms, Error as FsError}; use rand::{distributions::Alphanumeric, Rng}; -use serde::{Deserialize, Serialize}; +use std::fs::{self, File}; use std::io; use std::io::prelude::*; use std::path::{Path, PathBuf}; use std::str::from_utf8; use std::thread::sleep; use std::time::Duration; -use std::{ - fs::{self, File}, - str::FromStr, -}; -use zeroize::Zeroize; +use zeroize::Zeroizing; pub mod validator_definitions; @@ -69,8 +65,8 @@ pub fn read_password>(path: P) -> Result { fs::read(path).map(strip_off_newlines).map(Into::into) } -/// Reads a password file into a `ZeroizeString` struct, with new-lines removed. -pub fn read_password_string>(path: P) -> Result { +/// Reads a password file into a `Zeroizing` struct, with new-lines removed. +pub fn read_password_string>(path: P) -> Result, String> { fs::read(path) .map_err(|e| format!("Error opening file: {:?}", e)) .map(strip_off_newlines) @@ -112,8 +108,8 @@ pub fn random_password() -> PlainText { random_password_raw_string().into_bytes().into() } -/// Generates a random alphanumeric password of length `DEFAULT_PASSWORD_LEN` as `ZeroizeString`. -pub fn random_password_string() -> ZeroizeString { +/// Generates a random alphanumeric password of length `DEFAULT_PASSWORD_LEN` as `Zeroizing`. +pub fn random_password_string() -> Zeroizing { random_password_raw_string().into() } @@ -141,7 +137,7 @@ pub fn strip_off_newlines(mut bytes: Vec) -> Vec { } /// Reads a password from TTY or stdin if `use_stdin == true`. -pub fn read_password_from_user(use_stdin: bool) -> Result { +pub fn read_password_from_user(use_stdin: bool) -> Result, String> { let result = if use_stdin { rpassword::prompt_password_stderr("") .map_err(|e| format!("Error reading from stdin: {}", e)) @@ -150,7 +146,7 @@ pub fn read_password_from_user(use_stdin: bool) -> Result .map_err(|e| format!("Error reading from tty: {}", e)) }; - result.map(ZeroizeString::from) + result.map(Zeroizing::from) } /// Reads a mnemonic phrase from TTY or stdin if `use_stdin == true`. @@ -210,46 +206,6 @@ pub fn mnemonic_from_phrase(phrase: &str) -> Result { Mnemonic::from_phrase(phrase, Language::English).map_err(|e| e.to_string()) } -/// Provides a new-type wrapper around `String` that is zeroized on `Drop`. -/// -/// Useful for ensuring that password memory is zeroed-out on drop. -#[derive(Clone, PartialEq, Serialize, Deserialize, Zeroize)] -#[zeroize(drop)] -#[serde(transparent)] -pub struct ZeroizeString(String); - -impl FromStr for ZeroizeString { - type Err = String; - - fn from_str(s: &str) -> Result { - Ok(Self(s.to_owned())) - } -} - -impl From for ZeroizeString { - fn from(s: String) -> Self { - Self(s) - } -} - -impl ZeroizeString { - pub fn as_str(&self) -> &str { - &self.0 - } - - /// Remove any number of newline or carriage returns from the end of a vector of bytes. - pub fn without_newlines(&self) -> ZeroizeString { - let stripped_string = self.0.trim_end_matches(['\r', '\n']).into(); - Self(stripped_string) - } -} - -impl AsRef<[u8]> for ZeroizeString { - fn as_ref(&self) -> &[u8] { - self.0.as_bytes() - } -} - pub fn read_mnemonic_from_cli( mnemonic_path: Option, stdin_inputs: bool, @@ -294,54 +250,6 @@ pub fn read_mnemonic_from_cli( mod test { use super::*; - #[test] - fn test_zeroize_strip_off() { - let expected = "hello world"; - - assert_eq!( - ZeroizeString::from("hello world\n".to_string()) - .without_newlines() - .as_str(), - expected - ); - assert_eq!( - ZeroizeString::from("hello world\n\n\n\n".to_string()) - .without_newlines() - .as_str(), - expected - ); - assert_eq!( - ZeroizeString::from("hello world\r".to_string()) - .without_newlines() - .as_str(), - expected - ); - assert_eq!( - ZeroizeString::from("hello world\r\r\r\r\r".to_string()) - .without_newlines() - .as_str(), - expected - ); - assert_eq!( - ZeroizeString::from("hello world\r\n".to_string()) - .without_newlines() - .as_str(), - expected - ); - assert_eq!( - ZeroizeString::from("hello world\r\n\r\n".to_string()) - .without_newlines() - .as_str(), - expected - ); - assert_eq!( - ZeroizeString::from("hello world".to_string()) - .without_newlines() - .as_str(), - expected - ); - } - #[test] fn test_strip_off() { let expected = b"hello world".to_vec(); diff --git a/common/account_utils/src/validator_definitions.rs b/common/account_utils/src/validator_definitions.rs index f228ce5fdf..a4850fc1c6 100644 --- a/common/account_utils/src/validator_definitions.rs +++ b/common/account_utils/src/validator_definitions.rs @@ -3,9 +3,7 @@ //! Serves as the source-of-truth of which validators this validator client should attempt (or not //! attempt) to load into the `crate::intialized_validators::InitializedValidators` struct. -use crate::{ - default_keystore_password_path, read_password_string, write_file_via_temporary, ZeroizeString, -}; +use crate::{default_keystore_password_path, read_password_string, write_file_via_temporary}; use directory::ensure_dir_exists; use eth2_keystore::Keystore; use regex::Regex; @@ -17,6 +15,7 @@ use std::io; use std::path::{Path, PathBuf}; use types::{graffiti::GraffitiString, Address, PublicKey}; use validator_dir::VOTING_KEYSTORE_FILE; +use zeroize::Zeroizing; /// The file name for the serialized `ValidatorDefinitions` struct. pub const CONFIG_FILENAME: &str = "validator_definitions.yml"; @@ -52,7 +51,7 @@ pub enum Error { /// Defines how a password for a validator keystore will be persisted. pub enum PasswordStorage { /// Store the password in the `validator_definitions.yml` file. - ValidatorDefinitions(ZeroizeString), + ValidatorDefinitions(Zeroizing), /// Store the password in a separate, dedicated file (likely in the "secrets" directory). File(PathBuf), /// Don't store the password at all. @@ -93,7 +92,7 @@ pub enum SigningDefinition { #[serde(skip_serializing_if = "Option::is_none")] voting_keystore_password_path: Option, #[serde(skip_serializing_if = "Option::is_none")] - voting_keystore_password: Option, + voting_keystore_password: Option>, }, /// A validator that defers to a Web3Signer HTTP server for signing. /// @@ -107,7 +106,7 @@ impl SigningDefinition { matches!(self, SigningDefinition::LocalKeystore { .. }) } - pub fn voting_keystore_password(&self) -> Result, Error> { + pub fn voting_keystore_password(&self) -> Result>, Error> { match self { SigningDefinition::LocalKeystore { voting_keystore_password: Some(password), diff --git a/common/eth2/Cargo.toml b/common/eth2/Cargo.toml index d23a4068f1..f735b4c688 100644 --- a/common/eth2/Cargo.toml +++ b/common/eth2/Cargo.toml @@ -16,10 +16,7 @@ lighthouse_network = { workspace = true } proto_array = { workspace = true } ethereum_serde_utils = { workspace = true } eth2_keystore = { workspace = true } -libsecp256k1 = { workspace = true } -ring = { workspace = true } -bytes = { workspace = true } -account_utils = { workspace = true } +zeroize = { workspace = true } sensitive_url = { workspace = true } ethereum_ssz = { workspace = true } ethereum_ssz_derive = { workspace = true } diff --git a/common/eth2/src/lighthouse_vc/http_client.rs b/common/eth2/src/lighthouse_vc/http_client.rs index 67fe77a315..1d1abcac79 100644 --- a/common/eth2/src/lighthouse_vc/http_client.rs +++ b/common/eth2/src/lighthouse_vc/http_client.rs @@ -1,6 +1,5 @@ use super::types::*; use crate::Error; -use account_utils::ZeroizeString; use reqwest::{ header::{HeaderMap, HeaderValue}, IntoUrl, @@ -14,6 +13,7 @@ use std::path::Path; pub use reqwest; pub use reqwest::{Response, StatusCode, Url}; use types::graffiti::GraffitiString; +use zeroize::Zeroizing; /// A wrapper around `reqwest::Client` which provides convenience methods for interfacing with a /// Lighthouse Validator Client HTTP server (`validator_client/src/http_api`). @@ -21,7 +21,7 @@ use types::graffiti::GraffitiString; pub struct ValidatorClientHttpClient { client: reqwest::Client, server: SensitiveUrl, - api_token: Option, + api_token: Option>, authorization_header: AuthorizationHeader, } @@ -79,18 +79,18 @@ impl ValidatorClientHttpClient { } /// Get a reference to this client's API token, if any. - pub fn api_token(&self) -> Option<&ZeroizeString> { + pub fn api_token(&self) -> Option<&Zeroizing> { self.api_token.as_ref() } /// Read an API token from the specified `path`, stripping any trailing whitespace. - pub fn load_api_token_from_file(path: &Path) -> Result { + pub fn load_api_token_from_file(path: &Path) -> Result, Error> { let token = fs::read_to_string(path).map_err(|e| Error::TokenReadError(path.into(), e))?; - Ok(ZeroizeString::from(token.trim_end().to_string())) + Ok(token.trim_end().to_string().into()) } /// Add an authentication token to use when making requests. - pub fn add_auth_token(&mut self, token: ZeroizeString) -> Result<(), Error> { + pub fn add_auth_token(&mut self, token: Zeroizing) -> Result<(), Error> { self.api_token = Some(token); self.authorization_header = AuthorizationHeader::Bearer; diff --git a/common/eth2/src/lighthouse_vc/std_types.rs b/common/eth2/src/lighthouse_vc/std_types.rs index ee05c29839..ae192312bd 100644 --- a/common/eth2/src/lighthouse_vc/std_types.rs +++ b/common/eth2/src/lighthouse_vc/std_types.rs @@ -1,7 +1,7 @@ -use account_utils::ZeroizeString; use eth2_keystore::Keystore; use serde::{Deserialize, Serialize}; use types::{Address, Graffiti, PublicKeyBytes}; +use zeroize::Zeroizing; pub use slashing_protection::interchange::Interchange; @@ -41,7 +41,7 @@ pub struct SingleKeystoreResponse { #[serde(deny_unknown_fields)] pub struct ImportKeystoresRequest { pub keystores: Vec, - pub passwords: Vec, + pub passwords: Vec>, pub slashing_protection: Option, } diff --git a/common/eth2/src/lighthouse_vc/types.rs b/common/eth2/src/lighthouse_vc/types.rs index 1921549bcb..d7d5a00df5 100644 --- a/common/eth2/src/lighthouse_vc/types.rs +++ b/common/eth2/src/lighthouse_vc/types.rs @@ -1,13 +1,12 @@ -use account_utils::ZeroizeString; +pub use crate::lighthouse::Health; +pub use crate::lighthouse_vc::std_types::*; +pub use crate::types::{GenericResponse, VersionData}; use eth2_keystore::Keystore; use graffiti::GraffitiString; use serde::{Deserialize, Serialize}; use std::path::PathBuf; - -pub use crate::lighthouse::Health; -pub use crate::lighthouse_vc::std_types::*; -pub use crate::types::{GenericResponse, VersionData}; pub use types::*; +use zeroize::Zeroizing; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct ValidatorData { @@ -44,7 +43,7 @@ pub struct ValidatorRequest { #[derive(Clone, PartialEq, Serialize, Deserialize)] pub struct CreateValidatorsMnemonicRequest { - pub mnemonic: ZeroizeString, + pub mnemonic: Zeroizing, #[serde(with = "serde_utils::quoted_u32")] pub key_derivation_path_offset: u32, pub validators: Vec, @@ -74,7 +73,7 @@ pub struct CreatedValidator { #[derive(Clone, PartialEq, Serialize, Deserialize)] pub struct PostValidatorsResponseData { - pub mnemonic: ZeroizeString, + pub mnemonic: Zeroizing, pub validators: Vec, } @@ -102,7 +101,7 @@ pub struct ValidatorPatchRequest { #[derive(Clone, PartialEq, Serialize, Deserialize)] pub struct KeystoreValidatorsPostRequest { - pub password: ZeroizeString, + pub password: Zeroizing, pub enable: bool, pub keystore: Keystore, #[serde(default)] @@ -191,7 +190,7 @@ pub struct SingleExportKeystoresResponse { #[serde(skip_serializing_if = "Option::is_none")] pub validating_keystore: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub validating_keystore_password: Option, + pub validating_keystore_password: Option>, } #[derive(Serialize, Deserialize, Debug)] diff --git a/crypto/eth2_keystore/src/keystore.rs b/crypto/eth2_keystore/src/keystore.rs index 304ea3ecd6..16a979cf63 100644 --- a/crypto/eth2_keystore/src/keystore.rs +++ b/crypto/eth2_keystore/src/keystore.rs @@ -26,7 +26,7 @@ use std::io::{Read, Write}; use std::path::Path; use std::str; use unicode_normalization::UnicodeNormalization; -use zeroize::Zeroize; +use zeroize::Zeroizing; /// The byte-length of a BLS secret key. const SECRET_KEY_LEN: usize = 32; @@ -60,45 +60,6 @@ pub const HASH_SIZE: usize = 32; /// The default iteraction count, `c`, for PBKDF2. pub const DEFAULT_PBKDF2_C: u32 = 262_144; -/// Provides a new-type wrapper around `String` that is zeroized on `Drop`. -/// -/// Useful for ensuring that password memory is zeroed-out on drop. -#[derive(Clone, PartialEq, Serialize, Deserialize, Zeroize)] -#[zeroize(drop)] -#[serde(transparent)] -struct ZeroizeString(String); - -impl From for ZeroizeString { - fn from(s: String) -> Self { - Self(s) - } -} - -impl AsRef<[u8]> for ZeroizeString { - fn as_ref(&self) -> &[u8] { - self.0.as_bytes() - } -} - -impl std::ops::Deref for ZeroizeString { - type Target = String; - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl std::ops::DerefMut for ZeroizeString { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl FromIterator for ZeroizeString { - fn from_iter>(iter: T) -> Self { - ZeroizeString(String::from_iter(iter)) - } -} - #[derive(Debug, PartialEq)] pub enum Error { InvalidSecretKeyLen { len: usize, expected: usize }, @@ -451,11 +412,12 @@ fn is_control_character(c: char) -> bool { /// Takes a slice of bytes and returns a NFKD normalized string representation. /// /// Returns an error if the bytes are not valid utf8. -fn normalize(bytes: &[u8]) -> Result { +fn normalize(bytes: &[u8]) -> Result, Error> { Ok(str::from_utf8(bytes) .map_err(|_| Error::InvalidPasswordBytes)? .nfkd() - .collect::()) + .collect::() + .into()) } /// Generates a checksum to indicate that the `derived_key` is associated with the diff --git a/lighthouse/Cargo.toml b/lighthouse/Cargo.toml index 329519fb54..1fd9e3dac8 100644 --- a/lighthouse/Cargo.toml +++ b/lighthouse/Cargo.toml @@ -73,6 +73,7 @@ eth2 = { workspace = true } beacon_processor = { workspace = true } beacon_node_fallback = { workspace = true } initialized_validators = { workspace = true } +zeroize = { workspace = true } [[test]] diff --git a/lighthouse/tests/account_manager.rs b/lighthouse/tests/account_manager.rs index 4d15593714..c7153f48ef 100644 --- a/lighthouse/tests/account_manager.rs +++ b/lighthouse/tests/account_manager.rs @@ -15,7 +15,7 @@ use account_manager::{ use account_utils::{ eth2_keystore::KeystoreBuilder, validator_definitions::{SigningDefinition, ValidatorDefinition, ValidatorDefinitions}, - ZeroizeString, STDIN_INPUTS_FLAG, + STDIN_INPUTS_FLAG, }; use slashing_protection::{SlashingDatabase, SLASHING_PROTECTION_FILENAME}; use std::env; @@ -27,6 +27,7 @@ use std::str::from_utf8; use tempfile::{tempdir, TempDir}; use types::{Keypair, PublicKey}; use validator_dir::ValidatorDir; +use zeroize::Zeroizing; /// Returns the `lighthouse account` command. fn account_cmd() -> Command { @@ -498,7 +499,7 @@ fn validator_import_launchpad() { signing_definition: SigningDefinition::LocalKeystore { voting_keystore_path, voting_keystore_password_path: None, - voting_keystore_password: Some(ZeroizeString::from(PASSWORD.to_string())), + voting_keystore_password: Some(Zeroizing::from(PASSWORD.to_string())), }, }; @@ -650,7 +651,7 @@ fn validator_import_launchpad_no_password_then_add_password() { signing_definition: SigningDefinition::LocalKeystore { voting_keystore_path: dst_keystore_dir.join(KEYSTORE_NAME), voting_keystore_password_path: None, - voting_keystore_password: Some(ZeroizeString::from(PASSWORD.to_string())), + voting_keystore_password: Some(Zeroizing::from(PASSWORD.to_string())), }, }; @@ -753,7 +754,7 @@ fn validator_import_launchpad_password_file() { signing_definition: SigningDefinition::LocalKeystore { voting_keystore_path, voting_keystore_password_path: None, - voting_keystore_password: Some(ZeroizeString::from(PASSWORD.to_string())), + voting_keystore_password: Some(Zeroizing::from(PASSWORD.to_string())), }, }; diff --git a/validator_client/http_api/Cargo.toml b/validator_client/http_api/Cargo.toml index b83acdc782..18e0604ad5 100644 --- a/validator_client/http_api/Cargo.toml +++ b/validator_client/http_api/Cargo.toml @@ -43,6 +43,7 @@ validator_services = { workspace = true } url = { workspace = true } warp_utils = { workspace = true } warp = { workspace = true } +zeroize = { workspace = true } [dev-dependencies] itertools = { workspace = true } diff --git a/validator_client/http_api/src/create_validator.rs b/validator_client/http_api/src/create_validator.rs index dfd092e8b4..f90a1057a4 100644 --- a/validator_client/http_api/src/create_validator.rs +++ b/validator_client/http_api/src/create_validator.rs @@ -2,7 +2,7 @@ use account_utils::validator_definitions::{PasswordStorage, ValidatorDefinition} use account_utils::{ eth2_keystore::Keystore, eth2_wallet::{bip39::Mnemonic, WalletBuilder}, - random_mnemonic, random_password, ZeroizeString, + random_mnemonic, random_password, }; use eth2::lighthouse_vc::types::{self as api_types}; use slot_clock::SlotClock; @@ -11,6 +11,7 @@ use types::ChainSpec; use types::EthSpec; use validator_dir::{keystore_password_path, Builder as ValidatorDirBuilder}; use validator_store::ValidatorStore; +use zeroize::Zeroizing; /// Create some validator EIP-2335 keystores and store them on disk. Then, enroll the validators in /// this validator client. @@ -59,7 +60,7 @@ pub async fn create_validators_mnemonic, T: 'static + SlotClock, for request in validator_requests { let voting_password = random_password(); let withdrawal_password = random_password(); - let voting_password_string = ZeroizeString::from( + let voting_password_string = Zeroizing::from( String::from_utf8(voting_password.as_bytes().to_vec()).map_err(|e| { warp_utils::reject::custom_server_error(format!( "locally generated password is not utf8: {:?}", @@ -199,7 +200,7 @@ pub async fn create_validators_web3signer( pub fn get_voting_password_storage( secrets_dir: &Option, voting_keystore: &Keystore, - voting_password_string: &ZeroizeString, + voting_password_string: &Zeroizing, ) -> Result { if let Some(secrets_dir) = &secrets_dir { let password_path = keystore_password_path(secrets_dir, voting_keystore); diff --git a/validator_client/http_api/src/keystores.rs b/validator_client/http_api/src/keystores.rs index 5822c89cb8..fd6b4fdae5 100644 --- a/validator_client/http_api/src/keystores.rs +++ b/validator_client/http_api/src/keystores.rs @@ -1,5 +1,5 @@ //! Implementation of the standard keystore management API. -use account_utils::{validator_definitions::PasswordStorage, ZeroizeString}; +use account_utils::validator_definitions::PasswordStorage; use eth2::lighthouse_vc::{ std_types::{ DeleteKeystoreStatus, DeleteKeystoresRequest, DeleteKeystoresResponse, @@ -22,6 +22,7 @@ use validator_dir::{keystore_password_path, Builder as ValidatorDirBuilder}; use validator_store::ValidatorStore; use warp::Rejection; use warp_utils::reject::{custom_bad_request, custom_server_error}; +use zeroize::Zeroizing; pub fn list( validator_store: Arc>, @@ -167,7 +168,7 @@ pub fn import( fn import_single_keystore( keystore: Keystore, - password: ZeroizeString, + password: Zeroizing, validator_dir_path: PathBuf, secrets_dir: Option, validator_store: &ValidatorStore, diff --git a/validator_client/http_api/src/test_utils.rs b/validator_client/http_api/src/test_utils.rs index 931c4ea08e..d033fdbf2d 100644 --- a/validator_client/http_api/src/test_utils.rs +++ b/validator_client/http_api/src/test_utils.rs @@ -2,7 +2,6 @@ use crate::{ApiSecret, Config as HttpConfig, Context}; use account_utils::validator_definitions::ValidatorDefinitions; use account_utils::{ eth2_wallet::WalletBuilder, mnemonic_from_phrase, random_mnemonic, random_password, - ZeroizeString, }; use deposit_contract::decode_eth1_tx_data; use doppelganger_service::DoppelgangerService; @@ -28,6 +27,7 @@ use task_executor::test_utils::TestRuntime; use tempfile::{tempdir, TempDir}; use tokio::sync::oneshot; use validator_store::{Config as ValidatorStoreConfig, ValidatorStore}; +use zeroize::Zeroizing; pub const PASSWORD_BYTES: &[u8] = &[42, 50, 37]; pub const TEST_DEFAULT_FEE_RECIPIENT: Address = Address::repeat_byte(42); @@ -321,7 +321,7 @@ impl ApiTester { .collect::>(); let (response, mnemonic) = if s.specify_mnemonic { - let mnemonic = ZeroizeString::from(random_mnemonic().phrase().to_string()); + let mnemonic = Zeroizing::from(random_mnemonic().phrase().to_string()); let request = CreateValidatorsMnemonicRequest { mnemonic: mnemonic.clone(), key_derivation_path_offset: s.key_derivation_path_offset, diff --git a/validator_client/http_api/src/tests.rs b/validator_client/http_api/src/tests.rs index 76a6952153..262bb64e69 100644 --- a/validator_client/http_api/src/tests.rs +++ b/validator_client/http_api/src/tests.rs @@ -9,7 +9,7 @@ use initialized_validators::{Config as InitializedValidatorsConfig, InitializedV use crate::{ApiSecret, Config as HttpConfig, Context}; use account_utils::{ eth2_wallet::WalletBuilder, mnemonic_from_phrase, random_mnemonic, random_password, - random_password_string, validator_definitions::ValidatorDefinitions, ZeroizeString, + random_password_string, validator_definitions::ValidatorDefinitions, }; use deposit_contract::decode_eth1_tx_data; use eth2::{ @@ -33,6 +33,7 @@ use task_executor::test_utils::TestRuntime; use tempfile::{tempdir, TempDir}; use types::graffiti::GraffitiString; use validator_store::{Config as ValidatorStoreConfig, ValidatorStore}; +use zeroize::Zeroizing; const PASSWORD_BYTES: &[u8] = &[42, 50, 37]; pub const TEST_DEFAULT_FEE_RECIPIENT: Address = Address::repeat_byte(42); @@ -282,7 +283,7 @@ impl ApiTester { .collect::>(); let (response, mnemonic) = if s.specify_mnemonic { - let mnemonic = ZeroizeString::from(random_mnemonic().phrase().to_string()); + let mnemonic = Zeroizing::from(random_mnemonic().phrase().to_string()); let request = CreateValidatorsMnemonicRequest { mnemonic: mnemonic.clone(), key_derivation_path_offset: s.key_derivation_path_offset, diff --git a/validator_client/http_api/src/tests/keystores.rs b/validator_client/http_api/src/tests/keystores.rs index f3f6de548b..2dde087a7f 100644 --- a/validator_client/http_api/src/tests/keystores.rs +++ b/validator_client/http_api/src/tests/keystores.rs @@ -14,8 +14,9 @@ use std::{collections::HashMap, path::Path}; use tokio::runtime::Handle; use types::{attestation::AttestationBase, Address}; use validator_store::DEFAULT_GAS_LIMIT; +use zeroize::Zeroizing; -fn new_keystore(password: ZeroizeString) -> Keystore { +fn new_keystore(password: Zeroizing) -> Keystore { let keypair = Keypair::random(); Keystore( KeystoreBuilder::new(&keypair, password.as_ref(), String::new()) diff --git a/validator_client/initialized_validators/Cargo.toml b/validator_client/initialized_validators/Cargo.toml index 426cb303f6..9c7a3f19d6 100644 --- a/validator_client/initialized_validators/Cargo.toml +++ b/validator_client/initialized_validators/Cargo.toml @@ -24,3 +24,4 @@ tokio = { workspace = true } bincode = { workspace = true } filesystem = { workspace = true } validator_metrics = { workspace = true } +zeroize = { workspace = true } diff --git a/validator_client/initialized_validators/src/lib.rs b/validator_client/initialized_validators/src/lib.rs index 0b36dbd62c..bd64091dae 100644 --- a/validator_client/initialized_validators/src/lib.rs +++ b/validator_client/initialized_validators/src/lib.rs @@ -14,7 +14,6 @@ use account_utils::{ self, SigningDefinition, ValidatorDefinition, ValidatorDefinitions, Web3SignerDefinition, CONFIG_FILENAME, }, - ZeroizeString, }; use eth2_keystore::Keystore; use lockfile::{Lockfile, LockfileError}; @@ -34,6 +33,7 @@ use types::graffiti::GraffitiString; use types::{Address, Graffiti, Keypair, PublicKey, PublicKeyBytes}; use url::{ParseError, Url}; use validator_dir::Builder as ValidatorDirBuilder; +use zeroize::Zeroizing; use key_cache::KeyCache; @@ -74,7 +74,7 @@ pub enum OnDecryptFailure { pub struct KeystoreAndPassword { pub keystore: Keystore, - pub password: Option, + pub password: Option>, } #[derive(Debug)] @@ -262,7 +262,7 @@ impl InitializedValidator { // If the password is supplied, use it and ignore the path // (if supplied). (_, Some(password)) => ( - password.as_ref().to_vec().into(), + password.as_bytes().to_vec().into(), keystore .decrypt_keypair(password.as_ref()) .map_err(Error::UnableToDecryptKeystore)?, @@ -282,7 +282,7 @@ impl InitializedValidator { &keystore, &keystore_path, )?; - (password.as_ref().to_vec().into(), keypair) + (password.as_bytes().to_vec().into(), keypair) } }, ) @@ -455,7 +455,7 @@ fn build_web3_signer_client( fn unlock_keystore_via_stdin_password( keystore: &Keystore, keystore_path: &Path, -) -> Result<(ZeroizeString, Keypair), Error> { +) -> Result<(Zeroizing, Keypair), Error> { eprintln!(); eprintln!( "The {} file does not contain either of the following fields for {:?}:", @@ -1172,14 +1172,14 @@ impl InitializedValidators { voting_keystore_path, } => { let pw = if let Some(p) = voting_keystore_password { - p.as_ref().to_vec().into() + p.as_bytes().to_vec().into() } else if let Some(path) = voting_keystore_password_path { read_password(path).map_err(Error::UnableToReadVotingKeystorePassword)? } else { let keystore = open_keystore(voting_keystore_path)?; unlock_keystore_via_stdin_password(&keystore, voting_keystore_path)? .0 - .as_ref() + .as_bytes() .to_vec() .into() }; @@ -1425,7 +1425,7 @@ impl InitializedValidators { /// This should only be used for testing, it's rather destructive. pub fn delete_passwords_from_validator_definitions( &mut self, - ) -> Result, Error> { + ) -> Result>, Error> { let mut passwords = HashMap::default(); for def in self.definitions.as_mut_slice() { diff --git a/validator_manager/Cargo.toml b/validator_manager/Cargo.toml index 4f367b8f5b..36df256841 100644 --- a/validator_manager/Cargo.toml +++ b/validator_manager/Cargo.toml @@ -21,6 +21,7 @@ eth2 = { workspace = true } hex = { workspace = true } tokio = { workspace = true } derivative = { workspace = true } +zeroize = { workspace = true } [dev-dependencies] tempfile = { workspace = true } diff --git a/validator_manager/src/common.rs b/validator_manager/src/common.rs index 4a35791b32..cc4157990f 100644 --- a/validator_manager/src/common.rs +++ b/validator_manager/src/common.rs @@ -1,5 +1,5 @@ +use account_utils::strip_off_newlines; pub use account_utils::STDIN_INPUTS_FLAG; -use account_utils::{strip_off_newlines, ZeroizeString}; use eth2::lighthouse_vc::std_types::{InterchangeJsonStr, KeystoreJsonStr}; use eth2::{ lighthouse_vc::{ @@ -14,6 +14,7 @@ use std::fs; use std::path::{Path, PathBuf}; use tree_hash::TreeHash; use types::*; +use zeroize::Zeroizing; pub const IGNORE_DUPLICATES_FLAG: &str = "ignore-duplicates"; pub const COUNT_FLAG: &str = "count"; @@ -41,7 +42,7 @@ pub enum UploadError { #[derive(Clone, Serialize, Deserialize)] pub struct ValidatorSpecification { pub voting_keystore: KeystoreJsonStr, - pub voting_keystore_password: ZeroizeString, + pub voting_keystore_password: Zeroizing, pub slashing_protection: Option, pub fee_recipient: Option

, pub gas_limit: Option, diff --git a/validator_manager/src/import_validators.rs b/validator_manager/src/import_validators.rs index 2a819a2a64..2e8821f0db 100644 --- a/validator_manager/src/import_validators.rs +++ b/validator_manager/src/import_validators.rs @@ -1,6 +1,6 @@ use super::common::*; use crate::DumpConfig; -use account_utils::{eth2_keystore::Keystore, ZeroizeString}; +use account_utils::eth2_keystore::Keystore; use clap::{Arg, ArgAction, ArgMatches, Command}; use clap_utils::FLAG_HEADER; use derivative::Derivative; @@ -10,6 +10,7 @@ use serde::{Deserialize, Serialize}; use std::fs; use std::path::PathBuf; use types::Address; +use zeroize::Zeroizing; pub const CMD: &str = "import"; pub const VALIDATORS_FILE_FLAG: &str = "validators-file"; @@ -167,7 +168,7 @@ pub struct ImportConfig { pub vc_token_path: PathBuf, pub ignore_duplicates: bool, #[derivative(Debug = "ignore")] - pub password: Option, + pub password: Option>, pub fee_recipient: Option
, pub gas_limit: Option, pub builder_proposals: Option, @@ -184,7 +185,7 @@ impl ImportConfig { vc_url: clap_utils::parse_required(matches, VC_URL_FLAG)?, vc_token_path: clap_utils::parse_required(matches, VC_TOKEN_FLAG)?, ignore_duplicates: matches.get_flag(IGNORE_DUPLICATES_FLAG), - password: clap_utils::parse_optional(matches, PASSWORD)?, + password: clap_utils::parse_optional(matches, PASSWORD)?.map(Zeroizing::new), fee_recipient: clap_utils::parse_optional(matches, FEE_RECIPIENT)?, gas_limit: clap_utils::parse_optional(matches, GAS_LIMIT)?, builder_proposals: clap_utils::parse_optional(matches, BUILDER_PROPOSALS)?, @@ -382,10 +383,7 @@ async fn run<'a>(config: ImportConfig) -> Result<(), String> { pub mod tests { use super::*; use crate::create_validators::tests::TestBuilder as CreateTestBuilder; - use std::{ - fs::{self, File}, - str::FromStr, - }; + use std::fs::{self, File}; use tempfile::{tempdir, TempDir}; use validator_http_api::{test_utils::ApiTester, Config as HttpConfig}; @@ -419,7 +417,7 @@ pub mod tests { vc_url: vc.url.clone(), vc_token_path, ignore_duplicates: false, - password: Some(ZeroizeString::from_str("password").unwrap()), + password: Some(Zeroizing::new("password".into())), fee_recipient: None, builder_boost_factor: None, gas_limit: None, diff --git a/validator_manager/src/move_validators.rs b/validator_manager/src/move_validators.rs index 807a147ca1..c039728e6f 100644 --- a/validator_manager/src/move_validators.rs +++ b/validator_manager/src/move_validators.rs @@ -1,6 +1,6 @@ use super::common::*; use crate::DumpConfig; -use account_utils::{read_password_from_user, ZeroizeString}; +use account_utils::read_password_from_user; use clap::{Arg, ArgAction, ArgMatches, Command}; use eth2::{ lighthouse_vc::{ @@ -19,6 +19,7 @@ use std::str::FromStr; use std::time::Duration; use tokio::time::sleep; use types::{Address, PublicKeyBytes}; +use zeroize::Zeroizing; pub const MOVE_DIR_NAME: &str = "lighthouse-validator-move"; pub const VALIDATOR_SPECIFICATION_FILE: &str = "validator-specification.json"; @@ -48,7 +49,7 @@ pub enum PasswordSource { } impl PasswordSource { - fn read_password(&mut self, pubkey: &PublicKeyBytes) -> Result { + fn read_password(&mut self, pubkey: &PublicKeyBytes) -> Result, String> { match self { PasswordSource::Interactive { stdin_inputs } => { eprintln!("Please enter a password for keystore {:?}:", pubkey); From b7ffcc8229e028bf43ddca5c5924b9ec10bd6931 Mon Sep 17 00:00:00 2001 From: antondlr Date: Thu, 12 Dec 2024 01:24:58 +0100 Subject: [PATCH 032/254] Fix: Docker CI to use org tokens (#6655) * update Dockerhub creds to new scheme * Merge branch 'release-v6.0.1' into fix-docker-ci --- .github/workflows/docker.yml | 4 ++-- .github/workflows/release.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index cd45bd6d98..e768208973 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -13,8 +13,8 @@ concurrency: cancel-in-progress: true env: - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} + DOCKER_PASSWORD: ${{ secrets.DH_KEY }} + DOCKER_USERNAME: ${{ secrets.DH_ORG }} # Enable self-hosted runners for the sigp repo only. SELF_HOSTED_RUNNERS: ${{ github.repository == 'sigp/lighthouse' }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f1ec2e4655..cfba601fad 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,8 +10,8 @@ concurrency: cancel-in-progress: true env: - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} + DOCKER_PASSWORD: ${{ secrets.DH_KEY }} + DOCKER_USERNAME: ${{ secrets.DH_ORG }} REPO_NAME: ${{ github.repository_owner }}/lighthouse IMAGE_NAME: ${{ github.repository_owner }}/lighthouse # Enable self-hosted runners for the sigp repo only. From fc0e0ae613a479a21e931b200f88b6e4ff9e6681 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Thu, 12 Dec 2024 12:58:41 +1100 Subject: [PATCH 033/254] Prevent reconstruction starting prematurely (#6669) * Prevent reconstruction starting prematurely * Simplify condition * Merge remote-tracking branch 'origin/release-v6.0.1' into dont-start-reconstruction-early --- beacon_node/beacon_chain/src/builder.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index 589db0af50..9d99ff9d8e 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -1037,7 +1037,9 @@ where ); // Check for states to reconstruct (in the background). - if beacon_chain.config.reconstruct_historic_states { + if beacon_chain.config.reconstruct_historic_states + && beacon_chain.store.get_oldest_block_slot() == 0 + { beacon_chain.store_migrator.process_reconstruction(); } From 494634399027b94f31759ba5bb4d3a5d2aaff503 Mon Sep 17 00:00:00 2001 From: Povilas Liubauskas Date: Thu, 12 Dec 2024 10:36:34 +0200 Subject: [PATCH 034/254] Fix subscribing to attestation subnets for aggregating (#6681) (#6682) * Fix subscribing to attestation subnets for aggregating (#6681) * Prevent scheduled subnet subscriptions from being overwritten by other subscriptions from same subnet with additional scoping by slot --- beacon_node/network/src/subnet_service/mod.rs | 9 ++- .../network/src/subnet_service/tests/mod.rs | 55 +++++++++++++++++-- 2 files changed, 57 insertions(+), 7 deletions(-) diff --git a/beacon_node/network/src/subnet_service/mod.rs b/beacon_node/network/src/subnet_service/mod.rs index ec6f3b10a3..da1f220f04 100644 --- a/beacon_node/network/src/subnet_service/mod.rs +++ b/beacon_node/network/src/subnet_service/mod.rs @@ -86,7 +86,7 @@ pub struct SubnetService { subscriptions: HashSetDelay, /// Subscriptions that need to be executed in the future. - scheduled_subscriptions: HashSetDelay, + scheduled_subscriptions: HashSetDelay, /// A list of permanent subnets that this node is subscribed to. // TODO: Shift this to a dynamic bitfield @@ -484,8 +484,10 @@ impl SubnetService { self.subscribe_to_subnet_immediately(subnet, slot + 1)?; } else { // This is a future slot, schedule subscribing. + // We need to include the slot to make the key unique to prevent overwriting the entry + // for the same subnet. self.scheduled_subscriptions - .insert_at(subnet, time_to_subscription_start); + .insert_at(ExactSubnet { subnet, slot }, time_to_subscription_start); } Ok(()) @@ -626,7 +628,8 @@ impl Stream for SubnetService { // Process scheduled subscriptions that might be ready, since those can extend a soon to // expire subscription. match self.scheduled_subscriptions.poll_next_unpin(cx) { - Poll::Ready(Some(Ok(subnet))) => { + Poll::Ready(Some(Ok(exact_subnet))) => { + let ExactSubnet { subnet, .. } = exact_subnet; let current_slot = self.beacon_chain.slot_clock.now().unwrap_or_default(); if let Err(e) = self.subscribe_to_subnet_immediately(subnet, current_slot + 1) { debug!(self.log, "Failed to subscribe to short lived subnet"; "subnet" => ?subnet, "err" => e); diff --git a/beacon_node/network/src/subnet_service/tests/mod.rs b/beacon_node/network/src/subnet_service/tests/mod.rs index 91e4841b26..7283b4af31 100644 --- a/beacon_node/network/src/subnet_service/tests/mod.rs +++ b/beacon_node/network/src/subnet_service/tests/mod.rs @@ -500,12 +500,15 @@ mod test { // subscription config let committee_count = 1; - // Makes 2 validator subscriptions to the same subnet but at different slots. - // There should be just 1 unsubscription event for the later slot subscription (subscription_slot2). + // Makes 3 validator subscriptions to the same subnet but at different slots. + // There should be just 1 unsubscription event for each of the later slots subscriptions + // (subscription_slot2 and subscription_slot3). let subscription_slot1 = 0; let subscription_slot2 = MIN_PEER_DISCOVERY_SLOT_LOOK_AHEAD + 4; + let subscription_slot3 = subscription_slot2 * 2; let com1 = MIN_PEER_DISCOVERY_SLOT_LOOK_AHEAD + 4; let com2 = 0; + let com3 = CHAIN.chain.spec.attestation_subnet_count - com1; // create the attestation service and subscriptions let mut subnet_service = get_subnet_service(); @@ -532,6 +535,13 @@ mod test { true, ); + let sub3 = get_subscription( + com3, + current_slot + Slot::new(subscription_slot3), + committee_count, + true, + ); + let subnet_id1 = SubnetId::compute_subnet::( current_slot + Slot::new(subscription_slot1), com1, @@ -548,12 +558,23 @@ mod test { ) .unwrap(); + let subnet_id3 = SubnetId::compute_subnet::( + current_slot + Slot::new(subscription_slot3), + com3, + committee_count, + &subnet_service.beacon_chain.spec, + ) + .unwrap(); + // Assert that subscriptions are different but their subnet is the same assert_ne!(sub1, sub2); + assert_ne!(sub1, sub3); + assert_ne!(sub2, sub3); assert_eq!(subnet_id1, subnet_id2); + assert_eq!(subnet_id1, subnet_id3); // submit the subscriptions - subnet_service.validator_subscriptions(vec![sub1, sub2].into_iter()); + subnet_service.validator_subscriptions(vec![sub1, sub2, sub3].into_iter()); // Unsubscription event should happen at the end of the slot. // We wait for 2 slots, to avoid timeout issues @@ -590,10 +611,36 @@ mod test { // If the permanent and short lived subnets are different, we should get an unsubscription event. if !subnet_service.is_subscribed(&Subnet::Attestation(subnet_id1)) { assert_eq!( - [expected_subscription, expected_unsubscription], + [ + expected_subscription.clone(), + expected_unsubscription.clone(), + ], second_subscribe_event[..] ); } + + let subscription_slot = current_slot + subscription_slot3 - 1; + + let wait_slots = subnet_service + .beacon_chain + .slot_clock + .duration_to_slot(subscription_slot) + .unwrap() + .as_millis() as u64 + / SLOT_DURATION_MILLIS; + + let no_events = dbg!(get_events(&mut subnet_service, None, wait_slots as u32).await); + + assert_eq!(no_events, []); + + let third_subscribe_event = get_events(&mut subnet_service, None, 2).await; + + if !subnet_service.is_subscribed(&Subnet::Attestation(subnet_id1)) { + assert_eq!( + [expected_subscription, expected_unsubscription], + third_subscribe_event[..] + ); + } } #[tokio::test] From 775fa6730b2ddd60b87344761cccf7a05b2a72d4 Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Fri, 13 Dec 2024 13:02:10 +0800 Subject: [PATCH 035/254] Stuck lookup v6 (#6658) * Fix stuck lookups if no peers on v6 * Merge branch 'release-v6.0.1' into stuck-lookup-v6 --- .../network/src/sync/block_lookups/single_block_lookup.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 d701cbbb8d..9bbd2bf295 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 @@ -171,7 +171,10 @@ impl SingleBlockLookup { self.awaiting_parent.is_some() || self.block_request_state.state.is_awaiting_event() || match &self.component_requests { - ComponentRequests::WaitingForBlock => true, + // If components are waiting for the block request to complete, here we should + // check if the`block_request_state.state.is_awaiting_event(). However we already + // checked that above, so `WaitingForBlock => false` is equivalent. + ComponentRequests::WaitingForBlock => false, ComponentRequests::ActiveBlobRequest(request, _) => { request.state.is_awaiting_event() } From 943716b9a22c1c2589739f3f5af4725f69f48c3a Mon Sep 17 00:00:00 2001 From: Shayan Eskandari Date: Fri, 13 Dec 2024 00:07:01 -0500 Subject: [PATCH 036/254] Fix for blank line in graffiti file (#6635) * Fix for blank line in graffiti file Fix as described in https://github.com/sigp/lighthouse/issues/5880 * add graffiti new line tests * cargo fmt --- validator_client/graffiti_file/src/lib.rs | 52 +++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/validator_client/graffiti_file/src/lib.rs b/validator_client/graffiti_file/src/lib.rs index 0328c14eeb..9dab2e7827 100644 --- a/validator_client/graffiti_file/src/lib.rs +++ b/validator_client/graffiti_file/src/lib.rs @@ -66,6 +66,9 @@ impl GraffitiFile { for line in lines { let line = line.map_err(|e| Error::InvalidLine(e.to_string()))?; + if line.trim().is_empty() { + continue; + } let (pk_opt, graffiti) = read_line(&line)?; match pk_opt { Some(pk) => { @@ -133,9 +136,15 @@ mod tests { const CUSTOM_GRAFFITI1: &str = "custom-graffiti1"; const CUSTOM_GRAFFITI2: &str = "graffitiwall:720:641:#ffff00"; const EMPTY_GRAFFITI: &str = ""; + // Newline test cases + const CUSTOM_GRAFFITI4: &str = "newlines-tests"; + const PK1: &str = "0x800012708dc03f611751aad7a43a082142832b5c1aceed07ff9b543cf836381861352aa923c70eeb02018b638aa306aa"; const PK2: &str = "0x80001866ce324de7d80ec73be15e2d064dcf121adf1b34a0d679f2b9ecbab40ce021e03bb877e1a2fe72eaaf475e6e21"; const PK3: &str = "0x9035d41a8bc11b08c17d0d93d876087958c9d055afe86fce558e3b988d92434769c8d50b0b463708db80c6aae1160c02"; + const PK4: &str = "0x8c0fca2cc70f44188a4b79e5623ac85898f1df479e14a1f4ebb615907810b6fb939c3fb4ba2081b7a5b6e33dc73621d2"; + const PK5: &str = "0x87998b0ea4a8826f03d1985e5a5ce7235bd3a56fb7559b02a55b737f4ebc69b0bf35444de5cf2680cb7eb2283eb62050"; + const PK6: &str = "0xa2af9b128255568e2ee5c42af118cc4301198123d210dbdbf2ca7ec0222f8d491f308e85076b09a2f44a75875cd6fa0f"; // Create a graffiti file in the required format and return a path to the file. fn create_graffiti_file() -> PathBuf { @@ -143,6 +152,9 @@ mod tests { let pk1 = PublicKeyBytes::deserialize(&hex::decode(&PK1[2..]).unwrap()).unwrap(); let pk2 = PublicKeyBytes::deserialize(&hex::decode(&PK2[2..]).unwrap()).unwrap(); let pk3 = PublicKeyBytes::deserialize(&hex::decode(&PK3[2..]).unwrap()).unwrap(); + let pk4 = PublicKeyBytes::deserialize(&hex::decode(&PK4[2..]).unwrap()).unwrap(); + let pk5 = PublicKeyBytes::deserialize(&hex::decode(&PK5[2..]).unwrap()).unwrap(); + let pk6 = PublicKeyBytes::deserialize(&hex::decode(&PK6[2..]).unwrap()).unwrap(); let file_name = temp.into_path().join("graffiti.txt"); @@ -160,6 +172,29 @@ mod tests { graffiti_file .write_all(format!("{}:{}\n", pk3.as_hex_string(), EMPTY_GRAFFITI).as_bytes()) .unwrap(); + + // Test Lines with leading newlines - these empty lines will be skipped + graffiti_file.write_all(b"\n").unwrap(); + graffiti_file.write_all(b" \n").unwrap(); + graffiti_file + .write_all(format!("{}: {}\n", pk4.as_hex_string(), CUSTOM_GRAFFITI4).as_bytes()) + .unwrap(); + + // Test Empty lines between entries - these will be skipped + graffiti_file.write_all(b"\n").unwrap(); + graffiti_file.write_all(b" \n").unwrap(); + graffiti_file.write_all(b"\t\n").unwrap(); + graffiti_file + .write_all(format!("{}: {}\n", pk5.as_hex_string(), CUSTOM_GRAFFITI4).as_bytes()) + .unwrap(); + + // Test Trailing empty lines - these will be skipped + graffiti_file + .write_all(format!("{}: {}\n", pk6.as_hex_string(), CUSTOM_GRAFFITI4).as_bytes()) + .unwrap(); + graffiti_file.write_all(b"\n").unwrap(); + graffiti_file.write_all(b" \n").unwrap(); + graffiti_file.flush().unwrap(); file_name } @@ -172,6 +207,9 @@ mod tests { let pk1 = PublicKeyBytes::deserialize(&hex::decode(&PK1[2..]).unwrap()).unwrap(); let pk2 = PublicKeyBytes::deserialize(&hex::decode(&PK2[2..]).unwrap()).unwrap(); let pk3 = PublicKeyBytes::deserialize(&hex::decode(&PK3[2..]).unwrap()).unwrap(); + let pk4 = PublicKeyBytes::deserialize(&hex::decode(&PK4[2..]).unwrap()).unwrap(); + let pk5 = PublicKeyBytes::deserialize(&hex::decode(&PK5[2..]).unwrap()).unwrap(); + let pk6 = PublicKeyBytes::deserialize(&hex::decode(&PK6[2..]).unwrap()).unwrap(); // Read once gf.read_graffiti_file().unwrap(); @@ -190,6 +228,20 @@ mod tests { GraffitiString::from_str(EMPTY_GRAFFITI).unwrap().into() ); + // Test newline cases - all empty lines should be skipped + assert_eq!( + gf.load_graffiti(&pk4).unwrap().unwrap(), + GraffitiString::from_str(CUSTOM_GRAFFITI4).unwrap().into() + ); + assert_eq!( + gf.load_graffiti(&pk5).unwrap().unwrap(), + GraffitiString::from_str(CUSTOM_GRAFFITI4).unwrap().into() + ); + assert_eq!( + gf.load_graffiti(&pk6).unwrap().unwrap(), + GraffitiString::from_str(CUSTOM_GRAFFITI4).unwrap().into() + ); + // Random pk should return the default graffiti let random_pk = Keypair::random().pk.compress(); assert_eq!( From d49e1be35d3776bb6ce074d9446b6ff3663bf7fe Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Fri, 13 Dec 2024 16:44:41 +1100 Subject: [PATCH 037/254] Remove heading that isn't rendered correctly (#6650) * Remove heading that isn't rendered correctly --- book/src/security.md | 1 - 1 file changed, 1 deletion(-) diff --git a/book/src/security.md b/book/src/security.md index 43fa0afc8f..0af57db7f9 100644 --- a/book/src/security.md +++ b/book/src/security.md @@ -1,6 +1,5 @@ # Security -======== Lighthouse takes security seriously. Please see our security policy on GitHub for our PGP key and information on reporting vulnerabilities: - [GitHub: Security Policy](https://github.com/sigp/lighthouse/blob/stable/SECURITY.md) From f3b78889e50752f40e6d371621764b49bca4090f Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Sat, 14 Dec 2024 19:43:00 +1100 Subject: [PATCH 038/254] Compact more when pruning states (#6667) * Compact more when pruning states * Merge branch 'release-v6.0.1' into compact-more --- .../src/schema_change/migration_schema_v22.rs | 2 +- beacon_node/store/src/hot_cold_store.rs | 42 ++++++++++++++++++- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/beacon_node/beacon_chain/src/schema_change/migration_schema_v22.rs b/beacon_node/beacon_chain/src/schema_change/migration_schema_v22.rs index f532c0e672..c34512eded 100644 --- a/beacon_node/beacon_chain/src/schema_change/migration_schema_v22.rs +++ b/beacon_node/beacon_chain/src/schema_change/migration_schema_v22.rs @@ -152,7 +152,7 @@ pub fn delete_old_schema_freezer_data( db.cold_db.do_atomically(cold_ops)?; // In order to reclaim space, we need to compact the freezer DB as well. - db.cold_db.compact()?; + db.compact_freezer()?; Ok(()) } diff --git a/beacon_node/store/src/hot_cold_store.rs b/beacon_node/store/src/hot_cold_store.rs index 4942b14881..da3e6d4ebc 100644 --- a/beacon_node/store/src/hot_cold_store.rs +++ b/beacon_node/store/src/hot_cold_store.rs @@ -2484,6 +2484,45 @@ impl, Cold: ItemStore> HotColdDB Ok(()) } + /// Run a compaction pass on the freezer DB to free up space used by deleted states. + pub fn compact_freezer(&self) -> Result<(), Error> { + let current_schema_columns = vec![ + DBColumn::BeaconColdStateSummary, + DBColumn::BeaconStateSnapshot, + DBColumn::BeaconStateDiff, + DBColumn::BeaconStateRoots, + ]; + + // We can remove this once schema V21 has been gone for a while. + let previous_schema_columns = vec![ + DBColumn::BeaconState, + DBColumn::BeaconStateSummary, + DBColumn::BeaconBlockRootsChunked, + DBColumn::BeaconStateRootsChunked, + DBColumn::BeaconRestorePoint, + DBColumn::BeaconHistoricalRoots, + DBColumn::BeaconRandaoMixes, + DBColumn::BeaconHistoricalSummaries, + ]; + let mut columns = current_schema_columns; + columns.extend(previous_schema_columns); + + for column in columns { + info!( + self.log, + "Starting compaction"; + "column" => ?column + ); + self.cold_db.compact_column(column)?; + info!( + self.log, + "Finishing compaction"; + "column" => ?column + ); + } + Ok(()) + } + /// Return `true` if compaction on finalization/pruning is enabled. pub fn compact_on_prune(&self) -> bool { self.config.compact_on_prune @@ -2875,6 +2914,7 @@ impl, Cold: ItemStore> HotColdDB // // We can remove this once schema V21 has been gone for a while. let previous_schema_columns = vec![ + DBColumn::BeaconState, DBColumn::BeaconStateSummary, DBColumn::BeaconBlockRootsChunked, DBColumn::BeaconStateRootsChunked, @@ -2916,7 +2956,7 @@ impl, Cold: ItemStore> HotColdDB self.cold_db.do_atomically(cold_ops)?; // In order to reclaim space, we need to compact the freezer DB as well. - self.cold_db.compact()?; + self.compact_freezer()?; Ok(()) } From c3a0757ad2c0d70bb0686463e6d5c4a2041114a3 Mon Sep 17 00:00:00 2001 From: Age Manning Date: Mon, 16 Dec 2024 10:16:53 +1100 Subject: [PATCH 039/254] Correct `/nat` API for libp2p (#6677) * Fix nat API --- .../lighthouse_network/src/peer_manager/network_behaviour.rs | 4 ---- common/system_health/src/lib.rs | 4 ++-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/beacon_node/lighthouse_network/src/peer_manager/network_behaviour.rs b/beacon_node/lighthouse_network/src/peer_manager/network_behaviour.rs index 11676f9a01..9fd059df85 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/network_behaviour.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/network_behaviour.rs @@ -141,10 +141,6 @@ impl NetworkBehaviour for PeerManager { debug!(self.log, "Failed to dial peer"; "peer_id"=> ?peer_id, "error" => %ClearDialError(error)); self.on_dial_failure(peer_id); } - FromSwarm::ExternalAddrConfirmed(_) => { - // We have an external address confirmed, means we are able to do NAT traversal. - metrics::set_gauge_vec(&metrics::NAT_OPEN, &["libp2p"], 1); - } _ => { // NOTE: FromSwarm is a non exhaustive enum so updates should be based on release // notes more than compiler feedback diff --git a/common/system_health/src/lib.rs b/common/system_health/src/lib.rs index 3431189842..9f351e943b 100644 --- a/common/system_health/src/lib.rs +++ b/common/system_health/src/lib.rs @@ -235,14 +235,14 @@ pub fn observe_nat() -> NatState { let libp2p_ipv4 = lighthouse_network::metrics::get_int_gauge( &lighthouse_network::metrics::NAT_OPEN, - &["libp2p"], + &["libp2p_ipv4"], ) .map(|g| g.get() == 1) .unwrap_or_default(); let libp2p_ipv6 = lighthouse_network::metrics::get_int_gauge( &lighthouse_network::metrics::NAT_OPEN, - &["libp2p"], + &["libp2p_ipv6"], ) .map(|g| g.get() == 1) .unwrap_or_default(); From 0d90135047519f4c2ee586d50e560f7bb2ff9b10 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Mon, 16 Dec 2024 14:03:22 +1100 Subject: [PATCH 040/254] Release v6.0.1 (#6659) * Release v6.0.1 --- Cargo.lock | 8 ++++---- beacon_node/Cargo.toml | 2 +- boot_node/Cargo.toml | 2 +- common/lighthouse_version/src/lib.rs | 4 ++-- lcli/Cargo.toml | 2 +- lighthouse/Cargo.toml | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1ddeecf711..c9744f500d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -833,7 +833,7 @@ dependencies = [ [[package]] name = "beacon_node" -version = "6.0.0" +version = "6.0.1" dependencies = [ "account_utils", "beacon_chain", @@ -1078,7 +1078,7 @@ dependencies = [ [[package]] name = "boot_node" -version = "6.0.0" +version = "6.0.1" dependencies = [ "beacon_node", "bytes", @@ -4674,7 +4674,7 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "lcli" -version = "6.0.0" +version = "6.0.1" dependencies = [ "account_utils", "beacon_chain", @@ -5244,7 +5244,7 @@ dependencies = [ [[package]] name = "lighthouse" -version = "6.0.0" +version = "6.0.1" dependencies = [ "account_manager", "account_utils", diff --git a/beacon_node/Cargo.toml b/beacon_node/Cargo.toml index fd4f0f6d4a..15cdf15dc5 100644 --- a/beacon_node/Cargo.toml +++ b/beacon_node/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "beacon_node" -version = "6.0.0" +version = "6.0.1" authors = [ "Paul Hauner ", "Age Manning "] edition = { workspace = true } diff --git a/common/lighthouse_version/src/lib.rs b/common/lighthouse_version/src/lib.rs index 07e51597e3..0751bdadff 100644 --- a/common/lighthouse_version/src/lib.rs +++ b/common/lighthouse_version/src/lib.rs @@ -17,8 +17,8 @@ pub const VERSION: &str = git_version!( // NOTE: using --match instead of --exclude for compatibility with old Git "--match=thiswillnevermatchlol" ], - prefix = "Lighthouse/v6.0.0-", - fallback = "Lighthouse/v6.0.0" + prefix = "Lighthouse/v6.0.1-", + fallback = "Lighthouse/v6.0.1" ); /// Returns the first eight characters of the latest commit hash for this build. diff --git a/lcli/Cargo.toml b/lcli/Cargo.toml index 88daddd8aa..9612bded47 100644 --- a/lcli/Cargo.toml +++ b/lcli/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "lcli" description = "Lighthouse CLI (modeled after zcli)" -version = "6.0.0" +version = "6.0.1" authors = ["Paul Hauner "] edition = { workspace = true } diff --git a/lighthouse/Cargo.toml b/lighthouse/Cargo.toml index 329519fb54..fa426daffa 100644 --- a/lighthouse/Cargo.toml +++ b/lighthouse/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lighthouse" -version = "6.0.0" +version = "6.0.1" authors = ["Sigma Prime "] edition = { workspace = true } autotests = false From c92c07ff498721d9eea60db8a5acfde399f47eea Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Mon, 16 Dec 2024 12:33:33 +0800 Subject: [PATCH 041/254] Track beacon processor import result metrics (#6541) * Track beacon processor import result metrics * Update metric name --- .../beacon_chain/src/block_verification.rs | 3 +- beacon_node/network/src/metrics.rs | 62 +++++++++++++++- .../gossip_methods.rs | 70 +++++++++---------- .../network_beacon_processor/sync_methods.rs | 7 +- 4 files changed, 99 insertions(+), 43 deletions(-) diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 4c5f53248f..ddb7bb614a 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -92,6 +92,7 @@ use std::fs; use std::io::Write; use std::sync::Arc; use store::{Error as DBError, HotStateSummary, KeyValueStore, StoreOp}; +use strum::AsRefStr; use task_executor::JoinHandle; use types::{ data_column_sidecar::DataColumnSidecarError, BeaconBlockRef, BeaconState, BeaconStateError, @@ -137,7 +138,7 @@ const WRITE_BLOCK_PROCESSING_SSZ: bool = cfg!(feature = "write_ssz_files"); /// /// - The block is malformed/invalid (indicated by all results other than `BeaconChainError`. /// - We encountered an error whilst trying to verify the block (a `BeaconChainError`). -#[derive(Debug)] +#[derive(Debug, AsRefStr)] pub enum BlockError { /// The parent block was unknown. /// diff --git a/beacon_node/network/src/metrics.rs b/beacon_node/network/src/metrics.rs index 4b7e8a50a3..154a59eade 100644 --- a/beacon_node/network/src/metrics.rs +++ b/beacon_node/network/src/metrics.rs @@ -2,7 +2,8 @@ use beacon_chain::{ attestation_verification::Error as AttnError, light_client_finality_update_verification::Error as LightClientFinalityUpdateError, light_client_optimistic_update_verification::Error as LightClientOptimisticUpdateError, - sync_committee_verification::Error as SyncCommitteeError, + sync_committee_verification::Error as SyncCommitteeError, AvailabilityProcessingStatus, + BlockError, }; use fnv::FnvHashMap; use lighthouse_network::{ @@ -11,12 +12,19 @@ use lighthouse_network::{ }; pub use metrics::*; use std::sync::{Arc, LazyLock}; +use strum::AsRefStr; use strum::IntoEnumIterator; use types::EthSpec; pub const SUCCESS: &str = "SUCCESS"; pub const FAILURE: &str = "FAILURE"; +#[derive(Debug, AsRefStr)] +pub(crate) enum BlockSource { + Gossip, + Rpc, +} + pub static BEACON_BLOCK_MESH_PEERS_PER_CLIENT: LazyLock> = LazyLock::new(|| { try_create_int_gauge_vec( @@ -59,6 +67,27 @@ pub static SYNC_COMMITTEE_SUBSCRIPTION_REQUESTS: LazyLock> = ) }); +/* + * Beacon processor + */ +pub static BEACON_PROCESSOR_MISSING_COMPONENTS: LazyLock> = LazyLock::new( + || { + try_create_int_counter_vec( + "beacon_processor_missing_components_total", + "Total number of imported individual block components that resulted in missing components", + &["source", "component"], + ) + }, +); +pub static BEACON_PROCESSOR_IMPORT_ERRORS_PER_TYPE: LazyLock> = + LazyLock::new(|| { + try_create_int_counter_vec( + "beacon_processor_import_errors_total", + "Total number of block components that were not verified", + &["source", "component", "type"], + ) + }); + /* * Gossip processor */ @@ -606,6 +635,37 @@ pub fn register_sync_committee_error(error: &SyncCommitteeError) { inc_counter_vec(&GOSSIP_SYNC_COMMITTEE_ERRORS_PER_TYPE, &[error.as_ref()]); } +pub(crate) fn register_process_result_metrics( + result: &std::result::Result, + source: BlockSource, + block_component: &'static str, +) { + match result { + Ok(status) => match status { + AvailabilityProcessingStatus::Imported { .. } => match source { + BlockSource::Gossip => { + inc_counter(&BEACON_PROCESSOR_GOSSIP_BLOCK_IMPORTED_TOTAL); + } + BlockSource::Rpc => { + inc_counter(&BEACON_PROCESSOR_RPC_BLOCK_IMPORTED_TOTAL); + } + }, + AvailabilityProcessingStatus::MissingComponents { .. } => { + inc_counter_vec( + &BEACON_PROCESSOR_MISSING_COMPONENTS, + &[source.as_ref(), block_component], + ); + } + }, + Err(error) => { + inc_counter_vec( + &BEACON_PROCESSOR_IMPORT_ERRORS_PER_TYPE, + &[source.as_ref(), block_component, error.as_ref()], + ); + } + } +} + pub fn from_result(result: &std::result::Result) -> &str { match result { Ok(_) => SUCCESS, 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 317bfb104b..4fc83b0923 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -1,5 +1,5 @@ use crate::{ - metrics, + metrics::{self, register_process_result_metrics}, network_beacon_processor::{InvalidBlockStorage, NetworkBeaconProcessor}, service::NetworkMessage, sync::SyncMessage, @@ -915,12 +915,11 @@ impl NetworkBeaconProcessor { let blob_index = verified_blob.id().index; let result = self.chain.process_gossip_blob(verified_blob).await; + register_process_result_metrics(&result, metrics::BlockSource::Gossip, "blob"); match &result { Ok(AvailabilityProcessingStatus::Imported(block_root)) => { - // Note: Reusing block imported metric here - metrics::inc_counter(&metrics::BEACON_PROCESSOR_GOSSIP_BLOCK_IMPORTED_TOTAL); - debug!( + info!( self.log, "Gossipsub blob processed - imported fully available block"; "block_root" => %block_root @@ -989,43 +988,39 @@ impl NetworkBeaconProcessor { let data_column_slot = verified_data_column.slot(); let data_column_index = verified_data_column.id().index; - match self + let result = self .chain .process_gossip_data_columns(vec![verified_data_column], || Ok(())) - .await - { - Ok(availability) => { - 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; + .await; + register_process_result_metrics(&result, metrics::BlockSource::Gossip, "data_column"); - metrics::set_gauge( - &metrics::BEACON_BLOB_DELAY_FULL_VERIFICATION, - processing_start_time.elapsed().as_millis() as i64, - ); - } - 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, - ); + match result { + Ok(availability) => match availability { + AvailabilityProcessingStatus::Imported(block_root) => { + info!( + self.log, + "Gossipsub data column processed, imported fully available block"; + "block_root" => %block_root + ); + self.chain.recompute_head_at_current_slot().await; - self.attempt_data_column_reconstruction(block_root).await; - } + metrics::set_gauge( + &metrics::BEACON_BLOB_DELAY_FULL_VERIFICATION, + processing_start_time.elapsed().as_millis() as i64, + ); } - } + 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, + ); + + self.attempt_data_column_reconstruction(block_root).await; + } + }, Err(BlockError::DuplicateFullyImported(_)) => { debug!( self.log, @@ -1467,11 +1462,10 @@ impl NetworkBeaconProcessor { NotifyExecutionLayer::Yes, ) .await; + register_process_result_metrics(&result, metrics::BlockSource::Gossip, "block"); match &result { Ok(AvailabilityProcessingStatus::Imported(block_root)) => { - metrics::inc_counter(&metrics::BEACON_PROCESSOR_GOSSIP_BLOCK_IMPORTED_TOTAL); - if reprocess_tx .try_send(ReprocessQueueMessage::BlockImported { block_root: *block_root, 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 6c6bb26ee0..817e6b6440 100644 --- a/beacon_node/network/src/network_beacon_processor/sync_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/sync_methods.rs @@ -1,4 +1,4 @@ -use crate::metrics; +use crate::metrics::{self, register_process_result_metrics}; use crate::network_beacon_processor::{NetworkBeaconProcessor, FUTURE_SLOT_TOLERANCE}; use crate::sync::BatchProcessResult; use crate::sync::{ @@ -163,8 +163,7 @@ impl NetworkBeaconProcessor { NotifyExecutionLayer::Yes, ) .await; - - metrics::inc_counter(&metrics::BEACON_PROCESSOR_RPC_BLOCK_IMPORTED_TOTAL); + register_process_result_metrics(&result, metrics::BlockSource::Rpc, "block"); // RPC block imported, regardless of process type match result.as_ref() { @@ -286,6 +285,7 @@ impl NetworkBeaconProcessor { } let result = self.chain.process_rpc_blobs(slot, block_root, blobs).await; + register_process_result_metrics(&result, metrics::BlockSource::Rpc, "blobs"); match &result { Ok(AvailabilityProcessingStatus::Imported(hash)) => { @@ -343,6 +343,7 @@ impl NetworkBeaconProcessor { .chain .process_rpc_custody_columns(custody_columns) .await; + register_process_result_metrics(&result, metrics::BlockSource::Rpc, "custody_columns"); match &result { Ok(availability) => match availability { From 11e1d5bf148784d1ccbaf8b1023e26b3d0fb4cd1 Mon Sep 17 00:00:00 2001 From: Jun Song <87601811+syjn99@users.noreply.github.com> Date: Mon, 16 Dec 2024 14:43:54 +0900 Subject: [PATCH 042/254] Add CLI flag for HTTP API token path (VC) (#6577) * Add cli flag for HTTP API token path (VC) * Add http_token_path_flag test * Add pre-check for directory case & Fix test utils * Update docs * Apply review: move http_token_path into validator_http_api config * Lint * Make diff lesser to replace PK_FILENAME * Merge branch 'unstable' into feature/cli-token-path * Applt review: help_vc.md Co-authored-by: chonghe <44791194+chong-he@users.noreply.github.com> * Fix help for cli * Fix issues on ci * Merge branch 'unstable' into feature/cli-token-path * Merge branch 'unstable' into feature/cli-token-path * Merge branch 'unstable' into feature/cli-token-path * Merge branch 'unstable' into feature/cli-token-path --- Cargo.lock | 2 ++ book/src/api-vc-auth-header.md | 3 +- book/src/api-vc-endpoints.md | 2 +- book/src/help_vc.md | 4 +++ lighthouse/tests/validator_client.rs | 15 +++++++++ validator_client/http_api/Cargo.toml | 2 ++ validator_client/http_api/src/api_secret.rs | 37 ++++++++++++++++----- validator_client/http_api/src/lib.rs | 10 ++++++ validator_client/http_api/src/test_utils.rs | 9 +++-- validator_client/http_api/src/tests.rs | 8 +++-- validator_client/src/cli.rs | 12 +++++++ validator_client/src/config.rs | 8 ++++- validator_client/src/lib.rs | 2 +- 13 files changed, 96 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9d0d38c1ae..2978a3a19f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9552,6 +9552,8 @@ dependencies = [ "beacon_node_fallback", "bls", "deposit_contract", + "directory", + "dirs", "doppelganger_service", "eth2", "eth2_keystore", diff --git a/book/src/api-vc-auth-header.md b/book/src/api-vc-auth-header.md index adde78270a..feb93724c0 100644 --- a/book/src/api-vc-auth-header.md +++ b/book/src/api-vc-auth-header.md @@ -18,7 +18,8 @@ Authorization: Bearer hGut6B8uEujufDXSmZsT0thnxvdvKFBvh ## Obtaining the API token The API token is stored as a file in the `validators` directory. For most users -this is `~/.lighthouse/{network}/validators/api-token.txt`. Here's an +this is `~/.lighthouse/{network}/validators/api-token.txt`, unless overridden using the +`--http-token-path` CLI parameter. Here's an example using the `cat` command to print the token to the terminal, but any text editor will suffice: diff --git a/book/src/api-vc-endpoints.md b/book/src/api-vc-endpoints.md index 80eba7a059..98605a3dcd 100644 --- a/book/src/api-vc-endpoints.md +++ b/book/src/api-vc-endpoints.md @@ -53,7 +53,7 @@ Example Response Body: } ``` -> Note: The command provided in this documentation links to the API token file. In this documentation, it is assumed that the API token file is located in `/var/lib/lighthouse/validators/api-token.txt`. If your database is saved in another directory, modify the `DATADIR` accordingly. If you are having permission issue with accessing the API token file, you can modify the header to become `-H "Authorization: Bearer $(sudo cat ${DATADIR}/validators/api-token.txt)"`. +> Note: The command provided in this documentation links to the API token file. In this documentation, it is assumed that the API token file is located in `/var/lib/lighthouse/validators/api-token.txt`. If your database is saved in another directory, modify the `DATADIR` accordingly. If you've specified a custom token path using `--http-token-path`, use that path instead. If you are having permission issue with accessing the API token file, you can modify the header to become `-H "Authorization: Bearer $(sudo cat ${DATADIR}/validators/api-token.txt)"`. > As an alternative, you can also provide the API token directly, for example, `-H "Authorization: Bearer hGut6B8uEujufDXSmZsT0thnxvdvKFBvh`. In this case, you obtain the token from the file `api-token.txt` and the command becomes: diff --git a/book/src/help_vc.md b/book/src/help_vc.md index 2cfbfbc857..71e21d68c9 100644 --- a/book/src/help_vc.md +++ b/book/src/help_vc.md @@ -69,6 +69,10 @@ Options: this server (e.g., http://localhost:5062). --http-port Set the listen TCP port for the RESTful HTTP API server. + --http-token-path + Path to file containing the HTTP API token for validator client + authentication. If not specified, defaults to + {validators-dir}/api-token.txt. --log-format Specifies the log format used when emitting logs to the terminal. [possible values: JSON] diff --git a/lighthouse/tests/validator_client.rs b/lighthouse/tests/validator_client.rs index 34fe04cc45..587001f77b 100644 --- a/lighthouse/tests/validator_client.rs +++ b/lighthouse/tests/validator_client.rs @@ -344,6 +344,21 @@ fn http_store_keystore_passwords_in_secrets_dir_present() { .with_config(|config| assert!(config.http_api.store_passwords_in_secrets_dir)); } +#[test] +fn http_token_path_flag() { + let dir = TempDir::new().expect("Unable to create temporary directory"); + CommandLineTest::new() + .flag("http", None) + .flag("http-token-path", dir.path().join("api-token.txt").to_str()) + .run() + .with_config(|config| { + assert_eq!( + config.http_api.http_token_path, + dir.path().join("api-token.txt") + ); + }); +} + // Tests for Metrics flags. #[test] fn metrics_flag() { diff --git a/validator_client/http_api/Cargo.toml b/validator_client/http_api/Cargo.toml index 18e0604ad5..96c836f6f3 100644 --- a/validator_client/http_api/Cargo.toml +++ b/validator_client/http_api/Cargo.toml @@ -13,7 +13,9 @@ account_utils = { workspace = true } bls = { workspace = true } beacon_node_fallback = { workspace = true } deposit_contract = { workspace = true } +directory = { workspace = true } doppelganger_service = { workspace = true } +dirs = { workspace = true } graffiti_file = { workspace = true } eth2 = { workspace = true } eth2_keystore = { workspace = true } diff --git a/validator_client/http_api/src/api_secret.rs b/validator_client/http_api/src/api_secret.rs index afcac477ec..bac54dc8b2 100644 --- a/validator_client/http_api/src/api_secret.rs +++ b/validator_client/http_api/src/api_secret.rs @@ -5,7 +5,7 @@ use std::fs; use std::path::{Path, PathBuf}; use warp::Filter; -/// The name of the file which stores the API token. +/// The default name of the file which stores the API token. pub const PK_FILENAME: &str = "api-token.txt"; pub const PK_LEN: usize = 33; @@ -31,14 +31,32 @@ pub struct ApiSecret { impl ApiSecret { /// If the public key is already on-disk, use it. /// - /// The provided `dir` is a directory containing `PK_FILENAME`. + /// The provided `pk_path` is a path containing API token. /// /// If the public key file is missing on disk, create a new key and /// write it to disk (over-writing any existing files). - pub fn create_or_open>(dir: P) -> Result { - let pk_path = dir.as_ref().join(PK_FILENAME); + pub fn create_or_open>(pk_path: P) -> Result { + let pk_path = pk_path.as_ref(); + + // Check if the path is a directory + if pk_path.is_dir() { + return Err(format!( + "API token path {:?} is a directory, not a file", + pk_path + )); + } if !pk_path.exists() { + // Create parent directories if they don't exist + if let Some(parent) = pk_path.parent() { + std::fs::create_dir_all(parent).map_err(|e| { + format!( + "Unable to create parent directories for {:?}: {:?}", + pk_path, e + ) + })?; + } + let length = PK_LEN; let pk: String = thread_rng() .sample_iter(&Alphanumeric) @@ -47,7 +65,7 @@ impl ApiSecret { .collect(); // Create and write the public key to file with appropriate permissions - create_with_600_perms(&pk_path, pk.to_string().as_bytes()).map_err(|e| { + create_with_600_perms(pk_path, pk.to_string().as_bytes()).map_err(|e| { format!( "Unable to create file with permissions for {:?}: {:?}", pk_path, e @@ -55,13 +73,16 @@ impl ApiSecret { })?; } - let pk = fs::read(&pk_path) - .map_err(|e| format!("cannot read {}: {}", PK_FILENAME, e))? + let pk = fs::read(pk_path) + .map_err(|e| format!("cannot read {}: {}", pk_path.display(), e))? .iter() .map(|&c| char::from(c)) .collect(); - Ok(Self { pk, pk_path }) + Ok(Self { + pk, + pk_path: pk_path.to_path_buf(), + }) } /// Returns the API token. diff --git a/validator_client/http_api/src/lib.rs b/validator_client/http_api/src/lib.rs index b58c7ccec0..f3dab3780c 100644 --- a/validator_client/http_api/src/lib.rs +++ b/validator_client/http_api/src/lib.rs @@ -7,6 +7,7 @@ mod remotekeys; mod tests; pub mod test_utils; +pub use api_secret::PK_FILENAME; use graffiti::{delete_graffiti, get_graffiti, set_graffiti}; @@ -23,6 +24,7 @@ use beacon_node_fallback::CandidateInfo; use create_validator::{ create_validators_mnemonic, create_validators_web3signer, get_voting_password_storage, }; +use directory::{DEFAULT_HARDCODED_NETWORK, DEFAULT_ROOT_DIR, DEFAULT_VALIDATOR_DIR}; use eth2::lighthouse_vc::{ std_types::{AuthResponse, GetFeeRecipientResponse, GetGasLimitResponse}, types::{ @@ -99,10 +101,17 @@ pub struct Config { pub allow_origin: Option, pub allow_keystore_export: bool, pub store_passwords_in_secrets_dir: bool, + pub http_token_path: PathBuf, } impl Default for Config { fn default() -> Self { + let http_token_path = dirs::home_dir() + .unwrap_or_else(|| PathBuf::from(".")) + .join(DEFAULT_ROOT_DIR) + .join(DEFAULT_HARDCODED_NETWORK) + .join(DEFAULT_VALIDATOR_DIR) + .join(PK_FILENAME); Self { enabled: false, listen_addr: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), @@ -110,6 +119,7 @@ impl Default for Config { allow_origin: None, allow_keystore_export: false, store_passwords_in_secrets_dir: false, + http_token_path, } } } diff --git a/validator_client/http_api/src/test_utils.rs b/validator_client/http_api/src/test_utils.rs index d033fdbf2d..390095eec7 100644 --- a/validator_client/http_api/src/test_utils.rs +++ b/validator_client/http_api/src/test_utils.rs @@ -1,3 +1,4 @@ +use crate::api_secret::PK_FILENAME; use crate::{ApiSecret, Config as HttpConfig, Context}; use account_utils::validator_definitions::ValidatorDefinitions; use account_utils::{ @@ -73,6 +74,7 @@ impl ApiTester { let validator_dir = tempdir().unwrap(); let secrets_dir = tempdir().unwrap(); + let token_path = tempdir().unwrap().path().join(PK_FILENAME); let validator_defs = ValidatorDefinitions::open_or_create(validator_dir.path()).unwrap(); @@ -85,7 +87,7 @@ impl ApiTester { .await .unwrap(); - let api_secret = ApiSecret::create_or_open(validator_dir.path()).unwrap(); + let api_secret = ApiSecret::create_or_open(token_path).unwrap(); let api_pubkey = api_secret.api_token(); let config = ValidatorStoreConfig { @@ -177,6 +179,7 @@ impl ApiTester { allow_origin: None, allow_keystore_export: true, store_passwords_in_secrets_dir: false, + http_token_path: tempdir().unwrap().path().join(PK_FILENAME), } } @@ -199,8 +202,8 @@ impl ApiTester { } pub fn invalid_token_client(&self) -> ValidatorClientHttpClient { - let tmp = tempdir().unwrap(); - let api_secret = ApiSecret::create_or_open(tmp.path()).unwrap(); + let tmp = tempdir().unwrap().path().join("invalid-token.txt"); + let api_secret = ApiSecret::create_or_open(tmp).unwrap(); let invalid_pubkey = api_secret.api_token(); ValidatorClientHttpClient::new(self.url.clone(), invalid_pubkey).unwrap() } diff --git a/validator_client/http_api/src/tests.rs b/validator_client/http_api/src/tests.rs index 262bb64e69..027b10e246 100644 --- a/validator_client/http_api/src/tests.rs +++ b/validator_client/http_api/src/tests.rs @@ -63,6 +63,7 @@ impl ApiTester { let validator_dir = tempdir().unwrap(); let secrets_dir = tempdir().unwrap(); + let token_path = tempdir().unwrap().path().join("api-token.txt"); let validator_defs = ValidatorDefinitions::open_or_create(validator_dir.path()).unwrap(); @@ -75,7 +76,7 @@ impl ApiTester { .await .unwrap(); - let api_secret = ApiSecret::create_or_open(validator_dir.path()).unwrap(); + let api_secret = ApiSecret::create_or_open(&token_path).unwrap(); let api_pubkey = api_secret.api_token(); let spec = Arc::new(E::default_spec()); @@ -127,6 +128,7 @@ impl ApiTester { allow_origin: None, allow_keystore_export: true, store_passwords_in_secrets_dir: false, + http_token_path: token_path, }, sse_logging_components: None, log, @@ -161,8 +163,8 @@ impl ApiTester { } pub fn invalid_token_client(&self) -> ValidatorClientHttpClient { - let tmp = tempdir().unwrap(); - let api_secret = ApiSecret::create_or_open(tmp.path()).unwrap(); + let tmp = tempdir().unwrap().path().join("invalid-token.txt"); + let api_secret = ApiSecret::create_or_open(tmp).unwrap(); let invalid_pubkey = api_secret.api_token(); ValidatorClientHttpClient::new(self.url.clone(), invalid_pubkey.clone()).unwrap() } diff --git a/validator_client/src/cli.rs b/validator_client/src/cli.rs index 209876f07b..b2d1ebb3c2 100644 --- a/validator_client/src/cli.rs +++ b/validator_client/src/cli.rs @@ -247,6 +247,18 @@ pub fn cli_app() -> Command { .help_heading(FLAG_HEADER) .display_order(0) ) + .arg( + Arg::new("http-token-path") + .long("http-token-path") + .requires("http") + .value_name("HTTP_TOKEN_PATH") + .help( + "Path to file containing the HTTP API token for validator client authentication. \ + If not specified, defaults to {validators-dir}/api-token.txt." + ) + .action(ArgAction::Set) + .display_order(0) + ) /* Prometheus metrics HTTP server related arguments */ .arg( Arg::new("metrics") diff --git a/validator_client/src/config.rs b/validator_client/src/config.rs index abdadeb393..0fecb5202d 100644 --- a/validator_client/src/config.rs +++ b/validator_client/src/config.rs @@ -17,7 +17,7 @@ use std::path::PathBuf; use std::str::FromStr; use std::time::Duration; use types::{Address, GRAFFITI_BYTES_LEN}; -use validator_http_api; +use validator_http_api::{self, PK_FILENAME}; use validator_http_metrics; use validator_store::Config as ValidatorStoreConfig; @@ -314,6 +314,12 @@ impl Config { config.http_api.store_passwords_in_secrets_dir = true; } + if cli_args.get_one::("http-token-path").is_some() { + config.http_api.http_token_path = parse_required(cli_args, "http-token-path") + // For backward compatibility, default to the path under the validator dir if not provided. + .unwrap_or_else(|_| config.validator_dir.join(PK_FILENAME)); + } + /* * Prometheus metrics HTTP server */ diff --git a/validator_client/src/lib.rs b/validator_client/src/lib.rs index 2cc22357fb..8ebfe98b15 100644 --- a/validator_client/src/lib.rs +++ b/validator_client/src/lib.rs @@ -551,7 +551,7 @@ impl ProductionValidatorClient { let (block_service_tx, block_service_rx) = mpsc::channel(channel_capacity); let log = self.context.log(); - let api_secret = ApiSecret::create_or_open(&self.config.validator_dir)?; + let api_secret = ApiSecret::create_or_open(&self.config.http_api.http_token_path)?; self.http_api_listen_addr = if self.config.http_api.enabled { let ctx = Arc::new(validator_http_api::Context { From 86891e6d0f111c318660aaea63ed39c58dd716a5 Mon Sep 17 00:00:00 2001 From: ethDreamer <37123614+ethDreamer@users.noreply.github.com> Date: Sun, 15 Dec 2024 21:43:58 -0800 Subject: [PATCH 043/254] builder gas limit & some refactoring (#6583) * Cache gas_limit * Payload Parameters Refactor * Enforce Proposer Gas Limit * Fixed and Added New Tests * Fix Beacon Chain Tests --- .../beacon_chain/src/execution_payload.rs | 24 +- .../tests/payload_invalidation.rs | 22 +- beacon_node/execution_layer/src/lib.rs | 284 +++++++++++------- .../test_utils/execution_block_generator.rs | 35 ++- .../src/test_utils/mock_builder.rs | 63 +++- .../src/test_utils/mock_execution_layer.rs | 29 +- .../execution_layer/src/test_utils/mod.rs | 5 +- beacon_node/http_api/src/lib.rs | 27 +- .../http_api/tests/interactive_tests.rs | 13 +- beacon_node/http_api/tests/tests.rs | 234 +++++++++++---- consensus/types/src/chain_spec.rs | 27 ++ consensus/types/src/payload.rs | 33 ++ testing/ef_tests/src/cases/fork_choice.rs | 11 +- .../src/test_rig.rs | 34 ++- 14 files changed, 598 insertions(+), 243 deletions(-) diff --git a/beacon_node/beacon_chain/src/execution_payload.rs b/beacon_node/beacon_chain/src/execution_payload.rs index f2420eea0d..5e13f0624d 100644 --- a/beacon_node/beacon_chain/src/execution_payload.rs +++ b/beacon_node/beacon_chain/src/execution_payload.rs @@ -14,7 +14,7 @@ use crate::{ }; use execution_layer::{ BlockProposalContents, BlockProposalContentsType, BuilderParams, NewPayloadRequest, - PayloadAttributes, PayloadStatus, + PayloadAttributes, PayloadParameters, PayloadStatus, }; use fork_choice::{InvalidationOperation, PayloadVerificationStatus}; use proto_array::{Block as ProtoBlock, ExecutionStatus}; @@ -375,8 +375,9 @@ pub fn get_execution_payload( let timestamp = compute_timestamp_at_slot(state, state.slot(), spec).map_err(BeaconStateError::from)?; let random = *state.get_randao_mix(current_epoch)?; - let latest_execution_payload_header_block_hash = - state.latest_execution_payload_header()?.block_hash(); + let latest_execution_payload_header = state.latest_execution_payload_header()?; + let latest_execution_payload_header_block_hash = latest_execution_payload_header.block_hash(); + let latest_execution_payload_header_gas_limit = latest_execution_payload_header.gas_limit(); let withdrawals = match state { &BeaconState::Capella(_) | &BeaconState::Deneb(_) | &BeaconState::Electra(_) => { Some(get_expected_withdrawals(state, spec)?.0.into()) @@ -406,6 +407,7 @@ pub fn get_execution_payload( random, proposer_index, latest_execution_payload_header_block_hash, + latest_execution_payload_header_gas_limit, builder_params, withdrawals, parent_beacon_block_root, @@ -443,6 +445,7 @@ pub async fn prepare_execution_payload( random: Hash256, proposer_index: u64, latest_execution_payload_header_block_hash: ExecutionBlockHash, + latest_execution_payload_header_gas_limit: u64, builder_params: BuilderParams, withdrawals: Option>, parent_beacon_block_root: Option, @@ -526,13 +529,20 @@ where parent_beacon_block_root, ); + let target_gas_limit = execution_layer.get_proposer_gas_limit(proposer_index).await; + let payload_parameters = PayloadParameters { + parent_hash, + parent_gas_limit: latest_execution_payload_header_gas_limit, + proposer_gas_limit: target_gas_limit, + payload_attributes: &payload_attributes, + forkchoice_update_params: &forkchoice_update_params, + current_fork: fork, + }; + let block_contents = execution_layer .get_payload( - parent_hash, - &payload_attributes, - forkchoice_update_params, + payload_parameters, builder_params, - fork, &chain.spec, builder_boost_factor, block_production_version, diff --git a/beacon_node/beacon_chain/tests/payload_invalidation.rs b/beacon_node/beacon_chain/tests/payload_invalidation.rs index 1325875a27..729d88450f 100644 --- a/beacon_node/beacon_chain/tests/payload_invalidation.rs +++ b/beacon_node/beacon_chain/tests/payload_invalidation.rs @@ -986,10 +986,13 @@ async fn payload_preparation() { // Provide preparation data to the EL for `proposer`. el.update_proposer_preparation( Epoch::new(1), - &[ProposerPreparationData { - validator_index: proposer as u64, - fee_recipient, - }], + [( + &ProposerPreparationData { + validator_index: proposer as u64, + fee_recipient, + }, + &None, + )], ) .await; @@ -1119,10 +1122,13 @@ async fn payload_preparation_before_transition_block() { // Provide preparation data to the EL for `proposer`. el.update_proposer_preparation( Epoch::new(0), - &[ProposerPreparationData { - validator_index: proposer as u64, - fee_recipient, - }], + [( + &ProposerPreparationData { + validator_index: proposer as u64, + fee_recipient, + }, + &None, + )], ) .await; diff --git a/beacon_node/execution_layer/src/lib.rs b/beacon_node/execution_layer/src/lib.rs index 08a00d7bf8..ae0dca9833 100644 --- a/beacon_node/execution_layer/src/lib.rs +++ b/beacon_node/execution_layer/src/lib.rs @@ -28,7 +28,7 @@ use sensitive_url::SensitiveUrl; use serde::{Deserialize, Serialize}; use slog::{crit, debug, error, info, warn, Logger}; use slot_clock::SlotClock; -use std::collections::HashMap; +use std::collections::{hash_map::Entry, HashMap}; use std::fmt; use std::future::Future; use std::io::Write; @@ -319,10 +319,52 @@ impl> BlockProposalContents { + pub parent_hash: ExecutionBlockHash, + pub parent_gas_limit: u64, + pub proposer_gas_limit: Option, + pub payload_attributes: &'a PayloadAttributes, + pub forkchoice_update_params: &'a ForkchoiceUpdateParameters, + pub current_fork: ForkName, +} + #[derive(Clone, PartialEq)] pub struct ProposerPreparationDataEntry { update_epoch: Epoch, preparation_data: ProposerPreparationData, + gas_limit: Option, +} + +impl ProposerPreparationDataEntry { + pub fn update(&mut self, updated: Self) -> bool { + let mut changed = false; + // Update `gas_limit` if `updated.gas_limit` is `Some` and: + // - `self.gas_limit` is `None`, or + // - both are `Some` but the values differ. + if let Some(updated_gas_limit) = updated.gas_limit { + if self.gas_limit != Some(updated_gas_limit) { + self.gas_limit = Some(updated_gas_limit); + changed = true; + } + } + + // Update `update_epoch` if it differs + if self.update_epoch != updated.update_epoch { + self.update_epoch = updated.update_epoch; + changed = true; + } + + // Update `preparation_data` if it differs + if self.preparation_data != updated.preparation_data { + self.preparation_data = updated.preparation_data; + changed = true; + } + + changed + } } #[derive(Hash, PartialEq, Eq)] @@ -711,23 +753,29 @@ impl ExecutionLayer { } /// Updates the proposer preparation data provided by validators - pub async fn update_proposer_preparation( - &self, - update_epoch: Epoch, - preparation_data: &[ProposerPreparationData], - ) { + pub async fn update_proposer_preparation<'a, I>(&self, update_epoch: Epoch, proposer_data: I) + where + I: IntoIterator)>, + { let mut proposer_preparation_data = self.proposer_preparation_data().await; - for preparation_entry in preparation_data { + + for (preparation_entry, gas_limit) in proposer_data { let new = ProposerPreparationDataEntry { update_epoch, preparation_data: preparation_entry.clone(), + gas_limit: *gas_limit, }; - let existing = - proposer_preparation_data.insert(preparation_entry.validator_index, new.clone()); - - if existing != Some(new) { - metrics::inc_counter(&metrics::EXECUTION_LAYER_PROPOSER_DATA_UPDATED); + match proposer_preparation_data.entry(preparation_entry.validator_index) { + Entry::Occupied(mut entry) => { + if entry.get_mut().update(new) { + metrics::inc_counter(&metrics::EXECUTION_LAYER_PROPOSER_DATA_UPDATED); + } + } + Entry::Vacant(entry) => { + entry.insert(new); + metrics::inc_counter(&metrics::EXECUTION_LAYER_PROPOSER_DATA_UPDATED); + } } } } @@ -809,6 +857,13 @@ impl ExecutionLayer { } } + pub async fn get_proposer_gas_limit(&self, proposer_index: u64) -> Option { + self.proposer_preparation_data() + .await + .get(&proposer_index) + .and_then(|entry| entry.gas_limit) + } + /// Maps to the `engine_getPayload` JSON-RPC call. /// /// However, it will attempt to call `self.prepare_payload` if it cannot find an existing @@ -818,14 +873,10 @@ impl ExecutionLayer { /// /// The result will be returned from the first node that returns successfully. No more nodes /// will be contacted. - #[allow(clippy::too_many_arguments)] pub async fn get_payload( &self, - parent_hash: ExecutionBlockHash, - payload_attributes: &PayloadAttributes, - forkchoice_update_params: ForkchoiceUpdateParameters, + payload_parameters: PayloadParameters<'_>, builder_params: BuilderParams, - current_fork: ForkName, spec: &ChainSpec, builder_boost_factor: Option, block_production_version: BlockProductionVersion, @@ -833,11 +884,8 @@ impl ExecutionLayer { let payload_result_type = match block_production_version { BlockProductionVersion::V3 => match self .determine_and_fetch_payload( - parent_hash, - payload_attributes, - forkchoice_update_params, + payload_parameters, builder_params, - current_fork, builder_boost_factor, spec, ) @@ -857,25 +905,11 @@ impl ExecutionLayer { &metrics::EXECUTION_LAYER_REQUEST_TIMES, &[metrics::GET_BLINDED_PAYLOAD], ); - self.determine_and_fetch_payload( - parent_hash, - payload_attributes, - forkchoice_update_params, - builder_params, - current_fork, - None, - spec, - ) - .await? + self.determine_and_fetch_payload(payload_parameters, builder_params, None, spec) + .await? } BlockProductionVersion::FullV2 => self - .get_full_payload_with( - parent_hash, - payload_attributes, - forkchoice_update_params, - current_fork, - noop, - ) + .get_full_payload_with(payload_parameters, noop) .await .and_then(GetPayloadResponseType::try_into) .map(ProvenancedPayload::Local)?, @@ -922,17 +956,15 @@ impl ExecutionLayer { async fn fetch_builder_and_local_payloads( &self, builder: &BuilderHttpClient, - parent_hash: ExecutionBlockHash, builder_params: &BuilderParams, - payload_attributes: &PayloadAttributes, - forkchoice_update_params: ForkchoiceUpdateParameters, - current_fork: ForkName, + payload_parameters: PayloadParameters<'_>, ) -> ( Result>>, builder_client::Error>, Result, Error>, ) { let slot = builder_params.slot; let pubkey = &builder_params.pubkey; + let parent_hash = payload_parameters.parent_hash; info!( self.log(), @@ -950,17 +982,12 @@ impl ExecutionLayer { .await }), timed_future(metrics::GET_BLINDED_PAYLOAD_LOCAL, async { - self.get_full_payload_caching( - parent_hash, - payload_attributes, - forkchoice_update_params, - current_fork, - ) - .await - .and_then(|local_result_type| match local_result_type { - GetPayloadResponseType::Full(payload) => Ok(payload), - GetPayloadResponseType::Blinded(_) => Err(Error::PayloadTypeMismatch), - }) + self.get_full_payload_caching(payload_parameters) + .await + .and_then(|local_result_type| match local_result_type { + GetPayloadResponseType::Full(payload) => Ok(payload), + GetPayloadResponseType::Blinded(_) => Err(Error::PayloadTypeMismatch), + }) }) ); @@ -984,26 +1011,17 @@ impl ExecutionLayer { (relay_result, local_result) } - #[allow(clippy::too_many_arguments)] async fn determine_and_fetch_payload( &self, - parent_hash: ExecutionBlockHash, - payload_attributes: &PayloadAttributes, - forkchoice_update_params: ForkchoiceUpdateParameters, + payload_parameters: PayloadParameters<'_>, builder_params: BuilderParams, - current_fork: ForkName, builder_boost_factor: Option, spec: &ChainSpec, ) -> Result>, Error> { let Some(builder) = self.builder() else { // no builder.. return local payload return self - .get_full_payload_caching( - parent_hash, - payload_attributes, - forkchoice_update_params, - current_fork, - ) + .get_full_payload_caching(payload_parameters) .await .and_then(GetPayloadResponseType::try_into) .map(ProvenancedPayload::Local); @@ -1034,26 +1052,15 @@ impl ExecutionLayer { ), } return self - .get_full_payload_caching( - parent_hash, - payload_attributes, - forkchoice_update_params, - current_fork, - ) + .get_full_payload_caching(payload_parameters) .await .and_then(GetPayloadResponseType::try_into) .map(ProvenancedPayload::Local); } + let parent_hash = payload_parameters.parent_hash; let (relay_result, local_result) = self - .fetch_builder_and_local_payloads( - builder.as_ref(), - parent_hash, - &builder_params, - payload_attributes, - forkchoice_update_params, - current_fork, - ) + .fetch_builder_and_local_payloads(builder.as_ref(), &builder_params, payload_parameters) .await; match (relay_result, local_result) { @@ -1118,14 +1125,9 @@ impl ExecutionLayer { ); // check relay payload validity - if let Err(reason) = verify_builder_bid( - &relay, - parent_hash, - payload_attributes, - Some(local.block_number()), - current_fork, - spec, - ) { + if let Err(reason) = + verify_builder_bid(&relay, payload_parameters, Some(local.block_number()), spec) + { // relay payload invalid -> return local metrics::inc_counter_vec( &metrics::EXECUTION_LAYER_GET_PAYLOAD_BUILDER_REJECTIONS, @@ -1202,14 +1204,7 @@ impl ExecutionLayer { "parent_hash" => ?parent_hash, ); - match verify_builder_bid( - &relay, - parent_hash, - payload_attributes, - None, - current_fork, - spec, - ) { + match verify_builder_bid(&relay, payload_parameters, None, spec) { Ok(()) => Ok(ProvenancedPayload::try_from(relay.data.message)?), Err(reason) => { metrics::inc_counter_vec( @@ -1234,32 +1229,28 @@ impl ExecutionLayer { /// Get a full payload and cache its result in the execution layer's payload cache. async fn get_full_payload_caching( &self, - parent_hash: ExecutionBlockHash, - payload_attributes: &PayloadAttributes, - forkchoice_update_params: ForkchoiceUpdateParameters, - current_fork: ForkName, + payload_parameters: PayloadParameters<'_>, ) -> Result, Error> { - self.get_full_payload_with( - parent_hash, - payload_attributes, - forkchoice_update_params, - current_fork, - Self::cache_payload, - ) - .await + self.get_full_payload_with(payload_parameters, Self::cache_payload) + .await } async fn get_full_payload_with( &self, - parent_hash: ExecutionBlockHash, - payload_attributes: &PayloadAttributes, - forkchoice_update_params: ForkchoiceUpdateParameters, - current_fork: ForkName, + payload_parameters: PayloadParameters<'_>, cache_fn: fn( &ExecutionLayer, PayloadContentsRefTuple, ) -> Option>, ) -> Result, Error> { + let PayloadParameters { + parent_hash, + payload_attributes, + forkchoice_update_params, + current_fork, + .. + } = payload_parameters; + self.engine() .request(move |engine| async move { let payload_id = if let Some(id) = engine @@ -1984,6 +1975,10 @@ enum InvalidBuilderPayload { payload: Option, expected: Option, }, + GasLimitMismatch { + payload: u64, + expected: u64, + }, } impl fmt::Display for InvalidBuilderPayload { @@ -2022,19 +2017,51 @@ impl fmt::Display for InvalidBuilderPayload { opt_string(expected) ) } + InvalidBuilderPayload::GasLimitMismatch { payload, expected } => { + write!(f, "payload gas limit was {} not {}", payload, expected) + } } } } +/// Calculate the expected gas limit for a block. +pub fn expected_gas_limit( + parent_gas_limit: u64, + target_gas_limit: u64, + spec: &ChainSpec, +) -> Option { + // Calculate the maximum gas limit difference allowed safely + let max_gas_limit_difference = parent_gas_limit + .checked_div(spec.gas_limit_adjustment_factor) + .and_then(|result| result.checked_sub(1)) + .unwrap_or(0); + + // Adjust the gas limit safely + if target_gas_limit > parent_gas_limit { + let gas_diff = target_gas_limit.saturating_sub(parent_gas_limit); + parent_gas_limit.checked_add(std::cmp::min(gas_diff, max_gas_limit_difference)) + } else { + let gas_diff = parent_gas_limit.saturating_sub(target_gas_limit); + parent_gas_limit.checked_sub(std::cmp::min(gas_diff, max_gas_limit_difference)) + } +} + /// Perform some cursory, non-exhaustive validation of the bid returned from the builder. fn verify_builder_bid( bid: &ForkVersionedResponse>, - parent_hash: ExecutionBlockHash, - payload_attributes: &PayloadAttributes, + payload_parameters: PayloadParameters<'_>, block_number: Option, - current_fork: ForkName, spec: &ChainSpec, ) -> Result<(), Box> { + let PayloadParameters { + parent_hash, + payload_attributes, + current_fork, + parent_gas_limit, + proposer_gas_limit, + .. + } = payload_parameters; + let is_signature_valid = bid.data.verify_signature(spec); let header = &bid.data.message.header(); @@ -2050,6 +2077,8 @@ fn verify_builder_bid( .cloned() .map(|withdrawals| Withdrawals::::from(withdrawals).tree_hash_root()); let payload_withdrawals_root = header.withdrawals_root().ok(); + let expected_gas_limit = proposer_gas_limit + .and_then(|target_gas_limit| expected_gas_limit(parent_gas_limit, target_gas_limit, spec)); if header.parent_hash() != parent_hash { Err(Box::new(InvalidBuilderPayload::ParentHash { @@ -2086,6 +2115,14 @@ fn verify_builder_bid( payload: payload_withdrawals_root, expected: expected_withdrawals_root, })) + } else if expected_gas_limit + .map(|gas_limit| header.gas_limit() != gas_limit) + .unwrap_or(false) + { + Err(Box::new(InvalidBuilderPayload::GasLimitMismatch { + payload: header.gas_limit(), + expected: expected_gas_limit.unwrap_or(0), + })) } else { Ok(()) } @@ -2138,6 +2175,27 @@ mod test { .await; } + #[tokio::test] + async fn test_expected_gas_limit() { + let spec = ChainSpec::mainnet(); + assert_eq!( + expected_gas_limit(30_000_000, 30_000_000, &spec), + Some(30_000_000) + ); + assert_eq!( + expected_gas_limit(30_000_000, 40_000_000, &spec), + Some(30_029_295) + ); + assert_eq!( + expected_gas_limit(30_029_295, 40_000_000, &spec), + Some(30_058_619) + ); + assert_eq!( + expected_gas_limit(30_058_619, 30_000_000, &spec), + Some(30_029_266) + ); + } + #[tokio::test] async fn test_forked_terminal_block() { let runtime = TestRuntime::default(); diff --git a/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs b/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs index 4deb91e056..4fab7150ce 100644 --- a/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs +++ b/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs @@ -28,8 +28,8 @@ use super::DEFAULT_TERMINAL_BLOCK; const TEST_BLOB_BUNDLE: &[u8] = include_bytes!("fixtures/mainnet/test_blobs_bundle.ssz"); -const GAS_LIMIT: u64 = 16384; -const GAS_USED: u64 = GAS_LIMIT - 1; +pub const DEFAULT_GAS_LIMIT: u64 = 30_000_000; +const GAS_USED: u64 = DEFAULT_GAS_LIMIT - 1; #[derive(Clone, Debug, PartialEq)] #[allow(clippy::large_enum_variant)] // This struct is only for testing. @@ -38,6 +38,10 @@ pub enum Block { PoS(ExecutionPayload), } +pub fn mock_el_extra_data() -> types::VariableList { + "block gen was here".as_bytes().to_vec().into() +} + impl Block { pub fn block_number(&self) -> u64 { match self { @@ -67,6 +71,13 @@ impl Block { } } + pub fn gas_limit(&self) -> u64 { + match self { + Block::PoW(_) => DEFAULT_GAS_LIMIT, + Block::PoS(payload) => payload.gas_limit(), + } + } + pub fn as_execution_block(&self, total_difficulty: Uint256) -> ExecutionBlock { match self { Block::PoW(block) => ExecutionBlock { @@ -570,10 +581,10 @@ impl ExecutionBlockGenerator { logs_bloom: vec![0; 256].into(), prev_randao: pa.prev_randao, block_number: parent.block_number() + 1, - gas_limit: GAS_LIMIT, + gas_limit: DEFAULT_GAS_LIMIT, gas_used: GAS_USED, timestamp: pa.timestamp, - extra_data: "block gen was here".as_bytes().to_vec().into(), + extra_data: mock_el_extra_data::(), base_fee_per_gas: Uint256::from(1u64), block_hash: ExecutionBlockHash::zero(), transactions: vec![].into(), @@ -587,10 +598,10 @@ impl ExecutionBlockGenerator { logs_bloom: vec![0; 256].into(), prev_randao: pa.prev_randao, block_number: parent.block_number() + 1, - gas_limit: GAS_LIMIT, + gas_limit: DEFAULT_GAS_LIMIT, gas_used: GAS_USED, timestamp: pa.timestamp, - extra_data: "block gen was here".as_bytes().to_vec().into(), + extra_data: mock_el_extra_data::(), base_fee_per_gas: Uint256::from(1u64), block_hash: ExecutionBlockHash::zero(), transactions: vec![].into(), @@ -603,10 +614,10 @@ impl ExecutionBlockGenerator { logs_bloom: vec![0; 256].into(), prev_randao: pa.prev_randao, block_number: parent.block_number() + 1, - gas_limit: GAS_LIMIT, + gas_limit: DEFAULT_GAS_LIMIT, gas_used: GAS_USED, timestamp: pa.timestamp, - extra_data: "block gen was here".as_bytes().to_vec().into(), + extra_data: mock_el_extra_data::(), base_fee_per_gas: Uint256::from(1u64), block_hash: ExecutionBlockHash::zero(), transactions: vec![].into(), @@ -623,10 +634,10 @@ impl ExecutionBlockGenerator { logs_bloom: vec![0; 256].into(), prev_randao: pa.prev_randao, block_number: parent.block_number() + 1, - gas_limit: GAS_LIMIT, + gas_limit: DEFAULT_GAS_LIMIT, gas_used: GAS_USED, timestamp: pa.timestamp, - extra_data: "block gen was here".as_bytes().to_vec().into(), + extra_data: mock_el_extra_data::(), base_fee_per_gas: Uint256::from(1u64), block_hash: ExecutionBlockHash::zero(), transactions: vec![].into(), @@ -642,10 +653,10 @@ impl ExecutionBlockGenerator { logs_bloom: vec![0; 256].into(), prev_randao: pa.prev_randao, block_number: parent.block_number() + 1, - gas_limit: GAS_LIMIT, + gas_limit: DEFAULT_GAS_LIMIT, gas_used: GAS_USED, timestamp: pa.timestamp, - extra_data: "block gen was here".as_bytes().to_vec().into(), + extra_data: mock_el_extra_data::(), base_fee_per_gas: Uint256::from(1u64), block_hash: ExecutionBlockHash::zero(), transactions: vec![].into(), diff --git a/beacon_node/execution_layer/src/test_utils/mock_builder.rs b/beacon_node/execution_layer/src/test_utils/mock_builder.rs index 341daedbc8..879b54eb07 100644 --- a/beacon_node/execution_layer/src/test_utils/mock_builder.rs +++ b/beacon_node/execution_layer/src/test_utils/mock_builder.rs @@ -1,5 +1,5 @@ use crate::test_utils::{DEFAULT_BUILDER_PAYLOAD_VALUE_WEI, DEFAULT_JWT_SECRET}; -use crate::{Config, ExecutionLayer, PayloadAttributes}; +use crate::{Config, ExecutionLayer, PayloadAttributes, PayloadParameters}; use eth2::types::{BlobsBundle, BlockId, StateId, ValidatorId}; use eth2::{BeaconNodeHttpClient, Timeouts, CONSENSUS_VERSION_HEADER}; use fork_choice::ForkchoiceUpdateParameters; @@ -54,6 +54,10 @@ impl Operation { } } +pub fn mock_builder_extra_data() -> types::VariableList { + "mock_builder".as_bytes().to_vec().into() +} + #[derive(Debug)] // We don't use the string value directly, but it's used in the Debug impl which is required by `warp::reject::Reject`. struct Custom(#[allow(dead_code)] String); @@ -72,6 +76,8 @@ pub trait BidStuff { fn set_withdrawals_root(&mut self, withdrawals_root: Hash256); fn sign_builder_message(&mut self, sk: &SecretKey, spec: &ChainSpec) -> Signature; + + fn stamp_payload(&mut self); } impl BidStuff for BuilderBid { @@ -203,6 +209,29 @@ impl BidStuff for BuilderBid { let message = self.signing_root(domain); sk.sign(message) } + + // this helps differentiate a builder block from a regular block + fn stamp_payload(&mut self) { + let extra_data = mock_builder_extra_data::(); + match self.to_mut().header_mut() { + ExecutionPayloadHeaderRefMut::Bellatrix(header) => { + header.extra_data = extra_data; + header.block_hash = ExecutionBlockHash::from_root(header.tree_hash_root()); + } + ExecutionPayloadHeaderRefMut::Capella(header) => { + header.extra_data = extra_data; + header.block_hash = ExecutionBlockHash::from_root(header.tree_hash_root()); + } + ExecutionPayloadHeaderRefMut::Deneb(header) => { + header.extra_data = extra_data; + header.block_hash = ExecutionBlockHash::from_root(header.tree_hash_root()); + } + ExecutionPayloadHeaderRefMut::Electra(header) => { + header.extra_data = extra_data; + header.block_hash = ExecutionBlockHash::from_root(header.tree_hash_root()); + } + } + } } #[derive(Clone)] @@ -286,6 +315,7 @@ impl MockBuilder { while let Some(op) = guard.pop() { op.apply(bid); } + bid.stamp_payload(); } } @@ -413,11 +443,12 @@ pub fn serve( let block = head.data.message(); let head_block_root = block.tree_hash_root(); - let head_execution_hash = block + let head_execution_payload = block .body() .execution_payload() - .map_err(|_| reject("pre-merge block"))? - .block_hash(); + .map_err(|_| reject("pre-merge block"))?; + let head_execution_hash = head_execution_payload.block_hash(); + let head_gas_limit = head_execution_payload.gas_limit(); if head_execution_hash != parent_hash { return Err(reject("head mismatch")); } @@ -529,14 +560,24 @@ pub fn serve( finalized_hash: Some(finalized_execution_hash), }; + let proposer_gas_limit = builder + .val_registration_cache + .read() + .get(&pubkey) + .map(|v| v.message.gas_limit); + + let payload_parameters = PayloadParameters { + parent_hash: head_execution_hash, + parent_gas_limit: head_gas_limit, + proposer_gas_limit, + payload_attributes: &payload_attributes, + forkchoice_update_params: &forkchoice_update_params, + current_fork: fork, + }; + let payload_response_type = builder .el - .get_full_payload_caching( - head_execution_hash, - &payload_attributes, - forkchoice_update_params, - fork, - ) + .get_full_payload_caching(payload_parameters) .await .map_err(|_| reject("couldn't get payload"))?; @@ -648,8 +689,6 @@ pub fn serve( } }; - message.set_gas_limit(cached_data.gas_limit); - builder.apply_operations(&mut message); let mut signature = diff --git a/beacon_node/execution_layer/src/test_utils/mock_execution_layer.rs b/beacon_node/execution_layer/src/test_utils/mock_execution_layer.rs index a9f1313e46..48372a39be 100644 --- a/beacon_node/execution_layer/src/test_utils/mock_execution_layer.rs +++ b/beacon_node/execution_layer/src/test_utils/mock_execution_layer.rs @@ -90,6 +90,7 @@ impl MockExecutionLayer { }; let parent_hash = latest_execution_block.block_hash(); + let parent_gas_limit = latest_execution_block.gas_limit(); let block_number = latest_execution_block.block_number() + 1; let timestamp = block_number; let prev_randao = Hash256::from_low_u64_be(block_number); @@ -131,14 +132,20 @@ impl MockExecutionLayer { let payload_attributes = PayloadAttributes::new(timestamp, prev_randao, suggested_fee_recipient, None, None); + let payload_parameters = PayloadParameters { + parent_hash, + parent_gas_limit, + proposer_gas_limit: None, + payload_attributes: &payload_attributes, + forkchoice_update_params: &forkchoice_update_params, + current_fork: ForkName::Bellatrix, + }; + let block_proposal_content_type = self .el .get_payload( - parent_hash, - &payload_attributes, - forkchoice_update_params, + payload_parameters, builder_params, - ForkName::Bellatrix, &self.spec, None, BlockProductionVersion::FullV2, @@ -171,14 +178,20 @@ impl MockExecutionLayer { let payload_attributes = PayloadAttributes::new(timestamp, prev_randao, suggested_fee_recipient, None, None); + let payload_parameters = PayloadParameters { + parent_hash, + parent_gas_limit, + proposer_gas_limit: None, + payload_attributes: &payload_attributes, + forkchoice_update_params: &forkchoice_update_params, + current_fork: ForkName::Bellatrix, + }; + let block_proposal_content_type = self .el .get_payload( - parent_hash, - &payload_attributes, - forkchoice_update_params, + payload_parameters, builder_params, - ForkName::Bellatrix, &self.spec, None, BlockProductionVersion::BlindedV2, diff --git a/beacon_node/execution_layer/src/test_utils/mod.rs b/beacon_node/execution_layer/src/test_utils/mod.rs index 1e71fde255..faf6d4ef0b 100644 --- a/beacon_node/execution_layer/src/test_utils/mod.rs +++ b/beacon_node/execution_layer/src/test_utils/mod.rs @@ -25,12 +25,13 @@ use types::{EthSpec, ExecutionBlockHash, Uint256}; use warp::{http::StatusCode, Filter, Rejection}; use crate::EngineCapabilities; +pub use execution_block_generator::DEFAULT_GAS_LIMIT; pub use execution_block_generator::{ generate_blobs, generate_genesis_block, generate_genesis_header, generate_pow_block, - static_valid_tx, Block, ExecutionBlockGenerator, + mock_el_extra_data, static_valid_tx, Block, ExecutionBlockGenerator, }; pub use hook::Hook; -pub use mock_builder::{MockBuilder, Operation}; +pub use mock_builder::{mock_builder_extra_data, MockBuilder, Operation}; pub use mock_execution_layer::MockExecutionLayer; pub const DEFAULT_TERMINAL_DIFFICULTY: u64 = 6400; diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index fe05f55a01..23d177da78 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -3704,7 +3704,10 @@ pub fn serve( ); execution_layer - .update_proposer_preparation(current_epoch, &preparation_data) + .update_proposer_preparation( + current_epoch, + preparation_data.iter().map(|data| (data, &None)), + ) .await; chain @@ -3762,7 +3765,7 @@ pub fn serve( let spec = &chain.spec; let (preparation_data, filtered_registration_data): ( - Vec, + Vec<(ProposerPreparationData, Option)>, Vec, ) = register_val_data .into_iter() @@ -3792,12 +3795,15 @@ pub fn serve( // Filter out validators who are not 'active' or 'pending'. is_active_or_pending.then_some({ ( - ProposerPreparationData { - validator_index: validator_index as u64, - fee_recipient: register_data - .message - .fee_recipient, - }, + ( + ProposerPreparationData { + validator_index: validator_index as u64, + fee_recipient: register_data + .message + .fee_recipient, + }, + Some(register_data.message.gas_limit), + ), register_data, ) }) @@ -3807,7 +3813,10 @@ pub fn serve( // Update the prepare beacon proposer cache based on this request. execution_layer - .update_proposer_preparation(current_epoch, &preparation_data) + .update_proposer_preparation( + current_epoch, + preparation_data.iter().map(|(data, limit)| (data, limit)), + ) .await; // Call prepare beacon proposer blocking with the latest update in order to make diff --git a/beacon_node/http_api/tests/interactive_tests.rs b/beacon_node/http_api/tests/interactive_tests.rs index c3ed334782..627b0d0b17 100644 --- a/beacon_node/http_api/tests/interactive_tests.rs +++ b/beacon_node/http_api/tests/interactive_tests.rs @@ -447,9 +447,14 @@ pub async fn proposer_boost_re_org_test( // Send proposer preparation data for all validators. let proposer_preparation_data = all_validators .iter() - .map(|i| ProposerPreparationData { - validator_index: *i as u64, - fee_recipient: Address::from_low_u64_be(*i as u64), + .map(|i| { + ( + ProposerPreparationData { + validator_index: *i as u64, + fee_recipient: Address::from_low_u64_be(*i as u64), + }, + None, + ) }) .collect::>(); harness @@ -459,7 +464,7 @@ pub async fn proposer_boost_re_org_test( .unwrap() .update_proposer_preparation( head_slot.epoch(E::slots_per_epoch()) + 1, - &proposer_preparation_data, + proposer_preparation_data.iter().map(|(a, b)| (a, b)), ) .await; diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 940f3ae9c0..080a393b4d 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -13,8 +13,10 @@ use eth2::{ Error::ServerMessage, StatusCode, Timeouts, }; +use execution_layer::expected_gas_limit; use execution_layer::test_utils::{ - MockBuilder, Operation, DEFAULT_BUILDER_PAYLOAD_VALUE_WEI, DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI, + mock_builder_extra_data, mock_el_extra_data, MockBuilder, Operation, + DEFAULT_BUILDER_PAYLOAD_VALUE_WEI, DEFAULT_GAS_LIMIT, DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI, }; use futures::stream::{Stream, StreamExt}; use futures::FutureExt; @@ -348,7 +350,6 @@ impl ApiTester { let bls_to_execution_change = harness.make_bls_to_execution_change(4, Address::zero()); let chain = harness.chain.clone(); - let log = test_logger(); let ApiServer { @@ -3755,7 +3756,11 @@ impl ApiTester { self } - pub async fn test_post_validator_register_validator(self) -> Self { + async fn generate_validator_registration_data( + &self, + fee_recipient_generator: impl Fn(usize) -> Address, + gas_limit: u64, + ) -> (Vec, Vec
) { let mut registrations = vec![]; let mut fee_recipients = vec![]; @@ -3766,15 +3771,13 @@ impl ApiTester { epoch: genesis_epoch, }; - let expected_gas_limit = 11_111_111; - for (val_index, keypair) in self.validator_keypairs().iter().enumerate() { let pubkey = keypair.pk.compress(); - let fee_recipient = Address::from_low_u64_be(val_index as u64); + let fee_recipient = fee_recipient_generator(val_index); let data = ValidatorRegistrationData { fee_recipient, - gas_limit: expected_gas_limit, + gas_limit, timestamp: 0, pubkey, }; @@ -3797,6 +3800,17 @@ impl ApiTester { registrations.push(signed); } + (registrations, fee_recipients) + } + + pub async fn test_post_validator_register_validator(self) -> Self { + let (registrations, fee_recipients) = self + .generate_validator_registration_data( + |val_index| Address::from_low_u64_be(val_index as u64), + DEFAULT_GAS_LIMIT, + ) + .await; + self.client .post_validator_register_validator(®istrations) .await @@ -3811,14 +3825,22 @@ impl ApiTester { .zip(fee_recipients.into_iter()) .enumerate() { - let actual = self + let actual_fee_recipient = self .chain .execution_layer .as_ref() .unwrap() .get_suggested_fee_recipient(val_index as u64) .await; - assert_eq!(actual, fee_recipient); + let actual_gas_limit = self + .chain + .execution_layer + .as_ref() + .unwrap() + .get_proposer_gas_limit(val_index as u64) + .await; + assert_eq!(actual_fee_recipient, fee_recipient); + assert_eq!(actual_gas_limit, Some(DEFAULT_GAS_LIMIT)); } self @@ -3839,46 +3861,12 @@ impl ApiTester { ) .await; - let mut registrations = vec![]; - let mut fee_recipients = vec![]; - - let genesis_epoch = self.chain.spec.genesis_slot.epoch(E::slots_per_epoch()); - let fork = Fork { - current_version: self.chain.spec.genesis_fork_version, - previous_version: self.chain.spec.genesis_fork_version, - epoch: genesis_epoch, - }; - - let expected_gas_limit = 11_111_111; - - for (val_index, keypair) in self.validator_keypairs().iter().enumerate() { - let pubkey = keypair.pk.compress(); - let fee_recipient = Address::from_low_u64_be(val_index as u64); - - let data = ValidatorRegistrationData { - fee_recipient, - gas_limit: expected_gas_limit, - timestamp: 0, - pubkey, - }; - - let domain = self.chain.spec.get_domain( - genesis_epoch, - Domain::ApplicationMask(ApplicationDomain::Builder), - &fork, - Hash256::zero(), - ); - let message = data.signing_root(domain); - let signature = keypair.sk.sign(message); - - let signed = SignedValidatorRegistrationData { - message: data, - signature, - }; - - fee_recipients.push(fee_recipient); - registrations.push(signed); - } + let (registrations, fee_recipients) = self + .generate_validator_registration_data( + |val_index| Address::from_low_u64_be(val_index as u64), + DEFAULT_GAS_LIMIT, + ) + .await; self.client .post_validator_register_validator(®istrations) @@ -3911,6 +3899,47 @@ impl ApiTester { self } + pub async fn test_post_validator_register_validator_higher_gas_limit(&self) { + let (registrations, fee_recipients) = self + .generate_validator_registration_data( + |val_index| Address::from_low_u64_be(val_index as u64), + DEFAULT_GAS_LIMIT + 10_000_000, + ) + .await; + + self.client + .post_validator_register_validator(®istrations) + .await + .unwrap(); + + for (val_index, (_, fee_recipient)) in self + .chain + .head_snapshot() + .beacon_state + .validators() + .into_iter() + .zip(fee_recipients.into_iter()) + .enumerate() + { + let actual_fee_recipient = self + .chain + .execution_layer + .as_ref() + .unwrap() + .get_suggested_fee_recipient(val_index as u64) + .await; + let actual_gas_limit = self + .chain + .execution_layer + .as_ref() + .unwrap() + .get_proposer_gas_limit(val_index as u64) + .await; + assert_eq!(actual_fee_recipient, fee_recipient); + assert_eq!(actual_gas_limit, Some(DEFAULT_GAS_LIMIT + 10_000_000)); + } + } + pub async fn test_post_validator_liveness_epoch(self) -> Self { let epoch = self.chain.epoch().unwrap(); let head_state = self.chain.head_beacon_state_cloned(); @@ -4031,7 +4060,7 @@ impl ApiTester { let expected_fee_recipient = Address::from_low_u64_be(proposer_index as u64); assert_eq!(payload.fee_recipient(), expected_fee_recipient); - assert_eq!(payload.gas_limit(), 11_111_111); + assert_eq!(payload.gas_limit(), DEFAULT_GAS_LIMIT); self } @@ -4058,7 +4087,8 @@ impl ApiTester { let expected_fee_recipient = Address::from_low_u64_be(proposer_index as u64); assert_eq!(payload.fee_recipient(), expected_fee_recipient); - assert_eq!(payload.gas_limit(), 16_384); + // This is the graffiti of the mock execution layer, not the builder. + assert_eq!(payload.extra_data(), mock_el_extra_data::()); self } @@ -4085,7 +4115,7 @@ impl ApiTester { let expected_fee_recipient = Address::from_low_u64_be(proposer_index as u64); assert_eq!(payload.fee_recipient(), expected_fee_recipient); - assert_eq!(payload.gas_limit(), 11_111_111); + assert_eq!(payload.gas_limit(), DEFAULT_GAS_LIMIT); self } @@ -4109,7 +4139,7 @@ impl ApiTester { let expected_fee_recipient = Address::from_low_u64_be(proposer_index as u64); assert_eq!(payload.fee_recipient(), expected_fee_recipient); - assert_eq!(payload.gas_limit(), 11_111_111); + assert_eq!(payload.gas_limit(), DEFAULT_GAS_LIMIT); // If this cache is empty, it indicates fallback was not used, so the payload came from the // mock builder. @@ -4126,10 +4156,16 @@ impl ApiTester { pub async fn test_payload_accepts_mutated_gas_limit(self) -> Self { // Mutate gas limit. + let builder_limit = expected_gas_limit( + DEFAULT_GAS_LIMIT, + DEFAULT_GAS_LIMIT + 10_000_000, + self.chain.spec.as_ref(), + ) + .expect("calculate expected gas limit"); self.mock_builder .as_ref() .unwrap() - .add_operation(Operation::GasLimit(30_000_000)); + .add_operation(Operation::GasLimit(builder_limit as usize)); let slot = self.chain.slot().unwrap(); let epoch = self.chain.epoch().unwrap(); @@ -4149,7 +4185,7 @@ impl ApiTester { let expected_fee_recipient = Address::from_low_u64_be(proposer_index as u64); assert_eq!(payload.fee_recipient(), expected_fee_recipient); - assert_eq!(payload.gas_limit(), 30_000_000); + assert_eq!(payload.gas_limit(), builder_limit); // This cache should not be populated because fallback should not have been used. assert!(self @@ -4159,6 +4195,49 @@ impl ApiTester { .unwrap() .get_payload_by_root(&payload.tree_hash_root()) .is_none()); + // Another way is to check for the extra data of the mock builder + assert_eq!(payload.extra_data(), mock_builder_extra_data::()); + + self + } + + pub async fn test_builder_payload_rejected_when_gas_limit_incorrect(self) -> Self { + self.test_post_validator_register_validator_higher_gas_limit() + .await; + + // Mutate gas limit. + self.mock_builder + .as_ref() + .unwrap() + .add_operation(Operation::GasLimit(1)); + + let slot = self.chain.slot().unwrap(); + let epoch = self.chain.epoch().unwrap(); + + let (_, randao_reveal) = self.get_test_randao(slot, epoch).await; + + let payload: BlindedPayload = self + .client + .get_validator_blinded_blocks::(slot, &randao_reveal, None) + .await + .unwrap() + .data + .body() + .execution_payload() + .unwrap() + .into(); + + // If this cache is populated, it indicates fallback to the local EE was correctly used. + assert!(self + .chain + .execution_layer + .as_ref() + .unwrap() + .get_payload_by_root(&payload.tree_hash_root()) + .is_some()); + // another way is to check for the extra data of the local EE + assert_eq!(payload.extra_data(), mock_el_extra_data::()); + self } @@ -4232,6 +4311,9 @@ impl ApiTester { .unwrap() .get_payload_by_root(&payload.tree_hash_root()) .is_none()); + // Another way is to check for the extra data of the mock builder + assert_eq!(payload.extra_data(), mock_builder_extra_data::()); + self } @@ -4315,6 +4397,9 @@ impl ApiTester { .unwrap() .get_payload_by_root(&payload.tree_hash_root()) .is_some()); + // another way is to check for the extra data of the local EE + assert_eq!(payload.extra_data(), mock_el_extra_data::()); + self } @@ -4404,6 +4489,9 @@ impl ApiTester { .unwrap() .get_payload_by_root(&payload.tree_hash_root()) .is_some()); + // another way is to check for the extra data of the local EE + assert_eq!(payload.extra_data(), mock_el_extra_data::()); + self } @@ -4491,6 +4579,9 @@ impl ApiTester { .unwrap() .get_payload_by_root(&payload.tree_hash_root()) .is_some()); + // another way is to check for the extra data of the local EE + assert_eq!(payload.extra_data(), mock_el_extra_data::()); + self } @@ -4577,6 +4668,9 @@ impl ApiTester { .unwrap() .get_payload_by_root(&payload.tree_hash_root()) .is_some()); + // another way is to check for the extra data of the local EE + assert_eq!(payload.extra_data(), mock_el_extra_data::()); + self } @@ -4647,6 +4741,9 @@ impl ApiTester { .unwrap() .get_payload_by_root(&payload.tree_hash_root()) .is_some()); + // another way is to check for the extra data of the local EE + assert_eq!(payload.extra_data(), mock_el_extra_data::()); + self } @@ -4707,6 +4804,9 @@ impl ApiTester { .unwrap() .get_payload_by_root(&payload.tree_hash_root()) .is_some()); + // another way is to check for the extra data of the local EE + assert_eq!(payload.extra_data(), mock_el_extra_data::()); + self } @@ -4780,6 +4880,8 @@ impl ApiTester { .unwrap() .get_payload_by_root(&payload.tree_hash_root()) .is_none()); + // Another way is to check for the extra data of the mock builder + assert_eq!(payload.extra_data(), mock_builder_extra_data::()); // Without proposing, advance into the next slot, this should make us cross the threshold // number of skips, causing us to use the fallback. @@ -4809,6 +4911,8 @@ impl ApiTester { .unwrap() .get_payload_by_root(&payload.tree_hash_root()) .is_some()); + // another way is to check for the extra data of the local EE + assert_eq!(payload.extra_data(), mock_el_extra_data::()); self } @@ -4915,6 +5019,8 @@ impl ApiTester { .unwrap() .get_payload_by_root(&payload.tree_hash_root()) .is_some()); + // another way is to check for the extra data of the local EE + assert_eq!(payload.extra_data(), mock_el_extra_data::()); // Fill another epoch with blocks, should be enough to finalize. (Sneaky plus 1 because this // scenario starts at an epoch boundary). @@ -4954,6 +5060,8 @@ impl ApiTester { .unwrap() .get_payload_by_root(&payload.tree_hash_root()) .is_none()); + // Another way is to check for the extra data of the mock builder + assert_eq!(payload.extra_data(), mock_builder_extra_data::()); self } @@ -5072,6 +5180,8 @@ impl ApiTester { .unwrap() .get_payload_by_root(&payload.tree_hash_root()) .is_some()); + // another way is to check for the extra data of the local EE + assert_eq!(payload.extra_data(), mock_el_extra_data::()); self } @@ -5149,6 +5259,9 @@ impl ApiTester { .unwrap() .get_payload_by_root(&payload.tree_hash_root()) .is_none()); + // Another way is to check for the extra data of the mock builder + assert_eq!(payload.extra_data(), mock_builder_extra_data::()); + self } @@ -5214,6 +5327,9 @@ impl ApiTester { .unwrap() .get_payload_by_root(&payload.tree_hash_root()) .is_some()); + // another way is to check for the extra data of the local EE + assert_eq!(payload.extra_data(), mock_el_extra_data::()); + self } @@ -5279,6 +5395,9 @@ impl ApiTester { .unwrap() .get_payload_by_root(&payload.tree_hash_root()) .is_some()); + // another way is to check for the extra data of the local EE + assert_eq!(payload.extra_data(), mock_el_extra_data::()); + self } @@ -5343,6 +5462,9 @@ impl ApiTester { .unwrap() .get_payload_by_root(&payload.tree_hash_root()) .is_none()); + // Another way is to check for the extra data of the mock builder + assert_eq!(payload.extra_data(), mock_builder_extra_data::()); + self } @@ -6682,6 +6804,8 @@ async fn post_validator_register_valid_v3() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn post_validator_register_gas_limit_mutation() { ApiTester::new_mev_tester() + .await + .test_builder_payload_rejected_when_gas_limit_incorrect() .await .test_payload_accepts_mutated_gas_limit() .await; diff --git a/consensus/types/src/chain_spec.rs b/consensus/types/src/chain_spec.rs index 79dcc65ea3..0b33a76ff1 100644 --- a/consensus/types/src/chain_spec.rs +++ b/consensus/types/src/chain_spec.rs @@ -127,6 +127,11 @@ pub struct ChainSpec { pub deposit_network_id: u64, pub deposit_contract_address: Address, + /* + * Execution Specs + */ + pub gas_limit_adjustment_factor: u64, + /* * Altair hard fork params */ @@ -715,6 +720,11 @@ impl ChainSpec { .parse() .expect("chain spec deposit contract address"), + /* + * Execution Specs + */ + gas_limit_adjustment_factor: 1024, + /* * Altair hard fork params */ @@ -1029,6 +1039,11 @@ impl ChainSpec { .parse() .expect("chain spec deposit contract address"), + /* + * Execution Specs + */ + gas_limit_adjustment_factor: 1024, + /* * Altair hard fork params */ @@ -1285,6 +1300,10 @@ pub struct Config { #[serde(with = "serde_utils::address_hex")] deposit_contract_address: Address, + #[serde(default = "default_gas_limit_adjustment_factor")] + #[serde(with = "serde_utils::quoted_u64")] + gas_limit_adjustment_factor: u64, + #[serde(default = "default_gossip_max_size")] #[serde(with = "serde_utils::quoted_u64")] gossip_max_size: u64, @@ -1407,6 +1426,10 @@ const fn default_max_per_epoch_activation_churn_limit() -> u64 { 8 } +const fn default_gas_limit_adjustment_factor() -> u64 { + 1024 +} + const fn default_gossip_max_size() -> u64 { 10485760 } @@ -1659,6 +1682,8 @@ impl Config { deposit_network_id: spec.deposit_network_id, deposit_contract_address: spec.deposit_contract_address, + gas_limit_adjustment_factor: spec.gas_limit_adjustment_factor, + gossip_max_size: spec.gossip_max_size, max_request_blocks: spec.max_request_blocks, min_epochs_for_block_requests: spec.min_epochs_for_block_requests, @@ -1733,6 +1758,7 @@ impl Config { deposit_chain_id, deposit_network_id, deposit_contract_address, + gas_limit_adjustment_factor, gossip_max_size, min_epochs_for_block_requests, max_chunk_size, @@ -1794,6 +1820,7 @@ impl Config { deposit_chain_id, deposit_network_id, deposit_contract_address, + gas_limit_adjustment_factor, terminal_total_difficulty, terminal_block_hash, terminal_block_hash_activation_epoch, diff --git a/consensus/types/src/payload.rs b/consensus/types/src/payload.rs index b82a897da5..e68801840a 100644 --- a/consensus/types/src/payload.rs +++ b/consensus/types/src/payload.rs @@ -32,6 +32,7 @@ pub trait ExecPayload: Debug + Clone + PartialEq + Hash + TreeHash + fn prev_randao(&self) -> Hash256; fn block_number(&self) -> u64; fn timestamp(&self) -> u64; + fn extra_data(&self) -> VariableList; fn block_hash(&self) -> ExecutionBlockHash; fn fee_recipient(&self) -> Address; fn gas_limit(&self) -> u64; @@ -225,6 +226,13 @@ impl ExecPayload for FullPayload { }) } + fn extra_data<'a>(&'a self) -> VariableList { + map_full_payload_ref!(&'a _, self.to_ref(), move |payload, cons| { + cons(payload); + payload.execution_payload.extra_data.clone() + }) + } + fn block_hash<'a>(&'a self) -> ExecutionBlockHash { map_full_payload_ref!(&'a _, self.to_ref(), move |payload, cons| { cons(payload); @@ -357,6 +365,13 @@ impl ExecPayload for FullPayloadRef<'_, E> { }) } + fn extra_data<'a>(&'a self) -> VariableList { + map_full_payload_ref!(&'a _, self, move |payload, cons| { + cons(payload); + payload.execution_payload.extra_data.clone() + }) + } + fn block_hash<'a>(&'a self) -> ExecutionBlockHash { map_full_payload_ref!(&'a _, self, move |payload, cons| { cons(payload); @@ -542,6 +557,13 @@ impl ExecPayload for BlindedPayload { }) } + fn extra_data<'a>(&'a self) -> VariableList::MaxExtraDataBytes> { + map_blinded_payload_ref!(&'a _, self.to_ref(), move |payload, cons| { + cons(payload); + payload.execution_payload_header.extra_data.clone() + }) + } + fn block_hash<'a>(&'a self) -> ExecutionBlockHash { map_blinded_payload_ref!(&'a _, self.to_ref(), move |payload, cons| { cons(payload); @@ -643,6 +665,13 @@ impl<'b, E: EthSpec> ExecPayload for BlindedPayloadRef<'b, E> { }) } + fn extra_data<'a>(&'a self) -> VariableList::MaxExtraDataBytes> { + map_blinded_payload_ref!(&'a _, self, move |payload, cons| { + cons(payload); + payload.execution_payload_header.extra_data.clone() + }) + } + fn block_hash<'a>(&'a self) -> ExecutionBlockHash { map_blinded_payload_ref!(&'a _, self, move |payload, cons| { cons(payload); @@ -745,6 +774,10 @@ macro_rules! impl_exec_payload_common { self.$wrapped_field.timestamp } + fn extra_data(&self) -> VariableList { + self.$wrapped_field.extra_data.clone() + } + fn block_hash(&self) -> ExecutionBlockHash { self.$wrapped_field.block_hash } diff --git a/testing/ef_tests/src/cases/fork_choice.rs b/testing/ef_tests/src/cases/fork_choice.rs index 7d4d229fef..427bcf5e9c 100644 --- a/testing/ef_tests/src/cases/fork_choice.rs +++ b/testing/ef_tests/src/cases/fork_choice.rs @@ -809,10 +809,13 @@ impl Tester { if expected_should_override_fcu.validator_is_connected { el.update_proposer_preparation( next_slot_epoch, - &[ProposerPreparationData { - validator_index: dbg!(proposer_index) as u64, - fee_recipient: Default::default(), - }], + [( + &ProposerPreparationData { + validator_index: dbg!(proposer_index) as u64, + fee_recipient: Default::default(), + }, + &None, + )], ) .await; } else { diff --git a/testing/execution_engine_integration/src/test_rig.rs b/testing/execution_engine_integration/src/test_rig.rs index 0289fd4206..f664509304 100644 --- a/testing/execution_engine_integration/src/test_rig.rs +++ b/testing/execution_engine_integration/src/test_rig.rs @@ -3,9 +3,10 @@ use crate::execution_engine::{ }; use crate::transactions::transactions; use ethers_providers::Middleware; +use execution_layer::test_utils::DEFAULT_GAS_LIMIT; use execution_layer::{ BlockProposalContentsType, BuilderParams, ChainHealth, ExecutionLayer, PayloadAttributes, - PayloadStatus, + PayloadParameters, PayloadStatus, }; use fork_choice::ForkchoiceUpdateParameters; use reqwest::{header::CONTENT_TYPE, Client}; @@ -251,6 +252,7 @@ impl TestRig { */ let parent_hash = terminal_pow_block_hash; + let parent_gas_limit = DEFAULT_GAS_LIMIT; let timestamp = timestamp_now(); let prev_randao = Hash256::zero(); let head_root = Hash256::zero(); @@ -324,15 +326,22 @@ impl TestRig { Some(vec![]), None, ); + + let payload_parameters = PayloadParameters { + parent_hash, + parent_gas_limit, + proposer_gas_limit: None, + payload_attributes: &payload_attributes, + forkchoice_update_params: &forkchoice_update_params, + current_fork: TEST_FORK, + }; + let block_proposal_content_type = self .ee_a .execution_layer .get_payload( - parent_hash, - &payload_attributes, - forkchoice_update_params, + payload_parameters, builder_params, - TEST_FORK, &self.spec, None, BlockProductionVersion::FullV2, @@ -476,15 +485,22 @@ impl TestRig { Some(vec![]), None, ); + + let payload_parameters = PayloadParameters { + parent_hash, + parent_gas_limit, + proposer_gas_limit: None, + payload_attributes: &payload_attributes, + forkchoice_update_params: &forkchoice_update_params, + current_fork: TEST_FORK, + }; + let block_proposal_content_type = self .ee_a .execution_layer .get_payload( - parent_hash, - &payload_attributes, - forkchoice_update_params, + payload_parameters, builder_params, - TEST_FORK, &self.spec, None, BlockProductionVersion::FullV2, From 8e891a8bfd139dde3e63a5ed70bc8b76eea896bf Mon Sep 17 00:00:00 2001 From: Akihito Nakano Date: Mon, 16 Dec 2024 14:44:02 +0900 Subject: [PATCH 044/254] Fix web3signer test fails on macOS (#6588) * Add lighthouse/key_legacy.p12 for macOS * Specify `-days 825` to meet Apple's requirements for TLS server certificates * Remove `-aes256` as it's ignored on exporting The following warning will appear: Warning: output encryption option -aes256 ignored with -export * Update certificates and keys --- testing/web3signer_tests/src/lib.rs | 6 +- testing/web3signer_tests/tls/generate.sh | 21 +++- .../web3signer_tests/tls/lighthouse/cert.pem | 58 +++++----- .../web3signer_tests/tls/lighthouse/key.key | 100 +++++++++--------- .../web3signer_tests/tls/lighthouse/key.p12 | Bin 4371 -> 4387 bytes .../tls/lighthouse/key_legacy.p12 | Bin 0 -> 4221 bytes .../tls/lighthouse/web3signer.pem | 58 +++++----- .../web3signer_tests/tls/web3signer/cert.pem | 58 +++++----- .../web3signer_tests/tls/web3signer/key.key | 100 +++++++++--------- .../web3signer_tests/tls/web3signer/key.p12 | Bin 4371 -> 4387 bytes .../tls/web3signer/known_clients.txt | 2 +- 11 files changed, 210 insertions(+), 193 deletions(-) create mode 100644 testing/web3signer_tests/tls/lighthouse/key_legacy.p12 diff --git a/testing/web3signer_tests/src/lib.rs b/testing/web3signer_tests/src/lib.rs index a58dcb5fa0..bebc8fa13b 100644 --- a/testing/web3signer_tests/src/lib.rs +++ b/testing/web3signer_tests/src/lib.rs @@ -130,7 +130,11 @@ mod tests { } fn client_identity_path() -> PathBuf { - tls_dir().join("lighthouse").join("key.p12") + if cfg!(target_os = "macos") { + tls_dir().join("lighthouse").join("key_legacy.p12") + } else { + tls_dir().join("lighthouse").join("key.p12") + } } fn client_identity_password() -> String { diff --git a/testing/web3signer_tests/tls/generate.sh b/testing/web3signer_tests/tls/generate.sh index f918e87cf8..3b14dbddba 100755 --- a/testing/web3signer_tests/tls/generate.sh +++ b/testing/web3signer_tests/tls/generate.sh @@ -1,7 +1,20 @@ #!/bin/bash -openssl req -x509 -sha256 -nodes -days 36500 -newkey rsa:4096 -keyout web3signer/key.key -out web3signer/cert.pem -config web3signer/config && -openssl pkcs12 -export -aes256 -out web3signer/key.p12 -inkey web3signer/key.key -in web3signer/cert.pem -password pass:$(cat web3signer/password.txt) && + +# The lighthouse/key_legacy.p12 file is generated specifically for macOS because the default `openssl pkcs12` encoding +# algorithm in OpenSSL v3 is not compatible with the PKCS algorithm used by the Apple Security Framework. The client +# side (using the reqwest crate) relies on the Apple Security Framework to parse PKCS files. +# We don't need to generate web3signer/key_legacy.p12 because the compatibility issue doesn't occur on the web3signer +# side. It seems that web3signer (Java) uses its own implementation to parse PKCS files. +# See https://github.com/sigp/lighthouse/issues/6442#issuecomment-2469252651 + +# We specify `-days 825` when generating the certificate files because Apple requires TLS server certificates to have a +# validity period of 825 days or fewer. +# See https://github.com/sigp/lighthouse/issues/6442#issuecomment-2474979183 + +openssl req -x509 -sha256 -nodes -days 825 -newkey rsa:4096 -keyout web3signer/key.key -out web3signer/cert.pem -config web3signer/config && +openssl pkcs12 -export -out web3signer/key.p12 -inkey web3signer/key.key -in web3signer/cert.pem -password pass:$(cat web3signer/password.txt) && cp web3signer/cert.pem lighthouse/web3signer.pem && -openssl req -x509 -sha256 -nodes -days 36500 -newkey rsa:4096 -keyout lighthouse/key.key -out lighthouse/cert.pem -config lighthouse/config && -openssl pkcs12 -export -aes256 -out lighthouse/key.p12 -inkey lighthouse/key.key -in lighthouse/cert.pem -password pass:$(cat lighthouse/password.txt) && +openssl req -x509 -sha256 -nodes -days 825 -newkey rsa:4096 -keyout lighthouse/key.key -out lighthouse/cert.pem -config lighthouse/config && +openssl pkcs12 -export -out lighthouse/key.p12 -inkey lighthouse/key.key -in lighthouse/cert.pem -password pass:$(cat lighthouse/password.txt) && +openssl pkcs12 -export -legacy -out lighthouse/key_legacy.p12 -inkey lighthouse/key.key -in lighthouse/cert.pem -password pass:$(cat lighthouse/password.txt) && openssl x509 -noout -fingerprint -sha256 -inform pem -in lighthouse/cert.pem | cut -b 20-| sed "s/^/lighthouse /" > web3signer/known_clients.txt diff --git a/testing/web3signer_tests/tls/lighthouse/cert.pem b/testing/web3signer_tests/tls/lighthouse/cert.pem index 24b0a2e5c0..4aaf66b747 100644 --- a/testing/web3signer_tests/tls/lighthouse/cert.pem +++ b/testing/web3signer_tests/tls/lighthouse/cert.pem @@ -1,33 +1,33 @@ -----BEGIN CERTIFICATE----- -MIIFujCCA6KgAwIBAgIUXZijYo8W4/9dAq58ocFEbZDxohwwDQYJKoZIhvcNAQEL +MIIFuDCCA6CgAwIBAgIUa3O7icWD4W7c5yRMjG/EX422ODUwDQYJKoZIhvcNAQEL BQAwazELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAlZBMREwDwYDVQQHDAhTb21lQ2l0 eTESMBAGA1UECgwJTXlDb21wYW55MRMwEQYDVQQLDApNeURpdmlzaW9uMRMwEQYD -VQQDDApsaWdodGhvdXNlMCAXDTIzMDkyMDAyNTYzNloYDzIxMjMwODI3MDI1NjM2 -WjBrMQswCQYDVQQGEwJVUzELMAkGA1UECAwCVkExETAPBgNVBAcMCFNvbWVDaXR5 -MRIwEAYDVQQKDAlNeUNvbXBhbnkxEzARBgNVBAsMCk15RGl2aXNpb24xEzARBgNV -BAMMCmxpZ2h0aG91c2UwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC1 -R1M9NnRwUsqFvJzNWPKuY1PW7llwRRWCixiWNvcxukGTa6AMLZDrYO1Y7qlw5m52 -aHSA2fs2KyeA61yajG/BsLn1vmTtJMZXgLsG0MIqvhgOoh+ZZbl8biO0gQJSRSDE -jf0ogUVM9TCEt6ydbGnzgs8EESqvyXcreaXfmLI7jiX/BkwCdf+Ru+H3MF96QgAw -Oz1d8/fxYJvIpT/DOx4NuMZouSAcUVXgwcVb6JXeTg0xVcL33lluquhYDR0gD5Fe -V0fPth+e9XMAH7udim8E5wn2Ep8CAVoeVq6K9mBM3NqP7+2YmU//jLbkd6UvKPaI -0vps1zF9Bo8QewiRbM0IRse99ikCVZcjOcZSitw3kwTg59NjZ0Vk9R/2YQt/gGWM -VcR//EtbOZGqzGrLPFKOcWO85Ggz746Saj15N+bqT20hXHyiwYL8DLgJkMR2W9Nr -67Vyi9SWSM6rdRQlezlHq/yNEh+JuY7eoC3VeVw9K1ZXP+OKAwbpcnvd3uLwV91f -kpT6kjc6d2h4bK8fhvF16Em42JypQCl0xMhgg/8MFO+6ZLy5otWAdsSYyO5k9CAa -3zLeqd89dS7HNLdLZ0Y5SFWm6y5Kqu89ErIENafX5DxupHWsruiBV7zhDHNPaGcf -TPFe8xuDYsi155veOfEiDh4g+X1qjL8x8OEDjgsM3QIDAQABo1QwUjALBgNVHQ8E -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 +4IRAZKHZaeKYr1UXwaqzpwES+8ZZLjURkvqvnQ== -----END CERTIFICATE----- diff --git a/testing/web3signer_tests/tls/lighthouse/key.key b/testing/web3signer_tests/tls/lighthouse/key.key index d00b6c2122..2b510c6b6d 100644 --- a/testing/web3signer_tests/tls/lighthouse/key.key +++ b/testing/web3signer_tests/tls/lighthouse/key.key @@ -1,52 +1,52 @@ -----BEGIN PRIVATE KEY----- -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 -vc3Za4ClKSLmb3ZA9giTswYMev+3BQ== +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 +UyUzHP+SV/CL1Otbg/HjN6JGIcgY -----END PRIVATE KEY----- diff --git a/testing/web3signer_tests/tls/lighthouse/key.p12 b/testing/web3signer_tests/tls/lighthouse/key.p12 index 73468fa084b6f5f1b036afd643967e361d004fc4..f2ef6d20e27199c5f1d39d25763776470f5a9dd0 100644 GIT binary patch literal 4387 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 diff --git a/testing/web3signer_tests/tls/lighthouse/key_legacy.p12 b/testing/web3signer_tests/tls/lighthouse/key_legacy.p12 new file mode 100644 index 0000000000000000000000000000000000000000..c3394fae9af893142c035e087fa752c5225eefde GIT binary patch literal 4221 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 CpCCy9 diff --git a/testing/web3signer_tests/tls/web3signer/known_clients.txt b/testing/web3signer_tests/tls/web3signer/known_clients.txt index c4722fe587..86d61fba75 100644 --- a/testing/web3signer_tests/tls/web3signer/known_clients.txt +++ b/testing/web3signer_tests/tls/web3signer/known_clients.txt @@ -1 +1 @@ -lighthouse 02:D0:A8:C0:6A:59:90:40:54:67:D4:BD:AE:5A:D4:F5:14:A9:79:38:98:E0:62:93:C1:77:13:FC:B4:60:65:CE +lighthouse 49:99:C9:A4:05:4C:EC:BE:FD:0B:C3:C3:C1:2F:A4:D3:AB:70:96:47:51:F5:5B:3B:37:65:31:56:18:B7:B8:AD From 75d90795be0fe3ddbcb78402d35aab345dc88e2c Mon Sep 17 00:00:00 2001 From: Akihito Nakano Date: Mon, 16 Dec 2024 14:44:06 +0900 Subject: [PATCH 045/254] Remove req_id from CustodyId (#6589) * Remove req_id from CustodyId because it's not used --- beacon_node/lighthouse_network/src/service/api_types.rs | 1 - beacon_node/network/src/sync/network_context.rs | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/beacon_node/lighthouse_network/src/service/api_types.rs b/beacon_node/lighthouse_network/src/service/api_types.rs index cb22815390..85fabbb0c3 100644 --- a/beacon_node/lighthouse_network/src/service/api_types.rs +++ b/beacon_node/lighthouse_network/src/service/api_types.rs @@ -67,7 +67,6 @@ pub struct SamplingRequestId(pub usize); #[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. diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index c4d987e858..b6b7b315f3 100644 --- a/beacon_node/network/src/sync/network_context.rs +++ b/beacon_node/network/src/sync/network_context.rs @@ -763,8 +763,7 @@ impl SyncNetworkContext { let requester = CustodyRequester(id); let mut request = ActiveCustodyRequest::new( block_root, - // TODO(das): req_id is duplicated here, also present in id - CustodyId { requester, req_id }, + CustodyId { requester }, &custody_indexes_to_fetch, self.log.clone(), ); From 1c5be34def7ea46297524180d3b5a1fd2b4c1ac7 Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Mon, 16 Dec 2024 13:44:10 +0800 Subject: [PATCH 046/254] Write range sync tests in external event-driven form (#6618) * Write range sync tests in external event-driven form * Fix remaining test * Drop unused generics * Merge branch 'unstable' into range-sync-tests * Add reference to test author * Use async await * Fix failing test. Not sure how it was passing before without an EL. --- beacon_node/network/src/sync/manager.rs | 10 + .../src/sync/range_sync/block_storage.rs | 13 - .../src/sync/range_sync/chain_collection.rs | 21 +- .../network/src/sync/range_sync/mod.rs | 3 +- .../network/src/sync/range_sync/range.rs | 482 +----------------- .../network/src/sync/range_sync/sync_type.rs | 9 +- beacon_node/network/src/sync/tests/lookups.rs | 30 +- beacon_node/network/src/sync/tests/range.rs | 272 ++++++++++ 8 files changed, 328 insertions(+), 512 deletions(-) delete mode 100644 beacon_node/network/src/sync/range_sync/block_storage.rs diff --git a/beacon_node/network/src/sync/manager.rs b/beacon_node/network/src/sync/manager.rs index 344e91711c..5d02be2b4c 100644 --- a/beacon_node/network/src/sync/manager.rs +++ b/beacon_node/network/src/sync/manager.rs @@ -362,6 +362,16 @@ impl SyncManager { self.sampling.get_request_status(block_root, index) } + #[cfg(test)] + pub(crate) fn range_sync_state(&self) -> super::range_sync::SyncChainStatus { + self.range_sync.state() + } + + #[cfg(test)] + pub(crate) fn update_execution_engine_state(&mut self, state: EngineState) { + self.handle_new_execution_engine_state(state); + } + fn network_globals(&self) -> &NetworkGlobals { self.network.network_globals() } diff --git a/beacon_node/network/src/sync/range_sync/block_storage.rs b/beacon_node/network/src/sync/range_sync/block_storage.rs deleted file mode 100644 index df49543a6b..0000000000 --- a/beacon_node/network/src/sync/range_sync/block_storage.rs +++ /dev/null @@ -1,13 +0,0 @@ -use beacon_chain::{BeaconChain, BeaconChainTypes}; -use types::Hash256; - -/// Trait that helps maintain RangeSync's implementation split from the BeaconChain -pub trait BlockStorage { - fn is_block_known(&self, block_root: &Hash256) -> bool; -} - -impl BlockStorage for BeaconChain { - fn is_block_known(&self, block_root: &Hash256) -> bool { - self.block_is_known_to_fork_choice(block_root) - } -} diff --git a/beacon_node/network/src/sync/range_sync/chain_collection.rs b/beacon_node/network/src/sync/range_sync/chain_collection.rs index 1217fbf8fe..c030d0a19e 100644 --- a/beacon_node/network/src/sync/range_sync/chain_collection.rs +++ b/beacon_node/network/src/sync/range_sync/chain_collection.rs @@ -3,12 +3,11 @@ //! Each chain type is stored in it's own map. A variety of helper functions are given along with //! this struct to simplify the logic of the other layers of sync. -use super::block_storage::BlockStorage; use super::chain::{ChainId, ProcessingResult, RemoveChain, SyncingChain}; use super::sync_type::RangeSyncType; use crate::metrics; use crate::sync::network_context::SyncNetworkContext; -use beacon_chain::BeaconChainTypes; +use beacon_chain::{BeaconChain, BeaconChainTypes}; use fnv::FnvHashMap; use lighthouse_network::PeerId; use lighthouse_network::SyncInfo; @@ -37,10 +36,13 @@ pub enum RangeSyncState { Idle, } +pub type SyncChainStatus = + Result, &'static str>; + /// A collection of finalized and head chains currently being processed. -pub struct ChainCollection { +pub struct ChainCollection { /// The beacon chain for processing. - beacon_chain: Arc, + beacon_chain: Arc>, /// The set of finalized chains being synced. finalized_chains: FnvHashMap>, /// The set of head chains being synced. @@ -51,8 +53,8 @@ pub struct ChainCollection { log: slog::Logger, } -impl ChainCollection { - pub fn new(beacon_chain: Arc, log: slog::Logger) -> Self { +impl ChainCollection { + pub fn new(beacon_chain: Arc>, log: slog::Logger) -> Self { ChainCollection { beacon_chain, finalized_chains: FnvHashMap::default(), @@ -213,9 +215,7 @@ impl ChainCollection { } } - pub fn state( - &self, - ) -> Result, &'static str> { + pub fn state(&self) -> SyncChainStatus { match self.state { RangeSyncState::Finalized(ref syncing_id) => { let chain = self @@ -409,7 +409,8 @@ impl ChainCollection { let log_ref = &self.log; let is_outdated = |target_slot: &Slot, target_root: &Hash256| { - target_slot <= &local_finalized_slot || beacon_chain.is_block_known(target_root) + target_slot <= &local_finalized_slot + || beacon_chain.block_is_known_to_fork_choice(target_root) }; // Retain only head peers that remain relevant diff --git a/beacon_node/network/src/sync/range_sync/mod.rs b/beacon_node/network/src/sync/range_sync/mod.rs index d0f2f9217e..8f881fba90 100644 --- a/beacon_node/network/src/sync/range_sync/mod.rs +++ b/beacon_node/network/src/sync/range_sync/mod.rs @@ -2,7 +2,6 @@ //! peers. mod batch; -mod block_storage; mod chain; mod chain_collection; mod range; @@ -13,5 +12,7 @@ pub use batch::{ ByRangeRequestType, }; pub use chain::{BatchId, ChainId, EPOCHS_PER_BATCH}; +#[cfg(test)] +pub use chain_collection::SyncChainStatus; pub use range::RangeSync; pub use sync_type::RangeSyncType; diff --git a/beacon_node/network/src/sync/range_sync/range.rs b/beacon_node/network/src/sync/range_sync/range.rs index 0ef99838de..78679403bb 100644 --- a/beacon_node/network/src/sync/range_sync/range.rs +++ b/beacon_node/network/src/sync/range_sync/range.rs @@ -39,9 +39,8 @@ //! Each chain is downloaded in batches of blocks. The batched blocks are processed sequentially //! and further batches are requested as current blocks are being processed. -use super::block_storage::BlockStorage; use super::chain::{BatchId, ChainId, RemoveChain, SyncingChain}; -use super::chain_collection::ChainCollection; +use super::chain_collection::{ChainCollection, SyncChainStatus}; use super::sync_type::RangeSyncType; use crate::metrics; use crate::status::ToStatusMessage; @@ -56,7 +55,7 @@ use lru_cache::LRUTimeCache; use slog::{crit, debug, trace, warn}; use std::collections::HashMap; use std::sync::Arc; -use types::{Epoch, EthSpec, Hash256, Slot}; +use types::{Epoch, EthSpec, Hash256}; /// For how long we store failed finalized chains to prevent retries. const FAILED_CHAINS_EXPIRY_SECONDS: u64 = 30; @@ -64,27 +63,26 @@ const FAILED_CHAINS_EXPIRY_SECONDS: u64 = 30; /// The primary object dealing with long range/batch syncing. This contains all the active and /// non-active chains that need to be processed before the syncing is considered complete. This /// holds the current state of the long range sync. -pub struct RangeSync> { +pub struct RangeSync { /// The beacon chain for processing. - beacon_chain: Arc, + beacon_chain: Arc>, /// Last known sync info of our useful connected peers. We use this information to create Head /// chains after all finalized chains have ended. awaiting_head_peers: HashMap, /// A collection of chains that need to be downloaded. This stores any head or finalized chains /// that need to be downloaded. - chains: ChainCollection, + chains: ChainCollection, /// Chains that have failed and are stored to prevent being retried. failed_chains: LRUTimeCache, /// The syncing logger. log: slog::Logger, } -impl RangeSync +impl RangeSync where - C: BlockStorage + ToStatusMessage, T: BeaconChainTypes, { - pub fn new(beacon_chain: Arc, log: slog::Logger) -> Self { + pub fn new(beacon_chain: Arc>, log: slog::Logger) -> Self { RangeSync { beacon_chain: beacon_chain.clone(), chains: ChainCollection::new(beacon_chain, log.clone()), @@ -96,9 +94,7 @@ where } } - pub fn state( - &self, - ) -> Result, &'static str> { + pub fn state(&self) -> SyncChainStatus { self.chains.state() } @@ -382,465 +378,3 @@ where } } } - -#[cfg(test)] -mod tests { - use crate::network_beacon_processor::NetworkBeaconProcessor; - use crate::sync::SyncMessage; - use crate::NetworkMessage; - - use super::*; - use crate::sync::network_context::{BlockOrBlob, RangeRequestId}; - use beacon_chain::builder::Witness; - use beacon_chain::eth1_chain::CachingEth1Backend; - use beacon_chain::parking_lot::RwLock; - use beacon_chain::test_utils::{BeaconChainHarness, EphemeralHarnessType}; - use beacon_chain::EngineState; - use beacon_processor::WorkEvent as BeaconWorkEvent; - use lighthouse_network::service::api_types::SyncRequestId; - use lighthouse_network::{ - rpc::StatusMessage, service::api_types::AppRequestId, NetworkConfig, NetworkGlobals, - }; - use slog::{o, Drain}; - use slot_clock::TestingSlotClock; - use std::collections::HashSet; - use store::MemoryStore; - use tokio::sync::mpsc; - use types::{FixedBytesExtended, ForkName, MinimalEthSpec as E}; - - #[derive(Debug)] - struct FakeStorage { - known_blocks: RwLock>, - status: RwLock, - } - - impl Default for FakeStorage { - fn default() -> Self { - FakeStorage { - known_blocks: RwLock::new(HashSet::new()), - status: RwLock::new(StatusMessage { - fork_digest: [0; 4], - finalized_root: Hash256::zero(), - finalized_epoch: 0usize.into(), - head_root: Hash256::zero(), - head_slot: 0usize.into(), - }), - } - } - } - - impl FakeStorage { - fn remember_block(&self, block_root: Hash256) { - self.known_blocks.write().insert(block_root); - } - - #[allow(dead_code)] - fn forget_block(&self, block_root: &Hash256) { - self.known_blocks.write().remove(block_root); - } - } - - impl BlockStorage for FakeStorage { - fn is_block_known(&self, block_root: &store::Hash256) -> bool { - self.known_blocks.read().contains(block_root) - } - } - - impl ToStatusMessage for FakeStorage { - fn status_message(&self) -> StatusMessage { - self.status.read().clone() - } - } - - type TestBeaconChainType = - Witness, E, MemoryStore, MemoryStore>; - - fn build_log(level: slog::Level, enabled: bool) -> slog::Logger { - let decorator = slog_term::TermDecorator::new().build(); - let drain = slog_term::FullFormat::new(decorator).build().fuse(); - let drain = slog_async::Async::new(drain).build().fuse(); - - if enabled { - slog::Logger::root(drain.filter_level(level).fuse(), o!()) - } else { - slog::Logger::root(drain.filter(|_| false).fuse(), o!()) - } - } - - #[allow(unused)] - struct TestRig { - log: slog::Logger, - /// To check what does sync send to the beacon processor. - beacon_processor_rx: mpsc::Receiver>, - /// To set up different scenarios where sync is told about known/unknown blocks. - chain: Arc, - /// Needed by range to handle communication with the network. - cx: SyncNetworkContext, - /// To check what the network receives from Range. - network_rx: mpsc::UnboundedReceiver>, - /// To modify what the network declares about various global variables, in particular about - /// the sync state of a peer. - globals: Arc>, - } - - impl RangeSync { - fn assert_state(&self, expected_state: RangeSyncType) { - assert_eq!( - self.state() - .expect("State is ok") - .expect("Range is syncing") - .0, - expected_state - ) - } - - #[allow(dead_code)] - fn assert_not_syncing(&self) { - assert!( - self.state().expect("State is ok").is_none(), - "Range should not be syncing." - ); - } - } - - impl TestRig { - fn local_info(&self) -> SyncInfo { - let StatusMessage { - fork_digest: _, - finalized_root, - finalized_epoch, - head_root, - head_slot, - } = self.chain.status.read().clone(); - SyncInfo { - head_slot, - head_root, - finalized_epoch, - finalized_root, - } - } - - /// Reads an BlocksByRange request to a given peer from the network receiver channel. - #[track_caller] - fn grab_request( - &mut self, - expected_peer: &PeerId, - fork_name: ForkName, - ) -> (AppRequestId, Option) { - let block_req_id = if let Ok(NetworkMessage::SendRequest { - peer_id, - request: _, - request_id, - }) = self.network_rx.try_recv() - { - assert_eq!(&peer_id, expected_peer); - request_id - } else { - panic!("Should have sent a batch request to the peer") - }; - let blob_req_id = if fork_name.deneb_enabled() { - if let Ok(NetworkMessage::SendRequest { - peer_id, - request: _, - request_id, - }) = self.network_rx.try_recv() - { - assert_eq!(&peer_id, expected_peer); - Some(request_id) - } else { - panic!("Should have sent a batch request to the peer") - } - } else { - None - }; - (block_req_id, blob_req_id) - } - - fn complete_range_block_and_blobs_response( - &mut self, - block_req: AppRequestId, - blob_req_opt: Option, - ) -> (ChainId, BatchId, Id) { - if blob_req_opt.is_some() { - match block_req { - AppRequestId::Sync(SyncRequestId::RangeBlockAndBlobs { id }) => { - let _ = self - .cx - .range_block_and_blob_response(id, BlockOrBlob::Block(None)); - let response = self - .cx - .range_block_and_blob_response(id, BlockOrBlob::Blob(None)) - .unwrap(); - let (chain_id, batch_id) = - TestRig::unwrap_range_request_id(response.sender_id); - (chain_id, batch_id, id) - } - other => panic!("unexpected request {:?}", other), - } - } else { - match block_req { - AppRequestId::Sync(SyncRequestId::RangeBlockAndBlobs { id }) => { - let response = self - .cx - .range_block_and_blob_response(id, BlockOrBlob::Block(None)) - .unwrap(); - let (chain_id, batch_id) = - TestRig::unwrap_range_request_id(response.sender_id); - (chain_id, batch_id, id) - } - other => panic!("unexpected request {:?}", other), - } - } - } - - fn unwrap_range_request_id(sender_id: RangeRequestId) -> (ChainId, BatchId) { - if let RangeRequestId::RangeSync { chain_id, batch_id } = sender_id { - (chain_id, batch_id) - } else { - panic!("expected RangeSync request: {:?}", sender_id) - } - } - - /// Produce a head peer - fn head_peer( - &self, - ) -> ( - PeerId, - SyncInfo, /* Local info */ - SyncInfo, /* Remote info */ - ) { - let local_info = self.local_info(); - - // Get a peer with an advanced head - let head_root = Hash256::random(); - let head_slot = local_info.head_slot + 1; - let remote_info = SyncInfo { - head_root, - head_slot, - ..local_info - }; - let peer_id = PeerId::random(); - (peer_id, local_info, remote_info) - } - - fn finalized_peer( - &self, - ) -> ( - PeerId, - SyncInfo, /* Local info */ - SyncInfo, /* Remote info */ - ) { - let local_info = self.local_info(); - - let finalized_root = Hash256::random(); - let finalized_epoch = local_info.finalized_epoch + 2; - let head_slot = finalized_epoch.start_slot(E::slots_per_epoch()); - let head_root = Hash256::random(); - let remote_info = SyncInfo { - finalized_epoch, - finalized_root, - head_slot, - head_root, - }; - - let peer_id = PeerId::random(); - (peer_id, local_info, remote_info) - } - - #[track_caller] - fn expect_empty_processor(&mut self) { - match self.beacon_processor_rx.try_recv() { - Ok(work) => { - panic!( - "Expected empty processor. Instead got {}", - work.work_type_str() - ); - } - Err(e) => match e { - mpsc::error::TryRecvError::Empty => {} - mpsc::error::TryRecvError::Disconnected => unreachable!("bad coded test?"), - }, - } - } - - #[track_caller] - fn expect_chain_segment(&mut self) { - match self.beacon_processor_rx.try_recv() { - Ok(work) => { - assert_eq!(work.work_type(), beacon_processor::WorkType::ChainSegment); - } - other => panic!("Expected chain segment process, found {:?}", other), - } - } - } - - fn range(log_enabled: bool) -> (TestRig, RangeSync) { - let log = build_log(slog::Level::Trace, log_enabled); - // Initialise a new beacon chain - let harness = BeaconChainHarness::>::builder(E) - .default_spec() - .logger(log.clone()) - .deterministic_keypairs(1) - .fresh_ephemeral_store() - .build(); - let chain = harness.chain; - - let fake_store = Arc::new(FakeStorage::default()); - let range_sync = RangeSync::::new( - fake_store.clone(), - log.new(o!("component" => "range")), - ); - let (network_tx, network_rx) = mpsc::unbounded_channel(); - let (sync_tx, _sync_rx) = mpsc::unbounded_channel::>(); - let network_config = Arc::new(NetworkConfig::default()); - let globals = Arc::new(NetworkGlobals::new_test_globals( - Vec::new(), - &log, - network_config, - chain.spec.clone(), - )); - let (network_beacon_processor, beacon_processor_rx) = - NetworkBeaconProcessor::null_for_testing( - globals.clone(), - sync_tx, - chain.clone(), - harness.runtime.task_executor.clone(), - log.clone(), - ); - let cx = SyncNetworkContext::new( - network_tx, - Arc::new(network_beacon_processor), - chain, - log.new(o!("component" => "network_context")), - ); - let test_rig = TestRig { - log, - beacon_processor_rx, - chain: fake_store, - cx, - network_rx, - globals, - }; - (test_rig, range_sync) - } - - #[test] - fn head_chain_removed_while_finalized_syncing() { - // NOTE: this is a regression test. - let (mut rig, mut range) = range(false); - - // Get a peer with an advanced head - let (head_peer, local_info, remote_info) = rig.head_peer(); - range.add_peer(&mut rig.cx, local_info, head_peer, remote_info); - range.assert_state(RangeSyncType::Head); - - let fork = rig - .cx - .chain - .spec - .fork_name_at_epoch(rig.cx.chain.epoch().unwrap()); - - // Sync should have requested a batch, grab the request. - let _ = rig.grab_request(&head_peer, fork); - - // Now get a peer with an advanced finalized epoch. - let (finalized_peer, local_info, remote_info) = rig.finalized_peer(); - range.add_peer(&mut rig.cx, local_info, finalized_peer, remote_info); - range.assert_state(RangeSyncType::Finalized); - - // Sync should have requested a batch, grab the request - let _ = rig.grab_request(&finalized_peer, fork); - - // Fail the head chain by disconnecting the peer. - range.remove_peer(&mut rig.cx, &head_peer); - range.assert_state(RangeSyncType::Finalized); - } - - #[test] - fn state_update_while_purging() { - // NOTE: this is a regression test. - let (mut rig, mut range) = range(true); - - // Get a peer with an advanced head - let (head_peer, local_info, head_info) = rig.head_peer(); - let head_peer_root = head_info.head_root; - range.add_peer(&mut rig.cx, local_info, head_peer, head_info); - range.assert_state(RangeSyncType::Head); - - let fork = rig - .cx - .chain - .spec - .fork_name_at_epoch(rig.cx.chain.epoch().unwrap()); - - // Sync should have requested a batch, grab the request. - let _ = rig.grab_request(&head_peer, fork); - - // Now get a peer with an advanced finalized epoch. - let (finalized_peer, local_info, remote_info) = rig.finalized_peer(); - let finalized_peer_root = remote_info.finalized_root; - range.add_peer(&mut rig.cx, local_info, finalized_peer, remote_info); - range.assert_state(RangeSyncType::Finalized); - - // Sync should have requested a batch, grab the request - let _ = rig.grab_request(&finalized_peer, fork); - - // Now the chain knows both chains target roots. - rig.chain.remember_block(head_peer_root); - rig.chain.remember_block(finalized_peer_root); - - // Add an additional peer to the second chain to make range update it's status - let (finalized_peer, local_info, remote_info) = rig.finalized_peer(); - range.add_peer(&mut rig.cx, local_info, finalized_peer, remote_info); - } - - #[test] - fn pause_and_resume_on_ee_offline() { - let (mut rig, mut range) = range(true); - let fork = rig - .cx - .chain - .spec - .fork_name_at_epoch(rig.cx.chain.epoch().unwrap()); - - // add some peers - let (peer1, local_info, head_info) = rig.head_peer(); - range.add_peer(&mut rig.cx, local_info, peer1, head_info); - let (block_req, blob_req_opt) = rig.grab_request(&peer1, fork); - - let (chain1, batch1, id1) = - rig.complete_range_block_and_blobs_response(block_req, blob_req_opt); - - // make the ee offline - rig.cx.update_execution_engine_state(EngineState::Offline); - - // send the response to the request - range.blocks_by_range_response(&mut rig.cx, peer1, chain1, batch1, id1, vec![]); - - // the beacon processor shouldn't have received any work - rig.expect_empty_processor(); - - // while the ee is offline, more peers might arrive. Add a new finalized peer. - let (peer2, local_info, finalized_info) = rig.finalized_peer(); - range.add_peer(&mut rig.cx, local_info, peer2, finalized_info); - let (block_req, blob_req_opt) = rig.grab_request(&peer2, fork); - - let (chain2, batch2, id2) = - rig.complete_range_block_and_blobs_response(block_req, blob_req_opt); - - // send the response to the request - range.blocks_by_range_response(&mut rig.cx, peer2, chain2, batch2, id2, vec![]); - - // the beacon processor shouldn't have received any work - rig.expect_empty_processor(); - - // make the beacon processor available again. - rig.cx.update_execution_engine_state(EngineState::Online); - - // now resume range, we should have two processing requests in the beacon processor. - range.resume(&mut rig.cx); - - rig.expect_chain_segment(); - rig.expect_chain_segment(); - } -} diff --git a/beacon_node/network/src/sync/range_sync/sync_type.rs b/beacon_node/network/src/sync/range_sync/sync_type.rs index d6ffd4a5df..4ff7e39310 100644 --- a/beacon_node/network/src/sync/range_sync/sync_type.rs +++ b/beacon_node/network/src/sync/range_sync/sync_type.rs @@ -1,10 +1,9 @@ //! Contains logic about identifying which Sync to perform given PeerSyncInfo of ourselves and //! of a remote. +use beacon_chain::{BeaconChain, BeaconChainTypes}; use lighthouse_network::SyncInfo; -use super::block_storage::BlockStorage; - /// The type of Range sync that should be done relative to our current state. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum RangeSyncType { @@ -17,8 +16,8 @@ pub enum RangeSyncType { impl RangeSyncType { /// Determines the type of sync given our local `PeerSyncInfo` and the remote's /// `PeerSyncInfo`. - pub fn new( - chain: &C, + pub fn new( + chain: &BeaconChain, local_info: &SyncInfo, remote_info: &SyncInfo, ) -> RangeSyncType { @@ -29,7 +28,7 @@ impl RangeSyncType { // not seen the finalized hash before. if remote_info.finalized_epoch > local_info.finalized_epoch - && !chain.is_block_known(&remote_info.finalized_root) + && !chain.block_is_known_to_fork_choice(&remote_info.finalized_root) { RangeSyncType::Finalized } else { diff --git a/beacon_node/network/src/sync/tests/lookups.rs b/beacon_node/network/src/sync/tests/lookups.rs index 9f2c9ef66f..94aacad3e8 100644 --- a/beacon_node/network/src/sync/tests/lookups.rs +++ b/beacon_node/network/src/sync/tests/lookups.rs @@ -83,6 +83,7 @@ impl TestRig { .logger(log.clone()) .deterministic_keypairs(1) .fresh_ephemeral_store() + .mock_execution_layer() .testing_slot_clock(TestingSlotClock::new( Slot::new(0), Duration::from_secs(0), @@ -144,7 +145,7 @@ impl TestRig { } } - fn test_setup() -> Self { + pub fn test_setup() -> Self { Self::test_setup_with_config(None) } @@ -168,11 +169,11 @@ impl TestRig { } } - fn log(&self, msg: &str) { + pub fn log(&self, msg: &str) { info!(self.log, "TEST_RIG"; "msg" => msg); } - fn after_deneb(&self) -> bool { + pub fn after_deneb(&self) -> bool { matches!(self.fork_name, ForkName::Deneb | ForkName::Electra) } @@ -238,7 +239,7 @@ impl TestRig { (parent, block, parent_root, block_root) } - fn send_sync_message(&mut self, sync_message: SyncMessage) { + pub fn send_sync_message(&mut self, sync_message: SyncMessage) { self.sync_manager.handle_message(sync_message); } @@ -369,7 +370,7 @@ impl TestRig { self.expect_empty_network(); } - fn new_connected_peer(&mut self) -> PeerId { + pub fn new_connected_peer(&mut self) -> PeerId { self.network_globals .peers .write() @@ -811,7 +812,7 @@ impl TestRig { } } - fn peer_disconnected(&mut self, peer_id: PeerId) { + pub fn peer_disconnected(&mut self, peer_id: PeerId) { self.send_sync_message(SyncMessage::Disconnect(peer_id)); } @@ -827,7 +828,7 @@ impl TestRig { } } - fn pop_received_network_event) -> Option>( + pub fn pop_received_network_event) -> Option>( &mut self, predicate_transform: F, ) -> Result { @@ -847,7 +848,7 @@ impl TestRig { } } - fn pop_received_processor_event) -> Option>( + pub fn pop_received_processor_event) -> Option>( &mut self, predicate_transform: F, ) -> Result { @@ -871,6 +872,16 @@ impl TestRig { } } + pub fn expect_empty_processor(&mut self) { + self.drain_processor_rx(); + if !self.beacon_processor_rx_queue.is_empty() { + panic!( + "Expected processor to be empty, but has events: {:?}", + self.beacon_processor_rx_queue + ); + } + } + fn find_block_lookup_request( &mut self, for_block: Hash256, @@ -2173,7 +2184,8 @@ fn custody_lookup_happy_path() { mod deneb_only { use super::*; use beacon_chain::{ - block_verification_types::RpcBlock, data_availability_checker::AvailabilityCheckError, + block_verification_types::{AsBlock, RpcBlock}, + data_availability_checker::AvailabilityCheckError, }; use ssz_types::VariableList; use std::collections::VecDeque; diff --git a/beacon_node/network/src/sync/tests/range.rs b/beacon_node/network/src/sync/tests/range.rs index 8b13789179..6faa8b7247 100644 --- a/beacon_node/network/src/sync/tests/range.rs +++ b/beacon_node/network/src/sync/tests/range.rs @@ -1 +1,273 @@ +use super::*; +use crate::status::ToStatusMessage; +use crate::sync::manager::SLOT_IMPORT_TOLERANCE; +use crate::sync::range_sync::RangeSyncType; +use crate::sync::SyncMessage; +use beacon_chain::test_utils::{AttestationStrategy, BlockStrategy}; +use beacon_chain::EngineState; +use lighthouse_network::rpc::{RequestType, StatusMessage}; +use lighthouse_network::service::api_types::{AppRequestId, Id, SyncRequestId}; +use lighthouse_network::{PeerId, SyncInfo}; +use std::time::Duration; +use types::{EthSpec, Hash256, MinimalEthSpec as E, SignedBeaconBlock, Slot}; +const D: Duration = Duration::new(0, 0); + +impl TestRig { + /// Produce a head peer with an advanced head + fn add_head_peer(&mut self) -> PeerId { + self.add_head_peer_with_root(Hash256::random()) + } + + /// Produce a head peer with an advanced head + fn add_head_peer_with_root(&mut self, head_root: Hash256) -> PeerId { + let local_info = self.local_info(); + self.add_peer(SyncInfo { + head_root, + head_slot: local_info.head_slot + 1 + Slot::new(SLOT_IMPORT_TOLERANCE as u64), + ..local_info + }) + } + + // Produce a finalized peer with an advanced finalized epoch + fn add_finalized_peer(&mut self) -> PeerId { + self.add_finalized_peer_with_root(Hash256::random()) + } + + // Produce a finalized peer with an advanced finalized epoch + fn add_finalized_peer_with_root(&mut self, finalized_root: Hash256) -> PeerId { + let local_info = self.local_info(); + let finalized_epoch = local_info.finalized_epoch + 2; + self.add_peer(SyncInfo { + finalized_epoch, + finalized_root, + head_slot: finalized_epoch.start_slot(E::slots_per_epoch()), + head_root: Hash256::random(), + }) + } + + fn local_info(&self) -> SyncInfo { + let StatusMessage { + fork_digest: _, + finalized_root, + finalized_epoch, + head_root, + head_slot, + } = self.harness.chain.status_message(); + SyncInfo { + head_slot, + head_root, + finalized_epoch, + finalized_root, + } + } + + fn add_peer(&mut self, remote_info: SyncInfo) -> PeerId { + // Create valid peer known to network globals + let peer_id = self.new_connected_peer(); + // Send peer to sync + self.send_sync_message(SyncMessage::AddPeer(peer_id, remote_info.clone())); + peer_id + } + + fn assert_state(&self, state: RangeSyncType) { + assert_eq!( + self.sync_manager + .range_sync_state() + .expect("State is ok") + .expect("Range should be syncing") + .0, + state, + "not expected range sync state" + ); + } + + #[track_caller] + fn expect_chain_segment(&mut self) { + self.pop_received_processor_event(|ev| { + (ev.work_type() == beacon_processor::WorkType::ChainSegment).then_some(()) + }) + .unwrap_or_else(|e| panic!("Expect ChainSegment work event: {e:?}")); + } + + fn update_execution_engine_state(&mut self, state: EngineState) { + self.log(&format!("execution engine state updated: {state:?}")); + self.sync_manager.update_execution_engine_state(state); + } + + fn find_blocks_by_range_request(&mut self, target_peer_id: &PeerId) -> (Id, Option) { + let block_req_id = self + .pop_received_network_event(|ev| match ev { + NetworkMessage::SendRequest { + peer_id, + request: RequestType::BlocksByRange(_), + request_id: AppRequestId::Sync(SyncRequestId::RangeBlockAndBlobs { id }), + } if peer_id == target_peer_id => Some(*id), + _ => None, + }) + .expect("Should have a blocks by range request"); + + let blob_req_id = if self.after_deneb() { + Some( + self.pop_received_network_event(|ev| match ev { + NetworkMessage::SendRequest { + peer_id, + request: RequestType::BlobsByRange(_), + request_id: AppRequestId::Sync(SyncRequestId::RangeBlockAndBlobs { id }), + } if peer_id == target_peer_id => Some(*id), + _ => None, + }) + .expect("Should have a blobs by range request"), + ) + } else { + None + }; + + (block_req_id, blob_req_id) + } + + fn find_and_complete_blocks_by_range_request(&mut self, target_peer_id: PeerId) { + let (blocks_req_id, blobs_req_id) = self.find_blocks_by_range_request(&target_peer_id); + + // Complete the request with a single stream termination + self.log(&format!( + "Completing BlocksByRange request {blocks_req_id} with empty stream" + )); + self.send_sync_message(SyncMessage::RpcBlock { + request_id: SyncRequestId::RangeBlockAndBlobs { id: blocks_req_id }, + peer_id: target_peer_id, + beacon_block: None, + seen_timestamp: D, + }); + + if let Some(blobs_req_id) = blobs_req_id { + // Complete the request with a single stream termination + self.log(&format!( + "Completing BlobsByRange request {blobs_req_id} with empty stream" + )); + self.send_sync_message(SyncMessage::RpcBlob { + request_id: SyncRequestId::RangeBlockAndBlobs { id: blobs_req_id }, + peer_id: target_peer_id, + blob_sidecar: None, + seen_timestamp: D, + }); + } + } + + async fn create_canonical_block(&mut self) -> SignedBeaconBlock { + self.harness.advance_slot(); + + let block_root = self + .harness + .extend_chain( + 1, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + self.harness + .chain + .store + .get_full_block(&block_root) + .unwrap() + .unwrap() + } + + async fn remember_block(&mut self, block: SignedBeaconBlock) { + self.harness + .process_block(block.slot(), block.canonical_root(), (block.into(), None)) + .await + .unwrap(); + } +} + +#[test] +fn head_chain_removed_while_finalized_syncing() { + // NOTE: this is a regression test. + // Added in PR https://github.com/sigp/lighthouse/pull/2821 + let mut rig = TestRig::test_setup(); + + // Get a peer with an advanced head + let head_peer = rig.add_head_peer(); + rig.assert_state(RangeSyncType::Head); + + // Sync should have requested a batch, grab the request. + let _ = rig.find_blocks_by_range_request(&head_peer); + + // Now get a peer with an advanced finalized epoch. + let finalized_peer = rig.add_finalized_peer(); + rig.assert_state(RangeSyncType::Finalized); + + // Sync should have requested a batch, grab the request + let _ = rig.find_blocks_by_range_request(&finalized_peer); + + // Fail the head chain by disconnecting the peer. + rig.peer_disconnected(head_peer); + rig.assert_state(RangeSyncType::Finalized); +} + +#[tokio::test] +async fn state_update_while_purging() { + // NOTE: this is a regression test. + // Added in PR https://github.com/sigp/lighthouse/pull/2827 + let mut rig = TestRig::test_setup(); + + // Create blocks on a separate harness + let mut rig_2 = TestRig::test_setup(); + // Need to create blocks that can be inserted into the fork-choice and fit the "known + // conditions" below. + let head_peer_block = rig_2.create_canonical_block().await; + let head_peer_root = head_peer_block.canonical_root(); + let finalized_peer_block = rig_2.create_canonical_block().await; + let finalized_peer_root = finalized_peer_block.canonical_root(); + + // Get a peer with an advanced head + let head_peer = rig.add_head_peer_with_root(head_peer_root); + rig.assert_state(RangeSyncType::Head); + + // Sync should have requested a batch, grab the request. + let _ = rig.find_blocks_by_range_request(&head_peer); + + // Now get a peer with an advanced finalized epoch. + let finalized_peer = rig.add_finalized_peer_with_root(finalized_peer_root); + rig.assert_state(RangeSyncType::Finalized); + + // Sync should have requested a batch, grab the request + let _ = rig.find_blocks_by_range_request(&finalized_peer); + + // Now the chain knows both chains target roots. + rig.remember_block(head_peer_block).await; + rig.remember_block(finalized_peer_block).await; + + // Add an additional peer to the second chain to make range update it's status + rig.add_finalized_peer(); +} + +#[test] +fn pause_and_resume_on_ee_offline() { + let mut rig = TestRig::test_setup(); + + // add some peers + let peer1 = rig.add_head_peer(); + // make the ee offline + rig.update_execution_engine_state(EngineState::Offline); + // send the response to the request + rig.find_and_complete_blocks_by_range_request(peer1); + // the beacon processor shouldn't have received any work + rig.expect_empty_processor(); + + // while the ee is offline, more peers might arrive. Add a new finalized peer. + let peer2 = rig.add_finalized_peer(); + + // send the response to the request + rig.find_and_complete_blocks_by_range_request(peer2); + // the beacon processor shouldn't have received any work + rig.expect_empty_processor(); + // make the beacon processor available again. + // update_execution_engine_state implicitly calls resume + // now resume range, we should have two processing requests in the beacon processor. + rig.update_execution_engine_state(EngineState::Online); + + rig.expect_chain_segment(); + rig.expect_chain_segment(); +} From 847c8019c7867e3eaf65168e5259ea33e7e0eb5a Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Mon, 16 Dec 2024 16:44:14 +1100 Subject: [PATCH 047/254] Fix peer down-scoring behaviour when gossip blobs/columns are received after `getBlobs` or reconstruction (#6686) * Fix peer disconnection when gossip blobs/columns are received after they are recieved from the EL or available via column reconstruction. --- .../gossip_methods.rs | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 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 4fc83b0923..f3c48e42f0 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -710,8 +710,19 @@ impl NetworkBeaconProcessor { MessageAcceptance::Reject, ); } + GossipDataColumnError::PriorKnown { .. } => { + // Data column is available via either the EL or reconstruction. + // Do not penalise the peer. + // Gossip filter should filter any duplicates received after this. + debug!( + self.log, + "Received already available column sidecar. Ignoring the column sidecar"; + "slot" => %slot, + "block_root" => %block_root, + "index" => %index, + ) + } GossipDataColumnError::FutureSlot { .. } - | GossipDataColumnError::PriorKnown { .. } | GossipDataColumnError::PastFinalizedSlot { .. } => { debug!( self.log, @@ -852,7 +863,18 @@ impl NetworkBeaconProcessor { MessageAcceptance::Reject, ); } - GossipBlobError::FutureSlot { .. } | GossipBlobError::RepeatBlob { .. } => { + GossipBlobError::RepeatBlob { .. } => { + // We may have received the blob from the EL. Do not penalise the peer. + // Gossip filter should filter any duplicates received after this. + debug!( + self.log, + "Received already available blob sidecar. Ignoring the blob sidecar"; + "slot" => %slot, + "root" => %root, + "index" => %index, + ) + } + GossipBlobError::FutureSlot { .. } => { debug!( self.log, "Could not verify blob sidecar for gossip. Ignoring the blob sidecar"; From 02cb2d68ff6f5bc3a4bc34baff9926d6b449e144 Mon Sep 17 00:00:00 2001 From: Daniel Knopik <107140945+dknopik@users.noreply.github.com> Date: Tue, 17 Dec 2024 01:40:35 +0100 Subject: [PATCH 048/254] Enable lints for tests only running optimized (#6664) * enable linting optimized-only tests * fix automatically fixable or obvious lints * fix suspicious_open_options by removing manual options * fix `await_holding_lock`s * avoid failing lint due to now disabled `#[cfg(debug_assertions)]` * reduce future sizes in tests * fix accidently flipped assert logic * restore holding lock for web3signer download * Merge branch 'unstable' into lint-opt-tests --- Makefile | 2 +- account_manager/src/validator/exit.rs | 2 +- .../beacon_chain/src/shuffling_cache.rs | 2 +- .../tests/attestation_production.rs | 4 +- .../tests/attestation_verification.rs | 42 +- beacon_node/beacon_chain/tests/bellatrix.rs | 14 +- beacon_node/beacon_chain/tests/capella.rs | 8 +- .../tests/payload_invalidation.rs | 4 +- beacon_node/beacon_chain/tests/store_tests.rs | 93 ++-- .../tests/sync_committee_verification.rs | 4 +- beacon_node/beacon_chain/tests/tests.rs | 5 +- .../tests/broadcast_validation_tests.rs | 18 +- .../http_api/tests/interactive_tests.rs | 2 +- beacon_node/http_api/tests/status_tests.rs | 30 +- beacon_node/http_api/tests/tests.rs | 129 +++--- .../src/service/gossip_cache.rs | 23 +- .../src/network_beacon_processor/tests.rs | 2 +- beacon_node/network/src/service/tests.rs | 402 +++++++++--------- beacon_node/operation_pool/src/lib.rs | 62 ++- .../eth2_wallet_manager/src/wallet_manager.rs | 32 +- consensus/fork_choice/tests/tests.rs | 26 +- crypto/eth2_keystore/tests/eip2335_vectors.rs | 4 +- crypto/eth2_keystore/tests/tests.rs | 14 +- crypto/eth2_wallet/tests/tests.rs | 13 +- lighthouse/tests/account_manager.rs | 28 +- lighthouse/tests/beacon_node.rs | 51 ++- lighthouse/tests/boot_node.rs | 4 +- lighthouse/tests/validator_client.rs | 8 +- lighthouse/tests/validator_manager.rs | 16 +- testing/web3signer_tests/src/lib.rs | 4 +- validator_client/http_api/src/tests.rs | 15 +- .../http_api/src/tests/keystores.rs | 65 +-- validator_manager/src/import_validators.rs | 4 +- validator_manager/src/move_validators.rs | 14 +- 34 files changed, 572 insertions(+), 574 deletions(-) diff --git a/Makefile b/Makefile index ab239c94d3..958abf8705 100644 --- a/Makefile +++ b/Makefile @@ -204,7 +204,7 @@ test-full: cargo-fmt test-release test-debug test-ef test-exec-engine # Lints the code for bad style and potentially unsafe arithmetic using Clippy. # Clippy lints are opt-in per-crate for now. By default, everything is allowed except for performance and correctness lints. lint: - cargo clippy --workspace --benches --tests $(EXTRA_CLIPPY_OPTS) --features "$(TEST_FEATURES)" -- \ + RUSTFLAGS="-C debug-assertions=no $(RUSTFLAGS)" cargo clippy --workspace --benches --tests $(EXTRA_CLIPPY_OPTS) --features "$(TEST_FEATURES)" -- \ -D clippy::fn_to_numeric_cast_any \ -D clippy::manual_let_else \ -D clippy::large_stack_frames \ diff --git a/account_manager/src/validator/exit.rs b/account_manager/src/validator/exit.rs index 3fb0e50d22..ea1a24da1f 100644 --- a/account_manager/src/validator/exit.rs +++ b/account_manager/src/validator/exit.rs @@ -409,6 +409,6 @@ mod tests { ) .unwrap(); - assert_eq!(expected_pk, kp.pk.into()); + assert_eq!(expected_pk, kp.pk); } } diff --git a/beacon_node/beacon_chain/src/shuffling_cache.rs b/beacon_node/beacon_chain/src/shuffling_cache.rs index a662cc49c9..da1d60db17 100644 --- a/beacon_node/beacon_chain/src/shuffling_cache.rs +++ b/beacon_node/beacon_chain/src/shuffling_cache.rs @@ -512,7 +512,7 @@ mod test { } assert!( - !cache.contains(&shuffling_id_and_committee_caches.get(0).unwrap().0), + !cache.contains(&shuffling_id_and_committee_caches.first().unwrap().0), "should not contain oldest epoch shuffling id" ); assert_eq!( diff --git a/beacon_node/beacon_chain/tests/attestation_production.rs b/beacon_node/beacon_chain/tests/attestation_production.rs index 0b121356b9..87fefe7114 100644 --- a/beacon_node/beacon_chain/tests/attestation_production.rs +++ b/beacon_node/beacon_chain/tests/attestation_production.rs @@ -70,12 +70,12 @@ async fn produces_attestations_from_attestation_simulator_service() { } // Compare the prometheus metrics that evaluates the performance of the unaggregated attestations - let hit_prometheus_metrics = vec![ + let hit_prometheus_metrics = [ metrics::VALIDATOR_MONITOR_ATTESTATION_SIMULATOR_HEAD_ATTESTER_HIT_TOTAL, metrics::VALIDATOR_MONITOR_ATTESTATION_SIMULATOR_TARGET_ATTESTER_HIT_TOTAL, metrics::VALIDATOR_MONITOR_ATTESTATION_SIMULATOR_SOURCE_ATTESTER_HIT_TOTAL, ]; - let miss_prometheus_metrics = vec![ + let miss_prometheus_metrics = [ metrics::VALIDATOR_MONITOR_ATTESTATION_SIMULATOR_HEAD_ATTESTER_MISS_TOTAL, metrics::VALIDATOR_MONITOR_ATTESTATION_SIMULATOR_TARGET_ATTESTER_MISS_TOTAL, metrics::VALIDATOR_MONITOR_ATTESTATION_SIMULATOR_SOURCE_ATTESTER_MISS_TOTAL, diff --git a/beacon_node/beacon_chain/tests/attestation_verification.rs b/beacon_node/beacon_chain/tests/attestation_verification.rs index e168cbb6f4..dcc63ddf62 100644 --- a/beacon_node/beacon_chain/tests/attestation_verification.rs +++ b/beacon_node/beacon_chain/tests/attestation_verification.rs @@ -431,10 +431,12 @@ impl GossipTester { .chain .verify_aggregated_attestation_for_gossip(&aggregate) .err() - .expect(&format!( - "{} should error during verify_aggregated_attestation_for_gossip", - desc - )); + .unwrap_or_else(|| { + panic!( + "{} should error during verify_aggregated_attestation_for_gossip", + desc + ) + }); inspect_err(&self, err); /* @@ -449,10 +451,12 @@ impl GossipTester { .unwrap(); assert_eq!(results.len(), 2); - let batch_err = results.pop().unwrap().err().expect(&format!( - "{} should error during batch_verify_aggregated_attestations_for_gossip", - desc - )); + let batch_err = results.pop().unwrap().err().unwrap_or_else(|| { + panic!( + "{} should error during batch_verify_aggregated_attestations_for_gossip", + desc + ) + }); inspect_err(&self, batch_err); self @@ -475,10 +479,12 @@ impl GossipTester { .chain .verify_unaggregated_attestation_for_gossip(&attn, Some(subnet_id)) .err() - .expect(&format!( - "{} should error during verify_unaggregated_attestation_for_gossip", - desc - )); + .unwrap_or_else(|| { + panic!( + "{} should error during verify_unaggregated_attestation_for_gossip", + desc + ) + }); inspect_err(&self, err); /* @@ -496,10 +502,12 @@ impl GossipTester { ) .unwrap(); assert_eq!(results.len(), 2); - let batch_err = results.pop().unwrap().err().expect(&format!( - "{} should error during batch_verify_unaggregated_attestations_for_gossip", - desc - )); + let batch_err = results.pop().unwrap().err().unwrap_or_else(|| { + panic!( + "{} should error during batch_verify_unaggregated_attestations_for_gossip", + desc + ) + }); inspect_err(&self, batch_err); self @@ -816,7 +824,7 @@ async fn aggregated_gossip_verification() { let (index, sk) = tester.non_aggregator(); *a = SignedAggregateAndProof::from_aggregate( index as u64, - tester.valid_aggregate.message().aggregate().clone(), + tester.valid_aggregate.message().aggregate(), None, &sk, &chain.canonical_head.cached_head().head_fork(), diff --git a/beacon_node/beacon_chain/tests/bellatrix.rs b/beacon_node/beacon_chain/tests/bellatrix.rs index 5bd3452623..5080b0890b 100644 --- a/beacon_node/beacon_chain/tests/bellatrix.rs +++ b/beacon_node/beacon_chain/tests/bellatrix.rs @@ -82,7 +82,7 @@ async fn merge_with_terminal_block_hash_override() { let block = &harness.chain.head_snapshot().beacon_block; - let execution_payload = block.message().body().execution_payload().unwrap().clone(); + let execution_payload = block.message().body().execution_payload().unwrap(); if i == 0 { assert_eq!(execution_payload.block_hash(), genesis_pow_block_hash); } @@ -133,7 +133,7 @@ async fn base_altair_bellatrix_with_terminal_block_after_fork() { * Do the Bellatrix fork, without a terminal PoW block. */ - harness.extend_to_slot(bellatrix_fork_slot).await; + Box::pin(harness.extend_to_slot(bellatrix_fork_slot)).await; let bellatrix_head = &harness.chain.head_snapshot().beacon_block; assert!(bellatrix_head.as_bellatrix().is_ok()); @@ -207,15 +207,7 @@ async fn base_altair_bellatrix_with_terminal_block_after_fork() { harness.extend_slots(1).await; let block = &harness.chain.head_snapshot().beacon_block; - execution_payloads.push( - block - .message() - .body() - .execution_payload() - .unwrap() - .clone() - .into(), - ); + execution_payloads.push(block.message().body().execution_payload().unwrap().into()); } verify_execution_payload_chain(execution_payloads.as_slice()); diff --git a/beacon_node/beacon_chain/tests/capella.rs b/beacon_node/beacon_chain/tests/capella.rs index ac97a95721..3ce5702f2e 100644 --- a/beacon_node/beacon_chain/tests/capella.rs +++ b/beacon_node/beacon_chain/tests/capella.rs @@ -54,7 +54,7 @@ async fn base_altair_bellatrix_capella() { /* * Do the Altair fork. */ - harness.extend_to_slot(altair_fork_slot).await; + Box::pin(harness.extend_to_slot(altair_fork_slot)).await; let altair_head = &harness.chain.head_snapshot().beacon_block; assert!(altair_head.as_altair().is_ok()); @@ -63,7 +63,7 @@ async fn base_altair_bellatrix_capella() { /* * Do the Bellatrix fork, without a terminal PoW block. */ - harness.extend_to_slot(bellatrix_fork_slot).await; + Box::pin(harness.extend_to_slot(bellatrix_fork_slot)).await; let bellatrix_head = &harness.chain.head_snapshot().beacon_block; assert!(bellatrix_head.as_bellatrix().is_ok()); @@ -81,7 +81,7 @@ async fn base_altair_bellatrix_capella() { /* * Next Bellatrix block shouldn't include an exec payload. */ - harness.extend_slots(1).await; + Box::pin(harness.extend_slots(1)).await; let one_after_bellatrix_head = &harness.chain.head_snapshot().beacon_block; assert!( @@ -112,7 +112,7 @@ async fn base_altair_bellatrix_capella() { terminal_block.timestamp = timestamp; } }); - harness.extend_slots(1).await; + Box::pin(harness.extend_slots(1)).await; let two_after_bellatrix_head = &harness.chain.head_snapshot().beacon_block; assert!( diff --git a/beacon_node/beacon_chain/tests/payload_invalidation.rs b/beacon_node/beacon_chain/tests/payload_invalidation.rs index 729d88450f..01b790bb25 100644 --- a/beacon_node/beacon_chain/tests/payload_invalidation.rs +++ b/beacon_node/beacon_chain/tests/payload_invalidation.rs @@ -413,7 +413,7 @@ async fn invalid_payload_invalidates_parent() { rig.import_block(Payload::Valid).await; // Import a valid transition block. rig.move_to_first_justification(Payload::Syncing).await; - let roots = vec![ + let roots = [ rig.import_block(Payload::Syncing).await, rig.import_block(Payload::Syncing).await, rig.import_block(Payload::Syncing).await, @@ -1052,7 +1052,7 @@ async fn invalid_parent() { // Ensure the block built atop an invalid payload is invalid for gossip. assert!(matches!( - rig.harness.chain.clone().verify_block_for_gossip(block.clone().into()).await, + rig.harness.chain.clone().verify_block_for_gossip(block.clone()).await, Err(BlockError::ParentExecutionPayloadInvalid { parent_root: invalid_root }) if invalid_root == parent_root )); diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index 522020e476..73805a8525 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -330,7 +330,7 @@ async fn long_skip() { final_blocks as usize, BlockStrategy::ForkCanonicalChainAt { previous_slot: Slot::new(initial_blocks), - first_slot: Slot::new(initial_blocks + skip_slots as u64 + 1), + first_slot: Slot::new(initial_blocks + skip_slots + 1), }, AttestationStrategy::AllValidators, ) @@ -381,8 +381,7 @@ async fn randao_genesis_storage() { .beacon_state .randao_mixes() .iter() - .find(|x| **x == genesis_value) - .is_some()); + .any(|x| *x == genesis_value)); // Then upon adding one more block, it isn't harness.advance_slot(); @@ -393,14 +392,13 @@ async fn randao_genesis_storage() { AttestationStrategy::AllValidators, ) .await; - assert!(harness + assert!(!harness .chain .head_snapshot() .beacon_state .randao_mixes() .iter() - .find(|x| **x == genesis_value) - .is_none()); + .any(|x| *x == genesis_value)); check_finalization(&harness, num_slots); check_split_slot(&harness, store); @@ -1062,7 +1060,7 @@ fn check_shuffling_compatible( let current_epoch_shuffling_is_compatible = harness.chain.shuffling_is_compatible( &block_root, head_state.current_epoch(), - &head_state, + head_state, ); // Check for consistency with the more expensive shuffling lookup. @@ -1102,7 +1100,7 @@ fn check_shuffling_compatible( let previous_epoch_shuffling_is_compatible = harness.chain.shuffling_is_compatible( &block_root, head_state.previous_epoch(), - &head_state, + head_state, ); harness .chain @@ -1130,14 +1128,11 @@ fn check_shuffling_compatible( // Targeting two epochs before the current epoch should always return false if head_state.current_epoch() >= 2 { - assert_eq!( - harness.chain.shuffling_is_compatible( - &block_root, - head_state.current_epoch() - 2, - &head_state - ), - false - ); + assert!(!harness.chain.shuffling_is_compatible( + &block_root, + head_state.current_epoch() - 2, + head_state + )); } } } @@ -1559,14 +1554,13 @@ async fn prunes_fork_growing_past_youngest_finalized_checkpoint() { .map(Into::into) .collect(); let canonical_state_root = canonical_state.update_tree_hash_cache().unwrap(); - let (canonical_blocks, _, _, _) = rig - .add_attested_blocks_at_slots( - canonical_state, - canonical_state_root, - &canonical_slots, - &honest_validators, - ) - .await; + let (canonical_blocks, _, _, _) = Box::pin(rig.add_attested_blocks_at_slots( + canonical_state, + canonical_state_root, + &canonical_slots, + &honest_validators, + )) + .await; // Postconditions let canonical_blocks: HashMap = canonical_blocks_zeroth_epoch @@ -1939,7 +1933,7 @@ async fn prune_single_block_long_skip() { 2 * slots_per_epoch, 1, 2 * slots_per_epoch, - 2 * slots_per_epoch as u64, + 2 * slots_per_epoch, 1, ) .await; @@ -1961,31 +1955,45 @@ async fn prune_shared_skip_states_mid_epoch() { #[tokio::test] async fn prune_shared_skip_states_epoch_boundaries() { let slots_per_epoch = E::slots_per_epoch(); - pruning_test(slots_per_epoch - 1, 1, slots_per_epoch, 2, slots_per_epoch).await; - pruning_test(slots_per_epoch - 1, 2, slots_per_epoch, 1, slots_per_epoch).await; - pruning_test( - 2 * slots_per_epoch + slots_per_epoch / 2, - slots_per_epoch as u64 / 2, + Box::pin(pruning_test( + slots_per_epoch - 1, + 1, slots_per_epoch, - slots_per_epoch as u64 / 2 + 1, + 2, slots_per_epoch, - ) + )) .await; - pruning_test( - 2 * slots_per_epoch + slots_per_epoch / 2, - slots_per_epoch as u64 / 2, + Box::pin(pruning_test( + slots_per_epoch - 1, + 2, slots_per_epoch, - slots_per_epoch as u64 / 2 + 1, + 1, slots_per_epoch, - ) + )) .await; - pruning_test( + Box::pin(pruning_test( + 2 * slots_per_epoch + slots_per_epoch / 2, + slots_per_epoch / 2, + slots_per_epoch, + slots_per_epoch / 2 + 1, + slots_per_epoch, + )) + .await; + Box::pin(pruning_test( + 2 * slots_per_epoch + slots_per_epoch / 2, + slots_per_epoch / 2, + slots_per_epoch, + slots_per_epoch / 2 + 1, + slots_per_epoch, + )) + .await; + Box::pin(pruning_test( 2 * slots_per_epoch - 1, - slots_per_epoch as u64, + slots_per_epoch, 1, 0, 2 * slots_per_epoch, - ) + )) .await; } @@ -2094,7 +2102,7 @@ async fn pruning_test( ); check_chain_dump( &harness, - (num_initial_blocks + num_canonical_middle_blocks + num_finalization_blocks + 1) as u64, + num_initial_blocks + num_canonical_middle_blocks + num_finalization_blocks + 1, ); let all_canonical_states = harness @@ -2613,8 +2621,7 @@ async fn process_blocks_and_attestations_for_unaligned_checkpoint() { harness.advance_slot(); } harness.extend_to_slot(finalizing_slot - 1).await; - harness - .add_block_at_slot(finalizing_slot, harness.get_current_state()) + Box::pin(harness.add_block_at_slot(finalizing_slot, harness.get_current_state())) .await .unwrap(); diff --git a/beacon_node/beacon_chain/tests/sync_committee_verification.rs b/beacon_node/beacon_chain/tests/sync_committee_verification.rs index d1b3139d42..6d30b8a4e3 100644 --- a/beacon_node/beacon_chain/tests/sync_committee_verification.rs +++ b/beacon_node/beacon_chain/tests/sync_committee_verification.rs @@ -73,7 +73,7 @@ fn get_valid_sync_committee_message_for_block( let head_state = harness.chain.head_beacon_state_cloned(); let (signature, _) = harness .make_sync_committee_messages(&head_state, block_root, slot, relative_sync_committee) - .get(0) + .first() .expect("sync messages should exist") .get(message_index) .expect("first sync message should exist") @@ -104,7 +104,7 @@ fn get_valid_sync_contribution( ); let (_, contribution_opt) = sync_contributions - .get(0) + .first() .expect("sync contributions should exist"); let contribution = contribution_opt .as_ref() diff --git a/beacon_node/beacon_chain/tests/tests.rs b/beacon_node/beacon_chain/tests/tests.rs index 7ae34ccf38..c641f32b82 100644 --- a/beacon_node/beacon_chain/tests/tests.rs +++ b/beacon_node/beacon_chain/tests/tests.rs @@ -170,7 +170,7 @@ async fn find_reorgs() { harness .extend_chain( - num_blocks_produced as usize, + num_blocks_produced, BlockStrategy::OnCanonicalHead, // No need to produce attestations for this test. AttestationStrategy::SomeValidators(vec![]), @@ -203,7 +203,7 @@ async fn find_reorgs() { assert_eq!( find_reorg_slot( &harness.chain, - &head_state, + head_state, harness.chain.head_beacon_block().canonical_root() ), head_slot @@ -503,7 +503,6 @@ async fn unaggregated_attestations_added_to_fork_choice_some_none() { .unwrap(); let validator_slots: Vec<(usize, Slot)> = (0..VALIDATOR_COUNT) - .into_iter() .map(|validator_index| { let slot = state .get_attestation_duties(validator_index, RelativeEpoch::Current) diff --git a/beacon_node/http_api/tests/broadcast_validation_tests.rs b/beacon_node/http_api/tests/broadcast_validation_tests.rs index 1338f4f180..e1ecf2d4fc 100644 --- a/beacon_node/http_api/tests/broadcast_validation_tests.rs +++ b/beacon_node/http_api/tests/broadcast_validation_tests.rs @@ -322,7 +322,7 @@ pub async fn consensus_gossip() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] pub async fn consensus_partial_pass_only_consensus() { /* this test targets gossip-level validation */ - let validation_level: Option = Some(BroadcastValidation::Consensus); + let validation_level = BroadcastValidation::Consensus; // Validator count needs to be at least 32 or proposer boost gets set to 0 when computing // `validator_count // 32`. @@ -378,7 +378,7 @@ pub async fn consensus_partial_pass_only_consensus() { tester.harness.chain.clone(), &channel.0, test_logger, - validation_level.unwrap(), + validation_level, StatusCode::ACCEPTED, network_globals, ) @@ -615,8 +615,7 @@ pub async fn equivocation_gossip() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] pub async fn equivocation_consensus_late_equivocation() { /* this test targets gossip-level validation */ - let validation_level: Option = - Some(BroadcastValidation::ConsensusAndEquivocation); + let validation_level = BroadcastValidation::ConsensusAndEquivocation; // Validator count needs to be at least 32 or proposer boost gets set to 0 when computing // `validator_count // 32`. @@ -671,7 +670,7 @@ pub async fn equivocation_consensus_late_equivocation() { tester.harness.chain, &channel.0, test_logger, - validation_level.unwrap(), + validation_level, StatusCode::ACCEPTED, network_globals, ) @@ -1228,8 +1227,7 @@ pub async fn blinded_equivocation_gossip() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] pub async fn blinded_equivocation_consensus_late_equivocation() { /* this test targets gossip-level validation */ - let validation_level: Option = - Some(BroadcastValidation::ConsensusAndEquivocation); + let validation_level = BroadcastValidation::ConsensusAndEquivocation; // Validator count needs to be at least 32 or proposer boost gets set to 0 when computing // `validator_count // 32`. @@ -1311,7 +1309,7 @@ pub async fn blinded_equivocation_consensus_late_equivocation() { tester.harness.chain, &channel.0, test_logger, - validation_level.unwrap(), + validation_level, StatusCode::ACCEPTED, network_globals, ) @@ -1465,8 +1463,8 @@ pub async fn block_seen_on_gossip_with_some_blobs() { "need at least 2 blobs for partial reveal" ); - let partial_kzg_proofs = vec![blobs.0.get(0).unwrap().clone()]; - let partial_blobs = vec![blobs.1.get(0).unwrap().clone()]; + let partial_kzg_proofs = vec![*blobs.0.first().unwrap()]; + let partial_blobs = vec![blobs.1.first().unwrap().clone()]; // Simulate the block being seen on gossip. block diff --git a/beacon_node/http_api/tests/interactive_tests.rs b/beacon_node/http_api/tests/interactive_tests.rs index 627b0d0b17..e45dcf221c 100644 --- a/beacon_node/http_api/tests/interactive_tests.rs +++ b/beacon_node/http_api/tests/interactive_tests.rs @@ -139,7 +139,7 @@ impl ForkChoiceUpdates { fn insert(&mut self, update: ForkChoiceUpdateMetadata) { self.updates .entry(update.state.head_block_hash) - .or_insert_with(Vec::new) + .or_default() .push(update); } diff --git a/beacon_node/http_api/tests/status_tests.rs b/beacon_node/http_api/tests/status_tests.rs index 01731530d3..dd481f23ba 100644 --- a/beacon_node/http_api/tests/status_tests.rs +++ b/beacon_node/http_api/tests/status_tests.rs @@ -57,18 +57,18 @@ async fn el_syncing_then_synced() { mock_el.el.upcheck().await; let api_response = tester.client.get_node_syncing().await.unwrap().data; - assert_eq!(api_response.el_offline, false); - assert_eq!(api_response.is_optimistic, false); - assert_eq!(api_response.is_syncing, false); + assert!(!api_response.el_offline); + assert!(!api_response.is_optimistic); + assert!(!api_response.is_syncing); // EL synced mock_el.server.set_syncing_response(Ok(false)); mock_el.el.upcheck().await; let api_response = tester.client.get_node_syncing().await.unwrap().data; - assert_eq!(api_response.el_offline, false); - assert_eq!(api_response.is_optimistic, false); - assert_eq!(api_response.is_syncing, false); + assert!(!api_response.el_offline); + assert!(!api_response.is_optimistic); + assert!(!api_response.is_syncing); } /// Check `syncing` endpoint when the EL is offline (errors on upcheck). @@ -85,9 +85,9 @@ async fn el_offline() { mock_el.el.upcheck().await; let api_response = tester.client.get_node_syncing().await.unwrap().data; - assert_eq!(api_response.el_offline, true); - assert_eq!(api_response.is_optimistic, false); - assert_eq!(api_response.is_syncing, false); + assert!(api_response.el_offline); + assert!(!api_response.is_optimistic); + assert!(!api_response.is_syncing); } /// Check `syncing` endpoint when the EL errors on newPaylod but is not fully offline. @@ -128,9 +128,9 @@ async fn el_error_on_new_payload() { // The EL should now be *offline* according to the API. let api_response = tester.client.get_node_syncing().await.unwrap().data; - assert_eq!(api_response.el_offline, true); - assert_eq!(api_response.is_optimistic, false); - assert_eq!(api_response.is_syncing, false); + assert!(api_response.el_offline); + assert!(!api_response.is_optimistic); + assert!(!api_response.is_syncing); // Processing a block successfully should remove the status. mock_el.server.set_new_payload_status( @@ -144,9 +144,9 @@ async fn el_error_on_new_payload() { harness.process_block_result((block, blobs)).await.unwrap(); let api_response = tester.client.get_node_syncing().await.unwrap().data; - assert_eq!(api_response.el_offline, false); - assert_eq!(api_response.is_optimistic, false); - assert_eq!(api_response.is_syncing, false); + assert!(!api_response.el_offline); + assert!(!api_response.is_optimistic); + assert!(!api_response.is_syncing); } /// Check `node health` endpoint when the EL is offline. diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 080a393b4d..7007a14466 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -274,10 +274,10 @@ impl ApiTester { let mock_builder_server = harness.set_mock_builder(beacon_url.clone()); // Start the mock builder service prior to building the chain out. - harness.runtime.task_executor.spawn( - async move { mock_builder_server.await }, - "mock_builder_server", - ); + harness + .runtime + .task_executor + .spawn(mock_builder_server, "mock_builder_server"); let mock_builder = harness.mock_builder.clone(); @@ -641,7 +641,7 @@ impl ApiTester { self } - pub async fn test_beacon_blocks_finalized(self) -> Self { + pub async fn test_beacon_blocks_finalized(self) -> Self { for block_id in self.interesting_block_ids() { let block_root = block_id.root(&self.chain); let block = block_id.full_block(&self.chain).await; @@ -678,7 +678,7 @@ impl ApiTester { self } - pub async fn test_beacon_blinded_blocks_finalized(self) -> Self { + pub async fn test_beacon_blinded_blocks_finalized(self) -> Self { for block_id in self.interesting_block_ids() { let block_root = block_id.root(&self.chain); let block = block_id.full_block(&self.chain).await; @@ -819,7 +819,7 @@ impl ApiTester { let validator_index_ids = validator_indices .iter() .cloned() - .map(|i| ValidatorId::Index(i)) + .map(ValidatorId::Index) .collect::>(); let unsupported_media_response = self @@ -859,7 +859,7 @@ impl ApiTester { let validator_index_ids = validator_indices .iter() .cloned() - .map(|i| ValidatorId::Index(i)) + .map(ValidatorId::Index) .collect::>(); let validator_pubkey_ids = validator_indices .iter() @@ -910,7 +910,7 @@ impl ApiTester { for i in validator_indices { if i < state.balances().len() as u64 { validators.push(ValidatorBalanceData { - index: i as u64, + index: i, balance: *state.balances().get(i as usize).unwrap(), }); } @@ -944,7 +944,7 @@ impl ApiTester { let validator_index_ids = validator_indices .iter() .cloned() - .map(|i| ValidatorId::Index(i)) + .map(ValidatorId::Index) .collect::>(); let validator_pubkey_ids = validator_indices .iter() @@ -1012,7 +1012,7 @@ impl ApiTester { || statuses.contains(&status.superstatus()) { validators.push(ValidatorData { - index: i as u64, + index: i, balance: *state.balances().get(i as usize).unwrap(), status, validator, @@ -1641,11 +1641,7 @@ impl ApiTester { let (block, _, _) = block_id.full_block(&self.chain).await.unwrap(); let num_blobs = block.num_expected_blobs(); let blob_indices = if use_indices { - Some( - (0..num_blobs.saturating_sub(1) as u64) - .into_iter() - .collect::>(), - ) + Some((0..num_blobs.saturating_sub(1) as u64).collect::>()) } else { None }; @@ -1663,7 +1659,7 @@ impl ApiTester { blob_indices.map_or(num_blobs, |indices| indices.len()) ); let expected = block.slot(); - assert_eq!(result.get(0).unwrap().slot(), expected); + assert_eq!(result.first().unwrap().slot(), expected); self } @@ -1701,9 +1697,9 @@ impl ApiTester { break; } } - let test_slot = test_slot.expect(&format!( - "should be able to find a block matching zero_blobs={zero_blobs}" - )); + let test_slot = test_slot.unwrap_or_else(|| { + panic!("should be able to find a block matching zero_blobs={zero_blobs}") + }); match self .client @@ -1772,7 +1768,6 @@ impl ApiTester { .attestations() .map(|att| att.clone_as_attestation()) .collect::>() - .into() }, ); @@ -1909,7 +1904,7 @@ impl ApiTester { let result = match self .client - .get_beacon_light_client_updates::(current_sync_committee_period as u64, 1) + .get_beacon_light_client_updates::(current_sync_committee_period, 1) .await { Ok(result) => result, @@ -1921,7 +1916,7 @@ impl ApiTester { .light_client_server_cache .get_light_client_updates( &self.chain.store, - current_sync_committee_period as u64, + current_sync_committee_period, 1, &self.chain.spec, ) @@ -2314,7 +2309,7 @@ impl ApiTester { .unwrap() .data .is_syncing; - assert_eq!(is_syncing, true); + assert!(is_syncing); // Reset sync state. *self @@ -2364,7 +2359,7 @@ impl ApiTester { pub async fn test_get_node_peers_by_id(self) -> Self { let result = self .client - .get_node_peers_by_id(self.external_peer_id.clone()) + .get_node_peers_by_id(self.external_peer_id) .await .unwrap() .data; @@ -3514,6 +3509,7 @@ impl ApiTester { self } + #[allow(clippy::await_holding_lock)] // This is a test, so it should be fine. pub async fn test_get_validator_aggregate_attestation(self) -> Self { if self .chain @@ -4058,7 +4054,7 @@ impl ApiTester { ProduceBlockV3Response::Full(_) => panic!("Expecting a blinded payload"), }; - let expected_fee_recipient = Address::from_low_u64_be(proposer_index as u64); + let expected_fee_recipient = Address::from_low_u64_be(proposer_index); assert_eq!(payload.fee_recipient(), expected_fee_recipient); assert_eq!(payload.gas_limit(), DEFAULT_GAS_LIMIT); @@ -4085,7 +4081,7 @@ impl ApiTester { ProduceBlockV3Response::Blinded(_) => panic!("Expecting a full payload"), }; - let expected_fee_recipient = Address::from_low_u64_be(proposer_index as u64); + let expected_fee_recipient = Address::from_low_u64_be(proposer_index); assert_eq!(payload.fee_recipient(), expected_fee_recipient); // This is the graffiti of the mock execution layer, not the builder. assert_eq!(payload.extra_data(), mock_el_extra_data::()); @@ -4113,7 +4109,7 @@ impl ApiTester { ProduceBlockV3Response::Full(_) => panic!("Expecting a blinded payload"), }; - let expected_fee_recipient = Address::from_low_u64_be(proposer_index as u64); + let expected_fee_recipient = Address::from_low_u64_be(proposer_index); assert_eq!(payload.fee_recipient(), expected_fee_recipient); assert_eq!(payload.gas_limit(), DEFAULT_GAS_LIMIT); @@ -4137,7 +4133,7 @@ impl ApiTester { .unwrap() .into(); - let expected_fee_recipient = Address::from_low_u64_be(proposer_index as u64); + let expected_fee_recipient = Address::from_low_u64_be(proposer_index); assert_eq!(payload.fee_recipient(), expected_fee_recipient); assert_eq!(payload.gas_limit(), DEFAULT_GAS_LIMIT); @@ -4183,7 +4179,7 @@ impl ApiTester { .unwrap() .into(); - let expected_fee_recipient = Address::from_low_u64_be(proposer_index as u64); + let expected_fee_recipient = Address::from_low_u64_be(proposer_index); assert_eq!(payload.fee_recipient(), expected_fee_recipient); assert_eq!(payload.gas_limit(), builder_limit); @@ -4267,7 +4263,7 @@ impl ApiTester { ProduceBlockV3Response::Full(_) => panic!("Expecting a blinded payload"), }; - let expected_fee_recipient = Address::from_low_u64_be(proposer_index as u64); + let expected_fee_recipient = Address::from_low_u64_be(proposer_index); assert_eq!(payload.fee_recipient(), expected_fee_recipient); assert_eq!(payload.gas_limit(), 30_000_000); @@ -5140,9 +5136,8 @@ impl ApiTester { pub async fn test_builder_chain_health_optimistic_head(self) -> Self { // Make sure the next payload verification will return optimistic before advancing the chain. - self.harness.mock_execution_layer.as_ref().map(|el| { + self.harness.mock_execution_layer.as_ref().inspect(|el| { el.server.all_payloads_syncing(true); - el }); self.harness .extend_chain( @@ -5169,7 +5164,7 @@ impl ApiTester { .unwrap() .into(); - let expected_fee_recipient = Address::from_low_u64_be(proposer_index as u64); + let expected_fee_recipient = Address::from_low_u64_be(proposer_index); assert_eq!(payload.fee_recipient(), expected_fee_recipient); // If this cache is populated, it indicates fallback to the local EE was correctly used. @@ -5188,9 +5183,8 @@ impl ApiTester { pub async fn test_builder_v3_chain_health_optimistic_head(self) -> Self { // Make sure the next payload verification will return optimistic before advancing the chain. - self.harness.mock_execution_layer.as_ref().map(|el| { + self.harness.mock_execution_layer.as_ref().inspect(|el| { el.server.all_payloads_syncing(true); - el }); self.harness .extend_chain( @@ -5220,7 +5214,7 @@ impl ApiTester { ProduceBlockV3Response::Blinded(_) => panic!("Expecting a full payload"), }; - let expected_fee_recipient = Address::from_low_u64_be(proposer_index as u64); + let expected_fee_recipient = Address::from_low_u64_be(proposer_index); assert_eq!(payload.fee_recipient(), expected_fee_recipient); self @@ -6101,16 +6095,17 @@ impl ApiTester { assert_eq!(result.execution_optimistic, Some(false)); // Change head to be optimistic. - self.chain + if let Some(head_node) = self + .chain .canonical_head .fork_choice_write_lock() .proto_array_mut() .core_proto_array_mut() .nodes .last_mut() - .map(|head_node| { - head_node.execution_status = ExecutionStatus::Optimistic(ExecutionBlockHash::zero()) - }); + { + head_node.execution_status = ExecutionStatus::Optimistic(ExecutionBlockHash::zero()) + } // Check responses are now optimistic. let result = self @@ -6143,8 +6138,8 @@ async fn poll_events, eth2::Error>> + Unpin }; tokio::select! { - _ = collect_stream_fut => {events} - _ = tokio::time::sleep(timeout) => { return events; } + _ = collect_stream_fut => { events } + _ = tokio::time::sleep(timeout) => { events } } } @@ -6180,31 +6175,31 @@ async fn test_unsupported_media_response() { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn beacon_get() { +async fn beacon_get_state_hashes() { + ApiTester::new() + .await + .test_beacon_states_root_finalized() + .await + .test_beacon_states_finality_checkpoints_finalized() + .await + .test_beacon_states_root() + .await + .test_beacon_states_finality_checkpoints() + .await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn beacon_get_state_info() { ApiTester::new() .await .test_beacon_genesis() .await - .test_beacon_states_root_finalized() - .await .test_beacon_states_fork_finalized() .await - .test_beacon_states_finality_checkpoints_finalized() - .await - .test_beacon_headers_block_id_finalized() - .await - .test_beacon_blocks_finalized::() - .await - .test_beacon_blinded_blocks_finalized::() - .await .test_debug_beacon_states_finalized() .await - .test_beacon_states_root() - .await .test_beacon_states_fork() .await - .test_beacon_states_finality_checkpoints() - .await .test_beacon_states_validators() .await .test_beacon_states_validator_balances() @@ -6214,6 +6209,18 @@ async fn beacon_get() { .test_beacon_states_validator_id() .await .test_beacon_states_randao() + .await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn beacon_get_blocks() { + ApiTester::new() + .await + .test_beacon_headers_block_id_finalized() + .await + .test_beacon_blocks_finalized() + .await + .test_beacon_blinded_blocks_finalized() .await .test_beacon_headers_all_slots() .await @@ -6228,6 +6235,12 @@ async fn beacon_get() { .test_beacon_blocks_attestations() .await .test_beacon_blocks_root() + .await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn beacon_get_pools() { + ApiTester::new() .await .test_get_beacon_pool_attestations() .await diff --git a/beacon_node/lighthouse_network/src/service/gossip_cache.rs b/beacon_node/lighthouse_network/src/service/gossip_cache.rs index 0ad31ff2e8..e46c69dc71 100644 --- a/beacon_node/lighthouse_network/src/service/gossip_cache.rs +++ b/beacon_node/lighthouse_network/src/service/gossip_cache.rs @@ -250,18 +250,17 @@ impl futures::stream::Stream for GossipCache { Poll::Ready(Some(expired)) => { let expected_key = expired.key(); let (topic, data) = expired.into_inner(); - match self.topic_msgs.get_mut(&topic) { - Some(msgs) => { - let key = msgs.remove(&data); - debug_assert_eq!(key, Some(expected_key)); - if msgs.is_empty() { - // no more messages for this topic. - self.topic_msgs.remove(&topic); - } - } - None => { - #[cfg(debug_assertions)] - panic!("Topic for registered message is not present.") + let topic_msg = self.topic_msgs.get_mut(&topic); + debug_assert!( + topic_msg.is_some(), + "Topic for registered message is not present." + ); + if let Some(msgs) = topic_msg { + let key = msgs.remove(&data); + debug_assert_eq!(key, Some(expected_key)); + if msgs.is_empty() { + // no more messages for this topic. + self.topic_msgs.remove(&topic); } } Poll::Ready(Some(Ok(topic))) diff --git a/beacon_node/network/src/network_beacon_processor/tests.rs b/beacon_node/network/src/network_beacon_processor/tests.rs index 9d774d97c1..7e27a91bd6 100644 --- a/beacon_node/network/src/network_beacon_processor/tests.rs +++ b/beacon_node/network/src/network_beacon_processor/tests.rs @@ -527,7 +527,7 @@ impl TestRig { self.assert_event_journal( &expected .iter() - .map(|ev| Into::<&'static str>::into(ev)) + .map(Into::<&'static str>::into) .chain(std::iter::once(WORKER_FREED)) .chain(std::iter::once(NOTHING_TO_DO)) .collect::>(), diff --git a/beacon_node/network/src/service/tests.rs b/beacon_node/network/src/service/tests.rs index c46e46e0fa..32bbfcbcaa 100644 --- a/beacon_node/network/src/service/tests.rs +++ b/beacon_node/network/src/service/tests.rs @@ -1,235 +1,229 @@ -#[cfg(not(debug_assertions))] -#[cfg(test)] -mod tests { - use crate::persisted_dht::load_dht; - use crate::{NetworkConfig, NetworkService}; - use beacon_chain::test_utils::BeaconChainHarness; - use beacon_chain::BeaconChainTypes; - use beacon_processor::{BeaconProcessorChannels, BeaconProcessorConfig}; - use futures::StreamExt; - use lighthouse_network::types::{GossipEncoding, GossipKind}; - use lighthouse_network::{Enr, GossipTopic}; - use slog::{o, Drain, Level, Logger}; - use sloggers::{null::NullLoggerBuilder, Build}; - use std::str::FromStr; - use std::sync::Arc; - use tokio::runtime::Runtime; - use types::{Epoch, EthSpec, ForkName, MinimalEthSpec, SubnetId}; +#![cfg(not(debug_assertions))] +#![cfg(test)] +use crate::persisted_dht::load_dht; +use crate::{NetworkConfig, NetworkService}; +use beacon_chain::test_utils::BeaconChainHarness; +use beacon_chain::BeaconChainTypes; +use beacon_processor::{BeaconProcessorChannels, BeaconProcessorConfig}; +use futures::StreamExt; +use lighthouse_network::types::{GossipEncoding, GossipKind}; +use lighthouse_network::{Enr, GossipTopic}; +use slog::{o, Drain, Level, Logger}; +use sloggers::{null::NullLoggerBuilder, Build}; +use std::str::FromStr; +use std::sync::Arc; +use tokio::runtime::Runtime; +use types::{Epoch, EthSpec, ForkName, MinimalEthSpec, SubnetId}; - impl NetworkService { - fn get_topic_params(&self, topic: GossipTopic) -> Option<&gossipsub::TopicScoreParams> { - self.libp2p.get_topic_params(topic) - } +impl NetworkService { + fn get_topic_params(&self, topic: GossipTopic) -> Option<&gossipsub::TopicScoreParams> { + self.libp2p.get_topic_params(topic) } +} - fn get_logger(actual_log: bool) -> Logger { - if actual_log { - let drain = { - let decorator = slog_term::TermDecorator::new().build(); - let decorator = - logging::AlignedTermDecorator::new(decorator, logging::MAX_MESSAGE_WIDTH); - let drain = slog_term::FullFormat::new(decorator).build().fuse(); - let drain = slog_async::Async::new(drain).chan_size(2048).build(); - drain.filter_level(Level::Debug) - }; +fn get_logger(actual_log: bool) -> Logger { + if actual_log { + let drain = { + let decorator = slog_term::TermDecorator::new().build(); + let decorator = + logging::AlignedTermDecorator::new(decorator, logging::MAX_MESSAGE_WIDTH); + let drain = slog_term::FullFormat::new(decorator).build().fuse(); + let drain = slog_async::Async::new(drain).chan_size(2048).build(); + drain.filter_level(Level::Debug) + }; - Logger::root(drain.fuse(), o!()) - } else { - let builder = NullLoggerBuilder; - builder.build().expect("should build logger") - } + Logger::root(drain.fuse(), o!()) + } else { + let builder = NullLoggerBuilder; + builder.build().expect("should build logger") } +} - #[test] - fn test_dht_persistence() { - let log = get_logger(false); +#[test] +fn test_dht_persistence() { + let log = get_logger(false); - let beacon_chain = BeaconChainHarness::builder(MinimalEthSpec) - .default_spec() - .deterministic_keypairs(8) - .fresh_ephemeral_store() - .build() - .chain; + let beacon_chain = BeaconChainHarness::builder(MinimalEthSpec) + .default_spec() + .deterministic_keypairs(8) + .fresh_ephemeral_store() + .build() + .chain; - let store = beacon_chain.store.clone(); + let store = beacon_chain.store.clone(); - let enr1 = Enr::from_str("enr:-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnh4MHcBFZntXNFrdvJjX04jRzjzCBOonrkTfj499SZuOh8R33Ls8RRcy5wBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQPKY0yuDUmstAHYpMa2_oxVtw0RW_QAdpzBQA8yWM0xOIN1ZHCCdl8").unwrap(); - let enr2 = Enr::from_str("enr:-IS4QJ2d11eu6dC7E7LoXeLMgMP3kom1u3SE8esFSWvaHoo0dP1jg8O3-nx9ht-EO3CmG7L6OkHcMmoIh00IYWB92QABgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQIB_c-jQMOXsbjWkbN-Oj99H57gfId5pfb4wa1qxwV4CIN1ZHCCIyk").unwrap(); - let enrs = vec![enr1, enr2]; + let enr1 = Enr::from_str("enr:-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnh4MHcBFZntXNFrdvJjX04jRzjzCBOonrkTfj499SZuOh8R33Ls8RRcy5wBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQPKY0yuDUmstAHYpMa2_oxVtw0RW_QAdpzBQA8yWM0xOIN1ZHCCdl8").unwrap(); + let enr2 = Enr::from_str("enr:-IS4QJ2d11eu6dC7E7LoXeLMgMP3kom1u3SE8esFSWvaHoo0dP1jg8O3-nx9ht-EO3CmG7L6OkHcMmoIh00IYWB92QABgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQIB_c-jQMOXsbjWkbN-Oj99H57gfId5pfb4wa1qxwV4CIN1ZHCCIyk").unwrap(); + let enrs = vec![enr1, enr2]; - let runtime = Arc::new(Runtime::new().unwrap()); + let runtime = Arc::new(Runtime::new().unwrap()); - let (signal, exit) = async_channel::bounded(1); + let (signal, exit) = async_channel::bounded(1); + let (shutdown_tx, _) = futures::channel::mpsc::channel(1); + let executor = + task_executor::TaskExecutor::new(Arc::downgrade(&runtime), exit, log.clone(), shutdown_tx); + + let mut config = NetworkConfig::default(); + config.set_ipv4_listening_address(std::net::Ipv4Addr::UNSPECIFIED, 21212, 21212, 21213); + config.discv5_config.table_filter = |_| true; // Do not ignore local IPs + config.upnp_enabled = false; + config.boot_nodes_enr = enrs.clone(); + let config = Arc::new(config); + runtime.block_on(async move { + // Create a new network service which implicitly gets dropped at the + // end of the block. + + let BeaconProcessorChannels { + beacon_processor_tx, + beacon_processor_rx: _beacon_processor_rx, + work_reprocessing_tx, + work_reprocessing_rx: _work_reprocessing_rx, + } = <_>::default(); + + let _network_service = NetworkService::start( + beacon_chain.clone(), + config, + executor, + None, + beacon_processor_tx, + work_reprocessing_tx, + ) + .await + .unwrap(); + drop(signal); + }); + + let raw_runtime = Arc::try_unwrap(runtime).unwrap(); + raw_runtime.shutdown_timeout(tokio::time::Duration::from_secs(300)); + + // Load the persisted dht from the store + let persisted_enrs = load_dht(store); + assert!( + persisted_enrs.contains(&enrs[0]), + "should have persisted the first ENR to store" + ); + assert!( + persisted_enrs.contains(&enrs[1]), + "should have persisted the second ENR to store" + ); +} + +// Test removing topic weight on old topics when a fork happens. +#[test] +fn test_removing_topic_weight_on_old_topics() { + let runtime = Arc::new(Runtime::new().unwrap()); + + // Capella spec + let mut spec = MinimalEthSpec::default_spec(); + spec.altair_fork_epoch = Some(Epoch::new(0)); + spec.bellatrix_fork_epoch = Some(Epoch::new(0)); + spec.capella_fork_epoch = Some(Epoch::new(1)); + + // Build beacon chain. + let beacon_chain = BeaconChainHarness::builder(MinimalEthSpec) + .spec(spec.clone().into()) + .deterministic_keypairs(8) + .fresh_ephemeral_store() + .mock_execution_layer() + .build() + .chain; + let (next_fork_name, _) = beacon_chain.duration_to_next_fork().expect("next fork"); + assert_eq!(next_fork_name, ForkName::Capella); + + // Build network service. + let (mut network_service, network_globals, _network_senders) = runtime.block_on(async { + let (_, exit) = async_channel::bounded(1); let (shutdown_tx, _) = futures::channel::mpsc::channel(1); let executor = task_executor::TaskExecutor::new( Arc::downgrade(&runtime), exit, - log.clone(), + get_logger(false), shutdown_tx, ); let mut config = NetworkConfig::default(); - config.set_ipv4_listening_address(std::net::Ipv4Addr::UNSPECIFIED, 21212, 21212, 21213); + config.set_ipv4_listening_address(std::net::Ipv4Addr::UNSPECIFIED, 21214, 21214, 21215); config.discv5_config.table_filter = |_| true; // Do not ignore local IPs config.upnp_enabled = false; - config.boot_nodes_enr = enrs.clone(); let config = Arc::new(config); - runtime.block_on(async move { - // Create a new network service which implicitly gets dropped at the - // end of the block. - let BeaconProcessorChannels { - beacon_processor_tx, - beacon_processor_rx: _beacon_processor_rx, - work_reprocessing_tx, - work_reprocessing_rx: _work_reprocessing_rx, - } = <_>::default(); + let beacon_processor_channels = + BeaconProcessorChannels::new(&BeaconProcessorConfig::default()); + NetworkService::build( + beacon_chain.clone(), + config, + executor.clone(), + None, + beacon_processor_channels.beacon_processor_tx, + beacon_processor_channels.work_reprocessing_tx, + ) + .await + .unwrap() + }); - let _network_service = NetworkService::start( - beacon_chain.clone(), - config, - executor, - None, - beacon_processor_tx, - work_reprocessing_tx, - ) - .await - .unwrap(); - drop(signal); - }); - - let raw_runtime = Arc::try_unwrap(runtime).unwrap(); - raw_runtime.shutdown_timeout(tokio::time::Duration::from_secs(300)); - - // Load the persisted dht from the store - let persisted_enrs = load_dht(store); - assert!( - persisted_enrs.contains(&enrs[0]), - "should have persisted the first ENR to store" - ); - assert!( - persisted_enrs.contains(&enrs[1]), - "should have persisted the second ENR to store" - ); - } - - // Test removing topic weight on old topics when a fork happens. - #[test] - fn test_removing_topic_weight_on_old_topics() { - let runtime = Arc::new(Runtime::new().unwrap()); - - // Capella spec - let mut spec = MinimalEthSpec::default_spec(); - spec.altair_fork_epoch = Some(Epoch::new(0)); - spec.bellatrix_fork_epoch = Some(Epoch::new(0)); - spec.capella_fork_epoch = Some(Epoch::new(1)); - - // Build beacon chain. - let beacon_chain = BeaconChainHarness::builder(MinimalEthSpec) - .spec(spec.clone().into()) - .deterministic_keypairs(8) - .fresh_ephemeral_store() - .mock_execution_layer() - .build() - .chain; - let (next_fork_name, _) = beacon_chain.duration_to_next_fork().expect("next fork"); - assert_eq!(next_fork_name, ForkName::Capella); - - // Build network service. - let (mut network_service, network_globals, _network_senders) = runtime.block_on(async { - let (_, exit) = async_channel::bounded(1); - let (shutdown_tx, _) = futures::channel::mpsc::channel(1); - let executor = task_executor::TaskExecutor::new( - Arc::downgrade(&runtime), - exit, - get_logger(false), - shutdown_tx, - ); - - let mut config = NetworkConfig::default(); - config.set_ipv4_listening_address(std::net::Ipv4Addr::UNSPECIFIED, 21214, 21214, 21215); - config.discv5_config.table_filter = |_| true; // Do not ignore local IPs - config.upnp_enabled = false; - let config = Arc::new(config); - - let beacon_processor_channels = - BeaconProcessorChannels::new(&BeaconProcessorConfig::default()); - NetworkService::build( - beacon_chain.clone(), - config, - executor.clone(), - None, - beacon_processor_channels.beacon_processor_tx, - beacon_processor_channels.work_reprocessing_tx, - ) - .await - .unwrap() - }); - - // Subscribe to the topics. - runtime.block_on(async { - while network_globals.gossipsub_subscriptions.read().len() < 2 { - if let Some(msg) = network_service.subnet_service.next().await { - network_service.on_subnet_service_msg(msg); - } + // Subscribe to the topics. + runtime.block_on(async { + while network_globals.gossipsub_subscriptions.read().len() < 2 { + if let Some(msg) = network_service.subnet_service.next().await { + network_service.on_subnet_service_msg(msg); } - }); - - // Make sure the service is subscribed to the topics. - let (old_topic1, old_topic2) = { - let mut subnets = SubnetId::compute_attestation_subnets( - network_globals.local_enr().node_id().raw(), - &spec, - ) - .collect::>(); - assert_eq!(2, subnets.len()); - - let old_fork_digest = beacon_chain.enr_fork_id().fork_digest; - let old_topic1 = GossipTopic::new( - GossipKind::Attestation(subnets.pop().unwrap()), - GossipEncoding::SSZSnappy, - old_fork_digest, - ); - let old_topic2 = GossipTopic::new( - GossipKind::Attestation(subnets.pop().unwrap()), - GossipEncoding::SSZSnappy, - old_fork_digest, - ); - - (old_topic1, old_topic2) - }; - let subscriptions = network_globals.gossipsub_subscriptions.read().clone(); - assert_eq!(2, subscriptions.len()); - assert!(subscriptions.contains(&old_topic1)); - assert!(subscriptions.contains(&old_topic2)); - let old_topic_params1 = network_service - .get_topic_params(old_topic1.clone()) - .expect("topic score params"); - assert!(old_topic_params1.topic_weight > 0.0); - let old_topic_params2 = network_service - .get_topic_params(old_topic2.clone()) - .expect("topic score params"); - assert!(old_topic_params2.topic_weight > 0.0); - - // Advance slot to the next fork - for _ in 0..MinimalEthSpec::slots_per_epoch() { - beacon_chain.slot_clock.advance_slot(); } + }); - // Run `NetworkService::update_next_fork()`. - runtime.block_on(async { - network_service.update_next_fork(); - }); + // Make sure the service is subscribed to the topics. + let (old_topic1, old_topic2) = { + let mut subnets = SubnetId::compute_attestation_subnets( + network_globals.local_enr().node_id().raw(), + &spec, + ) + .collect::>(); + assert_eq!(2, subnets.len()); - // Check that topic_weight on the old topics has been zeroed. - let old_topic_params1 = network_service - .get_topic_params(old_topic1) - .expect("topic score params"); - assert_eq!(0.0, old_topic_params1.topic_weight); + let old_fork_digest = beacon_chain.enr_fork_id().fork_digest; + let old_topic1 = GossipTopic::new( + GossipKind::Attestation(subnets.pop().unwrap()), + GossipEncoding::SSZSnappy, + old_fork_digest, + ); + let old_topic2 = GossipTopic::new( + GossipKind::Attestation(subnets.pop().unwrap()), + GossipEncoding::SSZSnappy, + old_fork_digest, + ); - let old_topic_params2 = network_service - .get_topic_params(old_topic2) - .expect("topic score params"); - assert_eq!(0.0, old_topic_params2.topic_weight); + (old_topic1, old_topic2) + }; + let subscriptions = network_globals.gossipsub_subscriptions.read().clone(); + assert_eq!(2, subscriptions.len()); + assert!(subscriptions.contains(&old_topic1)); + assert!(subscriptions.contains(&old_topic2)); + let old_topic_params1 = network_service + .get_topic_params(old_topic1.clone()) + .expect("topic score params"); + assert!(old_topic_params1.topic_weight > 0.0); + let old_topic_params2 = network_service + .get_topic_params(old_topic2.clone()) + .expect("topic score params"); + assert!(old_topic_params2.topic_weight > 0.0); + + // Advance slot to the next fork + for _ in 0..MinimalEthSpec::slots_per_epoch() { + beacon_chain.slot_clock.advance_slot(); } + + // Run `NetworkService::update_next_fork()`. + runtime.block_on(async { + network_service.update_next_fork(); + }); + + // Check that topic_weight on the old topics has been zeroed. + let old_topic_params1 = network_service + .get_topic_params(old_topic1) + .expect("topic score params"); + assert_eq!(0.0, old_topic_params1.topic_weight); + + let old_topic_params2 = network_service + .get_topic_params(old_topic2) + .expect("topic score params"); + assert_eq!(0.0, old_topic_params2.topic_weight); } diff --git a/beacon_node/operation_pool/src/lib.rs b/beacon_node/operation_pool/src/lib.rs index 3a002bf870..d01c73118c 100644 --- a/beacon_node/operation_pool/src/lib.rs +++ b/beacon_node/operation_pool/src/lib.rs @@ -877,11 +877,11 @@ mod release_tests { let (harness, ref spec) = attestation_test_state::(1); // Only run this test on the phase0 hard-fork. - if spec.altair_fork_epoch != None { + if spec.altair_fork_epoch.is_some() { return; } - let mut state = get_current_state_initialize_epoch_cache(&harness, &spec); + let mut state = get_current_state_initialize_epoch_cache(&harness, spec); let slot = state.slot(); let committees = state .get_beacon_committees_at_slot(slot) @@ -902,10 +902,10 @@ mod release_tests { ); for (atts, aggregate) in &attestations { - let att2 = aggregate.as_ref().unwrap().message().aggregate().clone(); + let att2 = aggregate.as_ref().unwrap().message().aggregate(); let att1 = atts - .into_iter() + .iter() .map(|(att, _)| att) .take(2) .fold::>, _>(None, |att, new_att| { @@ -946,7 +946,7 @@ mod release_tests { .unwrap(); assert_eq!( - committees.get(0).unwrap().committee.len() - 2, + committees.first().unwrap().committee.len() - 2, earliest_attestation_validators( &att2_split.as_ref(), &state, @@ -963,7 +963,7 @@ mod release_tests { let (harness, ref spec) = attestation_test_state::(1); let op_pool = OperationPool::::new(); - let mut state = get_current_state_initialize_epoch_cache(&harness, &spec); + let mut state = get_current_state_initialize_epoch_cache(&harness, spec); let slot = state.slot(); let committees = state @@ -1020,7 +1020,7 @@ mod release_tests { let agg_att = &block_attestations[0]; assert_eq!( agg_att.num_set_aggregation_bits(), - spec.target_committee_size as usize + spec.target_committee_size ); // Prune attestations shouldn't do anything at this point. @@ -1039,7 +1039,7 @@ mod release_tests { fn attestation_duplicate() { let (harness, ref spec) = attestation_test_state::(1); - let state = get_current_state_initialize_epoch_cache(&harness, &spec); + let state = get_current_state_initialize_epoch_cache(&harness, spec); let op_pool = OperationPool::::new(); @@ -1082,7 +1082,7 @@ mod release_tests { fn attestation_pairwise_overlapping() { let (harness, ref spec) = attestation_test_state::(1); - let state = get_current_state_initialize_epoch_cache(&harness, &spec); + let state = get_current_state_initialize_epoch_cache(&harness, spec); let op_pool = OperationPool::::new(); @@ -1113,19 +1113,17 @@ mod release_tests { let aggs1 = atts1 .chunks_exact(step_size * 2) .map(|chunk| { - let agg = chunk.into_iter().map(|(att, _)| att).fold::, - >, _>( - None, - |att, new_att| { + let agg = chunk + .iter() + .map(|(att, _)| att) + .fold::>, _>(None, |att, new_att| { if let Some(mut a) = att { a.aggregate(new_att.to_ref()); Some(a) } else { Some(new_att.clone()) } - }, - ); + }); agg.unwrap() }) .collect::>(); @@ -1136,19 +1134,17 @@ mod release_tests { .as_slice() .chunks_exact(step_size * 2) .map(|chunk| { - let agg = chunk.into_iter().map(|(att, _)| att).fold::, - >, _>( - None, - |att, new_att| { + let agg = chunk + .iter() + .map(|(att, _)| att) + .fold::>, _>(None, |att, new_att| { if let Some(mut a) = att { a.aggregate(new_att.to_ref()); Some(a) } else { Some(new_att.clone()) } - }, - ); + }); agg.unwrap() }) .collect::>(); @@ -1181,7 +1177,7 @@ mod release_tests { let (harness, ref spec) = attestation_test_state::(num_committees); - let mut state = get_current_state_initialize_epoch_cache(&harness, &spec); + let mut state = get_current_state_initialize_epoch_cache(&harness, spec); let op_pool = OperationPool::::new(); @@ -1194,7 +1190,7 @@ mod release_tests { .collect::>(); let max_attestations = ::MaxAttestations::to_usize(); - let target_committee_size = spec.target_committee_size as usize; + let target_committee_size = spec.target_committee_size; let num_validators = num_committees * MainnetEthSpec::slots_per_epoch() as usize * spec.target_committee_size; @@ -1209,12 +1205,12 @@ mod release_tests { let insert_attestations = |attestations: Vec<(Attestation, SubnetId)>, step_size| { - let att_0 = attestations.get(0).unwrap().0.clone(); + let att_0 = attestations.first().unwrap().0.clone(); let aggs = attestations .chunks_exact(step_size) .map(|chunk| { chunk - .into_iter() + .iter() .map(|(att, _)| att) .fold::, _>( att_0.clone(), @@ -1296,7 +1292,7 @@ mod release_tests { let (harness, ref spec) = attestation_test_state::(num_committees); - let mut state = get_current_state_initialize_epoch_cache(&harness, &spec); + let mut state = get_current_state_initialize_epoch_cache(&harness, spec); let op_pool = OperationPool::::new(); let slot = state.slot(); @@ -1308,7 +1304,7 @@ mod release_tests { .collect::>(); let max_attestations = ::MaxAttestations::to_usize(); - let target_committee_size = spec.target_committee_size as usize; + let target_committee_size = spec.target_committee_size; // Each validator will have a multiple of 1_000_000_000 wei. // Safe from overflow unless there are about 18B validators (2^64 / 1_000_000_000). @@ -1329,12 +1325,12 @@ mod release_tests { let insert_attestations = |attestations: Vec<(Attestation, SubnetId)>, step_size| { - let att_0 = attestations.get(0).unwrap().0.clone(); + let att_0 = attestations.first().unwrap().0.clone(); let aggs = attestations .chunks_exact(step_size) .map(|chunk| { chunk - .into_iter() + .iter() .map(|(att, _)| att) .fold::, _>( att_0.clone(), @@ -1615,7 +1611,6 @@ mod release_tests { let block_root = *state .get_block_root(state.slot() - Slot::new(1)) - .ok() .expect("block root should exist at slot"); let contributions = harness.make_sync_contributions( &state, @@ -1674,7 +1669,6 @@ mod release_tests { let state = harness.get_current_state(); let block_root = *state .get_block_root(state.slot() - Slot::new(1)) - .ok() .expect("block root should exist at slot"); let contributions = harness.make_sync_contributions( &state, @@ -1711,7 +1705,6 @@ mod release_tests { let state = harness.get_current_state(); let block_root = *state .get_block_root(state.slot() - Slot::new(1)) - .ok() .expect("block root should exist at slot"); let contributions = harness.make_sync_contributions( &state, @@ -1791,7 +1784,6 @@ mod release_tests { let state = harness.get_current_state(); let block_root = *state .get_block_root(state.slot() - Slot::new(1)) - .ok() .expect("block root should exist at slot"); let contributions = harness.make_sync_contributions( &state, diff --git a/common/eth2_wallet_manager/src/wallet_manager.rs b/common/eth2_wallet_manager/src/wallet_manager.rs index 3dd419a48b..c988ca4135 100644 --- a/common/eth2_wallet_manager/src/wallet_manager.rs +++ b/common/eth2_wallet_manager/src/wallet_manager.rs @@ -296,10 +296,10 @@ mod tests { ) .expect("should create first wallet"); - let uuid = w.wallet().uuid().clone(); + let uuid = *w.wallet().uuid(); assert_eq!( - load_wallet_raw(&base_dir, &uuid).nextaccount(), + load_wallet_raw(base_dir, &uuid).nextaccount(), 0, "should start wallet with nextaccount 0" ); @@ -308,7 +308,7 @@ mod tests { w.next_validator(WALLET_PASSWORD, &[50; 32], &[51; 32]) .expect("should create validator"); assert_eq!( - load_wallet_raw(&base_dir, &uuid).nextaccount(), + load_wallet_raw(base_dir, &uuid).nextaccount(), i, "should update wallet with nextaccount {}", i @@ -333,54 +333,54 @@ mod tests { let base_dir = dir.path(); let mgr = WalletManager::open(base_dir).unwrap(); - let uuid_a = create_wallet(&mgr, 0).wallet().uuid().clone(); - let uuid_b = create_wallet(&mgr, 1).wallet().uuid().clone(); + let uuid_a = *create_wallet(&mgr, 0).wallet().uuid(); + let uuid_b = *create_wallet(&mgr, 1).wallet().uuid(); - let locked_a = LockedWallet::open(&base_dir, &uuid_a).expect("should open wallet a"); + let locked_a = LockedWallet::open(base_dir, &uuid_a).expect("should open wallet a"); assert!( - lockfile_path(&base_dir, &uuid_a).exists(), + lockfile_path(base_dir, &uuid_a).exists(), "lockfile should exist" ); drop(locked_a); assert!( - !lockfile_path(&base_dir, &uuid_a).exists(), + !lockfile_path(base_dir, &uuid_a).exists(), "lockfile have been cleaned up" ); - let locked_a = LockedWallet::open(&base_dir, &uuid_a).expect("should open wallet a"); - let locked_b = LockedWallet::open(&base_dir, &uuid_b).expect("should open wallet b"); + let locked_a = LockedWallet::open(base_dir, &uuid_a).expect("should open wallet a"); + let locked_b = LockedWallet::open(base_dir, &uuid_b).expect("should open wallet b"); assert!( - lockfile_path(&base_dir, &uuid_a).exists(), + lockfile_path(base_dir, &uuid_a).exists(), "lockfile a should exist" ); assert!( - lockfile_path(&base_dir, &uuid_b).exists(), + lockfile_path(base_dir, &uuid_b).exists(), "lockfile b should exist" ); - match LockedWallet::open(&base_dir, &uuid_a) { + match LockedWallet::open(base_dir, &uuid_a) { Err(Error::LockfileError(_)) => {} _ => panic!("did not get locked error"), }; drop(locked_a); - LockedWallet::open(&base_dir, &uuid_a) + LockedWallet::open(base_dir, &uuid_a) .expect("should open wallet a after previous instance is dropped"); - match LockedWallet::open(&base_dir, &uuid_b) { + match LockedWallet::open(base_dir, &uuid_b) { Err(Error::LockfileError(_)) => {} _ => panic!("did not get locked error"), }; drop(locked_b); - LockedWallet::open(&base_dir, &uuid_b) + LockedWallet::open(base_dir, &uuid_b) .expect("should open wallet a after previous instance is dropped"); } } diff --git a/consensus/fork_choice/tests/tests.rs b/consensus/fork_choice/tests/tests.rs index 29265e34e4..ef017159a0 100644 --- a/consensus/fork_choice/tests/tests.rs +++ b/consensus/fork_choice/tests/tests.rs @@ -1156,18 +1156,20 @@ async fn weak_subjectivity_check_epoch_boundary_is_skip_slot() { }; // recreate the chain exactly - ForkChoiceTest::new_with_chain_config(chain_config.clone()) - .apply_blocks_while(|_, state| state.finalized_checkpoint().epoch == 0) - .await - .unwrap() - .skip_slots(E::slots_per_epoch() as usize) - .apply_blocks_while(|_, state| state.finalized_checkpoint().epoch < 5) - .await - .unwrap() - .apply_blocks(1) - .await - .assert_finalized_epoch(5) - .assert_shutdown_signal_not_sent(); + Box::pin( + ForkChoiceTest::new_with_chain_config(chain_config.clone()) + .apply_blocks_while(|_, state| state.finalized_checkpoint().epoch == 0) + .await + .unwrap() + .skip_slots(E::slots_per_epoch() as usize) + .apply_blocks_while(|_, state| state.finalized_checkpoint().epoch < 5) + .await + .unwrap() + .apply_blocks(1), + ) + .await + .assert_finalized_epoch(5) + .assert_shutdown_signal_not_sent(); } #[tokio::test] diff --git a/crypto/eth2_keystore/tests/eip2335_vectors.rs b/crypto/eth2_keystore/tests/eip2335_vectors.rs index 3702a21816..e6852cc608 100644 --- a/crypto/eth2_keystore/tests/eip2335_vectors.rs +++ b/crypto/eth2_keystore/tests/eip2335_vectors.rs @@ -58,7 +58,7 @@ fn eip2335_test_vector_scrypt() { } "#; - let keystore = decode_and_check_sk(&vector); + let keystore = decode_and_check_sk(vector); assert_eq!( *keystore.uuid(), Uuid::parse_str("1d85ae20-35c5-4611-98e8-aa14a633906f").unwrap(), @@ -102,7 +102,7 @@ fn eip2335_test_vector_pbkdf() { } "#; - let keystore = decode_and_check_sk(&vector); + let keystore = decode_and_check_sk(vector); assert_eq!( *keystore.uuid(), Uuid::parse_str("64625def-3331-4eea-ab6f-782f3ed16a83").unwrap(), diff --git a/crypto/eth2_keystore/tests/tests.rs b/crypto/eth2_keystore/tests/tests.rs index 0df884b8a2..20bf9f1653 100644 --- a/crypto/eth2_keystore/tests/tests.rs +++ b/crypto/eth2_keystore/tests/tests.rs @@ -54,25 +54,17 @@ fn file() { let dir = tempdir().unwrap(); let path = dir.path().join("keystore.json"); - let get_file = || { - File::options() - .write(true) - .read(true) - .create(true) - .open(path.clone()) - .expect("should create file") - }; - let keystore = KeystoreBuilder::new(&keypair, GOOD_PASSWORD, "".into()) .unwrap() .build() .unwrap(); keystore - .to_json_writer(&mut get_file()) + .to_json_writer(File::create_new(&path).unwrap()) .expect("should write to file"); - let decoded = Keystore::from_json_reader(&mut get_file()).expect("should read from file"); + let decoded = + Keystore::from_json_reader(File::open(&path).unwrap()).expect("should read from file"); assert_eq!( decoded.decrypt_keypair(BAD_PASSWORD).err().unwrap(), diff --git a/crypto/eth2_wallet/tests/tests.rs b/crypto/eth2_wallet/tests/tests.rs index fe4565e0db..3dc073f764 100644 --- a/crypto/eth2_wallet/tests/tests.rs +++ b/crypto/eth2_wallet/tests/tests.rs @@ -132,20 +132,11 @@ fn file_round_trip() { let dir = tempdir().unwrap(); let path = dir.path().join("keystore.json"); - let get_file = || { - File::options() - .write(true) - .read(true) - .create(true) - .open(path.clone()) - .expect("should create file") - }; - wallet - .to_json_writer(&mut get_file()) + .to_json_writer(File::create_new(&path).unwrap()) .expect("should write to file"); - let decoded = Wallet::from_json_reader(&mut get_file()).unwrap(); + let decoded = Wallet::from_json_reader(File::open(&path).unwrap()).unwrap(); assert_eq!( decoded.decrypt_seed(&[1, 2, 3]).err().unwrap(), diff --git a/lighthouse/tests/account_manager.rs b/lighthouse/tests/account_manager.rs index c7153f48ef..d53d042fa4 100644 --- a/lighthouse/tests/account_manager.rs +++ b/lighthouse/tests/account_manager.rs @@ -115,7 +115,7 @@ fn create_wallet>( .arg(base_dir.as_ref().as_os_str()) .arg(CREATE_CMD) .arg(format!("--{}", NAME_FLAG)) - .arg(&name) + .arg(name) .arg(format!("--{}", PASSWORD_FLAG)) .arg(password.as_ref().as_os_str()) .arg(format!("--{}", MNEMONIC_FLAG)) @@ -273,16 +273,16 @@ impl TestValidator { .expect("stdout is not utf8") .to_string(); - if stdout == "" { + if stdout.is_empty() { return Ok(vec![]); } let pubkeys = stdout[..stdout.len() - 1] .split("\n") - .filter_map(|line| { + .map(|line| { let tab = line.find("\t").expect("line must have tab"); let (_, pubkey) = line.split_at(tab + 1); - Some(pubkey.to_string()) + pubkey.to_string() }) .collect::>(); @@ -446,7 +446,9 @@ fn validator_import_launchpad() { } } - stdin.write(format!("{}\n", PASSWORD).as_bytes()).unwrap(); + stdin + .write_all(format!("{}\n", PASSWORD).as_bytes()) + .unwrap(); child.wait().unwrap(); @@ -504,7 +506,7 @@ fn validator_import_launchpad() { }; assert!( - defs.as_slice() == &[expected_def.clone()], + defs.as_slice() == [expected_def.clone()], "validator defs file should be accurate" ); @@ -525,7 +527,7 @@ fn validator_import_launchpad() { expected_def.enabled = true; assert!( - defs.as_slice() == &[expected_def.clone()], + defs.as_slice() == [expected_def.clone()], "validator defs file should be accurate" ); } @@ -582,7 +584,7 @@ fn validator_import_launchpad_no_password_then_add_password() { let mut child = validator_import_key_cmd(); wait_for_password_prompt(&mut child); let stdin = child.stdin.as_mut().unwrap(); - stdin.write("\n".as_bytes()).unwrap(); + stdin.write_all("\n".as_bytes()).unwrap(); child.wait().unwrap(); assert!( @@ -628,14 +630,16 @@ fn validator_import_launchpad_no_password_then_add_password() { }; assert!( - defs.as_slice() == &[expected_def.clone()], + defs.as_slice() == [expected_def.clone()], "validator defs file should be accurate" ); let mut child = validator_import_key_cmd(); wait_for_password_prompt(&mut child); let stdin = child.stdin.as_mut().unwrap(); - stdin.write(format!("{}\n", PASSWORD).as_bytes()).unwrap(); + stdin + .write_all(format!("{}\n", PASSWORD).as_bytes()) + .unwrap(); child.wait().unwrap(); let expected_def = ValidatorDefinition { @@ -657,7 +661,7 @@ fn validator_import_launchpad_no_password_then_add_password() { let defs = ValidatorDefinitions::open(&dst_dir).unwrap(); assert!( - defs.as_slice() == &[expected_def.clone()], + defs.as_slice() == [expected_def.clone()], "validator defs file should be accurate" ); } @@ -759,7 +763,7 @@ fn validator_import_launchpad_password_file() { }; assert!( - defs.as_slice() == &[expected_def], + defs.as_slice() == [expected_def], "validator defs file should be accurate" ); } diff --git a/lighthouse/tests/beacon_node.rs b/lighthouse/tests/beacon_node.rs index 80986653c1..88e05dfa12 100644 --- a/lighthouse/tests/beacon_node.rs +++ b/lighthouse/tests/beacon_node.rs @@ -9,7 +9,6 @@ use beacon_node::beacon_chain::graffiti_calculator::GraffitiOrigin; use beacon_processor::BeaconProcessorConfig; use eth1::Eth1Endpoint; use lighthouse_network::PeerId; -use lighthouse_version; use std::fs::File; use std::io::{Read, Write}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; @@ -128,7 +127,7 @@ fn allow_insecure_genesis_sync_default() { CommandLineTest::new() .run_with_zero_port_and_no_genesis_sync() .with_config(|config| { - assert_eq!(config.allow_insecure_genesis_sync, false); + assert!(!config.allow_insecure_genesis_sync); }); } @@ -146,7 +145,7 @@ fn allow_insecure_genesis_sync_enabled() { .flag("allow-insecure-genesis-sync", None) .run_with_zero_port_and_no_genesis_sync() .with_config(|config| { - assert_eq!(config.allow_insecure_genesis_sync, true); + assert!(config.allow_insecure_genesis_sync); }); } @@ -359,11 +358,11 @@ fn default_graffiti() { #[test] fn trusted_peers_flag() { - let peers = vec![PeerId::random(), PeerId::random()]; + let peers = [PeerId::random(), PeerId::random()]; CommandLineTest::new() .flag( "trusted-peers", - Some(format!("{},{}", peers[0].to_string(), peers[1].to_string()).as_str()), + Some(format!("{},{}", peers[0], peers[1]).as_str()), ) .run_with_zero_port() .with_config(|config| { @@ -383,7 +382,7 @@ fn genesis_backfill_flag() { CommandLineTest::new() .flag("genesis-backfill", None) .run_with_zero_port() - .with_config(|config| assert_eq!(config.chain.genesis_backfill, true)); + .with_config(|config| assert!(config.chain.genesis_backfill)); } /// The genesis backfill flag should be enabled if historic states flag is set. @@ -392,7 +391,7 @@ fn genesis_backfill_with_historic_flag() { CommandLineTest::new() .flag("reconstruct-historic-states", None) .run_with_zero_port() - .with_config(|config| assert_eq!(config.chain.genesis_backfill, true)); + .with_config(|config| assert!(config.chain.genesis_backfill)); } // Tests for Eth1 flags. @@ -448,7 +447,7 @@ fn eth1_cache_follow_distance_manual() { // Tests for Bellatrix flags. fn run_bellatrix_execution_endpoints_flag_test(flag: &str) { use sensitive_url::SensitiveUrl; - let urls = vec!["http://sigp.io/no-way:1337", "http://infura.not_real:4242"]; + let urls = ["http://sigp.io/no-way:1337", "http://infura.not_real:4242"]; // we don't support redundancy for execution-endpoints // only the first provided endpoint is parsed. @@ -480,10 +479,10 @@ fn run_bellatrix_execution_endpoints_flag_test(flag: &str) { .run_with_zero_port() .with_config(|config| { let config = config.execution_layer.as_ref().unwrap(); - assert_eq!(config.execution_endpoint.is_some(), true); + assert!(config.execution_endpoint.is_some()); assert_eq!( config.execution_endpoint.as_ref().unwrap().clone(), - SensitiveUrl::parse(&urls[0]).unwrap() + SensitiveUrl::parse(urls[0]).unwrap() ); // Only the first secret file should be used. assert_eq!( @@ -595,7 +594,7 @@ fn run_payload_builder_flag_test(flag: &str, builders: &str) { let config = config.execution_layer.as_ref().unwrap(); // Only first provided endpoint is parsed as we don't support // redundancy. - assert_eq!(config.builder_url, all_builders.get(0).cloned()); + assert_eq!(config.builder_url, all_builders.first().cloned()); }) } fn run_payload_builder_flag_test_with_config( @@ -661,7 +660,7 @@ fn builder_fallback_flags() { Some("builder-fallback-disable-checks"), None, |config| { - assert_eq!(config.chain.builder_fallback_disable_checks, true); + assert!(config.chain.builder_fallback_disable_checks); }, ); } @@ -1657,19 +1656,19 @@ fn http_enable_beacon_processor() { CommandLineTest::new() .flag("http", None) .run_with_zero_port() - .with_config(|config| assert_eq!(config.http_api.enable_beacon_processor, true)); + .with_config(|config| assert!(config.http_api.enable_beacon_processor)); CommandLineTest::new() .flag("http", None) .flag("http-enable-beacon-processor", Some("true")) .run_with_zero_port() - .with_config(|config| assert_eq!(config.http_api.enable_beacon_processor, true)); + .with_config(|config| assert!(config.http_api.enable_beacon_processor)); CommandLineTest::new() .flag("http", None) .flag("http-enable-beacon-processor", Some("false")) .run_with_zero_port() - .with_config(|config| assert_eq!(config.http_api.enable_beacon_processor, false)); + .with_config(|config| assert!(!config.http_api.enable_beacon_processor)); } #[test] fn http_tls_flags() { @@ -2221,7 +2220,7 @@ fn slasher_broadcast_flag_false() { }); } -#[cfg(all(feature = "slasher-lmdb"))] +#[cfg(feature = "slasher-lmdb")] #[test] fn slasher_backend_override_to_default() { // Hard to test this flag because all but one backend is disabled by default and the backend @@ -2429,7 +2428,7 @@ fn logfile_no_restricted_perms_flag() { .flag("logfile-no-restricted-perms", None) .run_with_zero_port() .with_config(|config| { - assert!(config.logger_config.is_restricted == false); + assert!(!config.logger_config.is_restricted); }); } #[test] @@ -2454,7 +2453,7 @@ fn logfile_format_flag() { fn sync_eth1_chain_default() { CommandLineTest::new() .run_with_zero_port() - .with_config(|config| assert_eq!(config.sync_eth1_chain, true)); + .with_config(|config| assert!(config.sync_eth1_chain)); } #[test] @@ -2467,7 +2466,7 @@ fn sync_eth1_chain_execution_endpoints_flag() { dir.path().join("jwt-file").as_os_str().to_str(), ) .run_with_zero_port() - .with_config(|config| assert_eq!(config.sync_eth1_chain, true)); + .with_config(|config| assert!(config.sync_eth1_chain)); } #[test] @@ -2481,7 +2480,7 @@ fn sync_eth1_chain_disable_deposit_contract_sync_flag() { dir.path().join("jwt-file").as_os_str().to_str(), ) .run_with_zero_port() - .with_config(|config| assert_eq!(config.sync_eth1_chain, false)); + .with_config(|config| assert!(!config.sync_eth1_chain)); } #[test] @@ -2504,9 +2503,9 @@ fn light_client_server_default() { CommandLineTest::new() .run_with_zero_port() .with_config(|config| { - assert_eq!(config.network.enable_light_client_server, false); - assert_eq!(config.chain.enable_light_client_server, false); - assert_eq!(config.http_api.enable_light_client_server, false); + assert!(!config.network.enable_light_client_server); + assert!(!config.chain.enable_light_client_server); + assert!(!config.http_api.enable_light_client_server); }); } @@ -2516,8 +2515,8 @@ fn light_client_server_enabled() { .flag("light-client-server", None) .run_with_zero_port() .with_config(|config| { - assert_eq!(config.network.enable_light_client_server, true); - assert_eq!(config.chain.enable_light_client_server, true); + assert!(config.network.enable_light_client_server); + assert!(config.chain.enable_light_client_server); }); } @@ -2528,7 +2527,7 @@ fn light_client_http_server_enabled() { .flag("light-client-server", None) .run_with_zero_port() .with_config(|config| { - assert_eq!(config.http_api.enable_light_client_server, true); + assert!(config.http_api.enable_light_client_server); }); } diff --git a/lighthouse/tests/boot_node.rs b/lighthouse/tests/boot_node.rs index 659dea468d..b243cd6001 100644 --- a/lighthouse/tests/boot_node.rs +++ b/lighthouse/tests/boot_node.rs @@ -149,7 +149,7 @@ fn disable_packet_filter_flag() { .flag("disable-packet-filter", None) .run_with_ip() .with_config(|config| { - assert_eq!(config.disable_packet_filter, true); + assert!(config.disable_packet_filter); }); } @@ -159,7 +159,7 @@ fn enable_enr_auto_update_flag() { .flag("enable-enr-auto-update", None) .run_with_ip() .with_config(|config| { - assert_eq!(config.enable_enr_auto_update, true); + assert!(config.enable_enr_auto_update); }); } diff --git a/lighthouse/tests/validator_client.rs b/lighthouse/tests/validator_client.rs index 587001f77b..c5b303e4d1 100644 --- a/lighthouse/tests/validator_client.rs +++ b/lighthouse/tests/validator_client.rs @@ -136,7 +136,7 @@ fn beacon_nodes_tls_certs_flag() { .flag( "beacon-nodes-tls-certs", Some( - vec![ + [ dir.path().join("certificate.crt").to_str().unwrap(), dir.path().join("certificate2.crt").to_str().unwrap(), ] @@ -205,7 +205,7 @@ fn graffiti_file_with_pk_flag() { let mut file = File::create(dir.path().join("graffiti.txt")).expect("Unable to create file"); let new_key = Keypair::random(); let pubkeybytes = PublicKeyBytes::from(new_key.pk); - let contents = format!("{}:nice-graffiti", pubkeybytes.to_string()); + let contents = format!("{}:nice-graffiti", pubkeybytes); file.write_all(contents.as_bytes()) .expect("Unable to write to file"); CommandLineTest::new() @@ -419,13 +419,13 @@ pub fn malloc_tuning_flag() { CommandLineTest::new() .flag("disable-malloc-tuning", None) .run() - .with_config(|config| assert_eq!(config.http_metrics.allocator_metrics_enabled, false)); + .with_config(|config| assert!(!config.http_metrics.allocator_metrics_enabled)); } #[test] pub fn malloc_tuning_default() { CommandLineTest::new() .run() - .with_config(|config| assert_eq!(config.http_metrics.allocator_metrics_enabled, true)); + .with_config(|config| assert!(config.http_metrics.allocator_metrics_enabled)); } #[test] fn doppelganger_protection_flag() { diff --git a/lighthouse/tests/validator_manager.rs b/lighthouse/tests/validator_manager.rs index 999f3c3141..04e3eafe6e 100644 --- a/lighthouse/tests/validator_manager.rs +++ b/lighthouse/tests/validator_manager.rs @@ -136,7 +136,7 @@ pub fn validator_create_defaults() { count: 1, deposit_gwei: MainnetEthSpec::default_spec().max_effective_balance, mnemonic_path: None, - stdin_inputs: cfg!(windows) || false, + stdin_inputs: cfg!(windows), disable_deposits: false, specify_voting_keystore_password: false, eth1_withdrawal_address: None, @@ -201,7 +201,7 @@ pub fn validator_create_disable_deposits() { .flag("--disable-deposits", None) .flag("--builder-proposals", Some("false")) .assert_success(|config| { - assert_eq!(config.disable_deposits, true); + assert!(config.disable_deposits); assert_eq!(config.builder_proposals, Some(false)); }); } @@ -300,7 +300,7 @@ pub fn validator_move_defaults() { fee_recipient: None, gas_limit: None, password_source: PasswordSource::Interactive { - stdin_inputs: cfg!(windows) || false, + stdin_inputs: cfg!(windows), }, }; assert_eq!(expected, config); @@ -350,7 +350,7 @@ pub fn validator_move_misc_flags_1() { .flag("--src-vc-token", Some("./1.json")) .flag("--dest-vc-url", Some("http://localhost:2")) .flag("--dest-vc-token", Some("./2.json")) - .flag("--validators", Some(&format!("{}", EXAMPLE_PUBKEY_0))) + .flag("--validators", Some(EXAMPLE_PUBKEY_0)) .flag("--builder-proposals", Some("false")) .flag("--prefer-builder-proposals", Some("false")) .assert_success(|config| { @@ -368,7 +368,7 @@ pub fn validator_move_misc_flags_1() { fee_recipient: None, gas_limit: None, password_source: PasswordSource::Interactive { - stdin_inputs: cfg!(windows) || false, + stdin_inputs: cfg!(windows), }, }; assert_eq!(expected, config); @@ -382,7 +382,7 @@ pub fn validator_move_misc_flags_2() { .flag("--src-vc-token", Some("./1.json")) .flag("--dest-vc-url", Some("http://localhost:2")) .flag("--dest-vc-token", Some("./2.json")) - .flag("--validators", Some(&format!("{}", EXAMPLE_PUBKEY_0))) + .flag("--validators", Some(EXAMPLE_PUBKEY_0)) .flag("--builder-proposals", Some("false")) .flag("--builder-boost-factor", Some("100")) .assert_success(|config| { @@ -400,7 +400,7 @@ pub fn validator_move_misc_flags_2() { fee_recipient: None, gas_limit: None, password_source: PasswordSource::Interactive { - stdin_inputs: cfg!(windows) || false, + stdin_inputs: cfg!(windows), }, }; assert_eq!(expected, config); @@ -428,7 +428,7 @@ pub fn validator_move_count() { fee_recipient: None, gas_limit: None, password_source: PasswordSource::Interactive { - stdin_inputs: cfg!(windows) || false, + stdin_inputs: cfg!(windows), }, }; assert_eq!(expected, config); diff --git a/testing/web3signer_tests/src/lib.rs b/testing/web3signer_tests/src/lib.rs index bebc8fa13b..e0dee9ceb4 100644 --- a/testing/web3signer_tests/src/lib.rs +++ b/testing/web3signer_tests/src/lib.rs @@ -173,6 +173,8 @@ mod tests { } impl Web3SignerRig { + // We need to hold that lock as we want to get the binary only once + #[allow(clippy::await_holding_lock)] pub async fn new(network: &str, listen_address: &str, listen_port: u16) -> Self { GET_WEB3SIGNER_BIN .get_or_init(|| async { @@ -210,7 +212,7 @@ mod tests { keystore_password_file: keystore_password_filename.to_string(), }; let key_config_file = - File::create(&keystore_dir.path().join("key-config.yaml")).unwrap(); + File::create(keystore_dir.path().join("key-config.yaml")).unwrap(); serde_yaml::to_writer(key_config_file, &key_config).unwrap(); let tls_keystore_file = tls_dir().join("web3signer").join("key.p12"); diff --git a/validator_client/http_api/src/tests.rs b/validator_client/http_api/src/tests.rs index 027b10e246..7ea3d7ebaa 100644 --- a/validator_client/http_api/src/tests.rs +++ b/validator_client/http_api/src/tests.rs @@ -53,8 +53,10 @@ struct ApiTester { impl ApiTester { pub async fn new() -> Self { - let mut config = ValidatorStoreConfig::default(); - config.fee_recipient = Some(TEST_DEFAULT_FEE_RECIPIENT); + let config = ValidatorStoreConfig { + fee_recipient: Some(TEST_DEFAULT_FEE_RECIPIENT), + ..Default::default() + }; Self::new_with_config(config).await } @@ -139,7 +141,7 @@ impl ApiTester { let (listening_socket, server) = super::serve(ctx, test_runtime.task_executor.exit()).unwrap(); - tokio::spawn(async { server.await }); + tokio::spawn(server); let url = SensitiveUrl::parse(&format!( "http://{}:{}", @@ -345,22 +347,21 @@ impl ApiTester { .set_nextaccount(s.key_derivation_path_offset) .unwrap(); - for i in 0..s.count { + for validator in response.iter().take(s.count) { let keypairs = wallet .next_validator(PASSWORD_BYTES, PASSWORD_BYTES, PASSWORD_BYTES) .unwrap(); let voting_keypair = keypairs.voting.decrypt_keypair(PASSWORD_BYTES).unwrap(); assert_eq!( - response[i].voting_pubkey, + validator.voting_pubkey, voting_keypair.pk.clone().into(), "the locally generated voting pk should match the server response" ); let withdrawal_keypair = keypairs.withdrawal.decrypt_keypair(PASSWORD_BYTES).unwrap(); - let deposit_bytes = - serde_utils::hex::decode(&response[i].eth1_deposit_tx_data).unwrap(); + let deposit_bytes = serde_utils::hex::decode(&validator.eth1_deposit_tx_data).unwrap(); let (deposit_data, _) = decode_eth1_tx_data(&deposit_bytes, E::default_spec().max_effective_balance) diff --git a/validator_client/http_api/src/tests/keystores.rs b/validator_client/http_api/src/tests/keystores.rs index 2dde087a7f..6559a2bb9e 100644 --- a/validator_client/http_api/src/tests/keystores.rs +++ b/validator_client/http_api/src/tests/keystores.rs @@ -130,7 +130,7 @@ fn check_keystore_get_response<'a>( for (ks1, ks2) in response.data.iter().zip_eq(expected_keystores) { assert_eq!(ks1.validating_pubkey, keystore_pubkey(ks2)); assert_eq!(ks1.derivation_path, ks2.path()); - assert!(ks1.readonly == None || ks1.readonly == Some(false)); + assert!(ks1.readonly.is_none() || ks1.readonly == Some(false)); } } @@ -147,7 +147,7 @@ fn check_keystore_import_response( } } -fn check_keystore_delete_response<'a>( +fn check_keystore_delete_response( response: &DeleteKeystoresResponse, expected_statuses: impl IntoIterator, ) { @@ -634,7 +634,7 @@ async fn check_get_set_fee_recipient() { assert_eq!( get_res, GetFeeRecipientResponse { - pubkey: pubkey.clone(), + pubkey: *pubkey, ethaddress: TEST_DEFAULT_FEE_RECIPIENT, } ); @@ -654,7 +654,7 @@ async fn check_get_set_fee_recipient() { .post_fee_recipient( &all_pubkeys[1], &UpdateFeeRecipientRequest { - ethaddress: fee_recipient_public_key_1.clone(), + ethaddress: fee_recipient_public_key_1, }, ) .await @@ -667,14 +667,14 @@ async fn check_get_set_fee_recipient() { .await .expect("should get fee recipient"); let expected = if i == 1 { - fee_recipient_public_key_1.clone() + fee_recipient_public_key_1 } else { TEST_DEFAULT_FEE_RECIPIENT }; assert_eq!( get_res, GetFeeRecipientResponse { - pubkey: pubkey.clone(), + pubkey: *pubkey, ethaddress: expected, } ); @@ -686,7 +686,7 @@ async fn check_get_set_fee_recipient() { .post_fee_recipient( &all_pubkeys[2], &UpdateFeeRecipientRequest { - ethaddress: fee_recipient_public_key_2.clone(), + ethaddress: fee_recipient_public_key_2, }, ) .await @@ -699,16 +699,16 @@ async fn check_get_set_fee_recipient() { .await .expect("should get fee recipient"); let expected = if i == 1 { - fee_recipient_public_key_1.clone() + fee_recipient_public_key_1 } else if i == 2 { - fee_recipient_public_key_2.clone() + fee_recipient_public_key_2 } else { TEST_DEFAULT_FEE_RECIPIENT }; assert_eq!( get_res, GetFeeRecipientResponse { - pubkey: pubkey.clone(), + pubkey: *pubkey, ethaddress: expected, } ); @@ -720,7 +720,7 @@ async fn check_get_set_fee_recipient() { .post_fee_recipient( &all_pubkeys[1], &UpdateFeeRecipientRequest { - ethaddress: fee_recipient_override.clone(), + ethaddress: fee_recipient_override, }, ) .await @@ -732,16 +732,16 @@ async fn check_get_set_fee_recipient() { .await .expect("should get fee recipient"); let expected = if i == 1 { - fee_recipient_override.clone() + fee_recipient_override } else if i == 2 { - fee_recipient_public_key_2.clone() + fee_recipient_public_key_2 } else { TEST_DEFAULT_FEE_RECIPIENT }; assert_eq!( get_res, GetFeeRecipientResponse { - pubkey: pubkey.clone(), + pubkey: *pubkey, ethaddress: expected, } ); @@ -761,14 +761,14 @@ async fn check_get_set_fee_recipient() { .await .expect("should get fee recipient"); let expected = if i == 2 { - fee_recipient_public_key_2.clone() + fee_recipient_public_key_2 } else { TEST_DEFAULT_FEE_RECIPIENT }; assert_eq!( get_res, GetFeeRecipientResponse { - pubkey: pubkey.clone(), + pubkey: *pubkey, ethaddress: expected, } ); @@ -814,7 +814,7 @@ async fn check_get_set_gas_limit() { assert_eq!( get_res, GetGasLimitResponse { - pubkey: pubkey.clone(), + pubkey: *pubkey, gas_limit: DEFAULT_GAS_LIMIT, } ); @@ -843,14 +843,14 @@ async fn check_get_set_gas_limit() { .await .expect("should get gas limit"); let expected = if i == 1 { - gas_limit_public_key_1.clone() + gas_limit_public_key_1 } else { DEFAULT_GAS_LIMIT }; assert_eq!( get_res, GetGasLimitResponse { - pubkey: pubkey.clone(), + pubkey: *pubkey, gas_limit: expected, } ); @@ -884,7 +884,7 @@ async fn check_get_set_gas_limit() { assert_eq!( get_res, GetGasLimitResponse { - pubkey: pubkey.clone(), + pubkey: *pubkey, gas_limit: expected, } ); @@ -917,7 +917,7 @@ async fn check_get_set_gas_limit() { assert_eq!( get_res, GetGasLimitResponse { - pubkey: pubkey.clone(), + pubkey: *pubkey, gas_limit: expected, } ); @@ -944,7 +944,7 @@ async fn check_get_set_gas_limit() { assert_eq!( get_res, GetGasLimitResponse { - pubkey: pubkey.clone(), + pubkey: *pubkey, gas_limit: expected, } ); @@ -1305,7 +1305,7 @@ async fn delete_concurrent_with_signing() { let handle = handle.spawn(async move { for j in 0..num_attestations { let mut att = make_attestation(j, j + 1); - for (_validator_id, public_key) in thread_pubkeys.iter().enumerate() { + for public_key in thread_pubkeys.iter() { let _ = validator_store .sign_attestation(*public_key, 0, &mut att, Epoch::new(j + 1)) .await; @@ -2084,7 +2084,7 @@ async fn import_remotekey_web3signer_disabled() { web3signer_req.enable = false; // Import web3signers. - let _ = tester + tester .client .post_lighthouse_validators_web3signer(&vec![web3signer_req]) .await @@ -2148,8 +2148,11 @@ async fn import_remotekey_web3signer_enabled() { // 1 validator imported. assert_eq!(tester.vals_total(), 1); assert_eq!(tester.vals_enabled(), 1); - let vals = tester.initialized_validators.read(); - let web3_vals = vals.validator_definitions(); + let web3_vals = tester + .initialized_validators + .read() + .validator_definitions() + .to_vec(); // Import remotekeys. let import_res = tester @@ -2166,11 +2169,13 @@ async fn import_remotekey_web3signer_enabled() { assert_eq!(tester.vals_total(), 1); assert_eq!(tester.vals_enabled(), 1); - let vals = tester.initialized_validators.read(); - let remote_vals = vals.validator_definitions(); + { + let vals = tester.initialized_validators.read(); + let remote_vals = vals.validator_definitions(); - // Web3signer should not be overwritten since it is enabled. - assert!(web3_vals == remote_vals); + // Web3signer should not be overwritten since it is enabled. + assert!(web3_vals == remote_vals); + } // Remotekey should not be imported. let expected_responses = vec![SingleListRemotekeysResponse { diff --git a/validator_manager/src/import_validators.rs b/validator_manager/src/import_validators.rs index 2e8821f0db..3cebc10bb3 100644 --- a/validator_manager/src/import_validators.rs +++ b/validator_manager/src/import_validators.rs @@ -520,7 +520,7 @@ pub mod tests { let local_validators: Vec = { let contents = - fs::read_to_string(&self.import_config.validators_file_path.unwrap()) + fs::read_to_string(self.import_config.validators_file_path.unwrap()) .unwrap(); serde_json::from_str(&contents).unwrap() }; @@ -557,7 +557,7 @@ pub mod tests { self.vc.ensure_key_cache_consistency().await; let local_keystore: Keystore = - Keystore::from_json_file(&self.import_config.keystore_file_path.unwrap()) + Keystore::from_json_file(self.import_config.keystore_file_path.unwrap()) .unwrap(); let list_keystores_response = self.vc.client.get_keystores().await.unwrap().data; diff --git a/validator_manager/src/move_validators.rs b/validator_manager/src/move_validators.rs index c039728e6f..4d0820f5a8 100644 --- a/validator_manager/src/move_validators.rs +++ b/validator_manager/src/move_validators.rs @@ -978,13 +978,13 @@ mod test { }) .unwrap(); // Set all definitions to use the same password path as the primary. - definitions.iter_mut().enumerate().for_each(|(_, def)| { - match &mut def.signing_definition { - SigningDefinition::LocalKeystore { - voting_keystore_password_path: Some(path), - .. - } => *path = primary_path.clone(), - _ => (), + definitions.iter_mut().for_each(|def| { + if let SigningDefinition::LocalKeystore { + voting_keystore_password_path: Some(path), + .. + } = &mut def.signing_definition + { + *path = primary_path.clone() } }) } From d74b2d96f58b97d807eff30e4fb1c0b964e7e6dd Mon Sep 17 00:00:00 2001 From: Pawan Dhananjay Date: Tue, 17 Dec 2024 07:44:24 +0530 Subject: [PATCH 049/254] Electra alpha8 spec updates (#6496) * Fix partial withdrawals count * Remove get_active_balance * Remove queue_entire_balance_and_reset_validator * Switch to compounding when consolidating with source==target * Queue deposit requests and apply them during epoch processing * Fix ef tests * Clear todos * Fix engine api formatting issues * Merge branch 'unstable' into electra-alpha7 * Make add_validator_to_registry more in line with the spec * Address some review comments * Cleanup * Update initialize_beacon_state_from_eth1 * Merge branch 'unstable' into electra-alpha7 * Fix rpc decoding for blobs by range/root * Fix block hash computation * Fix process_deposits bug * Merge branch 'unstable' into electra-alpha7 * Fix topup deposit processing bug * Update builder api for electra * Refactor mock builder to separate functionality * Merge branch 'unstable' into electra-alpha7 * Address review comments * Use copied for reference rather than cloned * Optimise and simplify PendingDepositsContext::new * Merge remote-tracking branch 'origin/unstable' into electra-alpha7 * Fix processing of deposits with invalid signatures * Remove redundant code in genesis init * Revert "Refactor mock builder to separate functionality" This reverts commit 6d10456912b3c39b8a8c9089db76e8ead20608a0. * Revert "Update builder api for electra" This reverts commit c5c9aca6db201c09c995756a11e9cb6a03b2ea99. * Simplify pre-activation sorting * Fix stale validators used in upgrade_to_electra * Merge branch 'unstable' into electra-alpha7 --- beacon_node/execution_layer/src/block_hash.rs | 44 +-- .../execution_layer/src/engine_api/http.rs | 2 +- .../src/engine_api/json_structures.rs | 30 +- .../src/engine_api/new_payload_request.rs | 11 +- .../src/test_utils/handle_rpc.rs | 2 +- beacon_node/store/src/partial_beacon_state.rs | 4 +- .../src/per_block_processing.rs | 4 +- .../process_operations.rs | 151 +++++++--- .../src/per_epoch_processing/errors.rs | 1 + .../src/per_epoch_processing/single_pass.rs | 262 +++++++++++++----- .../state_processing/src/upgrade/electra.rs | 47 +++- consensus/types/src/beacon_state.rs | 123 +++----- consensus/types/src/beacon_state/tests.rs | 37 --- consensus/types/src/deposit_request.rs | 8 +- consensus/types/src/eth_spec.rs | 23 +- consensus/types/src/execution_block_header.rs | 9 + consensus/types/src/execution_requests.rs | 40 ++- consensus/types/src/lib.rs | 6 +- ..._balance_deposit.rs => pending_deposit.rs} | 12 +- consensus/types/src/preset.rs | 2 +- consensus/types/src/validator.rs | 21 +- testing/ef_tests/Makefile | 2 +- .../ef_tests/src/cases/epoch_processing.rs | 6 +- testing/ef_tests/src/type_name.rs | 2 +- testing/ef_tests/tests/tests.rs | 7 +- 25 files changed, 519 insertions(+), 337 deletions(-) rename consensus/types/src/{pending_balance_deposit.rs => pending_deposit.rs} (68%) diff --git a/beacon_node/execution_layer/src/block_hash.rs b/beacon_node/execution_layer/src/block_hash.rs index cdc172cff4..d3a32c7929 100644 --- a/beacon_node/execution_layer/src/block_hash.rs +++ b/beacon_node/execution_layer/src/block_hash.rs @@ -7,7 +7,7 @@ use keccak_hash::KECCAK_EMPTY_LIST_RLP; use triehash::ordered_trie_root; use types::{ EncodableExecutionBlockHeader, EthSpec, ExecutionBlockHash, ExecutionBlockHeader, - ExecutionPayloadRef, Hash256, + ExecutionPayloadRef, ExecutionRequests, Hash256, }; /// Calculate the block hash of an execution block. @@ -17,6 +17,7 @@ use types::{ pub fn calculate_execution_block_hash( payload: ExecutionPayloadRef, parent_beacon_block_root: Option, + execution_requests: Option<&ExecutionRequests>, ) -> (ExecutionBlockHash, Hash256) { // Calculate the transactions root. // We're currently using a deprecated Parity library for this. We should move to a @@ -38,6 +39,7 @@ pub fn calculate_execution_block_hash( let rlp_blob_gas_used = payload.blob_gas_used().ok(); let rlp_excess_blob_gas = payload.excess_blob_gas().ok(); + let requests_root = execution_requests.map(|requests| requests.requests_hash()); // Construct the block header. let exec_block_header = ExecutionBlockHeader::from_payload( @@ -48,6 +50,7 @@ pub fn calculate_execution_block_hash( rlp_blob_gas_used, rlp_excess_blob_gas, parent_beacon_block_root, + requests_root, ); // Hash the RLP encoding of the block header. @@ -118,6 +121,7 @@ mod test { blob_gas_used: None, excess_blob_gas: None, parent_beacon_block_root: None, + requests_root: None, }; let expected_rlp = "f90200a0e0a94a7a3c9617401586b1a27025d2d9671332d22d540e0af72b069170380f2aa01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794ba5e000000000000000000000000000000000000a0ec3c94b18b8a1cff7d60f8d258ec723312932928626b4c9355eb4ab3568ec7f7a050f738580ed699f0469702c7ccc63ed2e51bc034be9479b7bff4e68dee84accfa029b0562f7140574dd0d50dee8a271b22e1a0a7b78fca58f7c60370d8317ba2a9b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000830200000188016345785d8a00008301553482079e42a0000000000000000000000000000000000000000000000000000000000000000088000000000000000082036b"; let expected_hash = @@ -149,6 +153,7 @@ mod test { blob_gas_used: None, excess_blob_gas: None, parent_beacon_block_root: None, + requests_root: None, }; let expected_rlp = "f901fda0927ca537f06c783a3a2635b8805eef1c8c2124f7444ad4a3389898dd832f2dbea01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794ba5e000000000000000000000000000000000000a0e97859b065bd8dbbb4519c7cb935024de2484c2b7f881181b4360492f0b06b82a050f738580ed699f0469702c7ccc63ed2e51bc034be9479b7bff4e68dee84accfa029b0562f7140574dd0d50dee8a271b22e1a0a7b78fca58f7c60370d8317ba2a9b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800188016345785d8a00008301553482079e42a0000000000000000000000000000000000000000000000000000000000002000088000000000000000082036b"; let expected_hash = @@ -181,6 +186,7 @@ mod test { blob_gas_used: None, excess_blob_gas: None, parent_beacon_block_root: None, + requests_root: None, }; let expected_hash = Hash256::from_str("6da69709cd5a34079b6604d29cd78fc01dacd7c6268980057ad92a2bede87351") @@ -211,6 +217,7 @@ mod test { blob_gas_used: Some(0x0u64), excess_blob_gas: Some(0x0u64), parent_beacon_block_root: Some(Hash256::from_str("f7d327d2c04e4f12e9cdd492e53d39a1d390f8b1571e3b2a22ac6e1e170e5b1a").unwrap()), + requests_root: None, }; let expected_hash = Hash256::from_str("a7448e600ead0a23d16f96aa46e8dea9eef8a7c5669a5f0a5ff32709afe9c408") @@ -221,29 +228,30 @@ mod test { #[test] fn test_rlp_encode_block_electra() { let header = ExecutionBlockHeader { - parent_hash: Hash256::from_str("172864416698b842f4c92f7b476be294b4ef720202779df194cd225f531053ab").unwrap(), + parent_hash: Hash256::from_str("a628f146df398a339768bd101f7dc41d828be79aca5dd02cc878a51bdbadd761").unwrap(), ommers_hash: Hash256::from_str("1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347").unwrap(), - beneficiary: Address::from_str("878705ba3f8bc32fcf7f4caa1a35e72af65cf766").unwrap(), - state_root: Hash256::from_str("c6457d0df85c84c62d1c68f68138b6e796e8a44fb44de221386fb2d5611c41e0").unwrap(), - transactions_root: Hash256::from_str("56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421").unwrap(), - receipts_root: Hash256::from_str("56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421").unwrap(), - logs_bloom:<[u8; 256]>::from_hex("00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000").unwrap().into(), + beneficiary: Address::from_str("f97e180c050e5ab072211ad2c213eb5aee4df134").unwrap(), + state_root: Hash256::from_str("fdff009f8280bd113ebb4df8ce4e2dcc9322d43184a0b506e70b7f4823ca1253").unwrap(), + transactions_root: Hash256::from_str("452806578b4fa881cafb019c47e767e37e2249accf859159f00cddefb2579bb5").unwrap(), + receipts_root: Hash256::from_str("72ceac0f16a32041c881b3220d39ca506a286bef163c01a4d0821cd4027d31c7").unwrap(), + logs_bloom:<[u8; 256]>::from_hex("10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000").unwrap().into(), difficulty: Uint256::ZERO, - number: Uint256::from(97), - gas_limit: Uint256::from(27482534), - gas_used: Uint256::ZERO, - timestamp: 1692132829u64, - extra_data: hex::decode("d883010d00846765746888676f312e32302e37856c696e7578").unwrap(), - mix_hash: Hash256::from_str("0b493c22d2ad4ca76c77ae6ad916af429b42b1dc98fdcb8e5ddbd049bbc5d623").unwrap(), + number: Uint256::from(8230), + gas_limit: Uint256::from(30000000), + gas_used: Uint256::from(3716848), + timestamp: 1730921268, + extra_data: hex::decode("d883010e0c846765746888676f312e32332e32856c696e7578").unwrap(), + mix_hash: Hash256::from_str("e87ca9a45b2e61bbe9080d897db1d584b5d2367d22e898af901091883b7b96ec").unwrap(), nonce: Hash64::ZERO, - base_fee_per_gas: Uint256::from(2374u64), + base_fee_per_gas: Uint256::from(7u64), withdrawals_root: Some(Hash256::from_str("56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421").unwrap()), - blob_gas_used: Some(0x0u64), - excess_blob_gas: Some(0x0u64), - parent_beacon_block_root: Some(Hash256::from_str("f7d327d2c04e4f12e9cdd492e53d39a1d390f8b1571e3b2a22ac6e1e170e5b1a").unwrap()), + blob_gas_used: Some(786432), + excess_blob_gas: Some(44695552), + parent_beacon_block_root: Some(Hash256::from_str("f3a888fee010ebb1ae083547004e96c254b240437823326fdff8354b1fc25629").unwrap()), + requests_root: Some(Hash256::from_str("9440d3365f07573919e1e9ac5178c20ec6fe267357ee4baf8b6409901f331b62").unwrap()), }; let expected_hash = - Hash256::from_str("a7448e600ead0a23d16f96aa46e8dea9eef8a7c5669a5f0a5ff32709afe9c408") + Hash256::from_str("61e67afc96bf21be6aab52c1ace1db48de7b83f03119b0644deb4b69e87e09e1") .unwrap(); test_rlp_encoding(&header, None, expected_hash); } diff --git a/beacon_node/execution_layer/src/engine_api/http.rs b/beacon_node/execution_layer/src/engine_api/http.rs index d4734be448..33dc60d037 100644 --- a/beacon_node/execution_layer/src/engine_api/http.rs +++ b/beacon_node/execution_layer/src/engine_api/http.rs @@ -812,7 +812,7 @@ impl HttpJsonRpc { new_payload_request_electra.versioned_hashes, new_payload_request_electra.parent_beacon_block_root, new_payload_request_electra - .execution_requests_list + .execution_requests .get_execution_requests_list(), ]); diff --git a/beacon_node/execution_layer/src/engine_api/json_structures.rs b/beacon_node/execution_layer/src/engine_api/json_structures.rs index efd68f1023..1c6639804e 100644 --- a/beacon_node/execution_layer/src/engine_api/json_structures.rs +++ b/beacon_node/execution_layer/src/engine_api/json_structures.rs @@ -6,7 +6,9 @@ use strum::EnumString; use superstruct::superstruct; use types::beacon_block_body::KzgCommitments; use types::blob_sidecar::BlobsList; -use types::execution_requests::{ConsolidationRequests, DepositRequests, WithdrawalRequests}; +use types::execution_requests::{ + ConsolidationRequests, DepositRequests, RequestPrefix, WithdrawalRequests, +}; use types::{Blob, FixedVector, KzgProof, Unsigned}; #[derive(Debug, PartialEq, Serialize, Deserialize)] @@ -339,25 +341,6 @@ impl From> for ExecutionPayload { } } -/// This is used to index into the `execution_requests` array. -#[derive(Debug, Copy, Clone)] -enum RequestPrefix { - Deposit, - Withdrawal, - Consolidation, -} - -impl RequestPrefix { - pub fn from_prefix(prefix: u8) -> Option { - match prefix { - 0 => Some(Self::Deposit), - 1 => Some(Self::Withdrawal), - 2 => Some(Self::Consolidation), - _ => None, - } - } -} - /// Format of `ExecutionRequests` received over the engine api. /// /// Array of ssz-encoded requests list encoded as hex bytes. @@ -379,7 +362,8 @@ impl TryFrom for ExecutionRequests { for (i, request) in value.0.into_iter().enumerate() { // hex string - let decoded_bytes = hex::decode(request).map_err(|e| format!("Invalid hex {:?}", e))?; + let decoded_bytes = hex::decode(request.strip_prefix("0x").unwrap_or(&request)) + .map_err(|e| format!("Invalid hex {:?}", e))?; match RequestPrefix::from_prefix(i as u8) { Some(RequestPrefix::Deposit) => { requests.deposits = DepositRequests::::from_ssz_bytes(&decoded_bytes) @@ -431,7 +415,7 @@ pub struct JsonGetPayloadResponse { #[superstruct(only(V3, V4))] pub should_override_builder: bool, #[superstruct(only(V4))] - pub requests: JsonExecutionRequests, + pub execution_requests: JsonExecutionRequests, } impl TryFrom> for GetPayloadResponse { @@ -464,7 +448,7 @@ impl TryFrom> for GetPayloadResponse { block_value: response.block_value, blobs_bundle: response.blobs_bundle.into(), should_override_builder: response.should_override_builder, - requests: response.requests.try_into()?, + requests: response.execution_requests.try_into()?, })) } } diff --git a/beacon_node/execution_layer/src/engine_api/new_payload_request.rs b/beacon_node/execution_layer/src/engine_api/new_payload_request.rs index 318779b7f3..60bc848974 100644 --- a/beacon_node/execution_layer/src/engine_api/new_payload_request.rs +++ b/beacon_node/execution_layer/src/engine_api/new_payload_request.rs @@ -44,7 +44,7 @@ pub struct NewPayloadRequest<'block, E: EthSpec> { #[superstruct(only(Deneb, Electra))] pub parent_beacon_block_root: Hash256, #[superstruct(only(Electra))] - pub execution_requests_list: &'block ExecutionRequests, + pub execution_requests: &'block ExecutionRequests, } impl<'block, E: EthSpec> NewPayloadRequest<'block, E> { @@ -121,8 +121,11 @@ impl<'block, E: EthSpec> NewPayloadRequest<'block, E> { let _timer = metrics::start_timer(&metrics::EXECUTION_LAYER_VERIFY_BLOCK_HASH); - let (header_hash, rlp_transactions_root) = - calculate_execution_block_hash(payload, parent_beacon_block_root); + let (header_hash, rlp_transactions_root) = calculate_execution_block_hash( + payload, + parent_beacon_block_root, + self.execution_requests().ok().copied(), + ); if header_hash != self.block_hash() { return Err(Error::BlockHashMismatch { @@ -185,7 +188,7 @@ impl<'a, E: EthSpec> TryFrom> for NewPayloadRequest<'a, E> .map(kzg_commitment_to_versioned_hash) .collect(), parent_beacon_block_root: block_ref.parent_root, - execution_requests_list: &block_ref.body.execution_requests, + execution_requests: &block_ref.body.execution_requests, })), } } diff --git a/beacon_node/execution_layer/src/test_utils/handle_rpc.rs b/beacon_node/execution_layer/src/test_utils/handle_rpc.rs index 786ac9ad9c..9365024ffb 100644 --- a/beacon_node/execution_layer/src/test_utils/handle_rpc.rs +++ b/beacon_node/execution_layer/src/test_utils/handle_rpc.rs @@ -374,7 +374,7 @@ pub async fn handle_rpc( .into(), should_override_builder: false, // TODO(electra): add EL requests in mock el - requests: Default::default(), + execution_requests: Default::default(), }) .unwrap() } diff --git a/beacon_node/store/src/partial_beacon_state.rs b/beacon_node/store/src/partial_beacon_state.rs index 2eb40f47b1..22eecdcc60 100644 --- a/beacon_node/store/src/partial_beacon_state.rs +++ b/beacon_node/store/src/partial_beacon_state.rs @@ -136,7 +136,7 @@ where pub earliest_consolidation_epoch: Epoch, #[superstruct(only(Electra))] - pub pending_balance_deposits: List, + pub pending_deposits: List, #[superstruct(only(Electra))] pub pending_partial_withdrawals: List, @@ -403,7 +403,7 @@ impl TryInto> for PartialBeaconState { earliest_exit_epoch, consolidation_balance_to_consume, earliest_consolidation_epoch, - pending_balance_deposits, + pending_deposits, pending_partial_withdrawals, pending_consolidations ], diff --git a/consensus/state_processing/src/per_block_processing.rs b/consensus/state_processing/src/per_block_processing.rs index f289b6e081..436f4934b9 100644 --- a/consensus/state_processing/src/per_block_processing.rs +++ b/consensus/state_processing/src/per_block_processing.rs @@ -514,6 +514,7 @@ pub fn get_expected_withdrawals( // Consume pending partial withdrawals let partial_withdrawals_count = if let Ok(partial_withdrawals) = state.pending_partial_withdrawals() { + let mut partial_withdrawals_count = 0; for withdrawal in partial_withdrawals { if withdrawal.withdrawable_epoch > epoch || withdrawals.len() == spec.max_pending_partials_per_withdrawals_sweep as usize @@ -546,8 +547,9 @@ pub fn get_expected_withdrawals( }); withdrawal_index.safe_add_assign(1)?; } + partial_withdrawals_count.safe_add_assign(1)?; } - Some(withdrawals.len()) + Some(partial_withdrawals_count) } else { None }; diff --git a/consensus/state_processing/src/per_block_processing/process_operations.rs b/consensus/state_processing/src/per_block_processing/process_operations.rs index a53dc15126..22d8592364 100644 --- a/consensus/state_processing/src/per_block_processing/process_operations.rs +++ b/consensus/state_processing/src/per_block_processing/process_operations.rs @@ -7,7 +7,6 @@ use crate::per_block_processing::errors::{BlockProcessingError, IntoWithIndex}; use crate::VerifySignatures; use types::consts::altair::{PARTICIPATION_FLAG_WEIGHTS, PROPOSER_WEIGHT, WEIGHT_DENOMINATOR}; use types::typenum::U33; -use types::validator::is_compounding_withdrawal_credential; pub fn process_operations>( state: &mut BeaconState, @@ -378,7 +377,7 @@ pub fn process_deposits( if state.eth1_deposit_index() < eth1_deposit_index_limit { let expected_deposit_len = std::cmp::min( E::MaxDeposits::to_u64(), - state.get_outstanding_deposit_len()?, + eth1_deposit_index_limit.safe_sub(state.eth1_deposit_index())?, ); block_verify!( deposits.len() as u64 == expected_deposit_len, @@ -450,39 +449,46 @@ pub fn apply_deposit( if let Some(index) = validator_index { // [Modified in Electra:EIP7251] - if let Ok(pending_balance_deposits) = state.pending_balance_deposits_mut() { - pending_balance_deposits.push(PendingBalanceDeposit { index, amount })?; - - let validator = state - .validators() - .get(index as usize) - .ok_or(BeaconStateError::UnknownValidator(index as usize))?; - - if is_compounding_withdrawal_credential(deposit_data.withdrawal_credentials, spec) - && validator.has_eth1_withdrawal_credential(spec) - && is_valid_deposit_signature(&deposit_data, spec).is_ok() - { - state.switch_to_compounding_validator(index as usize, spec)?; - } + if let Ok(pending_deposits) = state.pending_deposits_mut() { + pending_deposits.push(PendingDeposit { + pubkey: deposit_data.pubkey, + withdrawal_credentials: deposit_data.withdrawal_credentials, + amount, + signature: deposit_data.signature, + slot: spec.genesis_slot, // Use `genesis_slot` to distinguish from a pending deposit request + })?; } else { // Update the existing validator balance. increase_balance(state, index as usize, amount)?; } - } else { + } + // New validator + else { // The signature should be checked for new validators. Return early for a bad // signature. if is_valid_deposit_signature(&deposit_data, spec).is_err() { return Ok(()); } - state.add_validator_to_registry(&deposit_data, spec)?; - let new_validator_index = state.validators().len().safe_sub(1)? as u64; + state.add_validator_to_registry( + deposit_data.pubkey, + deposit_data.withdrawal_credentials, + if state.fork_name_unchecked() >= ForkName::Electra { + 0 + } else { + amount + }, + spec, + )?; // [New in Electra:EIP7251] - if let Ok(pending_balance_deposits) = state.pending_balance_deposits_mut() { - pending_balance_deposits.push(PendingBalanceDeposit { - index: new_validator_index, + if let Ok(pending_deposits) = state.pending_deposits_mut() { + pending_deposits.push(PendingDeposit { + pubkey: deposit_data.pubkey, + withdrawal_credentials: deposit_data.withdrawal_credentials, amount, + signature: deposit_data.signature, + slot: spec.genesis_slot, // Use `genesis_slot` to distinguish from a pending deposit request })?; } } @@ -596,13 +602,18 @@ pub fn process_deposit_requests( if state.deposit_requests_start_index()? == spec.unset_deposit_requests_start_index { *state.deposit_requests_start_index_mut()? = request.index } - let deposit_data = DepositData { - pubkey: request.pubkey, - withdrawal_credentials: request.withdrawal_credentials, - amount: request.amount, - signature: request.signature.clone().into(), - }; - apply_deposit(state, deposit_data, None, false, spec)? + let slot = state.slot(); + + // [New in Electra:EIP7251] + if let Ok(pending_deposits) = state.pending_deposits_mut() { + pending_deposits.push(PendingDeposit { + pubkey: request.pubkey, + withdrawal_credentials: request.withdrawal_credentials, + amount: request.amount, + signature: request.signature.clone(), + slot, + })?; + } } Ok(()) @@ -621,11 +632,84 @@ pub fn process_consolidation_requests( Ok(()) } +fn is_valid_switch_to_compounding_request( + state: &BeaconState, + consolidation_request: &ConsolidationRequest, + spec: &ChainSpec, +) -> Result { + // Switch to compounding requires source and target be equal + if consolidation_request.source_pubkey != consolidation_request.target_pubkey { + return Ok(false); + } + + // Verify pubkey exists + let Some(source_index) = state + .pubkey_cache() + .get(&consolidation_request.source_pubkey) + else { + // source validator doesn't exist + return Ok(false); + }; + + let source_validator = state.get_validator(source_index)?; + // Verify the source withdrawal credentials + // Note: We need to specifically check for eth1 withdrawal credentials here + // If the validator is already compounding, the compounding request is not valid. + if let Some(withdrawal_address) = source_validator + .has_eth1_withdrawal_credential(spec) + .then(|| { + source_validator + .withdrawal_credentials + .as_slice() + .get(12..) + .map(Address::from_slice) + }) + .flatten() + { + if withdrawal_address != consolidation_request.source_address { + return Ok(false); + } + } else { + // Source doesn't have eth1 withdrawal credentials + return Ok(false); + } + + // Verify the source is active + let current_epoch = state.current_epoch(); + if !source_validator.is_active_at(current_epoch) { + return Ok(false); + } + // Verify exits for source has not been initiated + if source_validator.exit_epoch != spec.far_future_epoch { + return Ok(false); + } + + Ok(true) +} + pub fn process_consolidation_request( state: &mut BeaconState, consolidation_request: &ConsolidationRequest, spec: &ChainSpec, ) -> Result<(), BlockProcessingError> { + if is_valid_switch_to_compounding_request(state, consolidation_request, spec)? { + let Some(source_index) = state + .pubkey_cache() + .get(&consolidation_request.source_pubkey) + else { + // source validator doesn't exist. This is unreachable as `is_valid_switch_to_compounding_request` + // will return false in that case. + return Ok(()); + }; + state.switch_to_compounding_validator(source_index, spec)?; + return Ok(()); + } + + // Verify that source != target, so a consolidation cannot be used as an exit. + if consolidation_request.source_pubkey == consolidation_request.target_pubkey { + return Ok(()); + } + // If the pending consolidations queue is full, consolidation requests are ignored if state.pending_consolidations()?.len() == E::PendingConsolidationsLimit::to_usize() { return Ok(()); @@ -649,10 +733,6 @@ pub fn process_consolidation_request( // target validator doesn't exist return Ok(()); }; - // Verify that source != target, so a consolidation cannot be used as an exit. - if source_index == target_index { - return Ok(()); - } let source_validator = state.get_validator(source_index)?; // Verify the source withdrawal credentials @@ -699,5 +779,10 @@ pub fn process_consolidation_request( target_index: target_index as u64, })?; + let target_validator = state.get_validator(target_index)?; + // Churn any target excess active balance of target and raise its max + if target_validator.has_eth1_withdrawal_credential(spec) { + state.switch_to_compounding_validator(target_index, spec)?; + } Ok(()) } diff --git a/consensus/state_processing/src/per_epoch_processing/errors.rs b/consensus/state_processing/src/per_epoch_processing/errors.rs index b6c9dbea52..f45c55a7ac 100644 --- a/consensus/state_processing/src/per_epoch_processing/errors.rs +++ b/consensus/state_processing/src/per_epoch_processing/errors.rs @@ -28,6 +28,7 @@ pub enum EpochProcessingError { SinglePassMissingActivationQueue, MissingEarliestExitEpoch, MissingExitBalanceToConsume, + PendingDepositsLogicError, } impl From for EpochProcessingError { diff --git a/consensus/state_processing/src/per_epoch_processing/single_pass.rs b/consensus/state_processing/src/per_epoch_processing/single_pass.rs index fcb480a37c..904e68e368 100644 --- a/consensus/state_processing/src/per_epoch_processing/single_pass.rs +++ b/consensus/state_processing/src/per_epoch_processing/single_pass.rs @@ -4,6 +4,7 @@ use crate::{ update_progressive_balances_cache::initialize_progressive_balances_cache, }, epoch_cache::{initialize_epoch_cache, PreEpochCache}, + per_block_processing::is_valid_deposit_signature, per_epoch_processing::{Delta, Error, ParticipationEpochSummary}, }; use itertools::izip; @@ -16,9 +17,9 @@ use types::{ TIMELY_TARGET_FLAG_INDEX, WEIGHT_DENOMINATOR, }, milhouse::Cow, - ActivationQueue, BeaconState, BeaconStateError, ChainSpec, Checkpoint, Epoch, EthSpec, - ExitCache, ForkName, List, ParticipationFlags, PendingBalanceDeposit, ProgressiveBalancesCache, - RelativeEpoch, Unsigned, Validator, + ActivationQueue, BeaconState, BeaconStateError, ChainSpec, Checkpoint, DepositData, Epoch, + EthSpec, ExitCache, ForkName, List, ParticipationFlags, PendingDeposit, + ProgressiveBalancesCache, RelativeEpoch, Unsigned, Validator, }; pub struct SinglePassConfig { @@ -26,7 +27,7 @@ pub struct SinglePassConfig { pub rewards_and_penalties: bool, pub registry_updates: bool, pub slashings: bool, - pub pending_balance_deposits: bool, + pub pending_deposits: bool, pub pending_consolidations: bool, pub effective_balance_updates: bool, } @@ -44,7 +45,7 @@ impl SinglePassConfig { rewards_and_penalties: true, registry_updates: true, slashings: true, - pending_balance_deposits: true, + pending_deposits: true, pending_consolidations: true, effective_balance_updates: true, } @@ -56,7 +57,7 @@ impl SinglePassConfig { rewards_and_penalties: false, registry_updates: false, slashings: false, - pending_balance_deposits: false, + pending_deposits: false, pending_consolidations: false, effective_balance_updates: false, } @@ -85,15 +86,17 @@ struct SlashingsContext { penalty_per_effective_balance_increment: u64, } -struct PendingBalanceDepositsContext { +struct PendingDepositsContext { /// The value to set `next_deposit_index` to *after* processing completes. next_deposit_index: usize, /// The value to set `deposit_balance_to_consume` to *after* processing completes. deposit_balance_to_consume: u64, /// Total balance increases for each validator due to pending balance deposits. validator_deposits_to_process: HashMap, - /// The deposits to append to `pending_balance_deposits` after processing all applicable deposits. - deposits_to_postpone: Vec, + /// The deposits to append to `pending_deposits` after processing all applicable deposits. + deposits_to_postpone: Vec, + /// New validators to be added to the state *after* processing completes. + new_validator_deposits: Vec, } struct EffectiveBalancesContext { @@ -138,6 +141,7 @@ pub fn process_epoch_single_pass( state.build_exit_cache(spec)?; state.build_committee_cache(RelativeEpoch::Previous, spec)?; state.build_committee_cache(RelativeEpoch::Current, spec)?; + state.update_pubkey_cache()?; let previous_epoch = state.previous_epoch(); let current_epoch = state.current_epoch(); @@ -163,12 +167,11 @@ pub fn process_epoch_single_pass( let slashings_ctxt = &SlashingsContext::new(state, state_ctxt, spec)?; let mut next_epoch_cache = PreEpochCache::new_for_next_epoch(state)?; - let pending_balance_deposits_ctxt = - if fork_name.electra_enabled() && conf.pending_balance_deposits { - Some(PendingBalanceDepositsContext::new(state, spec)?) - } else { - None - }; + let pending_deposits_ctxt = if fork_name.electra_enabled() && conf.pending_deposits { + Some(PendingDepositsContext::new(state, spec, &conf)?) + } else { + None + }; let mut earliest_exit_epoch = state.earliest_exit_epoch().ok(); let mut exit_balance_to_consume = state.exit_balance_to_consume().ok(); @@ -303,9 +306,9 @@ pub fn process_epoch_single_pass( process_single_slashing(&mut balance, &validator, slashings_ctxt, state_ctxt, spec)?; } - // `process_pending_balance_deposits` - if let Some(pending_balance_deposits_ctxt) = &pending_balance_deposits_ctxt { - process_pending_balance_deposits_for_validator( + // `process_pending_deposits` + if let Some(pending_balance_deposits_ctxt) = &pending_deposits_ctxt { + process_pending_deposits_for_validator( &mut balance, validator_info, pending_balance_deposits_ctxt, @@ -342,20 +345,84 @@ pub fn process_epoch_single_pass( // Finish processing pending balance deposits if relevant. // // This *could* be reordered after `process_pending_consolidations` which pushes only to the end - // of the `pending_balance_deposits` list. But we may as well preserve the write ordering used + // of the `pending_deposits` list. But we may as well preserve the write ordering used // by the spec and do this first. - if let Some(ctxt) = pending_balance_deposits_ctxt { - let mut new_pending_balance_deposits = List::try_from_iter( + if let Some(ctxt) = pending_deposits_ctxt { + let mut new_balance_deposits = List::try_from_iter( state - .pending_balance_deposits()? + .pending_deposits()? .iter_from(ctxt.next_deposit_index)? .cloned(), )?; for deposit in ctxt.deposits_to_postpone { - new_pending_balance_deposits.push(deposit)?; + new_balance_deposits.push(deposit)?; } - *state.pending_balance_deposits_mut()? = new_pending_balance_deposits; + *state.pending_deposits_mut()? = new_balance_deposits; *state.deposit_balance_to_consume_mut()? = ctxt.deposit_balance_to_consume; + + // `new_validator_deposits` may contain multiple deposits with the same pubkey where + // the first deposit creates the new validator and the others are topups. + // Each item in the vec is a (pubkey, validator_index) + let mut added_validators = Vec::new(); + for deposit in ctxt.new_validator_deposits { + let deposit_data = DepositData { + pubkey: deposit.pubkey, + withdrawal_credentials: deposit.withdrawal_credentials, + amount: deposit.amount, + signature: deposit.signature, + }; + // Only check the signature if this is the first deposit for the validator, + // following the logic from `apply_pending_deposit` in the spec. + if let Some(validator_index) = state.get_validator_index(&deposit_data.pubkey)? { + state + .get_balance_mut(validator_index)? + .safe_add_assign(deposit_data.amount)?; + } else if is_valid_deposit_signature(&deposit_data, spec).is_ok() { + // Apply the new deposit to the state + let validator_index = state.add_validator_to_registry( + deposit_data.pubkey, + deposit_data.withdrawal_credentials, + deposit_data.amount, + spec, + )?; + added_validators.push((deposit_data.pubkey, validator_index)); + } + } + if conf.effective_balance_updates { + // Re-process effective balance updates for validators affected by top-up of new validators. + let ( + validators, + balances, + _, + current_epoch_participation, + _, + progressive_balances, + _, + _, + ) = state.mutable_validator_fields()?; + for (_, validator_index) in added_validators.iter() { + let balance = *balances + .get(*validator_index) + .ok_or(BeaconStateError::UnknownValidator(*validator_index))?; + let mut validator = validators + .get_cow(*validator_index) + .ok_or(BeaconStateError::UnknownValidator(*validator_index))?; + let validator_current_epoch_participation = *current_epoch_participation + .get(*validator_index) + .ok_or(BeaconStateError::UnknownValidator(*validator_index))?; + process_single_effective_balance_update( + *validator_index, + balance, + &mut validator, + validator_current_epoch_participation, + &mut next_epoch_cache, + progressive_balances, + effective_balances_ctxt, + state_ctxt, + spec, + )?; + } + } } // Process consolidations outside the single-pass loop, as they depend on balances for multiple @@ -819,8 +886,12 @@ fn process_single_slashing( Ok(()) } -impl PendingBalanceDepositsContext { - fn new(state: &BeaconState, spec: &ChainSpec) -> Result { +impl PendingDepositsContext { + fn new( + state: &BeaconState, + spec: &ChainSpec, + config: &SinglePassConfig, + ) -> Result { let available_for_processing = state .deposit_balance_to_consume()? .safe_add(state.get_activation_exit_churn_limit(spec)?)?; @@ -830,10 +901,31 @@ impl PendingBalanceDepositsContext { let mut next_deposit_index = 0; let mut validator_deposits_to_process = HashMap::new(); let mut deposits_to_postpone = vec![]; + let mut new_validator_deposits = vec![]; + let mut is_churn_limit_reached = false; + let finalized_slot = state + .finalized_checkpoint() + .epoch + .start_slot(E::slots_per_epoch()); - let pending_balance_deposits = state.pending_balance_deposits()?; + let pending_deposits = state.pending_deposits()?; - for deposit in pending_balance_deposits.iter() { + for deposit in pending_deposits.iter() { + // Do not process deposit requests if the Eth1 bridge deposits are not yet applied. + if deposit.slot > spec.genesis_slot + && state.eth1_deposit_index() < state.deposit_requests_start_index()? + { + break; + } + // Do not process is deposit slot has not been finalized. + if deposit.slot > finalized_slot { + break; + } + // Do not process if we have reached the limit for the number of deposits + // processed in an epoch. + if next_deposit_index >= E::max_pending_deposits_per_epoch() { + break; + } // We have to do a bit of indexing into `validators` here, but I can't see any way // around that without changing the spec. // @@ -844,48 +936,70 @@ impl PendingBalanceDepositsContext { // take, just whether it is non-default. Nor do we need to know the value of // `withdrawable_epoch`, because `next_epoch <= withdrawable_epoch` will evaluate to // `true` both for the actual value & the default placeholder value (`FAR_FUTURE_EPOCH`). - let validator = state.get_validator(deposit.index as usize)?; - let already_exited = validator.exit_epoch < spec.far_future_epoch; - // In the spec process_registry_updates is called before process_pending_balance_deposits - // so we must account for process_registry_updates ejecting the validator for low balance - // and setting the exit_epoch to < far_future_epoch. Note that in the spec the effective - // balance update does not happen until *after* the registry update, so we don't need to - // account for changes to the effective balance that would push it below the ejection - // balance here. - let will_be_exited = validator.is_active_at(current_epoch) - && validator.effective_balance <= spec.ejection_balance; - if already_exited || will_be_exited { - if next_epoch <= validator.withdrawable_epoch { - deposits_to_postpone.push(deposit.clone()); - } else { - // Deposited balance will never become active. Increase balance but do not - // consume churn. - validator_deposits_to_process - .entry(deposit.index as usize) - .or_insert(0) - .safe_add_assign(deposit.amount)?; - } - } else { - // Deposit does not fit in the churn, no more deposit processing in this epoch. - if processed_amount.safe_add(deposit.amount)? > available_for_processing { - break; - } - // Deposit fits in the churn, process it. Increase balance and consume churn. + let mut is_validator_exited = false; + let mut is_validator_withdrawn = false; + let opt_validator_index = state.pubkey_cache().get(&deposit.pubkey); + if let Some(validator_index) = opt_validator_index { + let validator = state.get_validator(validator_index)?; + let already_exited = validator.exit_epoch < spec.far_future_epoch; + // In the spec process_registry_updates is called before process_pending_deposits + // so we must account for process_registry_updates ejecting the validator for low balance + // and setting the exit_epoch to < far_future_epoch. Note that in the spec the effective + // balance update does not happen until *after* the registry update, so we don't need to + // account for changes to the effective balance that would push it below the ejection + // balance here. + // Note: we only consider this if registry_updates are enabled in the config. + // EF tests require us to run epoch_processing functions in isolation. + let will_be_exited = config.registry_updates + && (validator.is_active_at(current_epoch) + && validator.effective_balance <= spec.ejection_balance); + is_validator_exited = already_exited || will_be_exited; + is_validator_withdrawn = validator.withdrawable_epoch < next_epoch; + } + + if is_validator_withdrawn { + // Deposited balance will never become active. Queue a balance increase but do not + // consume churn. Validator index must be known if the validator is known to be + // withdrawn (see calculation of `is_validator_withdrawn` above). + let validator_index = + opt_validator_index.ok_or(Error::PendingDepositsLogicError)?; validator_deposits_to_process - .entry(deposit.index as usize) + .entry(validator_index) .or_insert(0) .safe_add_assign(deposit.amount)?; + } else if is_validator_exited { + // Validator is exiting, postpone the deposit until after withdrawable epoch + deposits_to_postpone.push(deposit.clone()); + } else { + // Check if deposit fits in the churn, otherwise, do no more deposit processing in this epoch. + is_churn_limit_reached = + processed_amount.safe_add(deposit.amount)? > available_for_processing; + if is_churn_limit_reached { + break; + } processed_amount.safe_add_assign(deposit.amount)?; + + // Deposit fits in the churn, process it. Increase balance and consume churn. + if let Some(validator_index) = state.pubkey_cache().get(&deposit.pubkey) { + validator_deposits_to_process + .entry(validator_index) + .or_insert(0) + .safe_add_assign(deposit.amount)?; + } else { + // The `PendingDeposit` is for a new validator + new_validator_deposits.push(deposit.clone()); + } } // Regardless of how the deposit was handled, we move on in the queue. next_deposit_index.safe_add_assign(1)?; } - let deposit_balance_to_consume = if next_deposit_index == pending_balance_deposits.len() { - 0 - } else { + // Accumulate churn only if the churn limit has been hit. + let deposit_balance_to_consume = if is_churn_limit_reached { available_for_processing.safe_sub(processed_amount)? + } else { + 0 }; Ok(Self { @@ -893,14 +1007,15 @@ impl PendingBalanceDepositsContext { deposit_balance_to_consume, validator_deposits_to_process, deposits_to_postpone, + new_validator_deposits, }) } } -fn process_pending_balance_deposits_for_validator( +fn process_pending_deposits_for_validator( balance: &mut Cow, validator_info: &ValidatorInfo, - pending_balance_deposits_ctxt: &PendingBalanceDepositsContext, + pending_balance_deposits_ctxt: &PendingDepositsContext, ) -> Result<(), Error> { if let Some(deposit_amount) = pending_balance_deposits_ctxt .validator_deposits_to_process @@ -941,21 +1056,20 @@ fn process_pending_consolidations( break; } - // Calculate the active balance while we have the source validator loaded. This is a safe - // reordering. - let source_balance = *state - .balances() - .get(source_index) - .ok_or(BeaconStateError::UnknownValidator(source_index))?; - let active_balance = - source_validator.get_active_balance(source_balance, spec, state_ctxt.fork_name); - - // Churn any target excess active balance of target and raise its max. - state.switch_to_compounding_validator(target_index, spec)?; + // Calculate the consolidated balance + let max_effective_balance = + source_validator.get_max_effective_balance(spec, state_ctxt.fork_name); + let source_effective_balance = std::cmp::min( + *state + .balances() + .get(source_index) + .ok_or(BeaconStateError::UnknownValidator(source_index))?, + max_effective_balance, + ); // Move active balance to target. Excess balance is withdrawable. - decrease_balance(state, source_index, active_balance)?; - increase_balance(state, target_index, active_balance)?; + decrease_balance(state, source_index, source_effective_balance)?; + increase_balance(state, target_index, source_effective_balance)?; affected_validators.insert(source_index); affected_validators.insert(target_index); diff --git a/consensus/state_processing/src/upgrade/electra.rs b/consensus/state_processing/src/upgrade/electra.rs index 1e532d9f10..1e64ef2897 100644 --- a/consensus/state_processing/src/upgrade/electra.rs +++ b/consensus/state_processing/src/upgrade/electra.rs @@ -1,8 +1,10 @@ +use bls::Signature; +use itertools::Itertools; use safe_arith::SafeArith; use std::mem; use types::{ BeaconState, BeaconStateElectra, BeaconStateError as Error, ChainSpec, Epoch, EpochCache, - EthSpec, Fork, + EthSpec, Fork, PendingDeposit, }; /// Transform a `Deneb` state into an `Electra` state. @@ -38,29 +40,44 @@ pub fn upgrade_to_electra( // Add validators that are not yet active to pending balance deposits let validators = post.validators().clone(); - let mut pre_activation = validators + let pre_activation = validators .iter() .enumerate() .filter(|(_, validator)| validator.activation_epoch == spec.far_future_epoch) + .sorted_by_key(|(index, validator)| (validator.activation_eligibility_epoch, *index)) .collect::>(); - // Sort the indices by activation_eligibility_epoch and then by index - pre_activation.sort_by(|(index_a, val_a), (index_b, val_b)| { - if val_a.activation_eligibility_epoch == val_b.activation_eligibility_epoch { - index_a.cmp(index_b) - } else { - val_a - .activation_eligibility_epoch - .cmp(&val_b.activation_eligibility_epoch) - } - }); - // Process validators to queue entire balance and reset them for (index, _) in pre_activation { - post.queue_entire_balance_and_reset_validator(index, spec)?; + let balance = post + .balances_mut() + .get_mut(index) + .ok_or(Error::UnknownValidator(index))?; + let balance_copy = *balance; + *balance = 0_u64; + + let validator = post + .validators_mut() + .get_mut(index) + .ok_or(Error::UnknownValidator(index))?; + validator.effective_balance = 0; + validator.activation_eligibility_epoch = spec.far_future_epoch; + let pubkey = validator.pubkey; + let withdrawal_credentials = validator.withdrawal_credentials; + + post.pending_deposits_mut()? + .push(PendingDeposit { + pubkey, + withdrawal_credentials, + amount: balance_copy, + signature: Signature::infinity()?.into(), + slot: spec.genesis_slot, + }) + .map_err(Error::MilhouseError)?; } // Ensure early adopters of compounding credentials go through the activation churn + let validators = post.validators().clone(); for (index, validator) in validators.iter().enumerate() { if validator.has_compounding_withdrawal_credential(spec) { post.queue_excess_active_balance(index, spec)?; @@ -137,7 +154,7 @@ pub fn upgrade_state_to_electra( earliest_exit_epoch, consolidation_balance_to_consume: 0, earliest_consolidation_epoch, - pending_balance_deposits: Default::default(), + pending_deposits: Default::default(), pending_partial_withdrawals: Default::default(), pending_consolidations: Default::default(), // Caches diff --git a/consensus/types/src/beacon_state.rs b/consensus/types/src/beacon_state.rs index 833231dca3..77b72b209c 100644 --- a/consensus/types/src/beacon_state.rs +++ b/consensus/types/src/beacon_state.rs @@ -509,7 +509,7 @@ where #[compare_fields(as_iter)] #[test_random(default)] #[superstruct(only(Electra))] - pub pending_balance_deposits: List, + pub pending_deposits: List, #[compare_fields(as_iter)] #[test_random(default)] #[superstruct(only(Electra))] @@ -1547,19 +1547,23 @@ impl BeaconState { .ok_or(Error::UnknownValidator(validator_index)) } + /// Add a validator to the registry and return the validator index that was allocated for it. pub fn add_validator_to_registry( &mut self, - deposit_data: &DepositData, + pubkey: PublicKeyBytes, + withdrawal_credentials: Hash256, + amount: u64, spec: &ChainSpec, - ) -> Result<(), Error> { - let fork = self.fork_name_unchecked(); - let amount = if fork.electra_enabled() { - 0 - } else { - deposit_data.amount - }; - self.validators_mut() - .push(Validator::from_deposit(deposit_data, amount, fork, spec))?; + ) -> Result { + let index = self.validators().len(); + let fork_name = self.fork_name_unchecked(); + self.validators_mut().push(Validator::from_deposit( + pubkey, + withdrawal_credentials, + amount, + fork_name, + spec, + ))?; self.balances_mut().push(amount)?; // Altair or later initializations. @@ -1573,7 +1577,20 @@ impl BeaconState { inactivity_scores.push(0)?; } - Ok(()) + // Keep the pubkey cache up to date if it was up to date prior to this call. + // + // Doing this here while we know the pubkey and index is marginally quicker than doing it in + // a call to `update_pubkey_cache` later because we don't need to index into the validators + // tree again. + let pubkey_cache = self.pubkey_cache_mut(); + if pubkey_cache.len() == index { + let success = pubkey_cache.insert(pubkey, index); + if !success { + return Err(Error::PubkeyCacheInconsistent); + } + } + + Ok(index) } /// Safe copy-on-write accessor for the `validators` list. @@ -1780,19 +1797,6 @@ impl BeaconState { } } - /// Get the number of outstanding deposits. - /// - /// Returns `Err` if the state is invalid. - pub fn get_outstanding_deposit_len(&self) -> Result { - self.eth1_data() - .deposit_count - .checked_sub(self.eth1_deposit_index()) - .ok_or(Error::InvalidDepositState { - deposit_count: self.eth1_data().deposit_count, - deposit_index: self.eth1_deposit_index(), - }) - } - /// Build all caches (except the tree hash cache), if they need to be built. pub fn build_caches(&mut self, spec: &ChainSpec) -> Result<(), Error> { self.build_all_committee_caches(spec)?; @@ -2149,27 +2153,6 @@ impl BeaconState { .map_err(Into::into) } - /// Get active balance for the given `validator_index`. - pub fn get_active_balance( - &self, - validator_index: usize, - spec: &ChainSpec, - current_fork: ForkName, - ) -> Result { - let max_effective_balance = self - .validators() - .get(validator_index) - .map(|validator| validator.get_max_effective_balance(spec, current_fork)) - .ok_or(Error::UnknownValidator(validator_index))?; - Ok(std::cmp::min( - *self - .balances() - .get(validator_index) - .ok_or(Error::UnknownValidator(validator_index))?, - max_effective_balance, - )) - } - pub fn get_pending_balance_to_withdraw(&self, validator_index: usize) -> Result { let mut pending_balance = 0; for withdrawal in self @@ -2196,42 +2179,18 @@ impl BeaconState { if *balance > spec.min_activation_balance { let excess_balance = balance.safe_sub(spec.min_activation_balance)?; *balance = spec.min_activation_balance; - self.pending_balance_deposits_mut()? - .push(PendingBalanceDeposit { - index: validator_index as u64, - amount: excess_balance, - })?; + let validator = self.get_validator(validator_index)?.clone(); + self.pending_deposits_mut()?.push(PendingDeposit { + pubkey: validator.pubkey, + withdrawal_credentials: validator.withdrawal_credentials, + amount: excess_balance, + signature: Signature::infinity()?.into(), + slot: spec.genesis_slot, + })?; } Ok(()) } - pub fn queue_entire_balance_and_reset_validator( - &mut self, - validator_index: usize, - spec: &ChainSpec, - ) -> Result<(), Error> { - let balance = self - .balances_mut() - .get_mut(validator_index) - .ok_or(Error::UnknownValidator(validator_index))?; - let balance_copy = *balance; - *balance = 0_u64; - - let validator = self - .validators_mut() - .get_mut(validator_index) - .ok_or(Error::UnknownValidator(validator_index))?; - validator.effective_balance = 0; - validator.activation_eligibility_epoch = spec.far_future_epoch; - - self.pending_balance_deposits_mut()? - .push(PendingBalanceDeposit { - index: validator_index as u64, - amount: balance_copy, - }) - .map_err(Into::into) - } - /// Change the withdrawal prefix of the given `validator_index` to the compounding withdrawal validator prefix. pub fn switch_to_compounding_validator( &mut self, @@ -2242,12 +2201,10 @@ impl BeaconState { .validators_mut() .get_mut(validator_index) .ok_or(Error::UnknownValidator(validator_index))?; - if validator.has_eth1_withdrawal_credential(spec) { - AsMut::<[u8; 32]>::as_mut(&mut validator.withdrawal_credentials)[0] = - spec.compounding_withdrawal_prefix_byte; + AsMut::<[u8; 32]>::as_mut(&mut validator.withdrawal_credentials)[0] = + spec.compounding_withdrawal_prefix_byte; - self.queue_excess_active_balance(validator_index, spec)?; - } + self.queue_excess_active_balance(validator_index, spec)?; Ok(()) } diff --git a/consensus/types/src/beacon_state/tests.rs b/consensus/types/src/beacon_state/tests.rs index 3ad3ccf561..bfa7bb86d2 100644 --- a/consensus/types/src/beacon_state/tests.rs +++ b/consensus/types/src/beacon_state/tests.rs @@ -307,43 +307,6 @@ mod committees { } } -mod get_outstanding_deposit_len { - use super::*; - - async fn state() -> BeaconState { - get_harness(16, Slot::new(0)) - .await - .chain - .head_beacon_state_cloned() - } - - #[tokio::test] - async fn returns_ok() { - let mut state = state().await; - assert_eq!(state.get_outstanding_deposit_len(), Ok(0)); - - state.eth1_data_mut().deposit_count = 17; - *state.eth1_deposit_index_mut() = 16; - assert_eq!(state.get_outstanding_deposit_len(), Ok(1)); - } - - #[tokio::test] - async fn returns_err_if_the_state_is_invalid() { - let mut state = state().await; - // The state is invalid, deposit count is lower than deposit index. - state.eth1_data_mut().deposit_count = 16; - *state.eth1_deposit_index_mut() = 17; - - assert_eq!( - state.get_outstanding_deposit_len(), - Err(BeaconStateError::InvalidDepositState { - deposit_count: 16, - deposit_index: 17, - }) - ); - } -} - #[test] fn decode_base_and_altair() { type E = MainnetEthSpec; diff --git a/consensus/types/src/deposit_request.rs b/consensus/types/src/deposit_request.rs index 7af949fef3..a21760551b 100644 --- a/consensus/types/src/deposit_request.rs +++ b/consensus/types/src/deposit_request.rs @@ -1,5 +1,6 @@ use crate::test_utils::TestRandom; -use crate::{Hash256, PublicKeyBytes, Signature}; +use crate::{Hash256, PublicKeyBytes}; +use bls::SignatureBytes; use serde::{Deserialize, Serialize}; use ssz::Encode; use ssz_derive::{Decode, Encode}; @@ -10,7 +11,6 @@ use tree_hash_derive::TreeHash; arbitrary::Arbitrary, Debug, PartialEq, - Eq, Hash, Clone, Serialize, @@ -25,7 +25,7 @@ pub struct DepositRequest { pub withdrawal_credentials: Hash256, #[serde(with = "serde_utils::quoted_u64")] pub amount: u64, - pub signature: Signature, + pub signature: SignatureBytes, #[serde(with = "serde_utils::quoted_u64")] pub index: u64, } @@ -36,7 +36,7 @@ impl DepositRequest { pubkey: PublicKeyBytes::empty(), withdrawal_credentials: Hash256::ZERO, amount: 0, - signature: Signature::empty(), + signature: SignatureBytes::empty(), index: 0, } .as_ssz_bytes() diff --git a/consensus/types/src/eth_spec.rs b/consensus/types/src/eth_spec.rs index 09ef8e3c1a..23e8276209 100644 --- a/consensus/types/src/eth_spec.rs +++ b/consensus/types/src/eth_spec.rs @@ -151,7 +151,7 @@ pub trait EthSpec: /* * New in Electra */ - type PendingBalanceDepositsLimit: Unsigned + Clone + Sync + Send + Debug + PartialEq; + type PendingDepositsLimit: Unsigned + Clone + Sync + Send + Debug + PartialEq; type PendingPartialWithdrawalsLimit: Unsigned + Clone + Sync + Send + Debug + PartialEq; type PendingConsolidationsLimit: Unsigned + Clone + Sync + Send + Debug + PartialEq; type MaxConsolidationRequestsPerPayload: Unsigned + Clone + Sync + Send + Debug + PartialEq; @@ -159,6 +159,7 @@ pub trait EthSpec: type MaxAttesterSlashingsElectra: Unsigned + Clone + Sync + Send + Debug + PartialEq; type MaxAttestationsElectra: Unsigned + Clone + Sync + Send + Debug + PartialEq; type MaxWithdrawalRequestsPerPayload: Unsigned + Clone + Sync + Send + Debug + PartialEq; + type MaxPendingDepositsPerEpoch: Unsigned + Clone + Sync + Send + Debug + PartialEq; fn default_spec() -> ChainSpec; @@ -331,9 +332,9 @@ pub trait EthSpec: .expect("Preset values are not configurable and never result in non-positive block body depth") } - /// Returns the `PENDING_BALANCE_DEPOSITS_LIMIT` constant for this specification. - fn pending_balance_deposits_limit() -> usize { - Self::PendingBalanceDepositsLimit::to_usize() + /// Returns the `PENDING_DEPOSITS_LIMIT` constant for this specification. + fn pending_deposits_limit() -> usize { + Self::PendingDepositsLimit::to_usize() } /// Returns the `PENDING_PARTIAL_WITHDRAWALS_LIMIT` constant for this specification. @@ -371,6 +372,11 @@ pub trait EthSpec: Self::MaxWithdrawalRequestsPerPayload::to_usize() } + /// Returns the `MAX_PENDING_DEPOSITS_PER_EPOCH` constant for this specification. + fn max_pending_deposits_per_epoch() -> usize { + Self::MaxPendingDepositsPerEpoch::to_usize() + } + fn kzg_commitments_inclusion_proof_depth() -> usize { Self::KzgCommitmentsInclusionProofDepth::to_usize() } @@ -430,7 +436,7 @@ impl EthSpec for MainnetEthSpec { type SlotsPerEth1VotingPeriod = U2048; // 64 epochs * 32 slots per epoch type MaxBlsToExecutionChanges = U16; type MaxWithdrawalsPerPayload = U16; - type PendingBalanceDepositsLimit = U134217728; + type PendingDepositsLimit = U134217728; type PendingPartialWithdrawalsLimit = U134217728; type PendingConsolidationsLimit = U262144; type MaxConsolidationRequestsPerPayload = U1; @@ -438,6 +444,7 @@ impl EthSpec for MainnetEthSpec { type MaxAttesterSlashingsElectra = U1; type MaxAttestationsElectra = U8; type MaxWithdrawalRequestsPerPayload = U16; + type MaxPendingDepositsPerEpoch = U16; fn default_spec() -> ChainSpec { ChainSpec::mainnet() @@ -500,7 +507,8 @@ impl EthSpec for MinimalEthSpec { MaxBlsToExecutionChanges, MaxBlobsPerBlock, BytesPerFieldElement, - PendingBalanceDepositsLimit, + PendingDepositsLimit, + MaxPendingDepositsPerEpoch, MaxConsolidationRequestsPerPayload, MaxAttesterSlashingsElectra, MaxAttestationsElectra @@ -557,7 +565,7 @@ impl EthSpec for GnosisEthSpec { type BytesPerFieldElement = U32; type BytesPerBlob = U131072; type KzgCommitmentInclusionProofDepth = U17; - type PendingBalanceDepositsLimit = U134217728; + type PendingDepositsLimit = U134217728; type PendingPartialWithdrawalsLimit = U134217728; type PendingConsolidationsLimit = U262144; type MaxConsolidationRequestsPerPayload = U1; @@ -565,6 +573,7 @@ impl EthSpec for GnosisEthSpec { type MaxAttesterSlashingsElectra = U1; type MaxAttestationsElectra = U8; type MaxWithdrawalRequestsPerPayload = U16; + type MaxPendingDepositsPerEpoch = U16; type FieldElementsPerCell = U64; type FieldElementsPerExtBlob = U8192; type BytesPerCell = U2048; diff --git a/consensus/types/src/execution_block_header.rs b/consensus/types/src/execution_block_header.rs index 694162d6ff..60f2960afb 100644 --- a/consensus/types/src/execution_block_header.rs +++ b/consensus/types/src/execution_block_header.rs @@ -52,9 +52,11 @@ pub struct ExecutionBlockHeader { pub blob_gas_used: Option, pub excess_blob_gas: Option, pub parent_beacon_block_root: Option, + pub requests_root: Option, } impl ExecutionBlockHeader { + #[allow(clippy::too_many_arguments)] pub fn from_payload( payload: ExecutionPayloadRef, rlp_empty_list_root: Hash256, @@ -63,6 +65,7 @@ impl ExecutionBlockHeader { rlp_blob_gas_used: Option, rlp_excess_blob_gas: Option, rlp_parent_beacon_block_root: Option, + rlp_requests_root: Option, ) -> Self { // Most of these field mappings are defined in EIP-3675 except for `mixHash`, which is // defined in EIP-4399. @@ -87,6 +90,7 @@ impl ExecutionBlockHeader { blob_gas_used: rlp_blob_gas_used, excess_blob_gas: rlp_excess_blob_gas, parent_beacon_block_root: rlp_parent_beacon_block_root, + requests_root: rlp_requests_root, } } } @@ -114,6 +118,7 @@ pub struct EncodableExecutionBlockHeader<'a> { pub blob_gas_used: Option, pub excess_blob_gas: Option, pub parent_beacon_block_root: Option<&'a [u8]>, + pub requests_root: Option<&'a [u8]>, } impl<'a> From<&'a ExecutionBlockHeader> for EncodableExecutionBlockHeader<'a> { @@ -139,6 +144,7 @@ impl<'a> From<&'a ExecutionBlockHeader> for EncodableExecutionBlockHeader<'a> { blob_gas_used: header.blob_gas_used, excess_blob_gas: header.excess_blob_gas, parent_beacon_block_root: None, + requests_root: None, }; if let Some(withdrawals_root) = &header.withdrawals_root { encodable.withdrawals_root = Some(withdrawals_root.as_slice()); @@ -146,6 +152,9 @@ impl<'a> From<&'a ExecutionBlockHeader> for EncodableExecutionBlockHeader<'a> { if let Some(parent_beacon_block_root) = &header.parent_beacon_block_root { encodable.parent_beacon_block_root = Some(parent_beacon_block_root.as_slice()) } + if let Some(requests_root) = &header.requests_root { + encodable.requests_root = Some(requests_root.as_slice()) + } encodable } } diff --git a/consensus/types/src/execution_requests.rs b/consensus/types/src/execution_requests.rs index 778260dd84..96a3905420 100644 --- a/consensus/types/src/execution_requests.rs +++ b/consensus/types/src/execution_requests.rs @@ -1,7 +1,8 @@ use crate::test_utils::TestRandom; -use crate::{ConsolidationRequest, DepositRequest, EthSpec, WithdrawalRequest}; +use crate::{ConsolidationRequest, DepositRequest, EthSpec, Hash256, WithdrawalRequest}; use alloy_primitives::Bytes; use derivative::Derivative; +use ethereum_hashing::{DynamicContext, Sha256Context}; use serde::{Deserialize, Serialize}; use ssz::Encode; use ssz_derive::{Decode, Encode}; @@ -47,6 +48,43 @@ impl ExecutionRequests { let consolidation_bytes = Bytes::from(self.consolidations.as_ssz_bytes()); vec![deposit_bytes, withdrawal_bytes, consolidation_bytes] } + + /// Generate the execution layer `requests_hash` based on EIP-7685. + /// + /// `sha256(sha256(requests_0) ++ sha256(requests_1) ++ ...)` + pub fn requests_hash(&self) -> Hash256 { + let mut hasher = DynamicContext::new(); + + for (i, request) in self.get_execution_requests_list().iter().enumerate() { + let mut request_hasher = DynamicContext::new(); + request_hasher.update(&[i as u8]); + request_hasher.update(request); + let request_hash = request_hasher.finalize(); + + hasher.update(&request_hash); + } + + hasher.finalize().into() + } +} + +/// This is used to index into the `execution_requests` array. +#[derive(Debug, Copy, Clone)] +pub enum RequestPrefix { + Deposit, + Withdrawal, + Consolidation, +} + +impl RequestPrefix { + pub fn from_prefix(prefix: u8) -> Option { + match prefix { + 0 => Some(Self::Deposit), + 1 => Some(Self::Withdrawal), + 2 => Some(Self::Consolidation), + _ => None, + } + } } #[cfg(test)] diff --git a/consensus/types/src/lib.rs b/consensus/types/src/lib.rs index eff5237834..dd304c6296 100644 --- a/consensus/types/src/lib.rs +++ b/consensus/types/src/lib.rs @@ -54,8 +54,8 @@ pub mod light_client_finality_update; pub mod light_client_optimistic_update; pub mod light_client_update; pub mod pending_attestation; -pub mod pending_balance_deposit; pub mod pending_consolidation; +pub mod pending_deposit; pub mod pending_partial_withdrawal; pub mod proposer_preparation_data; pub mod proposer_slashing; @@ -170,7 +170,7 @@ pub use crate::execution_payload_header::{ ExecutionPayloadHeaderDeneb, ExecutionPayloadHeaderElectra, ExecutionPayloadHeaderRef, ExecutionPayloadHeaderRefMut, }; -pub use crate::execution_requests::ExecutionRequests; +pub use crate::execution_requests::{ExecutionRequests, RequestPrefix}; pub use crate::fork::Fork; pub use crate::fork_context::ForkContext; pub use crate::fork_data::ForkData; @@ -210,8 +210,8 @@ pub use crate::payload::{ FullPayloadRef, OwnedExecPayload, }; pub use crate::pending_attestation::PendingAttestation; -pub use crate::pending_balance_deposit::PendingBalanceDeposit; pub use crate::pending_consolidation::PendingConsolidation; +pub use crate::pending_deposit::PendingDeposit; pub use crate::pending_partial_withdrawal::PendingPartialWithdrawal; pub use crate::preset::{ AltairPreset, BasePreset, BellatrixPreset, CapellaPreset, DenebPreset, ElectraPreset, diff --git a/consensus/types/src/pending_balance_deposit.rs b/consensus/types/src/pending_deposit.rs similarity index 68% rename from consensus/types/src/pending_balance_deposit.rs rename to consensus/types/src/pending_deposit.rs index a2bce577f8..3bee86417d 100644 --- a/consensus/types/src/pending_balance_deposit.rs +++ b/consensus/types/src/pending_deposit.rs @@ -1,4 +1,5 @@ use crate::test_utils::TestRandom; +use crate::*; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; @@ -8,7 +9,6 @@ use tree_hash_derive::TreeHash; arbitrary::Arbitrary, Debug, PartialEq, - Eq, Hash, Clone, Serialize, @@ -18,16 +18,18 @@ use tree_hash_derive::TreeHash; TreeHash, TestRandom, )] -pub struct PendingBalanceDeposit { - #[serde(with = "serde_utils::quoted_u64")] - pub index: u64, +pub struct PendingDeposit { + pub pubkey: PublicKeyBytes, + pub withdrawal_credentials: Hash256, #[serde(with = "serde_utils::quoted_u64")] pub amount: u64, + pub signature: SignatureBytes, + pub slot: Slot, } #[cfg(test)] mod tests { use super::*; - ssz_and_tree_hash_tests!(PendingBalanceDeposit); + ssz_and_tree_hash_tests!(PendingDeposit); } diff --git a/consensus/types/src/preset.rs b/consensus/types/src/preset.rs index 435a74bdc3..b469b7b777 100644 --- a/consensus/types/src/preset.rs +++ b/consensus/types/src/preset.rs @@ -263,7 +263,7 @@ impl ElectraPreset { whistleblower_reward_quotient_electra: spec.whistleblower_reward_quotient_electra, max_pending_partials_per_withdrawals_sweep: spec .max_pending_partials_per_withdrawals_sweep, - pending_balance_deposits_limit: E::pending_balance_deposits_limit() as u64, + pending_balance_deposits_limit: E::pending_deposits_limit() as u64, pending_partial_withdrawals_limit: E::pending_partial_withdrawals_limit() as u64, pending_consolidations_limit: E::pending_consolidations_limit() as u64, max_consolidation_requests_per_payload: E::max_consolidation_requests_per_payload() diff --git a/consensus/types/src/validator.rs b/consensus/types/src/validator.rs index 275101ddbe..222b9292a2 100644 --- a/consensus/types/src/validator.rs +++ b/consensus/types/src/validator.rs @@ -1,6 +1,6 @@ use crate::{ - test_utils::TestRandom, Address, BeaconState, ChainSpec, Checkpoint, DepositData, Epoch, - EthSpec, FixedBytesExtended, ForkName, Hash256, PublicKeyBytes, + test_utils::TestRandom, Address, BeaconState, ChainSpec, Checkpoint, Epoch, EthSpec, + FixedBytesExtended, ForkName, Hash256, PublicKeyBytes, }; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; @@ -38,14 +38,15 @@ pub struct Validator { impl Validator { #[allow(clippy::arithmetic_side_effects)] pub fn from_deposit( - deposit_data: &DepositData, + pubkey: PublicKeyBytes, + withdrawal_credentials: Hash256, amount: u64, fork_name: ForkName, spec: &ChainSpec, ) -> Self { let mut validator = Validator { - pubkey: deposit_data.pubkey, - withdrawal_credentials: deposit_data.withdrawal_credentials, + pubkey, + withdrawal_credentials, activation_eligibility_epoch: spec.far_future_epoch, activation_epoch: spec.far_future_epoch, exit_epoch: spec.far_future_epoch, @@ -291,16 +292,6 @@ impl Validator { spec.max_effective_balance } } - - pub fn get_active_balance( - &self, - validator_balance: u64, - spec: &ChainSpec, - current_fork: ForkName, - ) -> u64 { - let max_effective_balance = self.get_max_effective_balance(spec, current_fork); - std::cmp::min(validator_balance, max_effective_balance) - } } impl Default for Validator { diff --git a/testing/ef_tests/Makefile b/testing/ef_tests/Makefile index 390711079f..d5f4997bb7 100644 --- a/testing/ef_tests/Makefile +++ b/testing/ef_tests/Makefile @@ -1,4 +1,4 @@ -TESTS_TAG := v1.5.0-alpha.6 +TESTS_TAG := v1.5.0-alpha.8 TESTS = general minimal mainnet TARBALLS = $(patsubst %,%-$(TESTS_TAG).tar.gz,$(TESTS)) diff --git a/testing/ef_tests/src/cases/epoch_processing.rs b/testing/ef_tests/src/cases/epoch_processing.rs index dfd782a22b..c1adf10770 100644 --- a/testing/ef_tests/src/cases/epoch_processing.rs +++ b/testing/ef_tests/src/cases/epoch_processing.rs @@ -86,7 +86,7 @@ type_name!(RewardsAndPenalties, "rewards_and_penalties"); type_name!(RegistryUpdates, "registry_updates"); type_name!(Slashings, "slashings"); type_name!(Eth1DataReset, "eth1_data_reset"); -type_name!(PendingBalanceDeposits, "pending_balance_deposits"); +type_name!(PendingBalanceDeposits, "pending_deposits"); type_name!(PendingConsolidations, "pending_consolidations"); type_name!(EffectiveBalanceUpdates, "effective_balance_updates"); type_name!(SlashingsReset, "slashings_reset"); @@ -193,7 +193,7 @@ impl EpochTransition for PendingBalanceDeposits { state, spec, SinglePassConfig { - pending_balance_deposits: true, + pending_deposits: true, ..SinglePassConfig::disable_all() }, ) @@ -363,7 +363,7 @@ impl> Case for EpochProcessing { } if !fork_name.electra_enabled() - && (T::name() == "pending_consolidations" || T::name() == "pending_balance_deposits") + && (T::name() == "pending_consolidations" || T::name() == "pending_deposits") { return false; } diff --git a/testing/ef_tests/src/type_name.rs b/testing/ef_tests/src/type_name.rs index a9322e5dd5..c50032a63d 100644 --- a/testing/ef_tests/src/type_name.rs +++ b/testing/ef_tests/src/type_name.rs @@ -134,7 +134,7 @@ type_name_generic!(LightClientUpdateElectra, "LightClientUpdate"); type_name_generic!(PendingAttestation); type_name!(PendingConsolidation); type_name!(PendingPartialWithdrawal); -type_name!(PendingBalanceDeposit); +type_name!(PendingDeposit); type_name!(ProposerSlashing); type_name_generic!(SignedAggregateAndProof); type_name_generic!(SignedAggregateAndProofBase, "SignedAggregateAndProof"); diff --git a/testing/ef_tests/tests/tests.rs b/testing/ef_tests/tests/tests.rs index 3f802d8944..292625a371 100644 --- a/testing/ef_tests/tests/tests.rs +++ b/testing/ef_tests/tests/tests.rs @@ -243,8 +243,7 @@ mod ssz_static { use types::historical_summary::HistoricalSummary; use types::{ AttesterSlashingBase, AttesterSlashingElectra, ConsolidationRequest, DepositRequest, - LightClientBootstrapAltair, PendingBalanceDeposit, PendingPartialWithdrawal, - WithdrawalRequest, *, + LightClientBootstrapAltair, PendingDeposit, PendingPartialWithdrawal, WithdrawalRequest, *, }; ssz_static_test!(attestation_data, AttestationData); @@ -661,8 +660,8 @@ mod ssz_static { #[test] fn pending_balance_deposit() { - SszStaticHandler::::electra_and_later().run(); - SszStaticHandler::::electra_and_later().run(); + SszStaticHandler::::electra_and_later().run(); + SszStaticHandler::::electra_and_later().run(); } #[test] From 1de498340c5166e4abbc3de12dae4af6dab7c6c3 Mon Sep 17 00:00:00 2001 From: chonghe <44791194+chong-he@users.noreply.github.com> Date: Tue, 17 Dec 2024 15:26:59 +0800 Subject: [PATCH 050/254] Add spell check and update Lighthouse book (#6627) * spellcheck config * Merge remote-tracking branch 'origin/unstable' into spellcheck * spellcheck update * update spellcheck * spell check passes * Remove ignored and add other md files * Remove some words in wordlist * CI * test spell check CI * correct spell check * Merge branch 'unstable' into spellcheck * minor fix * Merge branch 'spellcheck' of https://github.com/chong-he/lighthouse into spellcheck * Update book * mdlint * delete previous_epoch_active_gwei * Merge branch 'unstable' into spellcheck * Tweak "container runtime" wording * Try `BeaconState`s --- .github/workflows/test-suite.yml | 2 + .spellcheck.yml | 35 +++++ CONTRIBUTING.md | 2 +- README.md | 2 +- book/src/advanced_database.md | 2 +- book/src/advanced_networking.md | 6 +- book/src/api-lighthouse.md | 35 +++-- book/src/faq.md | 6 +- book/src/graffiti.md | 2 +- book/src/homebrew.md | 2 +- book/src/late-block-re-orgs.md | 19 ++- book/src/ui-faqs.md | 2 +- book/src/ui-installation.md | 2 +- book/src/validator-inclusion.md | 1 - book/src/validator-manager.md | 1 + book/src/validator-monitoring.md | 2 +- scripts/local_testnet/README.md | 8 +- wordlist.txt | 235 +++++++++++++++++++++++++++++++ 18 files changed, 331 insertions(+), 33 deletions(-) create mode 100644 .spellcheck.yml create mode 100644 wordlist.txt diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index 8da46ed8ee..bba670cc22 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -363,6 +363,8 @@ jobs: run: CARGO_HOME=$(readlink -f $HOME) make vendor - name: Markdown-linter run: make mdlint + - name: Spell-check + uses: rojopolis/spellcheck-github-actions@v0 check-msrv: name: check-msrv runs-on: ubuntu-latest diff --git a/.spellcheck.yml b/.spellcheck.yml new file mode 100644 index 0000000000..692bc4d176 --- /dev/null +++ b/.spellcheck.yml @@ -0,0 +1,35 @@ +matrix: +- name: Markdown + sources: + - './book/**/*.md' + - 'README.md' + - 'CONTRIBUTING.md' + - 'SECURITY.md' + - './scripts/local_testnet/README.md' + default_encoding: utf-8 + aspell: + lang: en + dictionary: + wordlists: + - wordlist.txt + encoding: utf-8 + pipeline: + - pyspelling.filters.url: + - pyspelling.filters.markdown: + markdown_extensions: + - pymdownx.superfences: + - pymdownx.highlight: + - pymdownx.striphtml: + - pymdownx.magiclink: + - pyspelling.filters.html: + comments: false + ignores: + - code + - pre + - pyspelling.filters.context: + context_visible_first: true + delimiters: + # Ignore hex strings + - open: '0x[a-fA-F0-9]' + close: '[^a-fA-F0-9]' + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3c53558a10..4cad219c89 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -85,7 +85,7 @@ steps: 5. Commit your changes and push them to your fork with `$ git push origin your_feature_name`. 6. Go to your fork on github.com and use the web interface to create a pull - request into the sigp/lighthouse repo. + request into the sigp/lighthouse repository. From there, the repository maintainers will review the PR and either accept it or provide some constructive feedback. diff --git a/README.md b/README.md index 4b22087bcd..147a06e504 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Lighthouse is: - Built in [Rust](https://www.rust-lang.org), a modern language providing unique safety guarantees and excellent performance (comparable to C++). - Funded by various organisations, including Sigma Prime, the - Ethereum Foundation, ConsenSys, the Decentralization Foundation and private individuals. + Ethereum Foundation, Consensys, the Decentralization Foundation and private individuals. - Actively involved in the specification and security analysis of the Ethereum proof-of-stake consensus specification. diff --git a/book/src/advanced_database.md b/book/src/advanced_database.md index d8d6ea61a1..b558279730 100644 --- a/book/src/advanced_database.md +++ b/book/src/advanced_database.md @@ -56,7 +56,7 @@ that we have observed are: _a lot_ of space. It's even possible to push beyond that with `--hierarchy-exponents 0` which would store a full state every single slot (NOT RECOMMENDED). - **Less diff layers are not necessarily faster**. One might expect that the fewer diff layers there - are, the less work Lighthouse would have to do to reconstruct any particular state. In practise + are, the less work Lighthouse would have to do to reconstruct any particular state. In practice this seems to be offset by the increased size of diffs in each layer making the diffs take longer to apply. We observed no significant performance benefit from `--hierarchy-exponents 5,7,11`, and a substantial increase in space consumed. diff --git a/book/src/advanced_networking.md b/book/src/advanced_networking.md index 732b4f51e6..c0f6b5485e 100644 --- a/book/src/advanced_networking.md +++ b/book/src/advanced_networking.md @@ -68,7 +68,7 @@ The steps to do port forwarding depends on the router, but the general steps are 1. Determine the default gateway IP: - On Linux: open a terminal and run `ip route | grep default`, the result should look something similar to `default via 192.168.50.1 dev wlp2s0 proto dhcp metric 600`. The `192.168.50.1` is your router management default gateway IP. - - On MacOS: open a terminal and run `netstat -nr|grep default` and it should return the default gateway IP. + - On macOS: open a terminal and run `netstat -nr|grep default` and it should return the default gateway IP. - On Windows: open a command prompt and run `ipconfig` and look for the `Default Gateway` which will show you the gateway IP. The default gateway IP usually looks like 192.168.X.X. Once you obtain the IP, enter it to a web browser and it will lead you to the router management page. @@ -91,7 +91,7 @@ The steps to do port forwarding depends on the router, but the general steps are - Internal port: `9001` - IP address: Choose the device that is running Lighthouse. -1. To check that you have successfully opened the ports, go to [yougetsignal](https://www.yougetsignal.com/tools/open-ports/) and enter `9000` in the `port number`. If it shows "open", then you have successfully set up port forwarding. If it shows "closed", double check your settings, and also check that you have allowed firewall rules on port 9000. Note: this will only confirm if port 9000/TCP is open. You will need to ensure you have correctly setup port forwarding for the UDP ports (`9000` and `9001` by default). +1. To check that you have successfully opened the ports, go to [`yougetsignal`](https://www.yougetsignal.com/tools/open-ports/) and enter `9000` in the `port number`. If it shows "open", then you have successfully set up port forwarding. If it shows "closed", double check your settings, and also check that you have allowed firewall rules on port 9000. Note: this will only confirm if port 9000/TCP is open. You will need to ensure you have correctly setup port forwarding for the UDP ports (`9000` and `9001` by default). ## ENR Configuration @@ -141,7 +141,7 @@ To listen over both IPv4 and IPv6: - Set two listening addresses using the `--listen-address` flag twice ensuring the two addresses are one IPv4, and the other IPv6. When doing so, the `--port` and `--discovery-port` flags will apply exclusively to IPv4. Note - that this behaviour differs from the Ipv6 only case described above. + that this behaviour differs from the IPv6 only case described above. - If necessary, set the `--port6` flag to configure the port used for TCP and UDP over IPv6. This flag has no effect when listening over IPv6 only. - If necessary, set the `--discovery-port6` flag to configure the IPv6 UDP diff --git a/book/src/api-lighthouse.md b/book/src/api-lighthouse.md index b63505c490..5428ab8f9a 100644 --- a/book/src/api-lighthouse.md +++ b/book/src/api-lighthouse.md @@ -508,23 +508,31 @@ curl "http://localhost:5052/lighthouse/database/info" | jq ```json { - "schema_version": 18, + "schema_version": 22, "config": { - "slots_per_restore_point": 8192, - "slots_per_restore_point_set_explicitly": false, "block_cache_size": 5, + "state_cache_size": 128, + "compression_level": 1, "historic_state_cache_size": 1, + "hdiff_buffer_cache_size": 16, "compact_on_init": false, "compact_on_prune": true, "prune_payloads": true, + "hierarchy_config": { + "exponents": [ + 5, + 7, + 11 + ] + }, "prune_blobs": true, "epochs_per_blob_prune": 1, "blob_prune_margin_epochs": 0 }, "split": { - "slot": "7454656", - "state_root": "0xbecfb1c8ee209854c611ebc967daa77da25b27f1a8ef51402fdbe060587d7653", - "block_root": "0x8730e946901b0a406313d36b3363a1b7091604e1346a3410c1a7edce93239a68" + "slot": "10530592", + "state_root": "0xd27e6ce699637cf9b5c7ca632118b7ce12c2f5070bb25a27ac353ff2799d4466", + "block_root": "0x71509a1cb374773d680cd77148c73ab3563526dacb0ab837bb0c87e686962eae" }, "anchor": { "anchor_slot": "7451168", @@ -543,8 +551,19 @@ curl "http://localhost:5052/lighthouse/database/info" | jq For more information about the split point, see the [Database Configuration](./advanced_database.md) docs. -The `anchor` will be `null` unless the node has been synced with checkpoint sync and state -reconstruction has yet to be completed. For more information +For archive nodes, the `anchor` will be: + +```json +"anchor": { + "anchor_slot": "0", + "oldest_block_slot": "0", + "oldest_block_parent": "0x0000000000000000000000000000000000000000000000000000000000000000", + "state_upper_limit": "0", + "state_lower_limit": "0" + }, +``` + +indicating that all states with slots `>= 0` are available, i.e., full state history. For more information on the specific meanings of these fields see the docs on [Checkpoint Sync](./checkpoint-sync.md#reconstructing-states). diff --git a/book/src/faq.md b/book/src/faq.md index 04e5ce5bc8..d23951c8c7 100644 --- a/book/src/faq.md +++ b/book/src/faq.md @@ -92,7 +92,7 @@ If the reason for the error message is caused by no. 1 above, you may want to lo - Power outage. If power outages are an issue at your place, consider getting a UPS to avoid ungraceful shutdown of services. - The service file is not stopped properly. To overcome this, make sure that the process is stopped properly, e.g., during client updates. -- Out of memory (oom) error. This can happen when the system memory usage has reached its maximum and causes the execution engine to be killed. To confirm that the error is due to oom, run `sudo dmesg -T | grep killed` to look for killed processes. If you are using geth as the execution client, a short term solution is to reduce the resources used. For example, you can reduce the cache by adding the flag `--cache 2048`. If the oom occurs rather frequently, a long term solution is to increase the memory capacity of the computer. +- Out of memory (oom) error. This can happen when the system memory usage has reached its maximum and causes the execution engine to be killed. To confirm that the error is due to oom, run `sudo dmesg -T | grep killed` to look for killed processes. If you are using Geth as the execution client, a short term solution is to reduce the resources used. For example, you can reduce the cache by adding the flag `--cache 2048`. If the oom occurs rather frequently, a long term solution is to increase the memory capacity of the computer. ### I see beacon logs showing `Error during execution engine upcheck`, what should I do? @@ -302,7 +302,7 @@ An example of the log: (debug logs can be found under `$datadir/beacon/logs`): Delayed head block, set_as_head_time_ms: 27, imported_time_ms: 168, attestable_delay_ms: 4209, available_delay_ms: 4186, execution_time_ms: 201, blob_delay_ms: 3815, observed_delay_ms: 3984, total_delay_ms: 4381, slot: 1886014, proposer_index: 733, block_root: 0xa7390baac88d50f1cbb5ad81691915f6402385a12521a670bbbd4cd5f8bf3934, service: beacon, module: beacon_chain::canonical_head:1441 ``` -The field to look for is `attestable_delay`, which defines the time when a block is ready for the validator to attest. If the `attestable_delay` is greater than 4s which has past the window of attestation, the attestation wil fail. In the above example, the delay is mostly caused by late block observed by the node, as shown in `observed_delay`. The `observed_delay` is determined mostly by the proposer and partly by your networking setup (e.g., how long it took for the node to receive the block). Ideally, `observed_delay` should be less than 3 seconds. In this example, the validator failed to attest the block due to the block arriving late. +The field to look for is `attestable_delay`, which defines the time when a block is ready for the validator to attest. If the `attestable_delay` is greater than 4s which has past the window of attestation, the attestation will fail. In the above example, the delay is mostly caused by late block observed by the node, as shown in `observed_delay`. The `observed_delay` is determined mostly by the proposer and partly by your networking setup (e.g., how long it took for the node to receive the block). Ideally, `observed_delay` should be less than 3 seconds. In this example, the validator failed to attest the block due to the block arriving late. Another example of log: @@ -315,7 +315,7 @@ In this example, we see that the `execution_time_ms` is 4694ms. The `execution_t ### Sometimes I miss the attestation head vote, resulting in penalty. Is this normal? -In general, it is unavoidable to have some penalties occasionally. This is particularly the case when you are assigned to attest on the first slot of an epoch and if the proposer of that slot releases the block late, then you will get penalised for missing the target and head votes. Your attestation performance does not only depend on your own setup, but also on everyone elses performance. +In general, it is unavoidable to have some penalties occasionally. This is particularly the case when you are assigned to attest on the first slot of an epoch and if the proposer of that slot releases the block late, then you will get penalised for missing the target and head votes. Your attestation performance does not only depend on your own setup, but also on everyone else's performance. You could also check for the sync aggregate participation percentage on block explorers such as [beaconcha.in](https://beaconcha.in/). A low sync aggregate participation percentage (e.g., 60-70%) indicates that the block that you are assigned to attest to may be published late. As a result, your validator fails to correctly attest to the block. diff --git a/book/src/graffiti.md b/book/src/graffiti.md index ba9c7d05d7..7b402ea866 100644 --- a/book/src/graffiti.md +++ b/book/src/graffiti.md @@ -4,7 +4,7 @@ Lighthouse provides four options for setting validator graffiti. ## 1. Using the "--graffiti-file" flag on the validator client -Users can specify a file with the `--graffiti-file` flag. This option is useful for dynamically changing graffitis for various use cases (e.g. drawing on the beaconcha.in graffiti wall). This file is loaded once on startup and reloaded everytime a validator is chosen to propose a block. +Users can specify a file with the `--graffiti-file` flag. This option is useful for dynamically changing graffitis for various use cases (e.g. drawing on the beaconcha.in graffiti wall). This file is loaded once on startup and reloaded every time a validator is chosen to propose a block. Usage: `lighthouse vc --graffiti-file graffiti_file.txt` diff --git a/book/src/homebrew.md b/book/src/homebrew.md index da92dcb26c..f94764889e 100644 --- a/book/src/homebrew.md +++ b/book/src/homebrew.md @@ -31,6 +31,6 @@ Alternatively, you can find the `lighthouse` binary at: The [formula][] is kept up-to-date by the Homebrew community and a bot that lists for new releases. -The package source can be found in the [homebrew-core](https://github.com/Homebrew/homebrew-core/blob/master/Formula/l/lighthouse.rb) repo. +The package source can be found in the [homebrew-core](https://github.com/Homebrew/homebrew-core/blob/master/Formula/l/lighthouse.rb) repository. [formula]: https://formulae.brew.sh/formula/lighthouse diff --git a/book/src/late-block-re-orgs.md b/book/src/late-block-re-orgs.md index 4a00f33aa4..fca156bda3 100644 --- a/book/src/late-block-re-orgs.md +++ b/book/src/late-block-re-orgs.md @@ -46,24 +46,31 @@ You can track the reasons for re-orgs being attempted (or not) via Lighthouse's A pair of messages at `INFO` level will be logged if a re-org opportunity is detected: -> INFO Attempting re-org due to weak head threshold_weight: 45455983852725, head_weight: 0, parent: 0x09d953b69041f280758400c671130d174113bbf57c2d26553a77fb514cad4890, weak_head: 0xf64f8e5ed617dc18c1e759dab5d008369767c3678416dac2fe1d389562842b49 - -> INFO Proposing block to re-org current head head_to_reorg: 0xf64f…2b49, slot: 1105320 +```text +INFO Attempting re-org due to weak head threshold_weight: 45455983852725, head_weight: 0, parent: 0x09d953b69041f280758400c671130d174113bbf57c2d26553a77fb514cad4890, weak_head: 0xf64f8e5ed617dc18c1e759dab5d008369767c3678416dac2fe1d389562842b49 +INFO Proposing block to re-org current head head_to_reorg: 0xf64f…2b49, slot: 1105320 +``` This should be followed shortly after by a `INFO` log indicating that a re-org occurred. This is expected and normal: -> INFO Beacon chain re-org reorg_distance: 1, new_slot: 1105320, new_head: 0x72791549e4ca792f91053bc7cf1e55c6fbe745f78ce7a16fc3acb6f09161becd, previous_slot: 1105319, previous_head: 0xf64f8e5ed617dc18c1e759dab5d008369767c3678416dac2fe1d389562842b49 +```text +INFO Beacon chain re-org reorg_distance: 1, new_slot: 1105320, new_head: 0x72791549e4ca792f91053bc7cf1e55c6fbe745f78ce7a16fc3acb6f09161becd, previous_slot: 1105319, previous_head: 0xf64f8e5ed617dc18c1e759dab5d008369767c3678416dac2fe1d389562842b49 +``` In case a re-org is not viable (which should be most of the time), Lighthouse will just propose a block as normal and log the reason the re-org was not attempted at debug level: -> DEBG Not attempting re-org reason: head not late +```text +DEBG Not attempting re-org reason: head not late +``` If you are interested in digging into the timing of `forkchoiceUpdated` messages sent to the execution layer, there is also a debug log for the suppression of `forkchoiceUpdated` messages when Lighthouse thinks that a re-org is likely: -> DEBG Fork choice update overridden slot: 1105320, override: 0x09d953b69041f280758400c671130d174113bbf57c2d26553a77fb514cad4890, canonical_head: 0xf64f8e5ed617dc18c1e759dab5d008369767c3678416dac2fe1d389562842b49 +```text +DEBG Fork choice update overridden slot: 1105320, override: 0x09d953b69041f280758400c671130d174113bbf57c2d26553a77fb514cad4890, canonical_head: 0xf64f8e5ed617dc18c1e759dab5d008369767c3678416dac2fe1d389562842b49 +``` [the spec]: https://github.com/ethereum/consensus-specs/pull/3034 diff --git a/book/src/ui-faqs.md b/book/src/ui-faqs.md index efa6d3d4ab..0887875316 100644 --- a/book/src/ui-faqs.md +++ b/book/src/ui-faqs.md @@ -6,7 +6,7 @@ Yes, the most current Siren version requires Lighthouse v4.3.0 or higher to func ## 2. Where can I find my API token? -The required Api token may be found in the default data directory of the validator client. For more information please refer to the lighthouse ui configuration [`api token section`](./api-vc-auth-header.md). +The required API token may be found in the default data directory of the validator client. For more information please refer to the lighthouse ui configuration [`api token section`](./api-vc-auth-header.md). ## 3. How do I fix the Node Network Errors? diff --git a/book/src/ui-installation.md b/book/src/ui-installation.md index 1444c0d633..9cd84e5160 100644 --- a/book/src/ui-installation.md +++ b/book/src/ui-installation.md @@ -1,6 +1,6 @@ # 📦 Installation -Siren supports any operating system that supports container runtimes and/or NodeJS 18, this includes Linux, MacOS, and Windows. The recommended way of running Siren is by launching the [docker container](https://hub.docker.com/r/sigp/siren) , but running the application directly is also possible. +Siren supports any operating system that supports containers and/or NodeJS 18, this includes Linux, macOS, and Windows. The recommended way of running Siren is by launching the [docker container](https://hub.docker.com/r/sigp/siren) , but running the application directly is also possible. ## Version Requirement diff --git a/book/src/validator-inclusion.md b/book/src/validator-inclusion.md index 092c813a1e..eef563dcdb 100644 --- a/book/src/validator-inclusion.md +++ b/book/src/validator-inclusion.md @@ -56,7 +56,6 @@ The following fields are returned: able to vote) during the current epoch. - `current_epoch_target_attesting_gwei`: the total staked gwei that attested to the majority-elected Casper FFG target epoch during the current epoch. -- `previous_epoch_active_gwei`: as per `current_epoch_active_gwei`, but during the previous epoch. - `previous_epoch_target_attesting_gwei`: see `current_epoch_target_attesting_gwei`. - `previous_epoch_head_attesting_gwei`: the total staked gwei that attested to a head beacon block that is in the canonical chain. diff --git a/book/src/validator-manager.md b/book/src/validator-manager.md index a71fab1e3a..11df2af037 100644 --- a/book/src/validator-manager.md +++ b/book/src/validator-manager.md @@ -32,3 +32,4 @@ The `validator-manager` boasts the following features: - [Creating and importing validators using the `create` and `import` commands.](./validator-manager-create.md) - [Moving validators between two VCs using the `move` command.](./validator-manager-move.md) +- [Managing validators such as delete, import and list validators.](./validator-manager-api.md) diff --git a/book/src/validator-monitoring.md b/book/src/validator-monitoring.md index 6439ea83a3..bbc95460ec 100644 --- a/book/src/validator-monitoring.md +++ b/book/src/validator-monitoring.md @@ -134,7 +134,7 @@ validator_monitor_attestation_simulator_source_attester_hit_total validator_monitor_attestation_simulator_source_attester_miss_total ``` -A grafana dashboard to view the metrics for attestation simulator is available [here](https://github.com/sigp/lighthouse-metrics/blob/master/dashboards/AttestationSimulator.json). +A Grafana dashboard to view the metrics for attestation simulator is available [here](https://github.com/sigp/lighthouse-metrics/blob/master/dashboards/AttestationSimulator.json). The attestation simulator provides an insight into the attestation performance of a beacon node. It can be used as an indication of how expediently the beacon node has completed importing blocks within the 4s time frame for an attestation to be made. diff --git a/scripts/local_testnet/README.md b/scripts/local_testnet/README.md index ca701eb7e9..159c89badb 100644 --- a/scripts/local_testnet/README.md +++ b/scripts/local_testnet/README.md @@ -1,6 +1,6 @@ # Simple Local Testnet -These scripts allow for running a small local testnet with a default of 4 beacon nodes, 4 validator clients and 4 geth execution clients using Kurtosis. +These scripts allow for running a small local testnet with a default of 4 beacon nodes, 4 validator clients and 4 Geth execution clients using Kurtosis. This setup can be useful for testing and development. ## Installation @@ -9,7 +9,7 @@ This setup can be useful for testing and development. 1. Install [Kurtosis](https://docs.kurtosis.com/install/). Verify that Kurtosis has been successfully installed by running `kurtosis version` which should display the version. -1. Install [yq](https://github.com/mikefarah/yq). If you are on Ubuntu, you can install `yq` by running `snap install yq`. +1. Install [`yq`](https://github.com/mikefarah/yq). If you are on Ubuntu, you can install `yq` by running `snap install yq`. ## Starting the testnet @@ -22,7 +22,7 @@ cd ./scripts/local_testnet It will build a Lighthouse docker image from the root of the directory and will take an approximately 12 minutes to complete. Once built, the testing will be started automatically. You will see a list of services running and "Started!" at the end. You can also select your own Lighthouse docker image to use by specifying it in `network_params.yml` under the `cl_image` key. -Full configuration reference for kurtosis is specified [here](https://github.com/ethpandaops/ethereum-package?tab=readme-ov-file#configuration). +Full configuration reference for Kurtosis is specified [here](https://github.com/ethpandaops/ethereum-package?tab=readme-ov-file#configuration). To view all running services: @@ -36,7 +36,7 @@ To view the logs: kurtosis service logs local-testnet $SERVICE_NAME ``` -where `$SERVICE_NAME` is obtained by inspecting the running services above. For example, to view the logs of the first beacon node, validator client and geth: +where `$SERVICE_NAME` is obtained by inspecting the running services above. For example, to view the logs of the first beacon node, validator client and Geth: ```bash kurtosis service logs local-testnet -f cl-1-lighthouse-geth diff --git a/wordlist.txt b/wordlist.txt new file mode 100644 index 0000000000..f06c278866 --- /dev/null +++ b/wordlist.txt @@ -0,0 +1,235 @@ +APIs +ARMv +AUR +Backends +Backfilling +Beaconcha +Besu +Broadwell +BIP +BLS +BN +BNs +BTC +BTEC +Casper +CentOS +Chiado +CMake +CoinCashew +Consensys +CORS +CPUs +DBs +DES +DHT +DNS +Dockerhub +DoS +EIP +ENR +Erigon +Esat's +ETH +EthDocker +Ethereum +Ethstaker +Exercism +Extractable +FFG +Geth +Gitcoin +Gnosis +Goerli +Grafana +Holesky +Homebrew +Infura +IPs +IPv +JSON +KeyManager +Kurtosis +LMDB +LLVM +LRU +LTO +Mainnet +MDBX +Merkle +MEV +MSRV +NAT's +Nethermind +NodeJS +NullLogger +PathBuf +PowerShell +PPA +Pre +Proto +PRs +Prysm +QUIC +RasPi +README +RESTful +Reth +RHEL +Ropsten +RPC +Ryzen +Sepolia +Somer +SSD +SSL +SSZ +Styleguide +TCP +Teku +TLS +TODOs +UDP +UI +UPnP +USD +UX +Validator +VC +VCs +VPN +Withdrawable +WSL +YAML +aarch +anonymize +api +attester +backend +backends +backfill +backfilling +beaconcha +bitfield +blockchain +bn +cli +clippy +config +cpu +cryptocurrencies +cryptographic +danksharding +datadir +datadirs +de +decrypt +decrypted +dest +dir +disincentivise +doppelgänger +dropdown +else's +env +eth +ethdo +ethereum +ethstaker +filesystem +frontend +gapped +github +graffitis +gwei +hdiffs +homebrew +hostname +html +http +https +hDiff +implementers +interoperable +io +iowait +jemalloc +json +jwt +kb +keymanager +keypair +keypairs +keystore +keystores +linter +linux +localhost +lossy +macOS +mainnet +makefile +mdBook +mev +misconfiguration +mkcert +namespace +natively +nd +ness +nginx +nitty +oom +orging +orgs +os +paul +pem +performant +pid +pre +pubkey +pubkeys +rc +reimport +resync +roadmap +runtime +rustfmt +rustup +schemas +sigmaprime +sigp +slashable +slashings +spec'd +src +stakers +subnet +subnets +systemd +testnet +testnets +th +toml +topologies +tradeoffs +transactional +tweakers +ui +unadvanced +unaggregated +unencrypted +unfinalized +untrusted +uptimes +url +validator +validators +validator's +vc +virt +webapp +withdrawable +yaml +yml From 1315c94adbc929df39c4ebbd17d627129903e3b6 Mon Sep 17 00:00:00 2001 From: Age Manning Date: Wed, 18 Dec 2024 07:10:53 +1100 Subject: [PATCH 051/254] Unsaturate dial negotiation queue (#6711) * Unsaturate dial-negotiation count --- beacon_node/lighthouse_network/src/rpc/handler.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/beacon_node/lighthouse_network/src/rpc/handler.rs b/beacon_node/lighthouse_network/src/rpc/handler.rs index e76d6d2786..0a0a6ca754 100644 --- a/beacon_node/lighthouse_network/src/rpc/handler.rs +++ b/beacon_node/lighthouse_network/src/rpc/handler.rs @@ -964,6 +964,9 @@ where request_info: (Id, RequestType), error: StreamUpgradeError, ) { + // This dialing is now considered failed + self.dial_negotiated -= 1; + let (id, req) = request_info; // map the error @@ -989,9 +992,6 @@ where StreamUpgradeError::Apply(other) => other, }; - // This dialing is now considered failed - self.dial_negotiated -= 1; - self.outbound_io_error_retries = 0; self.events_out .push(HandlerEvent::Err(HandlerErr::Outbound { From 2662dc7f8fba71a5682f8906a6f6a71374757c23 Mon Sep 17 00:00:00 2001 From: Pawan Dhananjay Date: Wed, 18 Dec 2024 05:35:58 +0530 Subject: [PATCH 052/254] Fix Sse client api (#6685) * Use reqwest eventsource for get_events api * await for Event::Open before returning stream * fmt * Merge branch 'unstable' into sse-client-fix * Ignore lint --- Cargo.lock | 28 ++++++++++++ beacon_node/beacon_chain/tests/store_tests.rs | 1 + common/eth2/Cargo.toml | 1 + common/eth2/src/lib.rs | 43 ++++++++++++++----- common/eth2/src/types.rs | 21 +-------- 5 files changed, 65 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2978a3a19f..c62e9fbc87 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2576,6 +2576,7 @@ dependencies = [ "proto_array", "psutil", "reqwest", + "reqwest-eventsource", "sensitive_url", "serde", "serde_json", @@ -2977,6 +2978,17 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "eventsource-stream" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74fef4569247a5f429d9156b9d0a2599914385dd189c539334c625d8099d90ab" +dependencies = [ + "futures-core", + "nom", + "pin-project-lite", +] + [[package]] name = "execution_engine_integration" version = "0.1.0" @@ -7179,6 +7191,22 @@ dependencies = [ "winreg", ] +[[package]] +name = "reqwest-eventsource" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f529a5ff327743addc322af460761dff5b50e0c826b9e6ac44c3195c50bb2026" +dependencies = [ + "eventsource-stream", + "futures-core", + "futures-timer", + "mime", + "nom", + "pin-project-lite", + "reqwest", + "thiserror 1.0.69", +] + [[package]] name = "resolv-conf" version = "0.7.0" diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index 73805a8525..e1258ccdea 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -2796,6 +2796,7 @@ async fn finalizes_after_resuming_from_db() { ); } +#[allow(clippy::large_stack_frames)] #[tokio::test] async fn revert_minority_fork_on_resume() { let validator_count = 16; diff --git a/common/eth2/Cargo.toml b/common/eth2/Cargo.toml index f735b4c688..912051da36 100644 --- a/common/eth2/Cargo.toml +++ b/common/eth2/Cargo.toml @@ -27,6 +27,7 @@ slashing_protection = { workspace = true } mediatype = "0.19.13" pretty_reqwest_error = { workspace = true } derivative = { workspace = true } +reqwest-eventsource = "0.5.0" [dev-dependencies] tokio = { workspace = true } diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index 522c6414ea..12b1538984 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -27,6 +27,7 @@ use reqwest::{ Body, IntoUrl, RequestBuilder, Response, }; pub use reqwest::{StatusCode, Url}; +use reqwest_eventsource::{Event, EventSource}; pub use sensitive_url::{SensitiveError, SensitiveUrl}; use serde::{de::DeserializeOwned, Serialize}; use ssz::Encode; @@ -52,6 +53,8 @@ pub const SSZ_CONTENT_TYPE_HEADER: &str = "application/octet-stream"; pub enum Error { /// The `reqwest` client raised an error. HttpClient(PrettyReqwestError), + /// The `reqwest_eventsource` client raised an error. + SseClient(reqwest_eventsource::Error), /// The server returned an error message where the body was able to be parsed. ServerMessage(ErrorMessage), /// The server returned an error message with an array of errors. @@ -93,6 +96,13 @@ impl Error { pub fn status(&self) -> Option { match self { Error::HttpClient(error) => error.inner().status(), + Error::SseClient(error) => { + if let reqwest_eventsource::Error::InvalidStatusCode(status, _) = error { + Some(*status) + } else { + None + } + } Error::ServerMessage(msg) => StatusCode::try_from(msg.code).ok(), Error::ServerIndexedMessage(msg) => StatusCode::try_from(msg.code).ok(), Error::StatusCode(status) => Some(*status), @@ -2592,16 +2602,29 @@ impl BeaconNodeHttpClient { .join(","); path.query_pairs_mut().append_pair("topics", &topic_string); - Ok(self - .client - .get(path) - .send() - .await? - .bytes_stream() - .map(|next| match next { - Ok(bytes) => EventKind::from_sse_bytes(bytes.as_ref()), - Err(e) => Err(Error::HttpClient(e.into())), - })) + let mut es = EventSource::get(path); + // If we don't await `Event::Open` here, then the consumer + // will not get any Message events until they start awaiting the stream. + // This is a way to register the stream with the sse server before + // message events start getting emitted. + while let Some(event) = es.next().await { + match event { + Ok(Event::Open) => break, + Err(err) => return Err(Error::SseClient(err)), + // This should never happen as we are guaranteed to get the + // Open event before any message starts coming through. + Ok(Event::Message(_)) => continue, + } + } + Ok(Box::pin(es.filter_map(|event| async move { + match event { + Ok(Event::Open) => None, + Ok(Event::Message(message)) => { + Some(EventKind::from_sse_bytes(&message.event, &message.data)) + } + Err(err) => Some(Err(Error::SseClient(err))), + } + }))) } /// `POST validator/duties/sync/{epoch}` diff --git a/common/eth2/src/types.rs b/common/eth2/src/types.rs index c187399ebd..a303953a86 100644 --- a/common/eth2/src/types.rs +++ b/common/eth2/src/types.rs @@ -13,7 +13,7 @@ use serde_json::Value; use ssz::{Decode, DecodeError}; use ssz_derive::{Decode, Encode}; use std::fmt::{self, Display}; -use std::str::{from_utf8, FromStr}; +use std::str::FromStr; use std::sync::Arc; use std::time::Duration; use types::beacon_block_body::KzgCommitments; @@ -1153,24 +1153,7 @@ impl EventKind { } } - pub fn from_sse_bytes(message: &[u8]) -> Result { - let s = from_utf8(message) - .map_err(|e| ServerError::InvalidServerSentEvent(format!("{:?}", e)))?; - - let mut split = s.split('\n'); - let event = split - .next() - .ok_or_else(|| { - ServerError::InvalidServerSentEvent("Could not parse event tag".to_string()) - })? - .trim_start_matches("event:"); - let data = split - .next() - .ok_or_else(|| { - ServerError::InvalidServerSentEvent("Could not parse data tag".to_string()) - })? - .trim_start_matches("data:"); - + pub fn from_sse_bytes(event: &str, data: &str) -> Result { match event { "attestation" => Ok(EventKind::Attestation(serde_json::from_str(data).map_err( |e| ServerError::InvalidServerSentEvent(format!("Attestation: {:?}", e)), From 10c96f8631d7db0d875dd60f1b9828712b96d01a Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Thu, 19 Dec 2024 16:45:59 +1100 Subject: [PATCH 053/254] Fix anvil 404 link in docs (#6724) * Fix anvil 404 link in docs --- testing/eth1_test_rig/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/eth1_test_rig/src/lib.rs b/testing/eth1_test_rig/src/lib.rs index 015a632ff4..3cba908261 100644 --- a/testing/eth1_test_rig/src/lib.rs +++ b/testing/eth1_test_rig/src/lib.rs @@ -1,6 +1,6 @@ //! Provides utilities for deploying and manipulating the eth2 deposit contract on the eth1 chain. //! -//! Presently used with [`anvil`](https://github.com/foundry-rs/foundry/tree/master/anvil) to simulate +//! Presently used with [`anvil`](https://github.com/foundry-rs/foundry/tree/master/crates/anvil) to simulate //! the deposit contract for testing beacon node eth1 integration. //! //! Not tested to work with actual clients (e.g., geth). It should work fine, however there may be From b2b1faad4e32e710eab905d6d227543c44335986 Mon Sep 17 00:00:00 2001 From: Mac L Date: Thu, 19 Dec 2024 09:46:03 +0400 Subject: [PATCH 054/254] Enforce alphabetically ordered cargo deps (#6678) * Enforce alphabetically ordered cargo deps * Fix test-suite * Another CI fix * Merge branch 'unstable' into cargo-sort * Fix conflicts * Merge remote-tracking branch 'origin/unstable' into cargo-sort --- .github/workflows/test-suite.yml | 16 ++++ Cargo.toml | 49 ++++++++---- account_manager/Cargo.toml | 22 +++--- beacon_node/Cargo.toml | 36 ++++----- beacon_node/beacon_chain/Cargo.toml | 4 +- beacon_node/beacon_processor/Cargo.toml | 24 +++--- beacon_node/builder_client/Cargo.toml | 4 +- beacon_node/client/Cargo.toml | 48 +++++------ beacon_node/eth1/Cargo.toml | 28 +++---- beacon_node/execution_layer/Cargo.toml | 79 +++++++++---------- beacon_node/http_api/Cargo.toml | 66 ++++++++-------- beacon_node/http_metrics/Cargo.toml | 21 +++-- beacon_node/lighthouse_network/Cargo.toml | 66 ++++++++-------- .../lighthouse_network/gossipsub/Cargo.toml | 4 +- beacon_node/network/Cargo.toml | 64 +++++++-------- beacon_node/store/Cargo.toml | 32 ++++---- beacon_node/timer/Cargo.toml | 4 +- boot_node/Cargo.toml | 18 ++--- common/account_utils/Cargo.toml | 13 ++- common/clap_utils/Cargo.toml | 3 +- common/compare_fields_derive/Cargo.toml | 2 +- common/deposit_contract/Cargo.toml | 6 +- common/directory/Cargo.toml | 1 - common/eth2/Cargo.toml | 29 ++++--- common/eth2_config/Cargo.toml | 2 +- common/eth2_interop_keypairs/Cargo.toml | 7 +- common/eth2_network_config/Cargo.toml | 26 +++--- common/eth2_wallet_manager/Cargo.toml | 1 - common/lighthouse_version/Cargo.toml | 1 - common/logging/Cargo.toml | 2 +- common/malloc_utils/Cargo.toml | 2 +- common/monitoring_api/Cargo.toml | 15 ++-- common/oneshot_broadcast/Cargo.toml | 1 - common/pretty_reqwest_error/Cargo.toml | 1 - common/sensitive_url/Cargo.toml | 3 +- common/slot_clock/Cargo.toml | 2 +- common/system_health/Cargo.toml | 6 +- common/task_executor/Cargo.toml | 8 +- common/test_random_derive/Cargo.toml | 2 +- common/unused_port/Cargo.toml | 1 - common/validator_dir/Cargo.toml | 13 ++- common/warp_utils/Cargo.toml | 19 +++-- consensus/fixed_bytes/Cargo.toml | 1 - consensus/fork_choice/Cargo.toml | 7 +- consensus/int_to_bytes/Cargo.toml | 2 +- consensus/proto_array/Cargo.toml | 4 +- consensus/safe_arith/Cargo.toml | 1 - consensus/state_processing/Cargo.toml | 26 +++--- crypto/bls/Cargo.toml | 18 ++--- crypto/eth2_key_derivation/Cargo.toml | 7 +- crypto/eth2_keystore/Cargo.toml | 19 +++-- crypto/eth2_wallet/Cargo.toml | 9 +-- crypto/kzg/Cargo.toml | 13 ++- database_manager/Cargo.toml | 8 +- lcli/Cargo.toml | 46 +++++------ lighthouse/Cargo.toml | 53 ++++++------- lighthouse/environment/Cargo.toml | 18 ++--- slasher/Cargo.toml | 30 +++---- testing/ef_tests/Cargo.toml | 22 +++--- testing/eth1_test_rig/Cargo.toml | 12 +-- .../execution_engine_integration/Cargo.toml | 22 +++--- testing/node_test_rig/Cargo.toml | 14 ++-- testing/simulator/Cargo.toml | 19 +++-- testing/state_transition_vectors/Cargo.toml | 9 +-- testing/test-test_logger/Cargo.toml | 1 - testing/web3signer_tests/Cargo.toml | 33 ++++---- validator_client/Cargo.toml | 10 +-- .../doppelganger_service/Cargo.toml | 2 +- validator_client/graffiti_file/Cargo.toml | 6 +- validator_client/http_api/Cargo.toml | 20 ++--- validator_client/http_metrics/Cargo.toml | 12 +-- .../initialized_validators/Cargo.toml | 18 ++--- validator_client/signing_method/Cargo.toml | 4 +- .../slashing_protection/Cargo.toml | 16 ++-- .../validator_services/Cargo.toml | 10 +-- validator_manager/Cargo.toml | 25 +++--- watch/Cargo.toml | 41 +++++----- 77 files changed, 655 insertions(+), 654 deletions(-) diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index bba670cc22..65663e0cf4 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -428,6 +428,21 @@ jobs: cache-target: release - name: Run Makefile to trigger the bash script run: make cli-local + cargo-sort: + name: cargo-sort + needs: [check-labels] + if: needs.check-labels.outputs.skip_ci != 'true' + 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 + bins: cargo-sort + - name: Run cargo sort to check if Cargo.toml files are sorted + run: cargo sort --check --workspace # 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: @@ -455,6 +470,7 @@ jobs: 'compile-with-beta-compiler', 'cli-check', 'lockbud', + 'cargo-sort', ] steps: - uses: actions/checkout@v4 diff --git a/Cargo.toml b/Cargo.toml index 9e921190b8..23e52a306b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,11 +8,11 @@ members = [ "beacon_node/builder_client", "beacon_node/client", "beacon_node/eth1", - "beacon_node/lighthouse_network", - "beacon_node/lighthouse_network/gossipsub", "beacon_node/execution_layer", "beacon_node/http_api", "beacon_node/http_metrics", + "beacon_node/lighthouse_network", + "beacon_node/lighthouse_network/gossipsub", "beacon_node/network", "beacon_node/store", "beacon_node/timer", @@ -30,40 +30,40 @@ members = [ "common/eth2_interop_keypairs", "common/eth2_network_config", "common/eth2_wallet_manager", - "common/metrics", "common/lighthouse_version", "common/lockfile", "common/logging", "common/lru_cache", "common/malloc_utils", + "common/metrics", + "common/monitoring_api", "common/oneshot_broadcast", "common/pretty_reqwest_error", "common/sensitive_url", "common/slot_clock", "common/system_health", - "common/task_executor", "common/target_check", + "common/task_executor", "common/test_random_derive", "common/unused_port", "common/validator_dir", "common/warp_utils", - "common/monitoring_api", - - "database_manager", - - "consensus/int_to_bytes", "consensus/fixed_bytes", "consensus/fork_choice", + + "consensus/int_to_bytes", "consensus/proto_array", "consensus/safe_arith", "consensus/state_processing", "consensus/swap_or_not_shuffle", "crypto/bls", - "crypto/kzg", "crypto/eth2_key_derivation", "crypto/eth2_keystore", "crypto/eth2_wallet", + "crypto/kzg", + + "database_manager", "lcli", @@ -78,8 +78,8 @@ members = [ "testing/execution_engine_integration", "testing/node_test_rig", "testing/simulator", - "testing/test-test_logger", "testing/state_transition_vectors", + "testing/test-test_logger", "testing/web3signer_tests", "validator_client", @@ -126,8 +126,8 @@ delay_map = "0.4" derivative = "2" dirs = "3" either = "1.9" - # TODO: rust_eth_kzg is pinned for now while a perf regression is investigated - # The crate_crypto_* dependencies can be removed from this file completely once we update +# TODO: rust_eth_kzg is pinned for now while a perf regression is investigated +# The crate_crypto_* dependencies can be removed from this file completely once we update rust_eth_kzg = "=0.5.1" crate_crypto_internal_eth_kzg_bls12_381 = "=0.5.1" crate_crypto_internal_eth_kzg_erasure_codes = "=0.5.1" @@ -167,7 +167,13 @@ r2d2 = "0.8" rand = "0.8" rayon = "1.7" regex = "1" -reqwest = { version = "0.11", default-features = false, features = ["blocking", "json", "stream", "rustls-tls", "native-tls-vendored"] } +reqwest = { version = "0.11", default-features = false, features = [ + "blocking", + "json", + "stream", + "rustls-tls", + "native-tls-vendored", +] } ring = "0.16" rpds = "0.11" rusqlite = { version = "0.28", features = ["bundled"] } @@ -176,7 +182,11 @@ serde_json = "1" serde_repr = "0.1" serde_yaml = "0.9" sha2 = "0.9" -slog = { version = "2", features = ["max_level_debug", "release_max_level_debug", "nested-values"] } +slog = { version = "2", features = [ + "max_level_debug", + "release_max_level_debug", + "nested-values", +] } slog-async = "2" slog-term = "2" sloggers = { version = "2", features = ["json"] } @@ -188,7 +198,12 @@ superstruct = "0.8" syn = "1" sysinfo = "0.26" tempfile = "3" -tokio = { version = "1", features = ["rt-multi-thread", "sync", "signal", "macros"] } +tokio = { version = "1", features = [ + "rt-multi-thread", + "sync", + "signal", + "macros", +] } tokio-stream = { version = "0.1", features = ["sync"] } tokio-util = { version = "0.7", features = ["codec", "compat", "time"] } tracing = "0.1.40" @@ -267,7 +282,7 @@ validator_dir = { path = "common/validator_dir" } validator_http_api = { path = "validator_client/http_api" } validator_http_metrics = { path = "validator_client/http_metrics" } validator_metrics = { path = "validator_client/validator_metrics" } -validator_store= { path = "validator_client/validator_store" } +validator_store = { path = "validator_client/validator_store" } warp_utils = { path = "common/warp_utils" } xdelta3 = { git = "http://github.com/sigp/xdelta3-rs", rev = "50d63cdf1878e5cf3538e9aae5eed34a22c64e4a" } zstd = "0.13" diff --git a/account_manager/Cargo.toml b/account_manager/Cargo.toml index 48230bb281..a7752d621f 100644 --- a/account_manager/Cargo.toml +++ b/account_manager/Cargo.toml @@ -8,25 +8,25 @@ authors = [ edition = { workspace = true } [dependencies] +account_utils = { workspace = true } bls = { workspace = true } clap = { workspace = true } -types = { workspace = true } -environment = { workspace = true } -eth2_network_config = { workspace = true } clap_utils = { workspace = true } directory = { workspace = true } +environment = { workspace = true } +eth2 = { workspace = true } +eth2_keystore = { workspace = true } +eth2_network_config = { workspace = true } eth2_wallet = { workspace = true } eth2_wallet_manager = { path = "../common/eth2_wallet_manager" } -validator_dir = { workspace = true } -tokio = { workspace = true } -eth2_keystore = { workspace = true } -account_utils = { workspace = true } -slashing_protection = { workspace = true } -eth2 = { workspace = true } -safe_arith = { workspace = true } -slot_clock = { workspace = true } filesystem = { workspace = true } +safe_arith = { workspace = true } sensitive_url = { workspace = true } +slashing_protection = { workspace = true } +slot_clock = { workspace = true } +tokio = { workspace = true } +types = { workspace = true } +validator_dir = { workspace = true } zeroize = { workspace = true } [dev-dependencies] diff --git a/beacon_node/Cargo.toml b/beacon_node/Cargo.toml index 15cdf15dc5..7da65ad742 100644 --- a/beacon_node/Cargo.toml +++ b/beacon_node/Cargo.toml @@ -20,28 +20,28 @@ write_ssz_files = [ ] # Writes debugging .ssz files to /tmp during block processing. [dependencies] -eth2_config = { workspace = true } +account_utils = { workspace = true } beacon_chain = { workspace = true } -types = { workspace = true } -store = { workspace = true } -client = { path = "client" } clap = { workspace = true } -slog = { workspace = true } -dirs = { workspace = true } -directory = { workspace = true } -environment = { workspace = true } -task_executor = { workspace = true } -genesis = { workspace = true } -execution_layer = { workspace = true } -lighthouse_network = { workspace = true } -serde_json = { workspace = true } clap_utils = { workspace = true } -hyper = { workspace = true } +client = { path = "client" } +directory = { workspace = true } +dirs = { workspace = true } +environment = { workspace = true } +eth2_config = { workspace = true } +execution_layer = { workspace = true } +genesis = { workspace = true } hex = { workspace = true } -slasher = { workspace = true } +http_api = { workspace = true } +hyper = { workspace = true } +lighthouse_network = { workspace = true } monitoring_api = { workspace = true } sensitive_url = { workspace = true } -http_api = { workspace = true } -unused_port = { workspace = true } +serde_json = { workspace = true } +slasher = { workspace = true } +slog = { workspace = true } +store = { workspace = true } strum = { workspace = true } -account_utils = { workspace = true } +task_executor = { workspace = true } +types = { workspace = true } +unused_port = { workspace = true } diff --git a/beacon_node/beacon_chain/Cargo.toml b/beacon_node/beacon_chain/Cargo.toml index b0fa013180..7b725d3519 100644 --- a/beacon_node/beacon_chain/Cargo.toml +++ b/beacon_node/beacon_chain/Cargo.toml @@ -18,9 +18,9 @@ portable = ["bls/supranational-portable"] test_backfill = [] [dev-dependencies] +criterion = { workspace = true } maplit = { workspace = true } serde_json = { workspace = true } -criterion = { workspace = true } [dependencies] alloy-primitives = { workspace = true } @@ -42,11 +42,11 @@ hex = { workspace = true } int_to_bytes = { workspace = true } itertools = { workspace = true } kzg = { workspace = true } -metrics = { workspace = true } lighthouse_version = { workspace = true } logging = { workspace = true } lru = { workspace = true } merkle_proof = { workspace = true } +metrics = { workspace = true } oneshot_broadcast = { path = "../../common/oneshot_broadcast/" } operation_pool = { workspace = true } parking_lot = { workspace = true } diff --git a/beacon_node/beacon_processor/Cargo.toml b/beacon_node/beacon_processor/Cargo.toml index 9273137bf6..c96e0868d7 100644 --- a/beacon_node/beacon_processor/Cargo.toml +++ b/beacon_node/beacon_processor/Cargo.toml @@ -4,22 +4,22 @@ version = "0.1.0" edition = { workspace = true } [dependencies] -slog = { workspace = true } -itertools = { workspace = true } -logging = { workspace = true } -tokio = { workspace = true } -tokio-util = { workspace = true } -futures = { workspace = true } fnv = { workspace = true } +futures = { workspace = true } +itertools = { workspace = true } +lighthouse_network = { workspace = true } +logging = { workspace = true } +metrics = { workspace = true } +num_cpus = { workspace = true } +parking_lot = { workspace = true } +serde = { workspace = true } +slog = { workspace = true } +slot_clock = { workspace = true } strum = { workspace = true } task_executor = { workspace = true } -slot_clock = { workspace = true } -lighthouse_network = { workspace = true } +tokio = { workspace = true } +tokio-util = { workspace = true } types = { workspace = true } -metrics = { workspace = true } -parking_lot = { workspace = true } -num_cpus = { workspace = true } -serde = { workspace = true } [dev-dependencies] tokio = { workspace = true, features = ["test-util"] } diff --git a/beacon_node/builder_client/Cargo.toml b/beacon_node/builder_client/Cargo.toml index c3658f45c7..3531e81c84 100644 --- a/beacon_node/builder_client/Cargo.toml +++ b/beacon_node/builder_client/Cargo.toml @@ -5,8 +5,8 @@ edition = { workspace = true } authors = ["Sean Anderson "] [dependencies] +eth2 = { workspace = true } +lighthouse_version = { workspace = true } reqwest = { workspace = true } sensitive_url = { workspace = true } -eth2 = { workspace = true } serde = { workspace = true } -lighthouse_version = { workspace = true } diff --git a/beacon_node/client/Cargo.toml b/beacon_node/client/Cargo.toml index 4df13eb3d4..614115eb58 100644 --- a/beacon_node/client/Cargo.toml +++ b/beacon_node/client/Cargo.toml @@ -5,41 +5,41 @@ authors = ["Sigma Prime "] edition = { workspace = true } [dev-dependencies] +operation_pool = { workspace = true } serde_yaml = { workspace = true } state_processing = { workspace = true } -operation_pool = { workspace = true } tokio = { workspace = true } [dependencies] beacon_chain = { workspace = true } -store = { workspace = true } -network = { workspace = true } -timer = { path = "../timer" } -lighthouse_network = { workspace = true } -types = { workspace = true } -eth2_config = { workspace = true } -slot_clock = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } -slog = { workspace = true } -tokio = { workspace = true } -futures = { workspace = true } +beacon_processor = { workspace = true } +directory = { workspace = true } dirs = { workspace = true } +environment = { workspace = true } eth1 = { workspace = true } eth2 = { workspace = true } -kzg = { workspace = true } -sensitive_url = { workspace = true } +eth2_config = { workspace = true } +ethereum_ssz = { workspace = true } +execution_layer = { workspace = true } +futures = { workspace = true } genesis = { workspace = true } -task_executor = { workspace = true } -environment = { workspace = true } -metrics = { workspace = true } -time = "0.3.5" -directory = { workspace = true } http_api = { workspace = true } http_metrics = { path = "../http_metrics" } +kzg = { workspace = true } +lighthouse_network = { workspace = true } +metrics = { workspace = true } +monitoring_api = { workspace = true } +network = { workspace = true } +sensitive_url = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } slasher = { workspace = true } slasher_service = { path = "../../slasher/service" } -monitoring_api = { workspace = true } -execution_layer = { workspace = true } -beacon_processor = { workspace = true } -ethereum_ssz = { workspace = true } +slog = { workspace = true } +slot_clock = { workspace = true } +store = { workspace = true } +task_executor = { workspace = true } +time = "0.3.5" +timer = { path = "../timer" } +tokio = { workspace = true } +types = { workspace = true } diff --git a/beacon_node/eth1/Cargo.toml b/beacon_node/eth1/Cargo.toml index 50400a77e0..8ccd50aad8 100644 --- a/beacon_node/eth1/Cargo.toml +++ b/beacon_node/eth1/Cargo.toml @@ -5,27 +5,27 @@ authors = ["Paul Hauner "] edition = { workspace = true } [dev-dependencies] +environment = { workspace = true } eth1_test_rig = { workspace = true } serde_yaml = { workspace = true } sloggers = { workspace = true } -environment = { workspace = true } [dependencies] -execution_layer = { workspace = true } -futures = { workspace = true } -serde = { workspace = true } -types = { workspace = true } -merkle_proof = { workspace = true } +eth2 = { workspace = true } ethereum_ssz = { workspace = true } ethereum_ssz_derive = { workspace = true } -tree_hash = { workspace = true } -parking_lot = { workspace = true } -slog = { workspace = true } +execution_layer = { workspace = true } +futures = { workspace = true } logging = { workspace = true } -superstruct = { workspace = true } -tokio = { workspace = true } -state_processing = { workspace = true } +merkle_proof = { workspace = true } metrics = { workspace = true } -task_executor = { workspace = true } -eth2 = { workspace = true } +parking_lot = { workspace = true } sensitive_url = { workspace = true } +serde = { workspace = true } +slog = { workspace = true } +state_processing = { workspace = true } +superstruct = { workspace = true } +task_executor = { workspace = true } +tokio = { workspace = true } +tree_hash = { workspace = true } +types = { workspace = true } diff --git a/beacon_node/execution_layer/Cargo.toml b/beacon_node/execution_layer/Cargo.toml index 0ef101fae7..7eb7b4a15e 100644 --- a/beacon_node/execution_layer/Cargo.toml +++ b/beacon_node/execution_layer/Cargo.toml @@ -2,54 +2,53 @@ name = "execution_layer" version = "0.1.0" edition = { workspace = true } - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +alloy-consensus = { workspace = true } alloy-primitives = { workspace = true } -types = { workspace = true } -tokio = { workspace = true } -slog = { workspace = true } -logging = { workspace = true } -sensitive_url = { workspace = true } -reqwest = { workspace = true } -ethereum_serde_utils = { workspace = true } -serde_json = { workspace = true } -serde = { workspace = true } -warp = { workspace = true } -jsonwebtoken = "9" +alloy-rlp = { workspace = true } +arc-swap = "1.6.0" +builder_client = { path = "../builder_client" } bytes = { workspace = true } -task_executor = { workspace = true } -hex = { workspace = true } -ethereum_ssz = { workspace = true } -ssz_types = { workspace = true } eth2 = { workspace = true } +eth2_network_config = { workspace = true } +ethereum_serde_utils = { workspace = true } +ethereum_ssz = { workspace = true } +ethers-core = { workspace = true } +fixed_bytes = { workspace = true } +fork_choice = { workspace = true } +hash-db = "0.15.2" +hash256-std-hasher = "0.15.2" +hex = { workspace = true } +jsonwebtoken = "9" +keccak-hash = "0.10.0" kzg = { workspace = true } -state_processing = { workspace = true } -superstruct = { workspace = true } +lighthouse_version = { workspace = true } +logging = { workspace = true } lru = { workspace = true } +metrics = { workspace = true } +parking_lot = { workspace = true } +pretty_reqwest_error = { workspace = true } +rand = { workspace = true } +reqwest = { workspace = true } +sensitive_url = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +sha2 = { workspace = true } +slog = { workspace = true } +slot_clock = { workspace = true } +ssz_types = { workspace = true } +state_processing = { workspace = true } +strum = { workspace = true } +superstruct = { workspace = true } +task_executor = { workspace = true } +tempfile = { workspace = true } +tokio = { workspace = true } +tokio-stream = { workspace = true } tree_hash = { workspace = true } tree_hash_derive = { workspace = true } -parking_lot = { workspace = true } -slot_clock = { workspace = true } -tempfile = { workspace = true } -rand = { workspace = true } -zeroize = { workspace = true } -metrics = { workspace = true } -ethers-core = { workspace = true } -builder_client = { path = "../builder_client" } -fork_choice = { workspace = true } -tokio-stream = { workspace = true } -strum = { workspace = true } -keccak-hash = "0.10.0" -hash256-std-hasher = "0.15.2" triehash = "0.8.4" -hash-db = "0.15.2" -pretty_reqwest_error = { workspace = true } -arc-swap = "1.6.0" -eth2_network_config = { workspace = true } -alloy-rlp = { workspace = true } -alloy-consensus = { workspace = true } -lighthouse_version = { workspace = true } -fixed_bytes = { workspace = true } -sha2 = { workspace = true } +types = { workspace = true } +warp = { workspace = true } +zeroize = { workspace = true } diff --git a/beacon_node/http_api/Cargo.toml b/beacon_node/http_api/Cargo.toml index 638fe0f219..5d601008bc 100644 --- a/beacon_node/http_api/Cargo.toml +++ b/beacon_node/http_api/Cargo.toml @@ -6,49 +6,49 @@ edition = { workspace = true } autotests = false # using a single test binary compiles faster [dependencies] -warp = { workspace = true } -serde = { workspace = true } -tokio = { workspace = true } -tokio-stream = { workspace = true } -types = { workspace = true } -hex = { workspace = true } beacon_chain = { workspace = true } -eth2 = { workspace = true } -slog = { workspace = true } -network = { workspace = true } -lighthouse_network = { workspace = true } -eth1 = { workspace = true } -state_processing = { workspace = true } -lighthouse_version = { workspace = true } -metrics = { workspace = true } -warp_utils = { workspace = true } -slot_clock = { workspace = true } -ethereum_ssz = { workspace = true } +beacon_processor = { workspace = true } bs58 = "0.4.0" -futures = { workspace = true } +bytes = { workspace = true } +directory = { workspace = true } +eth1 = { workspace = true } +eth2 = { workspace = true } +ethereum_serde_utils = { workspace = true } +ethereum_ssz = { workspace = true } execution_layer = { workspace = true } -parking_lot = { workspace = true } -safe_arith = { workspace = true } -task_executor = { workspace = true } +futures = { workspace = true } +hex = { workspace = true } +lighthouse_network = { workspace = true } +lighthouse_version = { workspace = true } +logging = { workspace = true } lru = { workspace = true } -tree_hash = { workspace = true } +metrics = { workspace = true } +network = { workspace = true } +operation_pool = { workspace = true } +parking_lot = { workspace = true } +rand = { workspace = true } +safe_arith = { workspace = true } +sensitive_url = { workspace = true } +serde = { workspace = true } +slog = { workspace = true } +slot_clock = { workspace = true } +state_processing = { workspace = true } +store = { workspace = true } sysinfo = { workspace = true } system_health = { path = "../../common/system_health" } -directory = { workspace = true } -logging = { workspace = true } -ethereum_serde_utils = { workspace = true } -operation_pool = { workspace = true } -sensitive_url = { workspace = true } -store = { workspace = true } -bytes = { workspace = true } -beacon_processor = { workspace = true } -rand = { workspace = true } +task_executor = { workspace = true } +tokio = { workspace = true } +tokio-stream = { workspace = true } +tree_hash = { workspace = true } +types = { workspace = true } +warp = { workspace = true } +warp_utils = { workspace = true } [dev-dependencies] -serde_json = { workspace = true } -proto_array = { workspace = true } genesis = { workspace = true } logging = { workspace = true } +proto_array = { workspace = true } +serde_json = { workspace = true } [[test]] name = "bn_http_api_tests" diff --git a/beacon_node/http_metrics/Cargo.toml b/beacon_node/http_metrics/Cargo.toml index 97ba72a2ac..d92f986440 100644 --- a/beacon_node/http_metrics/Cargo.toml +++ b/beacon_node/http_metrics/Cargo.toml @@ -3,24 +3,23 @@ name = "http_metrics" version = "0.1.0" authors = ["Paul Hauner "] edition = { workspace = true } - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -warp = { workspace = true } +beacon_chain = { workspace = true } +lighthouse_network = { workspace = true } +lighthouse_version = { workspace = true } +malloc_utils = { workspace = true } +metrics = { workspace = true } serde = { workspace = true } slog = { workspace = true } -beacon_chain = { workspace = true } -store = { workspace = true } -lighthouse_network = { workspace = true } slot_clock = { workspace = true } -metrics = { workspace = true } -lighthouse_version = { workspace = true } +store = { workspace = true } +warp = { workspace = true } warp_utils = { workspace = true } -malloc_utils = { workspace = true } [dev-dependencies] -tokio = { workspace = true } -reqwest = { workspace = true } -types = { workspace = true } logging = { workspace = true } +reqwest = { workspace = true } +tokio = { workspace = true } +types = { workspace = true } diff --git a/beacon_node/lighthouse_network/Cargo.toml b/beacon_node/lighthouse_network/Cargo.toml index eccc244d59..485f32b37a 100644 --- a/beacon_node/lighthouse_network/Cargo.toml +++ b/beacon_node/lighthouse_network/Cargo.toml @@ -5,49 +5,49 @@ authors = ["Sigma Prime "] edition = { workspace = true } [dependencies] -alloy-primitives = { workspace = true} +alloy-primitives = { workspace = true } +alloy-rlp = { workspace = true } +bytes = { workspace = true } +delay_map = { workspace = true } +directory = { workspace = true } +dirs = { workspace = true } discv5 = { workspace = true } -gossipsub = { workspace = true } -unsigned-varint = { version = "0.8", features = ["codec"] } -ssz_types = { workspace = true } -types = { workspace = true } -serde = { workspace = true } +either = { workspace = true } ethereum_ssz = { workspace = true } ethereum_ssz_derive = { workspace = true } -slog = { workspace = true } -lighthouse_version = { workspace = true } -tokio = { workspace = true } -futures = { workspace = true } -dirs = { workspace = true } fnv = { workspace = true } -metrics = { workspace = true } -smallvec = { workspace = true } -tokio-io-timeout = "1" +futures = { workspace = true } +gossipsub = { workspace = true } +hex = { workspace = true } +itertools = { workspace = true } +libp2p-mplex = "0.42" +lighthouse_version = { workspace = true } lru = { workspace = true } lru_cache = { workspace = true } +metrics = { workspace = true } parking_lot = { workspace = true } -sha2 = { workspace = true } -snap = { workspace = true } -hex = { workspace = true } -tokio-util = { workspace = true } -tiny-keccak = "2" -task_executor = { workspace = true } +prometheus-client = "0.22.0" rand = { workspace = true } -directory = { workspace = true } regex = { workspace = true } +serde = { workspace = true } +sha2 = { workspace = true } +slog = { workspace = true } +smallvec = { workspace = true } +snap = { workspace = true } +ssz_types = { workspace = true } strum = { workspace = true } superstruct = { workspace = true } -prometheus-client = "0.22.0" +task_executor = { workspace = true } +tiny-keccak = "2" +tokio = { workspace = true } +tokio-io-timeout = "1" +tokio-util = { workspace = true } +types = { workspace = true } +unsigned-varint = { version = "0.8", features = ["codec"] } unused_port = { workspace = true } -delay_map = { workspace = true } -bytes = { workspace = true } -either = { workspace = true } -itertools = { workspace = true } -alloy-rlp = { workspace = true } # Local dependencies void = "1.0.2" -libp2p-mplex = "0.42" [dependencies.libp2p] version = "0.54" @@ -55,13 +55,13 @@ default-features = false features = ["identify", "yamux", "noise", "dns", "tcp", "tokio", "plaintext", "secp256k1", "macros", "ecdsa", "metrics", "quic", "upnp"] [dev-dependencies] -slog-term = { workspace = true } -slog-async = { workspace = true } -tempfile = { workspace = true } -quickcheck = { workspace = true } -quickcheck_macros = { workspace = true } async-channel = { workspace = true } logging = { workspace = true } +quickcheck = { workspace = true } +quickcheck_macros = { workspace = true } +slog-async = { workspace = true } +slog-term = { workspace = true } +tempfile = { workspace = true } [features] libp2p-websocket = [] diff --git a/beacon_node/lighthouse_network/gossipsub/Cargo.toml b/beacon_node/lighthouse_network/gossipsub/Cargo.toml index 6cbe6d3a1c..61f5730c08 100644 --- a/beacon_node/lighthouse_network/gossipsub/Cargo.toml +++ b/beacon_node/lighthouse_network/gossipsub/Cargo.toml @@ -24,9 +24,10 @@ fnv = "1.0.7" futures = "0.3.30" futures-timer = "3.0.2" getrandom = "0.2.12" -hashlink.workspace = true +hashlink = { workspace = true } hex_fmt = "0.3.0" libp2p = { version = "0.54", default-features = false } +prometheus-client = "0.22.0" quick-protobuf = "0.8" quick-protobuf-codec = "0.3" rand = "0.8" @@ -35,7 +36,6 @@ serde = { version = "1", optional = true, features = ["derive"] } sha2 = "0.10.8" tracing = "0.1.37" void = "1.0.2" -prometheus-client = "0.22.0" web-time = "1.1.0" [dev-dependencies] diff --git a/beacon_node/network/Cargo.toml b/beacon_node/network/Cargo.toml index 6fc818e9c9..44f6c54bbc 100644 --- a/beacon_node/network/Cargo.toml +++ b/beacon_node/network/Cargo.toml @@ -5,51 +5,51 @@ authors = ["Sigma Prime "] edition = { workspace = true } [dev-dependencies] -sloggers = { workspace = true } +bls = { workspace = true } +eth2 = { workspace = true } +eth2_network_config = { workspace = true } genesis = { workspace = true } +gossipsub = { workspace = true } +kzg = { workspace = true } matches = "0.1.8" serde_json = { workspace = true } -slog-term = { workspace = true } slog-async = { workspace = true } -eth2 = { workspace = true } -gossipsub = { workspace = true } -eth2_network_config = { workspace = true } -kzg = { workspace = true } -bls = { workspace = true } +slog-term = { workspace = true } +sloggers = { workspace = true } [dependencies] alloy-primitives = { workspace = true } -async-channel = { workspace = true } -anyhow = { workspace = true } -beacon_chain = { workspace = true } -store = { workspace = true } -lighthouse_network = { workspace = true } -types = { workspace = true } -slot_clock = { workspace = true } -slog = { workspace = true } -hex = { workspace = true } -ethereum_ssz = { workspace = true } -ssz_types = { workspace = true } -futures = { workspace = true } -tokio = { workspace = true } -tokio-stream = { workspace = true } -smallvec = { workspace = true } -rand = { workspace = true } -fnv = { workspace = true } alloy-rlp = { workspace = true } -metrics = { workspace = true } -logging = { workspace = true } -task_executor = { workspace = true } +anyhow = { workspace = true } +async-channel = { workspace = true } +beacon_chain = { workspace = true } +beacon_processor = { workspace = true } +delay_map = { workspace = true } +derivative = { workspace = true } +ethereum_ssz = { workspace = true } +execution_layer = { workspace = true } +fnv = { workspace = true } +futures = { workspace = true } +hex = { workspace = true } igd-next = "0.14" itertools = { workspace = true } +lighthouse_network = { workspace = true } +logging = { workspace = true } lru_cache = { workspace = true } -strum = { workspace = true } -derivative = { workspace = true } -delay_map = { workspace = true } +metrics = { workspace = true } operation_pool = { workspace = true } -execution_layer = { workspace = true } -beacon_processor = { workspace = true } parking_lot = { workspace = true } +rand = { workspace = true } +slog = { workspace = true } +slot_clock = { workspace = true } +smallvec = { workspace = true } +ssz_types = { workspace = true } +store = { workspace = true } +strum = { workspace = true } +task_executor = { workspace = true } +tokio = { workspace = true } +tokio-stream = { workspace = true } +types = { workspace = true } [features] # NOTE: This can be run via cargo build --bin lighthouse --features network/disable-backfill diff --git a/beacon_node/store/Cargo.toml b/beacon_node/store/Cargo.toml index 7cee16c353..21d0cf8dec 100644 --- a/beacon_node/store/Cargo.toml +++ b/beacon_node/store/Cargo.toml @@ -5,34 +5,34 @@ authors = ["Paul Hauner "] edition = { workspace = true } [dev-dependencies] -tempfile = { workspace = true } beacon_chain = { workspace = true } criterion = { workspace = true } rand = { workspace = true, features = ["small_rng"] } +tempfile = { workspace = true } [dependencies] +bls = { workspace = true } db-key = "0.0.5" -leveldb = { version = "0.8" } -parking_lot = { workspace = true } -itertools = { workspace = true } +directory = { workspace = true } ethereum_ssz = { workspace = true } ethereum_ssz_derive = { workspace = true } +itertools = { workspace = true } +leveldb = { version = "0.8" } +logging = { workspace = true } +lru = { workspace = true } +metrics = { workspace = true } +parking_lot = { workspace = true } +safe_arith = { workspace = true } +serde = { workspace = true } +slog = { workspace = true } +sloggers = { workspace = true } +smallvec = { workspace = true } +state_processing = { workspace = true } +strum = { workspace = true } superstruct = { workspace = true } types = { workspace = true } -safe_arith = { workspace = true } -state_processing = { workspace = true } -slog = { workspace = true } -serde = { workspace = true } -metrics = { workspace = true } -lru = { workspace = true } -sloggers = { workspace = true } -directory = { workspace = true } -strum = { workspace = true } xdelta3 = { workspace = true } zstd = { workspace = true } -bls = { workspace = true } -smallvec = { workspace = true } -logging = { workspace = true } [[bench]] name = "hdiff" diff --git a/beacon_node/timer/Cargo.toml b/beacon_node/timer/Cargo.toml index afb93f3657..546cc2ed41 100644 --- a/beacon_node/timer/Cargo.toml +++ b/beacon_node/timer/Cargo.toml @@ -6,7 +6,7 @@ edition = { workspace = true } [dependencies] beacon_chain = { workspace = true } -slot_clock = { workspace = true } -tokio = { workspace = true } slog = { workspace = true } +slot_clock = { workspace = true } task_executor = { workspace = true } +tokio = { workspace = true } diff --git a/boot_node/Cargo.toml b/boot_node/Cargo.toml index c60d308cbb..7c8d2b16fd 100644 --- a/boot_node/Cargo.toml +++ b/boot_node/Cargo.toml @@ -6,19 +6,19 @@ edition = { workspace = true } [dependencies] beacon_node = { workspace = true } +bytes = { workspace = true } clap = { workspace = true } clap_utils = { workspace = true } -lighthouse_network = { workspace = true } -types = { workspace = true } +eth2_network_config = { workspace = true } ethereum_ssz = { workspace = true } -slog = { workspace = true } -tokio = { workspace = true } +hex = { workspace = true } +lighthouse_network = { workspace = true } log = { workspace = true } -slog-term = { workspace = true } logging = { workspace = true } +serde = { workspace = true } +slog = { workspace = true } slog-async = { workspace = true } slog-scope = "4.3.0" -hex = { workspace = true } -serde = { workspace = true } -eth2_network_config = { workspace = true } -bytes = { workspace = true } +slog-term = { workspace = true } +tokio = { workspace = true } +types = { workspace = true } diff --git a/common/account_utils/Cargo.toml b/common/account_utils/Cargo.toml index e66bf14233..dece975d37 100644 --- a/common/account_utils/Cargo.toml +++ b/common/account_utils/Cargo.toml @@ -3,20 +3,19 @@ name = "account_utils" version = "0.1.0" authors = ["Paul Hauner "] edition = { workspace = true } - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -rand = { workspace = true } -eth2_wallet = { workspace = true } +directory = { workspace = true } eth2_keystore = { workspace = true } +eth2_wallet = { workspace = true } filesystem = { workspace = true } -zeroize = { workspace = true } +rand = { workspace = true } +regex = { workspace = true } +rpassword = "5.0.0" serde = { workspace = true } serde_yaml = { workspace = true } slog = { workspace = true } types = { workspace = true } validator_dir = { workspace = true } -regex = { workspace = true } -rpassword = "5.0.0" -directory = { workspace = true } +zeroize = { workspace = true } diff --git a/common/clap_utils/Cargo.toml b/common/clap_utils/Cargo.toml index 73823ae24e..f3c166bda9 100644 --- a/common/clap_utils/Cargo.toml +++ b/common/clap_utils/Cargo.toml @@ -3,16 +3,15 @@ name = "clap_utils" version = "0.1.0" authors = ["Paul Hauner "] edition = { workspace = true } - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] alloy-primitives = { workspace = true } clap = { workspace = true } -hex = { workspace = true } dirs = { workspace = true } eth2_network_config = { workspace = true } ethereum_ssz = { workspace = true } +hex = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } serde_yaml = { workspace = true } diff --git a/common/compare_fields_derive/Cargo.toml b/common/compare_fields_derive/Cargo.toml index b4bbbaa436..19682bf367 100644 --- a/common/compare_fields_derive/Cargo.toml +++ b/common/compare_fields_derive/Cargo.toml @@ -8,5 +8,5 @@ edition = { workspace = true } proc-macro = true [dependencies] -syn = { workspace = true } quote = { workspace = true } +syn = { workspace = true } diff --git a/common/deposit_contract/Cargo.toml b/common/deposit_contract/Cargo.toml index a03ac2178f..953fde1af7 100644 --- a/common/deposit_contract/Cargo.toml +++ b/common/deposit_contract/Cargo.toml @@ -7,13 +7,13 @@ edition = { workspace = true } build = "build.rs" [build-dependencies] +hex = { workspace = true } reqwest = { workspace = true } serde_json = { workspace = true } sha2 = { workspace = true } -hex = { workspace = true } [dependencies] -types = { workspace = true } +ethabi = "16.0.0" ethereum_ssz = { workspace = true } tree_hash = { workspace = true } -ethabi = "16.0.0" +types = { workspace = true } diff --git a/common/directory/Cargo.toml b/common/directory/Cargo.toml index f724337261..9c3ced9097 100644 --- a/common/directory/Cargo.toml +++ b/common/directory/Cargo.toml @@ -3,7 +3,6 @@ name = "directory" version = "0.1.0" authors = ["pawan "] edition = { workspace = true } - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] diff --git a/common/eth2/Cargo.toml b/common/eth2/Cargo.toml index 912051da36..9d6dea100d 100644 --- a/common/eth2/Cargo.toml +++ b/common/eth2/Cargo.toml @@ -3,31 +3,30 @@ name = "eth2" version = "0.1.0" authors = ["Paul Hauner "] edition = { workspace = true } - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -serde = { workspace = true } -serde_json = { workspace = true } -ssz_types = { workspace = true } -types = { workspace = true } -reqwest = { workspace = true } -lighthouse_network = { workspace = true } -proto_array = { workspace = true } -ethereum_serde_utils = { workspace = true } +derivative = { workspace = true } eth2_keystore = { workspace = true } -zeroize = { workspace = true } -sensitive_url = { workspace = true } +ethereum_serde_utils = { workspace = true } ethereum_ssz = { workspace = true } ethereum_ssz_derive = { workspace = true } -futures-util = "0.3.8" futures = { workspace = true } -store = { workspace = true } -slashing_protection = { workspace = true } +futures-util = "0.3.8" +lighthouse_network = { workspace = true } mediatype = "0.19.13" pretty_reqwest_error = { workspace = true } -derivative = { workspace = true } +proto_array = { workspace = true } +reqwest = { workspace = true } reqwest-eventsource = "0.5.0" +sensitive_url = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +slashing_protection = { workspace = true } +ssz_types = { workspace = true } +store = { workspace = true } +types = { workspace = true } +zeroize = { workspace = true } [dev-dependencies] tokio = { workspace = true } diff --git a/common/eth2_config/Cargo.toml b/common/eth2_config/Cargo.toml index 20c3b0b6f2..509f5ff87e 100644 --- a/common/eth2_config/Cargo.toml +++ b/common/eth2_config/Cargo.toml @@ -5,5 +5,5 @@ authors = ["Paul Hauner "] edition = { workspace = true } [dependencies] -types = { workspace = true } paste = { workspace = true } +types = { workspace = true } diff --git a/common/eth2_interop_keypairs/Cargo.toml b/common/eth2_interop_keypairs/Cargo.toml index 5971b934e0..c19b32014e 100644 --- a/common/eth2_interop_keypairs/Cargo.toml +++ b/common/eth2_interop_keypairs/Cargo.toml @@ -3,16 +3,15 @@ name = "eth2_interop_keypairs" version = "0.2.0" authors = ["Paul Hauner "] edition = { workspace = true } - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -num-bigint = "0.4.2" +bls = { workspace = true } ethereum_hashing = { workspace = true } hex = { workspace = true } -serde_yaml = { workspace = true } +num-bigint = "0.4.2" serde = { workspace = true } -bls = { workspace = true } +serde_yaml = { workspace = true } [dev-dependencies] base64 = "0.13.0" diff --git a/common/eth2_network_config/Cargo.toml b/common/eth2_network_config/Cargo.toml index 09cf2072d2..a255e04229 100644 --- a/common/eth2_network_config/Cargo.toml +++ b/common/eth2_network_config/Cargo.toml @@ -7,25 +7,25 @@ edition = { workspace = true } build = "build.rs" [build-dependencies] -zip = { workspace = true } eth2_config = { workspace = true } +zip = { workspace = true } [dev-dependencies] +ethereum_ssz = { workspace = true } tempfile = { workspace = true } tokio = { workspace = true } -ethereum_ssz = { workspace = true } [dependencies] -serde_yaml = { workspace = true } -types = { workspace = true } -eth2_config = { workspace = true } -discv5 = { workspace = true } -reqwest = { workspace = true } -pretty_reqwest_error = { workspace = true } -sha2 = { workspace = true } -url = { workspace = true } -sensitive_url = { workspace = true } -slog = { workspace = true } -logging = { workspace = true } bytes = { workspace = true } +discv5 = { workspace = true } +eth2_config = { workspace = true } kzg = { workspace = true } +logging = { workspace = true } +pretty_reqwest_error = { workspace = true } +reqwest = { workspace = true } +sensitive_url = { workspace = true } +serde_yaml = { workspace = true } +sha2 = { workspace = true } +slog = { workspace = true } +types = { workspace = true } +url = { workspace = true } diff --git a/common/eth2_wallet_manager/Cargo.toml b/common/eth2_wallet_manager/Cargo.toml index f471757065..a6eb24c78c 100644 --- a/common/eth2_wallet_manager/Cargo.toml +++ b/common/eth2_wallet_manager/Cargo.toml @@ -3,7 +3,6 @@ name = "eth2_wallet_manager" version = "0.1.0" authors = ["Paul Hauner "] edition = { workspace = true } - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] diff --git a/common/lighthouse_version/Cargo.toml b/common/lighthouse_version/Cargo.toml index 3c4f9fe50c..164e3e47a7 100644 --- a/common/lighthouse_version/Cargo.toml +++ b/common/lighthouse_version/Cargo.toml @@ -3,7 +3,6 @@ name = "lighthouse_version" version = "0.1.0" authors = ["Paul Hauner "] edition = { workspace = true } - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] diff --git a/common/logging/Cargo.toml b/common/logging/Cargo.toml index 73cbdf44d4..b2829a48d8 100644 --- a/common/logging/Cargo.toml +++ b/common/logging/Cargo.toml @@ -19,7 +19,7 @@ sloggers = { workspace = true } take_mut = "0.2.2" tokio = { workspace = true, features = [ "time" ] } tracing = "0.1" +tracing-appender = { workspace = true } tracing-core = { workspace = true } tracing-log = { workspace = true } tracing-subscriber = { workspace = true } -tracing-appender = { workspace = true } diff --git a/common/malloc_utils/Cargo.toml b/common/malloc_utils/Cargo.toml index 79a07eed16..64fb7b9aad 100644 --- a/common/malloc_utils/Cargo.toml +++ b/common/malloc_utils/Cargo.toml @@ -5,8 +5,8 @@ authors = ["Paul Hauner "] edition = { workspace = true } [dependencies] -metrics = { workspace = true } libc = "0.2.79" +metrics = { workspace = true } parking_lot = { workspace = true } tikv-jemalloc-ctl = { version = "0.6.0", optional = true, features = ["stats"] } diff --git a/common/monitoring_api/Cargo.toml b/common/monitoring_api/Cargo.toml index 2da32c307e..5008c86e85 100644 --- a/common/monitoring_api/Cargo.toml +++ b/common/monitoring_api/Cargo.toml @@ -3,19 +3,18 @@ name = "monitoring_api" version = "0.1.0" authors = ["pawan "] edition = { workspace = true } - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -reqwest = { workspace = true } -task_executor = { workspace = true } -tokio = { workspace = true } eth2 = { workspace = true } -serde_json = { workspace = true } -serde = { workspace = true } lighthouse_version = { workspace = true } metrics = { workspace = true } +regex = { workspace = true } +reqwest = { workspace = true } +sensitive_url = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } slog = { workspace = true } store = { workspace = true } -regex = { workspace = true } -sensitive_url = { workspace = true } +task_executor = { workspace = true } +tokio = { workspace = true } diff --git a/common/oneshot_broadcast/Cargo.toml b/common/oneshot_broadcast/Cargo.toml index 12c9b40bc8..8a358ef851 100644 --- a/common/oneshot_broadcast/Cargo.toml +++ b/common/oneshot_broadcast/Cargo.toml @@ -2,7 +2,6 @@ name = "oneshot_broadcast" version = "0.1.0" edition = { workspace = true } - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] diff --git a/common/pretty_reqwest_error/Cargo.toml b/common/pretty_reqwest_error/Cargo.toml index dc79832cd3..4311601bcd 100644 --- a/common/pretty_reqwest_error/Cargo.toml +++ b/common/pretty_reqwest_error/Cargo.toml @@ -2,7 +2,6 @@ name = "pretty_reqwest_error" version = "0.1.0" edition = { workspace = true } - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] diff --git a/common/sensitive_url/Cargo.toml b/common/sensitive_url/Cargo.toml index d218c8d93a..ff56209722 100644 --- a/common/sensitive_url/Cargo.toml +++ b/common/sensitive_url/Cargo.toml @@ -3,9 +3,8 @@ name = "sensitive_url" version = "0.1.0" authors = ["Mac L "] edition = { workspace = true } - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -url = { workspace = true } serde = { workspace = true } +url = { workspace = true } diff --git a/common/slot_clock/Cargo.toml b/common/slot_clock/Cargo.toml index c2f330cd50..2e1982efb1 100644 --- a/common/slot_clock/Cargo.toml +++ b/common/slot_clock/Cargo.toml @@ -5,6 +5,6 @@ authors = ["Paul Hauner "] edition = { workspace = true } [dependencies] -types = { workspace = true } metrics = { workspace = true } parking_lot = { workspace = true } +types = { workspace = true } diff --git a/common/system_health/Cargo.toml b/common/system_health/Cargo.toml index be339f2779..034683f72e 100644 --- a/common/system_health/Cargo.toml +++ b/common/system_health/Cargo.toml @@ -5,7 +5,7 @@ edition = { workspace = true } [dependencies] lighthouse_network = { workspace = true } -types = { workspace = true } -sysinfo = { workspace = true } -serde = { workspace = true } parking_lot = { workspace = true } +serde = { workspace = true } +sysinfo = { workspace = true } +types = { workspace = true } diff --git a/common/task_executor/Cargo.toml b/common/task_executor/Cargo.toml index 26bcd7b339..c1ac4b55a9 100644 --- a/common/task_executor/Cargo.toml +++ b/common/task_executor/Cargo.toml @@ -11,10 +11,10 @@ tracing = ["dep:tracing"] [dependencies] async-channel = { workspace = true } -tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } -slog = { workspace = true, optional = true } futures = { workspace = true } -metrics = { workspace = true } -sloggers = { workspace = true, optional = true } logging = { workspace = true, optional = true } +metrics = { workspace = true } +slog = { workspace = true, optional = true } +sloggers = { workspace = true, optional = true } +tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } tracing = { workspace = true, optional = true } diff --git a/common/test_random_derive/Cargo.toml b/common/test_random_derive/Cargo.toml index 79308797a4..b38d5ef63a 100644 --- a/common/test_random_derive/Cargo.toml +++ b/common/test_random_derive/Cargo.toml @@ -9,5 +9,5 @@ description = "Procedural derive macros for implementation of TestRandom trait" proc-macro = true [dependencies] -syn = { workspace = true } quote = { workspace = true } +syn = { workspace = true } diff --git a/common/unused_port/Cargo.toml b/common/unused_port/Cargo.toml index 95dbf59186..2d771cd600 100644 --- a/common/unused_port/Cargo.toml +++ b/common/unused_port/Cargo.toml @@ -2,7 +2,6 @@ name = "unused_port" version = "0.1.0" edition = { workspace = true } - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] diff --git a/common/validator_dir/Cargo.toml b/common/validator_dir/Cargo.toml index ae8742fe07..773431c93c 100644 --- a/common/validator_dir/Cargo.toml +++ b/common/validator_dir/Cargo.toml @@ -6,21 +6,20 @@ edition = { workspace = true } [features] insecure_keys = [] - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] bls = { workspace = true } +deposit_contract = { workspace = true } +derivative = { workspace = true } +directory = { workspace = true } eth2_keystore = { workspace = true } filesystem = { workspace = true } -types = { workspace = true } -rand = { workspace = true } -deposit_contract = { workspace = true } -tree_hash = { workspace = true } hex = { workspace = true } -derivative = { workspace = true } lockfile = { workspace = true } -directory = { workspace = true } +rand = { workspace = true } +tree_hash = { workspace = true } +types = { workspace = true } [dev-dependencies] tempfile = { workspace = true } diff --git a/common/warp_utils/Cargo.toml b/common/warp_utils/Cargo.toml index a9407c392d..4a3cde54a9 100644 --- a/common/warp_utils/Cargo.toml +++ b/common/warp_utils/Cargo.toml @@ -3,20 +3,19 @@ name = "warp_utils" version = "0.1.0" authors = ["Paul Hauner "] edition = { workspace = true } - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -warp = { workspace = true } -eth2 = { workspace = true } -types = { workspace = true } beacon_chain = { workspace = true } -state_processing = { workspace = true } -safe_arith = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } -tokio = { workspace = true } +bytes = { workspace = true } +eth2 = { workspace = true } headers = "0.3.2" metrics = { workspace = true } +safe_arith = { workspace = true } +serde = { workspace = true } serde_array_query = "0.1.0" -bytes = { workspace = true } +serde_json = { workspace = true } +state_processing = { workspace = true } +tokio = { workspace = true } +types = { workspace = true } +warp = { workspace = true } diff --git a/consensus/fixed_bytes/Cargo.toml b/consensus/fixed_bytes/Cargo.toml index e5201a0455..ab29adfb1b 100644 --- a/consensus/fixed_bytes/Cargo.toml +++ b/consensus/fixed_bytes/Cargo.toml @@ -3,7 +3,6 @@ name = "fixed_bytes" version = "0.1.0" authors = ["Eitan Seri-Levi "] edition = { workspace = true } - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] diff --git a/consensus/fork_choice/Cargo.toml b/consensus/fork_choice/Cargo.toml index b32e0aa665..3bd18e922a 100644 --- a/consensus/fork_choice/Cargo.toml +++ b/consensus/fork_choice/Cargo.toml @@ -3,17 +3,16 @@ name = "fork_choice" version = "0.1.0" authors = ["Paul Hauner "] edition = { workspace = true } - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -types = { workspace = true } -state_processing = { workspace = true } -proto_array = { workspace = true } ethereum_ssz = { workspace = true } ethereum_ssz_derive = { workspace = true } metrics = { workspace = true } +proto_array = { workspace = true } slog = { workspace = true } +state_processing = { workspace = true } +types = { workspace = true } [dev-dependencies] beacon_chain = { workspace = true } diff --git a/consensus/int_to_bytes/Cargo.toml b/consensus/int_to_bytes/Cargo.toml index e99d1af8e5..c639dfce8d 100644 --- a/consensus/int_to_bytes/Cargo.toml +++ b/consensus/int_to_bytes/Cargo.toml @@ -8,5 +8,5 @@ edition = { workspace = true } bytes = { workspace = true } [dev-dependencies] -yaml-rust2 = "0.8" hex = { workspace = true } +yaml-rust2 = "0.8" diff --git a/consensus/proto_array/Cargo.toml b/consensus/proto_array/Cargo.toml index 99f98cf545..bd6757c0fa 100644 --- a/consensus/proto_array/Cargo.toml +++ b/consensus/proto_array/Cargo.toml @@ -9,10 +9,10 @@ name = "proto_array" path = "src/bin.rs" [dependencies] -types = { workspace = true } ethereum_ssz = { workspace = true } ethereum_ssz_derive = { workspace = true } +safe_arith = { workspace = true } serde = { workspace = true } serde_yaml = { workspace = true } -safe_arith = { workspace = true } superstruct = { workspace = true } +types = { workspace = true } diff --git a/consensus/safe_arith/Cargo.toml b/consensus/safe_arith/Cargo.toml index 6f2e4b811c..9ac9fe28d3 100644 --- a/consensus/safe_arith/Cargo.toml +++ b/consensus/safe_arith/Cargo.toml @@ -3,7 +3,6 @@ name = "safe_arith" version = "0.1.0" authors = ["Michael Sproul "] edition = { workspace = true } - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] diff --git a/consensus/state_processing/Cargo.toml b/consensus/state_processing/Cargo.toml index b7f6ef7b2a..502ffe3cf6 100644 --- a/consensus/state_processing/Cargo.toml +++ b/consensus/state_processing/Cargo.toml @@ -5,30 +5,30 @@ authors = ["Paul Hauner ", "Michael Sproul "] edition = { workspace = true } - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -sha2 = { workspace = true } -zeroize = { workspace = true } +bls = { workspace = true } num-bigint-dig = { version = "0.8.4", features = ["zeroize"] } ring = { workspace = true } -bls = { workspace = true } +sha2 = { workspace = true } +zeroize = { workspace = true } [dev-dependencies] hex = { workspace = true } diff --git a/crypto/eth2_keystore/Cargo.toml b/crypto/eth2_keystore/Cargo.toml index bb6222807b..61d2722efb 100644 --- a/crypto/eth2_keystore/Cargo.toml +++ b/crypto/eth2_keystore/Cargo.toml @@ -3,25 +3,24 @@ name = "eth2_keystore" version = "0.1.0" authors = ["Pawan Dhananjay "] edition = { workspace = true } - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -rand = { workspace = true } +aes = { version = "0.7", features = ["ctr"] } +bls = { workspace = true } +eth2_key_derivation = { workspace = true } +hex = { workspace = true } hmac = "0.11.0" pbkdf2 = { version = "0.8.0", default-features = false } +rand = { workspace = true } scrypt = { version = "0.7.0", default-features = false } +serde = { workspace = true } +serde_json = { workspace = true } +serde_repr = { workspace = true } sha2 = { workspace = true } +unicode-normalization = "0.1.16" uuid = { workspace = true } zeroize = { workspace = true } -serde = { workspace = true } -serde_repr = { workspace = true } -hex = { workspace = true } -bls = { workspace = true } -serde_json = { workspace = true } -eth2_key_derivation = { workspace = true } -unicode-normalization = "0.1.16" -aes = { version = "0.7", features = ["ctr"] } [dev-dependencies] tempfile = { workspace = true } diff --git a/crypto/eth2_wallet/Cargo.toml b/crypto/eth2_wallet/Cargo.toml index f3af6aab59..5327bdc163 100644 --- a/crypto/eth2_wallet/Cargo.toml +++ b/crypto/eth2_wallet/Cargo.toml @@ -3,18 +3,17 @@ name = "eth2_wallet" version = "0.1.0" authors = ["Paul Hauner "] edition = { workspace = true } - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +eth2_key_derivation = { workspace = true } +eth2_keystore = { workspace = true } +rand = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } serde_repr = { workspace = true } -uuid = { workspace = true } -rand = { workspace = true } -eth2_keystore = { workspace = true } -eth2_key_derivation = { workspace = true } tiny-bip39 = "1" +uuid = { workspace = true } [dev-dependencies] hex = { workspace = true } diff --git a/crypto/kzg/Cargo.toml b/crypto/kzg/Cargo.toml index ce55f83639..bfe0f19cd0 100644 --- a/crypto/kzg/Cargo.toml +++ b/crypto/kzg/Cargo.toml @@ -3,22 +3,21 @@ name = "kzg" version = "0.1.0" authors = ["Pawan Dhananjay "] edition = "2021" - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] arbitrary = { workspace = true } +c-kzg = { workspace = true } +derivative = { workspace = true } +ethereum_hashing = { workspace = true } +ethereum_serde_utils = { workspace = true } ethereum_ssz = { workspace = true } ethereum_ssz_derive = { workspace = true } -tree_hash = { workspace = true } -derivative = { workspace = true } -serde = { workspace = true } -ethereum_serde_utils = { workspace = true } hex = { workspace = true } -ethereum_hashing = { workspace = true } -c-kzg = { workspace = true } rust_eth_kzg = { workspace = true } +serde = { workspace = true } serde_json = { workspace = true } +tree_hash = { workspace = true } [dev-dependencies] criterion = { workspace = true } diff --git a/database_manager/Cargo.toml b/database_manager/Cargo.toml index 96176f3fba..a7a54b1416 100644 --- a/database_manager/Cargo.toml +++ b/database_manager/Cargo.toml @@ -10,8 +10,8 @@ clap = { workspace = true } clap_utils = { workspace = true } environment = { workspace = true } hex = { workspace = true } -store = { workspace = true } -types = { workspace = true } -slog = { workspace = true } -strum = { workspace = true } serde = { workspace = true } +slog = { workspace = true } +store = { workspace = true } +strum = { workspace = true } +types = { workspace = true } diff --git a/lcli/Cargo.toml b/lcli/Cargo.toml index 9612bded47..72be77a70b 100644 --- a/lcli/Cargo.toml +++ b/lcli/Cargo.toml @@ -11,36 +11,36 @@ fake_crypto = ['bls/fake_crypto'] jemalloc = ["malloc_utils/jemalloc"] [dependencies] +account_utils = { workspace = true } +beacon_chain = { workspace = true } bls = { workspace = true } clap = { workspace = true } -log = { workspace = true } -sloggers = { workspace = true } -serde = { workspace = true } -serde_yaml = { workspace = true } -serde_json = { workspace = true } +clap_utils = { workspace = true } +deposit_contract = { workspace = true } env_logger = { workspace = true } -types = { workspace = true } -state_processing = { workspace = true } +environment = { workspace = true } +eth2 = { workspace = true } +eth2_network_config = { workspace = true } +eth2_wallet = { workspace = true } ethereum_hashing = { workspace = true } ethereum_ssz = { workspace = true } -environment = { workspace = true } -eth2_network_config = { workspace = true } -deposit_contract = { workspace = true } -tree_hash = { workspace = true } -clap_utils = { workspace = true } -lighthouse_network = { workspace = true } -validator_dir = { workspace = true } -lighthouse_version = { workspace = true } -account_utils = { workspace = true } -eth2_wallet = { workspace = true } -eth2 = { workspace = true } -snap = { workspace = true } -beacon_chain = { workspace = true } -store = { workspace = true } -malloc_utils = { workspace = true } -rayon = { workspace = true } execution_layer = { workspace = true } hex = { workspace = true } +lighthouse_network = { workspace = true } +lighthouse_version = { workspace = true } +log = { workspace = true } +malloc_utils = { workspace = true } +rayon = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +serde_yaml = { workspace = true } +sloggers = { workspace = true } +snap = { workspace = true } +state_processing = { workspace = true } +store = { workspace = true } +tree_hash = { workspace = true } +types = { workspace = true } +validator_dir = { workspace = true } [package.metadata.cargo-udeps.ignore] normal = ["malloc_utils"] diff --git a/lighthouse/Cargo.toml b/lighthouse/Cargo.toml index 1c91b18e9c..eda9a2ebf2 100644 --- a/lighthouse/Cargo.toml +++ b/lighthouse/Cargo.toml @@ -34,48 +34,47 @@ malloc_utils = { workspace = true, features = ["jemalloc"] } malloc_utils = { workspace = true } [dependencies] -beacon_node = { workspace = true } -slog = { workspace = true } -types = { workspace = true } -bls = { workspace = true } -ethereum_hashing = { workspace = true } -clap = { workspace = true } -environment = { workspace = true } -boot_node = { path = "../boot_node" } -futures = { workspace = true } -validator_client = { workspace = true } account_manager = { "path" = "../account_manager" } -clap_utils = { workspace = true } -eth2_network_config = { workspace = true } -lighthouse_version = { workspace = true } account_utils = { workspace = true } +beacon_node = { workspace = true } +bls = { workspace = true } +boot_node = { path = "../boot_node" } +clap = { workspace = true } +clap_utils = { workspace = true } +database_manager = { path = "../database_manager" } +directory = { workspace = true } +environment = { workspace = true } +eth2_network_config = { workspace = true } +ethereum_hashing = { workspace = true } +futures = { workspace = true } +lighthouse_version = { workspace = true } +logging = { workspace = true } +malloc_utils = { workspace = true } metrics = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } serde_yaml = { workspace = true } -task_executor = { workspace = true } -malloc_utils = { workspace = true } -directory = { workspace = true } -unused_port = { workspace = true } -database_manager = { path = "../database_manager" } slasher = { workspace = true } +slog = { workspace = true } +task_executor = { workspace = true } +types = { workspace = true } +unused_port = { workspace = true } +validator_client = { workspace = true } validator_manager = { path = "../validator_manager" } -logging = { workspace = true } [dev-dependencies] -tempfile = { workspace = true } -validator_dir = { workspace = true } -slashing_protection = { workspace = true } -lighthouse_network = { workspace = true } -sensitive_url = { workspace = true } +beacon_node_fallback = { workspace = true } +beacon_processor = { workspace = true } eth1 = { workspace = true } eth2 = { workspace = true } -beacon_processor = { workspace = true } -beacon_node_fallback = { workspace = true } initialized_validators = { workspace = true } +lighthouse_network = { workspace = true } +sensitive_url = { workspace = true } +slashing_protection = { workspace = true } +tempfile = { workspace = true } +validator_dir = { workspace = true } zeroize = { workspace = true } - [[test]] name = "lighthouse_tests" path = "tests/main.rs" diff --git a/lighthouse/environment/Cargo.toml b/lighthouse/environment/Cargo.toml index f95751392c..02b8e0b655 100644 --- a/lighthouse/environment/Cargo.toml +++ b/lighthouse/environment/Cargo.toml @@ -6,19 +6,19 @@ edition = { workspace = true } [dependencies] async-channel = { workspace = true } -tokio = { workspace = true } -slog = { workspace = true } -sloggers = { workspace = true } -types = { workspace = true } eth2_config = { workspace = true } -task_executor = { workspace = true } eth2_network_config = { workspace = true } -logging = { workspace = true } -slog-term = { workspace = true } -slog-async = { workspace = true } futures = { workspace = true } -slog-json = "2.3.0" +logging = { workspace = true } serde = { workspace = true } +slog = { workspace = true } +slog-async = { workspace = true } +slog-json = "2.3.0" +slog-term = { workspace = true } +sloggers = { workspace = true } +task_executor = { workspace = true } +tokio = { workspace = true } +types = { workspace = true } [target.'cfg(not(target_family = "unix"))'.dependencies] ctrlc = { version = "3.1.6", features = ["termination"] } diff --git a/slasher/Cargo.toml b/slasher/Cargo.toml index 56a023df0b..fcecc2fc23 100644 --- a/slasher/Cargo.toml +++ b/slasher/Cargo.toml @@ -17,31 +17,31 @@ byteorder = { workspace = true } derivative = { workspace = true } ethereum_ssz = { workspace = true } ethereum_ssz_derive = { workspace = true } -flate2 = { version = "1.0.14", features = ["zlib"], default-features = false } -metrics = { workspace = true } filesystem = { workspace = true } +flate2 = { version = "1.0.14", features = ["zlib"], default-features = false } +lmdb-rkv = { git = "https://github.com/sigp/lmdb-rs", rev = "f33845c6469b94265319aac0ed5085597862c27e", optional = true } +lmdb-rkv-sys = { git = "https://github.com/sigp/lmdb-rs", rev = "f33845c6469b94265319aac0ed5085597862c27e", optional = true } lru = { workspace = true } -parking_lot = { workspace = true } -rand = { workspace = true } -safe_arith = { workspace = true } -serde = { workspace = true } -slog = { workspace = true } -tree_hash = { workspace = true } -tree_hash_derive = { workspace = true } -types = { workspace = true } -strum = { workspace = true } -ssz_types = { workspace = true } # MDBX is pinned at the last version with Windows and macOS support. mdbx = { package = "libmdbx", git = "https://github.com/sigp/libmdbx-rs", rev = "e6ff4b9377c1619bcf0bfdf52bee5a980a432a1a", optional = true } -lmdb-rkv = { git = "https://github.com/sigp/lmdb-rs", rev = "f33845c6469b94265319aac0ed5085597862c27e", optional = true } -lmdb-rkv-sys = { git = "https://github.com/sigp/lmdb-rs", rev = "f33845c6469b94265319aac0ed5085597862c27e", optional = true } +metrics = { workspace = true } +parking_lot = { workspace = true } +rand = { workspace = true } redb = { version = "2.1.4", optional = true } +safe_arith = { workspace = true } +serde = { workspace = true } +slog = { workspace = true } +ssz_types = { workspace = true } +strum = { workspace = true } +tree_hash = { workspace = true } +tree_hash_derive = { workspace = true } +types = { workspace = true } [dev-dependencies] +logging = { workspace = true } maplit = { workspace = true } rayon = { workspace = true } tempfile = { workspace = true } -logging = { workspace = true } diff --git a/testing/ef_tests/Cargo.toml b/testing/ef_tests/Cargo.toml index 6012283e11..d93f3a5578 100644 --- a/testing/ef_tests/Cargo.toml +++ b/testing/ef_tests/Cargo.toml @@ -12,28 +12,28 @@ portable = ["beacon_chain/portable"] [dependencies] alloy-primitives = { workspace = true } +beacon_chain = { workspace = true } bls = { workspace = true } compare_fields = { workspace = true } compare_fields_derive = { workspace = true } derivative = { workspace = true } +eth2_network_config = { workspace = true } +ethereum_ssz = { workspace = true } +ethereum_ssz_derive = { workspace = true } +execution_layer = { workspace = true } +fork_choice = { workspace = true } +fs2 = { workspace = true } hex = { workspace = true } kzg = { workspace = true } +logging = { workspace = true } rayon = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } serde_repr = { workspace = true } serde_yaml = { workspace = true } -eth2_network_config = { workspace = true } -ethereum_ssz = { workspace = true } -ethereum_ssz_derive = { workspace = true } -tree_hash = { workspace = true } -tree_hash_derive = { workspace = true } +snap = { workspace = true } state_processing = { workspace = true } swap_or_not_shuffle = { workspace = true } +tree_hash = { workspace = true } +tree_hash_derive = { workspace = true } types = { workspace = true } -snap = { workspace = true } -fs2 = { workspace = true } -beacon_chain = { workspace = true } -fork_choice = { workspace = true } -execution_layer = { workspace = true } -logging = { workspace = true } diff --git a/testing/eth1_test_rig/Cargo.toml b/testing/eth1_test_rig/Cargo.toml index c76ef91183..9b0ac5ec9b 100644 --- a/testing/eth1_test_rig/Cargo.toml +++ b/testing/eth1_test_rig/Cargo.toml @@ -5,12 +5,12 @@ authors = ["Paul Hauner "] edition = { workspace = true } [dependencies] -tokio = { workspace = true } +deposit_contract = { workspace = true } +ethers-contract = "1.0.2" ethers-core = { workspace = true } ethers-providers = { workspace = true } -ethers-contract = "1.0.2" -types = { workspace = true } -serde_json = { workspace = true } -deposit_contract = { workspace = true } -unused_port = { workspace = true } hex = { workspace = true } +serde_json = { workspace = true } +tokio = { workspace = true } +types = { workspace = true } +unused_port = { workspace = true } diff --git a/testing/execution_engine_integration/Cargo.toml b/testing/execution_engine_integration/Cargo.toml index 159561d5dd..28ff944799 100644 --- a/testing/execution_engine_integration/Cargo.toml +++ b/testing/execution_engine_integration/Cargo.toml @@ -5,22 +5,22 @@ edition = { workspace = true } [dependencies] async-channel = { workspace = true } -tempfile = { workspace = true } +deposit_contract = { workspace = true } +ethers-core = { workspace = true } +ethers-providers = { workspace = true } +execution_layer = { workspace = true } +fork_choice = { workspace = true } +futures = { workspace = true } +hex = { workspace = true } +logging = { workspace = true } +reqwest = { workspace = true } +sensitive_url = { workspace = true } serde_json = { workspace = true } task_executor = { workspace = true } +tempfile = { workspace = true } tokio = { workspace = true } -futures = { workspace = true } -execution_layer = { workspace = true } -sensitive_url = { workspace = true } types = { workspace = true } unused_port = { workspace = true } -ethers-providers = { workspace = true } -ethers-core = { workspace = true } -deposit_contract = { workspace = true } -reqwest = { workspace = true } -hex = { workspace = true } -fork_choice = { workspace = true } -logging = { workspace = true } [features] portable = ["types/portable"] diff --git a/testing/node_test_rig/Cargo.toml b/testing/node_test_rig/Cargo.toml index 97e73b8a2f..0d9db528da 100644 --- a/testing/node_test_rig/Cargo.toml +++ b/testing/node_test_rig/Cargo.toml @@ -5,14 +5,14 @@ authors = ["Paul Hauner "] edition = { workspace = true } [dependencies] -environment = { workspace = true } beacon_node = { workspace = true } -types = { workspace = true } -tempfile = { workspace = true } -eth2 = { workspace = true } -validator_client = { workspace = true } beacon_node_fallback = { workspace = true } -validator_dir = { workspace = true, features = ["insecure_keys"] } -sensitive_url = { workspace = true } +environment = { workspace = true } +eth2 = { workspace = true } execution_layer = { workspace = true } +sensitive_url = { workspace = true } +tempfile = { workspace = true } tokio = { workspace = true } +types = { workspace = true } +validator_client = { workspace = true } +validator_dir = { workspace = true, features = ["insecure_keys"] } diff --git a/testing/simulator/Cargo.toml b/testing/simulator/Cargo.toml index 7772523284..77645dba45 100644 --- a/testing/simulator/Cargo.toml +++ b/testing/simulator/Cargo.toml @@ -3,20 +3,19 @@ name = "simulator" version = "0.2.0" authors = ["Paul Hauner "] edition = { workspace = true } - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -node_test_rig = { path = "../node_test_rig" } -execution_layer = { workspace = true } -types = { workspace = true } -parking_lot = { workspace = true } -futures = { workspace = true } -tokio = { workspace = true } -env_logger = { workspace = true } clap = { workspace = true } +env_logger = { workspace = true } +eth2_network_config = { workspace = true } +execution_layer = { workspace = true } +futures = { workspace = true } +kzg = { workspace = true } +node_test_rig = { path = "../node_test_rig" } +parking_lot = { workspace = true } rayon = { workspace = true } sensitive_url = { path = "../../common/sensitive_url" } -eth2_network_config = { workspace = true } serde_json = { workspace = true } -kzg = { workspace = true } +tokio = { workspace = true } +types = { workspace = true } diff --git a/testing/state_transition_vectors/Cargo.toml b/testing/state_transition_vectors/Cargo.toml index 142a657f07..7c29715346 100644 --- a/testing/state_transition_vectors/Cargo.toml +++ b/testing/state_transition_vectors/Cargo.toml @@ -3,15 +3,14 @@ name = "state_transition_vectors" version = "0.1.0" authors = ["Paul Hauner "] edition = { workspace = true } - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -state_processing = { workspace = true } -types = { workspace = true } -ethereum_ssz = { workspace = true } beacon_chain = { workspace = true } +ethereum_ssz = { workspace = true } +state_processing = { workspace = true } tokio = { workspace = true } +types = { workspace = true } [features] -portable = ["beacon_chain/portable"] \ No newline at end of file +portable = ["beacon_chain/portable"] diff --git a/testing/test-test_logger/Cargo.toml b/testing/test-test_logger/Cargo.toml index 63bb87c06e..d2d705f714 100644 --- a/testing/test-test_logger/Cargo.toml +++ b/testing/test-test_logger/Cargo.toml @@ -2,7 +2,6 @@ name = "test-test_logger" version = "0.1.0" edition = { workspace = true } - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] diff --git a/testing/web3signer_tests/Cargo.toml b/testing/web3signer_tests/Cargo.toml index 0096d74f64..376aa13406 100644 --- a/testing/web3signer_tests/Cargo.toml +++ b/testing/web3signer_tests/Cargo.toml @@ -2,31 +2,30 @@ name = "web3signer_tests" version = "0.1.0" edition = { workspace = true } - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] [dev-dependencies] +account_utils = { workspace = true } async-channel = { workspace = true } +environment = { workspace = true } eth2_keystore = { workspace = true } -types = { workspace = true } +eth2_network_config = { workspace = true } +futures = { workspace = true } +initialized_validators = { workspace = true } +logging = { workspace = true } +parking_lot = { workspace = true } +reqwest = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +serde_yaml = { workspace = true } +slashing_protection = { workspace = true } +slot_clock = { workspace = true } +task_executor = { workspace = true } tempfile = { workspace = true } tokio = { workspace = true } -reqwest = { workspace = true } +types = { workspace = true } url = { workspace = true } -slot_clock = { workspace = true } -futures = { workspace = true } -task_executor = { workspace = true } -environment = { workspace = true } -account_utils = { workspace = true } -serde = { workspace = true } -serde_yaml = { workspace = true } -eth2_network_config = { workspace = true } -serde_json = { workspace = true } -zip = { workspace = true } -parking_lot = { workspace = true } -logging = { workspace = true } -initialized_validators = { workspace = true } -slashing_protection = { workspace = true } validator_store = { workspace = true } +zip = { workspace = true } diff --git a/validator_client/Cargo.toml b/validator_client/Cargo.toml index 044a622d54..504d96ae1c 100644 --- a/validator_client/Cargo.toml +++ b/validator_client/Cargo.toml @@ -17,10 +17,11 @@ beacon_node_fallback = { workspace = true } clap = { workspace = true } clap_utils = { workspace = true } directory = { workspace = true } -doppelganger_service = { workspace = true } dirs = { workspace = true } -eth2 = { workspace = true } +doppelganger_service = { workspace = true } environment = { workspace = true } +eth2 = { workspace = true } +fdlimit = "0.3.0" graffiti_file = { workspace = true } hyper = { workspace = true } initialized_validators = { workspace = true } @@ -29,15 +30,14 @@ monitoring_api = { workspace = true } parking_lot = { workspace = true } reqwest = { workspace = true } sensitive_url = { workspace = true } -slashing_protection = { workspace = true } serde = { workspace = true } +slashing_protection = { workspace = true } slog = { workspace = true } slot_clock = { workspace = true } +tokio = { workspace = true } types = { workspace = true } validator_http_api = { workspace = true } validator_http_metrics = { workspace = true } validator_metrics = { workspace = true } validator_services = { workspace = true } validator_store = { workspace = true } -tokio = { workspace = true } -fdlimit = "0.3.0" diff --git a/validator_client/doppelganger_service/Cargo.toml b/validator_client/doppelganger_service/Cargo.toml index e5f7d3f2ba..66b61a411b 100644 --- a/validator_client/doppelganger_service/Cargo.toml +++ b/validator_client/doppelganger_service/Cargo.toml @@ -17,4 +17,4 @@ types = { workspace = true } [dev-dependencies] futures = { workspace = true } -logging = {workspace = true } +logging = { workspace = true } diff --git a/validator_client/graffiti_file/Cargo.toml b/validator_client/graffiti_file/Cargo.toml index 02e48849d1..8868f5aec8 100644 --- a/validator_client/graffiti_file/Cargo.toml +++ b/validator_client/graffiti_file/Cargo.toml @@ -9,11 +9,11 @@ name = "graffiti_file" path = "src/lib.rs" [dependencies] -serde = { workspace = true } bls = { workspace = true } -types = { workspace = true } +serde = { workspace = true } slog = { workspace = true } +types = { workspace = true } [dev-dependencies] -tempfile = { workspace = true } hex = { workspace = true } +tempfile = { workspace = true } diff --git a/validator_client/http_api/Cargo.toml b/validator_client/http_api/Cargo.toml index 96c836f6f3..76a021ab8c 100644 --- a/validator_client/http_api/Cargo.toml +++ b/validator_client/http_api/Cargo.toml @@ -10,25 +10,25 @@ path = "src/lib.rs" [dependencies] account_utils = { workspace = true } -bls = { workspace = true } beacon_node_fallback = { workspace = true } +bls = { workspace = true } deposit_contract = { workspace = true } directory = { workspace = true } -doppelganger_service = { workspace = true } dirs = { workspace = true } -graffiti_file = { workspace = true } +doppelganger_service = { workspace = true } eth2 = { workspace = true } eth2_keystore = { workspace = true } ethereum_serde_utils = { workspace = true } +filesystem = { workspace = true } +graffiti_file = { workspace = true } initialized_validators = { workspace = true } lighthouse_version = { workspace = true } logging = { workspace = true } parking_lot = { workspace = true } -filesystem = { workspace = true } rand = { workspace = true } +sensitive_url = { workspace = true } serde = { workspace = true } signing_method = { workspace = true } -sensitive_url = { workspace = true } slashing_protection = { workspace = true } slog = { workspace = true } slot_clock = { workspace = true } @@ -39,15 +39,15 @@ tempfile = { workspace = true } tokio = { workspace = true } tokio-stream = { workspace = true } types = { workspace = true } -validator_dir = { workspace = true } -validator_store = { workspace = true } -validator_services = { workspace = true } url = { workspace = true } -warp_utils = { workspace = true } +validator_dir = { workspace = true } +validator_services = { workspace = true } +validator_store = { workspace = true } warp = { workspace = true } +warp_utils = { workspace = true } zeroize = { workspace = true } [dev-dependencies] -itertools = { workspace = true } futures = { workspace = true } +itertools = { workspace = true } rand = { workspace = true, features = ["small_rng"] } diff --git a/validator_client/http_metrics/Cargo.toml b/validator_client/http_metrics/Cargo.toml index a9de26a55b..c29a4d18fa 100644 --- a/validator_client/http_metrics/Cargo.toml +++ b/validator_client/http_metrics/Cargo.toml @@ -5,16 +5,16 @@ edition = { workspace = true } authors = ["Sigma Prime "] [dependencies] +lighthouse_version = { workspace = true } malloc_utils = { workspace = true } -slot_clock = { workspace = true } metrics = { workspace = true } parking_lot = { workspace = true } serde = { workspace = true } slog = { workspace = true } -warp_utils = { workspace = true } -warp = { workspace = true } -lighthouse_version = { workspace = true } +slot_clock = { workspace = true } +types = { workspace = true } +validator_metrics = { workspace = true } validator_services = { workspace = true } validator_store = { workspace = true } -validator_metrics = { workspace = true } -types = { workspace = true } +warp = { workspace = true } +warp_utils = { workspace = true } diff --git a/validator_client/initialized_validators/Cargo.toml b/validator_client/initialized_validators/Cargo.toml index 9c7a3f19d6..05e85261f9 100644 --- a/validator_client/initialized_validators/Cargo.toml +++ b/validator_client/initialized_validators/Cargo.toml @@ -5,23 +5,23 @@ edition = { workspace = true } authors = ["Sigma Prime "] [dependencies] -signing_method = { workspace = true } account_utils = { workspace = true } +bincode = { workspace = true } +bls = { workspace = true } eth2_keystore = { workspace = true } -metrics = { workspace = true } +filesystem = { workspace = true } lockfile = { workspace = true } +metrics = { workspace = true } parking_lot = { workspace = true } +rand = { workspace = true } reqwest = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +signing_method = { workspace = true } slog = { workspace = true } +tokio = { workspace = true } types = { workspace = true } url = { workspace = true } validator_dir = { workspace = true } -rand = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } -bls = { workspace = true } -tokio = { workspace = true } -bincode = { workspace = true } -filesystem = { workspace = true } validator_metrics = { workspace = true } zeroize = { workspace = true } diff --git a/validator_client/signing_method/Cargo.toml b/validator_client/signing_method/Cargo.toml index 0f3852eff6..3e1a48142f 100644 --- a/validator_client/signing_method/Cargo.toml +++ b/validator_client/signing_method/Cargo.toml @@ -6,12 +6,12 @@ authors = ["Sigma Prime "] [dependencies] eth2_keystore = { workspace = true } +ethereum_serde_utils = { workspace = true } lockfile = { workspace = true } parking_lot = { workspace = true } reqwest = { workspace = true } +serde = { workspace = true } task_executor = { workspace = true } types = { workspace = true } url = { workspace = true } validator_metrics = { workspace = true } -serde = { workspace = true } -ethereum_serde_utils = { workspace = true } diff --git a/validator_client/slashing_protection/Cargo.toml b/validator_client/slashing_protection/Cargo.toml index 6982958bd5..1a098742d8 100644 --- a/validator_client/slashing_protection/Cargo.toml +++ b/validator_client/slashing_protection/Cargo.toml @@ -10,16 +10,16 @@ name = "slashing_protection_tests" path = "tests/main.rs" [dependencies] -tempfile = { workspace = true } -types = { workspace = true } -rusqlite = { workspace = true } -r2d2 = { workspace = true } -r2d2_sqlite = "0.21.0" -serde = { workspace = true } -serde_json = { workspace = true } +arbitrary = { workspace = true, features = ["derive"] } ethereum_serde_utils = { workspace = true } filesystem = { workspace = true } -arbitrary = { workspace = true, features = ["derive"] } +r2d2 = { workspace = true } +r2d2_sqlite = "0.21.0" +rusqlite = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +tempfile = { workspace = true } +types = { workspace = true } [dev-dependencies] rayon = { workspace = true } diff --git a/validator_client/validator_services/Cargo.toml b/validator_client/validator_services/Cargo.toml index 7dcd815541..21f0ae2d77 100644 --- a/validator_client/validator_services/Cargo.toml +++ b/validator_client/validator_services/Cargo.toml @@ -6,18 +6,18 @@ authors = ["Sigma Prime "] [dependencies] beacon_node_fallback = { workspace = true } -validator_metrics = { workspace = true } -validator_store = { workspace = true } -graffiti_file = { workspace = true } +bls = { workspace = true } doppelganger_service = { workspace = true } environment = { workspace = true } eth2 = { workspace = true } futures = { workspace = true } +graffiti_file = { workspace = true } parking_lot = { workspace = true } safe_arith = { workspace = true } slog = { workspace = true } slot_clock = { workspace = true } tokio = { workspace = true } -types = { workspace = true } tree_hash = { workspace = true } -bls = { workspace = true } +types = { workspace = true } +validator_metrics = { workspace = true } +validator_store = { workspace = true } diff --git a/validator_manager/Cargo.toml b/validator_manager/Cargo.toml index 36df256841..7cb05616f4 100644 --- a/validator_manager/Cargo.toml +++ b/validator_manager/Cargo.toml @@ -2,28 +2,27 @@ name = "validator_manager" version = "0.1.0" edition = { workspace = true } - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -clap = { workspace = true } -types = { workspace = true } -environment = { workspace = true } -eth2_network_config = { workspace = true } -clap_utils = { workspace = true } -eth2_wallet = { workspace = true } account_utils = { workspace = true } +clap = { workspace = true } +clap_utils = { workspace = true } +derivative = { workspace = true } +environment = { workspace = true } +eth2 = { workspace = true } +eth2_network_config = { workspace = true } +eth2_wallet = { workspace = true } +ethereum_serde_utils = { workspace = true } +hex = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } -ethereum_serde_utils = { workspace = true } -tree_hash = { workspace = true } -eth2 = { workspace = true } -hex = { workspace = true } tokio = { workspace = true } -derivative = { workspace = true } +tree_hash = { workspace = true } +types = { workspace = true } zeroize = { workspace = true } [dev-dependencies] -tempfile = { workspace = true } regex = { workspace = true } +tempfile = { workspace = true } validator_http_api = { workspace = true } diff --git a/watch/Cargo.toml b/watch/Cargo.toml index 9e8da3b293..41cfb58e28 100644 --- a/watch/Cargo.toml +++ b/watch/Cargo.toml @@ -10,37 +10,36 @@ path = "src/lib.rs" [[bin]] name = "watch" path = "src/main.rs" - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +axum = "0.7" +beacon_node = { workspace = true } +bls = { workspace = true } clap = { workspace = true } clap_utils = { workspace = true } -log = { workspace = true } -env_logger = { workspace = true } -types = { workspace = true } -eth2 = { workspace = true } -beacon_node = { workspace = true } -tokio = { workspace = true } -axum = "0.7" -hyper = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } -reqwest = { workspace = true } -url = { workspace = true } -rand = { workspace = true } diesel = { version = "2.0.2", features = ["postgres", "r2d2"] } diesel_migrations = { version = "2.0.0", features = ["postgres"] } -bls = { workspace = true } +env_logger = { workspace = true } +eth2 = { workspace = true } +hyper = { workspace = true } +log = { workspace = true } r2d2 = { workspace = true } +rand = { workspace = true } +reqwest = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } serde_yaml = { workspace = true } +tokio = { workspace = true } +types = { workspace = true } +url = { workspace = true } [dev-dependencies] -tokio-postgres = "0.7.5" -http_api = { workspace = true } beacon_chain = { workspace = true } -network = { workspace = true } -testcontainers = "0.15" -unused_port = { workspace = true } -task_executor = { workspace = true } +http_api = { workspace = true } logging = { workspace = true } +network = { workspace = true } +task_executor = { workspace = true } +testcontainers = "0.15" +tokio-postgres = "0.7.5" +unused_port = { workspace = true } From 07e82dabc06f8e0f7d92ca3edf4ca061899e20da Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Thu, 19 Dec 2024 16:46:06 +1100 Subject: [PATCH 055/254] Delete OTB verification service (#6631) * Delete OTB verification service * Merge branch 'unstable' into delete-otb --- .../beacon_chain/src/execution_payload.rs | 4 - beacon_node/beacon_chain/src/lib.rs | 1 - .../src/otb_verification_service.rs | 381 ------------------ beacon_node/client/src/builder.rs | 2 - beacon_node/store/src/lib.rs | 2 +- 5 files changed, 1 insertion(+), 389 deletions(-) delete mode 100644 beacon_node/beacon_chain/src/otb_verification_service.rs diff --git a/beacon_node/beacon_chain/src/execution_payload.rs b/beacon_node/beacon_chain/src/execution_payload.rs index 5e13f0624d..92d24c53c0 100644 --- a/beacon_node/beacon_chain/src/execution_payload.rs +++ b/beacon_node/beacon_chain/src/execution_payload.rs @@ -7,7 +7,6 @@ //! So, this module contains functions that one might expect to find in other crates, but they live //! here for good reason. -use crate::otb_verification_service::OptimisticTransitionBlock; use crate::{ BeaconChain, BeaconChainError, BeaconChainTypes, BlockError, BlockProductionError, ExecutionPayloadError, @@ -284,9 +283,6 @@ pub async fn validate_merge_block<'a, T: BeaconChainTypes>( "block_hash" => ?execution_payload.parent_hash(), "msg" => "the terminal block/parent was unavailable" ); - // Store Optimistic Transition Block in Database for later Verification - OptimisticTransitionBlock::from_block(block) - .persist_in_store::(&chain.store)?; Ok(()) } else { Err(ExecutionPayloadError::UnverifiedNonOptimisticCandidate.into()) diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index 2953516fb1..d9728b9fd4 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -47,7 +47,6 @@ pub mod observed_block_producers; pub mod observed_data_sidecars; pub mod observed_operations; mod observed_slashable; -pub mod otb_verification_service; mod persisted_beacon_chain; mod persisted_fork_choice; mod pre_finalization_cache; diff --git a/beacon_node/beacon_chain/src/otb_verification_service.rs b/beacon_node/beacon_chain/src/otb_verification_service.rs deleted file mode 100644 index 31034a7d59..0000000000 --- a/beacon_node/beacon_chain/src/otb_verification_service.rs +++ /dev/null @@ -1,381 +0,0 @@ -use crate::execution_payload::{validate_merge_block, AllowOptimisticImport}; -use crate::{ - BeaconChain, BeaconChainError, BeaconChainTypes, BlockError, ExecutionPayloadError, - INVALID_FINALIZED_MERGE_TRANSITION_BLOCK_SHUTDOWN_REASON, -}; -use itertools::process_results; -use proto_array::InvalidationOperation; -use slog::{crit, debug, error, info, warn}; -use slot_clock::SlotClock; -use ssz::{Decode, Encode}; -use ssz_derive::{Decode, Encode}; -use state_processing::per_block_processing::is_merge_transition_complete; -use std::sync::Arc; -use store::{DBColumn, Error as StoreError, HotColdDB, KeyValueStore, StoreItem}; -use task_executor::{ShutdownReason, TaskExecutor}; -use tokio::time::sleep; -use tree_hash::TreeHash; -use types::{BeaconBlockRef, EthSpec, Hash256, Slot}; -use DBColumn::OptimisticTransitionBlock as OTBColumn; - -#[derive(Clone, Debug, Decode, Encode, PartialEq)] -pub struct OptimisticTransitionBlock { - root: Hash256, - slot: Slot, -} - -impl OptimisticTransitionBlock { - // types::BeaconBlockRef<'_, ::EthSpec> - pub fn from_block(block: BeaconBlockRef) -> Self { - Self { - root: block.tree_hash_root(), - slot: block.slot(), - } - } - - pub fn root(&self) -> &Hash256 { - &self.root - } - - pub fn slot(&self) -> &Slot { - &self.slot - } - - pub fn persist_in_store(&self, store: A) -> Result<(), StoreError> - where - T: BeaconChainTypes, - A: AsRef>, - { - if store - .as_ref() - .item_exists::(&self.root)? - { - Ok(()) - } else { - store.as_ref().put_item(&self.root, self) - } - } - - pub fn remove_from_store(&self, store: A) -> Result<(), StoreError> - where - T: BeaconChainTypes, - A: AsRef>, - { - store - .as_ref() - .hot_db - .key_delete(OTBColumn.into(), self.root.as_slice()) - } - - fn is_canonical( - &self, - chain: &BeaconChain, - ) -> Result { - Ok(chain - .forwards_iter_block_roots_until(self.slot, self.slot)? - .next() - .transpose()? - .map(|(root, _)| root) - == Some(self.root)) - } -} - -impl StoreItem for OptimisticTransitionBlock { - fn db_column() -> DBColumn { - OTBColumn - } - - fn as_store_bytes(&self) -> Vec { - self.as_ssz_bytes() - } - - fn from_store_bytes(bytes: &[u8]) -> Result { - Ok(Self::from_ssz_bytes(bytes)?) - } -} - -/// The routine is expected to run once per epoch, 1/4th through the epoch. -pub const EPOCH_DELAY_FACTOR: u32 = 4; - -/// Spawns a routine which checks the validity of any optimistically imported transition blocks -/// -/// This routine will run once per epoch, at `epoch_duration / EPOCH_DELAY_FACTOR` after -/// the start of each epoch. -/// -/// The service will not be started if there is no `execution_layer` on the `chain`. -pub fn start_otb_verification_service( - executor: TaskExecutor, - chain: Arc>, -) { - // Avoid spawning the service if there's no EL, it'll just error anyway. - if chain.execution_layer.is_some() { - executor.spawn( - async move { otb_verification_service(chain).await }, - "otb_verification_service", - ); - } -} - -pub fn load_optimistic_transition_blocks( - chain: &BeaconChain, -) -> Result, StoreError> { - process_results( - chain.store.hot_db.iter_column::(OTBColumn), - |iter| { - iter.map(|(_, bytes)| OptimisticTransitionBlock::from_store_bytes(&bytes)) - .collect() - }, - )? -} - -#[derive(Debug)] -pub enum Error { - ForkChoice(String), - BeaconChain(BeaconChainError), - StoreError(StoreError), - NoBlockFound(OptimisticTransitionBlock), -} - -pub async fn validate_optimistic_transition_blocks( - chain: &Arc>, - otbs: Vec, -) -> Result<(), Error> { - let finalized_slot = chain - .canonical_head - .fork_choice_read_lock() - .get_finalized_block() - .map_err(|e| Error::ForkChoice(format!("{:?}", e)))? - .slot; - - // separate otbs into - // non-canonical - // finalized canonical - // unfinalized canonical - let mut non_canonical_otbs = vec![]; - let (finalized_canonical_otbs, unfinalized_canonical_otbs) = process_results( - otbs.into_iter().map(|otb| { - otb.is_canonical(chain) - .map(|is_canonical| (otb, is_canonical)) - }), - |pair_iter| { - pair_iter - .filter_map(|(otb, is_canonical)| { - if is_canonical { - Some(otb) - } else { - non_canonical_otbs.push(otb); - None - } - }) - .partition::, _>(|otb| *otb.slot() <= finalized_slot) - }, - ) - .map_err(Error::BeaconChain)?; - - // remove non-canonical blocks that conflict with finalized checkpoint from the database - for otb in non_canonical_otbs { - if *otb.slot() <= finalized_slot { - otb.remove_from_store::(&chain.store) - .map_err(Error::StoreError)?; - } - } - - // ensure finalized canonical otb are valid, otherwise kill client - for otb in finalized_canonical_otbs { - match chain.get_block(otb.root()).await { - Ok(Some(block)) => { - match validate_merge_block(chain, block.message(), AllowOptimisticImport::No).await - { - Ok(()) => { - // merge transition block is valid, remove it from OTB - otb.remove_from_store::(&chain.store) - .map_err(Error::StoreError)?; - info!( - chain.log, - "Validated merge transition block"; - "block_root" => ?otb.root(), - "type" => "finalized" - ); - } - // The block was not able to be verified by the EL. Leave the OTB in the - // database since the EL is likely still syncing and may verify the block - // later. - Err(BlockError::ExecutionPayloadError( - ExecutionPayloadError::UnverifiedNonOptimisticCandidate, - )) => (), - Err(BlockError::ExecutionPayloadError( - ExecutionPayloadError::InvalidTerminalPoWBlock { .. }, - )) => { - // Finalized Merge Transition Block is Invalid! Kill the Client! - crit!( - chain.log, - "Finalized merge transition block is invalid!"; - "msg" => "You must use the `--purge-db` flag to clear the database and restart sync. \ - You may be on a hostile network.", - "block_hash" => ?block.canonical_root() - ); - let mut shutdown_sender = chain.shutdown_sender(); - if let Err(e) = shutdown_sender.try_send(ShutdownReason::Failure( - INVALID_FINALIZED_MERGE_TRANSITION_BLOCK_SHUTDOWN_REASON, - )) { - crit!( - chain.log, - "Failed to shut down client"; - "error" => ?e, - "shutdown_reason" => INVALID_FINALIZED_MERGE_TRANSITION_BLOCK_SHUTDOWN_REASON - ); - } - } - _ => {} - } - } - Ok(None) => return Err(Error::NoBlockFound(otb)), - // Our database has pruned the payload and the payload was unavailable on the EL since - // the EL is still syncing or the payload is non-canonical. - Err(BeaconChainError::BlockHashMissingFromExecutionLayer(_)) => (), - Err(e) => return Err(Error::BeaconChain(e)), - } - } - - // attempt to validate any non-finalized canonical otb blocks - for otb in unfinalized_canonical_otbs { - match chain.get_block(otb.root()).await { - Ok(Some(block)) => { - match validate_merge_block(chain, block.message(), AllowOptimisticImport::No).await - { - Ok(()) => { - // merge transition block is valid, remove it from OTB - otb.remove_from_store::(&chain.store) - .map_err(Error::StoreError)?; - info!( - chain.log, - "Validated merge transition block"; - "block_root" => ?otb.root(), - "type" => "not finalized" - ); - } - // The block was not able to be verified by the EL. Leave the OTB in the - // database since the EL is likely still syncing and may verify the block - // later. - Err(BlockError::ExecutionPayloadError( - ExecutionPayloadError::UnverifiedNonOptimisticCandidate, - )) => (), - Err(BlockError::ExecutionPayloadError( - ExecutionPayloadError::InvalidTerminalPoWBlock { .. }, - )) => { - // Unfinalized Merge Transition Block is Invalid -> Run process_invalid_execution_payload - warn!( - chain.log, - "Merge transition block invalid"; - "block_root" => ?otb.root() - ); - chain - .process_invalid_execution_payload( - &InvalidationOperation::InvalidateOne { - block_root: *otb.root(), - }, - ) - .await - .map_err(|e| { - warn!( - chain.log, - "Error checking merge transition block"; - "error" => ?e, - "location" => "process_invalid_execution_payload" - ); - Error::BeaconChain(e) - })?; - } - _ => {} - } - } - Ok(None) => return Err(Error::NoBlockFound(otb)), - // Our database has pruned the payload and the payload was unavailable on the EL since - // the EL is still syncing or the payload is non-canonical. - Err(BeaconChainError::BlockHashMissingFromExecutionLayer(_)) => (), - Err(e) => return Err(Error::BeaconChain(e)), - } - } - - Ok(()) -} - -/// Loop until any optimistically imported merge transition blocks have been verified and -/// the merge has been finalized. -async fn otb_verification_service(chain: Arc>) { - let epoch_duration = chain.slot_clock.slot_duration() * T::EthSpec::slots_per_epoch() as u32; - loop { - match chain - .slot_clock - .duration_to_next_epoch(T::EthSpec::slots_per_epoch()) - { - Some(duration) => { - let additional_delay = epoch_duration / EPOCH_DELAY_FACTOR; - sleep(duration + additional_delay).await; - - debug!( - chain.log, - "OTB verification service firing"; - ); - - if !is_merge_transition_complete( - &chain.canonical_head.cached_head().snapshot.beacon_state, - ) { - // We are pre-merge. Nothing to do yet. - continue; - } - - // load all optimistically imported transition blocks from the database - match load_optimistic_transition_blocks(chain.as_ref()) { - Ok(otbs) => { - if otbs.is_empty() { - if chain - .canonical_head - .fork_choice_read_lock() - .get_finalized_block() - .map_or(false, |block| { - block.execution_status.is_execution_enabled() - }) - { - // there are no optimistic blocks in the database, we can exit - // the service since the merge transition is finalized and we'll - // never see another transition block - break; - } else { - debug!( - chain.log, - "No optimistic transition blocks"; - "info" => "waiting for the merge transition to finalize" - ) - } - } - if let Err(e) = validate_optimistic_transition_blocks(&chain, otbs).await { - warn!( - chain.log, - "Error while validating optimistic transition blocks"; - "error" => ?e - ); - } - } - Err(e) => { - error!( - chain.log, - "Error loading optimistic transition blocks"; - "error" => ?e - ); - } - }; - } - None => { - error!(chain.log, "Failed to read slot clock"); - // If we can't read the slot clock, just wait another slot. - sleep(chain.slot_clock.slot_duration()).await; - } - }; - } - debug!( - chain.log, - "No optimistic transition blocks in database"; - "msg" => "shutting down OTB verification service" - ); -} diff --git a/beacon_node/client/src/builder.rs b/beacon_node/client/src/builder.rs index 961f5140f9..7c6a253aca 100644 --- a/beacon_node/client/src/builder.rs +++ b/beacon_node/client/src/builder.rs @@ -7,7 +7,6 @@ use crate::Client; use beacon_chain::attestation_simulator::start_attestation_simulator_service; use beacon_chain::data_availability_checker::start_availability_cache_maintenance_service; use beacon_chain::graffiti_calculator::start_engine_version_cache_refresh_service; -use beacon_chain::otb_verification_service::start_otb_verification_service; use beacon_chain::proposer_prep_service::start_proposer_prep_service; use beacon_chain::schema_change::migrate_schema; use beacon_chain::{ @@ -970,7 +969,6 @@ where } start_proposer_prep_service(runtime_context.executor.clone(), beacon_chain.clone()); - start_otb_verification_service(runtime_context.executor.clone(), beacon_chain.clone()); start_availability_cache_maintenance_service( runtime_context.executor.clone(), beacon_chain.clone(), diff --git a/beacon_node/store/src/lib.rs b/beacon_node/store/src/lib.rs index 0498c7c1e2..09ae9a32dd 100644 --- a/beacon_node/store/src/lib.rs +++ b/beacon_node/store/src/lib.rs @@ -332,7 +332,7 @@ pub enum DBColumn { BeaconRandaoMixes, #[strum(serialize = "dht")] DhtEnrs, - /// For Optimistically Imported Merge Transition Blocks + /// DEPRECATED. For Optimistically Imported Merge Transition Blocks #[strum(serialize = "otb")] OptimisticTransitionBlock, /// DEPRECATED. Can be removed once schema v22 is buried by a hard fork. From 502239871508b6a39f27a6fc54c82d46d59f9bca Mon Sep 17 00:00:00 2001 From: chonghe <44791194+chong-he@users.noreply.github.com> Date: Thu, 19 Dec 2024 13:46:10 +0800 Subject: [PATCH 056/254] Revise Siren documentation (#6553) * revise Siren doc * Fix broken links * Fix broken links * broken links * mdlint * mdlint * mdlint again * Merge branch 'unstable' into book-siren * test whether I have the required privs :-) * revise * some minor siren related changes for the book * updates re: `--net=host` * lint * Minor revision * Add note * mdlint * Merge branch 'unstable' into book-siren * Merge branch 'unstable' into book-siren * Merge remote-tracking branch 'origin/unstable' into book-siren * Fix spellcheck * Capital letters SSL --- book/src/SUMMARY.md | 3 +- book/src/api-vc-auth-header.md | 4 +- book/src/lighthouse-ui.md | 1 - book/src/ui-authentication.md | 4 +- book/src/ui-configuration.md | 121 +++++++++++++++++++++++++++------ book/src/ui-faqs.md | 7 +- wordlist.txt | 1 - 7 files changed, 109 insertions(+), 32 deletions(-) diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 02683a1172..44d7702e5f 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -33,9 +33,8 @@ * [Signature Header](./api-vc-sig-header.md) * [Prometheus Metrics](./advanced_metrics.md) * [Lighthouse UI (Siren)](./lighthouse-ui.md) - * [Installation](./ui-installation.md) - * [Authentication](./ui-authentication.md) * [Configuration](./ui-configuration.md) + * [Authentication](./ui-authentication.md) * [Usage](./ui-usage.md) * [FAQs](./ui-faqs.md) * [Advanced Usage](./advanced.md) diff --git a/book/src/api-vc-auth-header.md b/book/src/api-vc-auth-header.md index feb93724c0..f792ee870e 100644 --- a/book/src/api-vc-auth-header.md +++ b/book/src/api-vc-auth-header.md @@ -20,11 +20,11 @@ Authorization: Bearer hGut6B8uEujufDXSmZsT0thnxvdvKFBvh The API token is stored as a file in the `validators` directory. For most users this is `~/.lighthouse/{network}/validators/api-token.txt`, unless overridden using the `--http-token-path` CLI parameter. Here's an -example using the `cat` command to print the token to the terminal, but any +example using the `cat` command to print the token for mainnet to the terminal, but any text editor will suffice: ```bash -cat api-token.txt +cat ~/.lighthouse/mainnet/validators/api-token.txt hGut6B8uEujufDXSmZsT0thnxvdvKFBvh ``` diff --git a/book/src/lighthouse-ui.md b/book/src/lighthouse-ui.md index 106a5e8947..f2662f4a69 100644 --- a/book/src/lighthouse-ui.md +++ b/book/src/lighthouse-ui.md @@ -21,7 +21,6 @@ The UI is currently in active development. It resides in the See the following Siren specific topics for more context-specific information: -- [Installation Guide](./ui-installation.md) - Information to install and run the Lighthouse UI. - [Configuration Guide](./ui-configuration.md) - Explanation of how to setup and configure Siren. - [Authentication Guide](./ui-authentication.md) - Explanation of how Siren authentication works and protects validator actions. diff --git a/book/src/ui-authentication.md b/book/src/ui-authentication.md index 9e3a94db78..81b867bae2 100644 --- a/book/src/ui-authentication.md +++ b/book/src/ui-authentication.md @@ -2,12 +2,12 @@ ## Siren Session -For enhanced security, Siren will require users to authenticate with their session password to access the dashboard. This is crucial because Siren now includes features that can permanently alter the status of user validators. The session password must be set during the [installation](./ui-installation.md) process before running the Docker or local build, either in an `.env` file or via Docker flags. +For enhanced security, Siren will require users to authenticate with their session password to access the dashboard. This is crucial because Siren now includes features that can permanently alter the status of the user's validators. The session password must be set during the [configuration](./ui-configuration.md) process before running the Docker or local build, either in an `.env` file or via Docker flags. ![exit](imgs/ui-session.png) ## Protected Actions -Prior to executing any sensitive validator action, Siren will request authentication of the session password. If you wish to update your password please refer to the Siren [installation process](./ui-installation.md). +Prior to executing any sensitive validator action, Siren will request authentication of the session password. If you wish to update your password please refer to the Siren [configuration process](./ui-configuration.md). ![exit](imgs/ui-auth.png) diff --git a/book/src/ui-configuration.md b/book/src/ui-configuration.md index eeb2c9a51c..34cc9fe7ca 100644 --- a/book/src/ui-configuration.md +++ b/book/src/ui-configuration.md @@ -1,37 +1,116 @@ -# Configuration +# 📦 Installation + +Siren supports any operating system that supports containers and/or NodeJS 18, this includes Linux, MacOS, and Windows. The recommended way of running Siren is by launching the [docker container](https://hub.docker.com/r/sigp/siren). + +## Version Requirement + +To ensure proper functionality, the Siren app requires Lighthouse v4.3.0 or higher. You can find these versions on the [releases](https://github.com/sigp/lighthouse/releases) page of the Lighthouse repository. + +## Configuration Siren requires a connection to both a Lighthouse Validator Client and a Lighthouse Beacon Node. -To enable connection, you must generate .env file based on the provided .env.example - -## Connecting to the Clients Both the Beacon node and the Validator client need to have their HTTP APIs enabled. -These ports should be accessible from Siren. +These ports should be accessible from Siren. This means adding the flag `--http` on both beacon node and validator client. To enable the HTTP API for the beacon node, utilize the `--gui` CLI flag. This action ensures that the HTTP API can be accessed by other software on the same machine. > The Beacon Node must be run with the `--gui` flag set. -If you require accessibility from another machine within the network, configure the `--http-address` to match the local LAN IP of the system running the Beacon Node and Validator Client. +## Running the Docker container (Recommended) -> To access from another machine on the same network (192.168.0.200) set the Beacon Node and Validator Client `--http-address` as `192.168.0.200`. When this is set, the validator client requires the flag `--beacon-nodes http://192.168.0.200:5052` to connect to the beacon node. +We recommend running Siren's container next to your beacon node (on the same server), as it's essentially a webapp that you can access with any browser. -In a similar manner, the validator client requires activation of the `--http` flag, along with the optional consideration of configuring the `--http-address` flag. If `--http-address` flag is set on the Validator Client, then the `--unencrypted-http-transport` flag is required as well. These settings will ensure compatibility with Siren's connectivity requirements. + 1. Create a directory to run Siren: -If you run the Docker container, it will fail to startup if your BN/VC are not accessible, or if you provided a wrong API token. + ```bash + cd ~ + mkdir Siren + cd Siren + ``` -## API Token + 1. Create a configuration file in the `Siren` directory: `nano .env` and insert the following fields to the `.env` file. The field values are given here as an example, modify the fields as necessary. For example, the `API_TOKEN` can be obtained from [`Validator Client Authorization Header`](./api-vc-auth-header.md) -The API Token is a secret key that allows you to connect to the validator -client. The validator client's HTTP API is guarded by this key because it -contains sensitive validator information and the ability to modify -validators. Please see [`Validator Authorization`](./api-vc-auth-header.md) -for further details. + A full example with all possible configuration options can be found [here](https://github.com/sigp/siren/blob/stable/.env.example). -Siren requires this token in order to connect to the Validator client. -The token is located in the default data directory of the validator -client. The default path is -`~/.lighthouse//validators/api-token.txt`. + ``` + BEACON_URL=http://localhost:5052 + VALIDATOR_URL=http://localhost:5062 + API_TOKEN=R6YhbDO6gKjNMydtZHcaCovFbQ0izq5Hk + SESSION_PASSWORD=your_password + ``` -The contents of this file for the desired validator client needs to be -entered. + 1. You can now start Siren with: + + ```bash + docker run --rm -ti --name siren --env-file $PWD/.env --net host sigp/siren + ``` + + Note that, due to the `--net=host` flag, this will expose Siren on ports 3000, 80, and 443. Preferably, only the latter should be accessible. Adjust your firewall and/or skip the flag wherever possible. + + If it fails to start, an error message will be shown. For example, the error + + ``` + http://localhost:5062 unreachable, check settings and connection + ``` + + means that the validator client is not running, or the `--http` flag is not provided, or otherwise inaccessible from within the container. Another common error is: + + ``` + validator api issue, server response: 403 + ``` + + which means that the API token is incorrect. Check that you have provided the correct token in the field `API_TOKEN` in `.env`. + + When Siren has successfully started, you should see the log `LOG [NestApplication] Nest application successfully started +118ms`, indicating that Siren has started. + + 1. Siren is now accessible at `https://` (when used with `--net=host`). You will get a warning about an invalid certificate, this can be safely ignored. + + > Note: We recommend setting a strong password when running Siren to protect it from unauthorized access. + +Advanced users can mount their own certificates or disable SSL altogether, see the `SSL Certificates` section below. + +## Building From Source + +### Docker + +The docker image can be built with the following command: +`docker build -f Dockerfile -t siren .` + +### Building locally + +To build from source, ensure that your system has `Node v18.18` and `yarn` installed. + +#### Build and run the backend + +Navigate to the backend directory `cd backend`. Install all required Node packages by running `yarn`. Once the installation is complete, compile the backend with `yarn build`. Deploy the backend in a production environment, `yarn start:production`. This ensures optimal performance. + +#### Build and run the frontend + +After initializing the backend, return to the root directory. Install all frontend dependencies by executing `yarn`. Build the frontend using `yarn build`. Start the frontend production server with `yarn start`. + +This will allow you to access siren at `http://localhost:3000` by default. + +## Advanced configuration + +### About self-signed SSL certificates + +By default, internally, Siren is running on port 80 (plain, behind nginx), port 3000 (plain, direct) and port 443 (with SSL, behind nginx)). Siren will generate and use a self-signed certificate on startup. This will generate a security warning when you try to access the interface. We recommend to only disable SSL if you would access Siren over a local LAN or otherwise highly trusted or encrypted network (i.e. VPN). + +#### Generating persistent SSL certificates and installing them to your system + +[mkcert](https://github.com/FiloSottile/mkcert) is a tool that makes it super easy to generate a self-signed certificate that is trusted by your browser. + +To use it for `siren`, install it following the instructions. Then, run `mkdir certs; mkcert -cert-file certs/cert.pem -key-file certs/key.pem 127.0.0.1 localhost` (add or replace any IP or hostname that you would use to access it at the end of this command). +To use these generated certificates, add this to to your `docker run` command: `-v $PWD/certs:/certs` + +The nginx SSL config inside Siren's container expects 3 files: `/certs/cert.pem` `/certs/key.pem` `/certs/key.pass`. If `/certs/cert.pem` does not exist, it will generate a self-signed certificate as mentioned above. If `/certs/cert.pem` does exist, it will attempt to use your provided or persisted certificates. + +### Configuration through environment variables + +For those who prefer to use environment variables to configure Siren instead of using an `.env` file, this is fully supported. In some cases this may even be preferred. + +#### Docker installed through `snap` + +If you installed Docker through a snap (i.e. on Ubuntu), Docker will have trouble accessing the `.env` file. In this case it is highly recommended to pass the config to the container with environment variables. +Note that the defaults in `.env.example` will be used as fallback, if no other value is provided. diff --git a/book/src/ui-faqs.md b/book/src/ui-faqs.md index 0887875316..29de889e5f 100644 --- a/book/src/ui-faqs.md +++ b/book/src/ui-faqs.md @@ -10,15 +10,16 @@ The required API token may be found in the default data directory of the validat ## 3. How do I fix the Node Network Errors? -If you receive a red notification with a BEACON or VALIDATOR NODE NETWORK ERROR you can refer to the lighthouse ui configuration and [`connecting to clients section`](./ui-configuration.md#connecting-to-the-clients). +If you receive a red notification with a BEACON or VALIDATOR NODE NETWORK ERROR you can refer to the lighthouse ui [`configuration`](./ui-configuration.md#configuration). ## 4. How do I connect Siren to Lighthouse from a different computer on the same network? -Siren is a webapp, you can access it like any other website. We don't recommend exposing it to the internet; if you require remote access a VPN or (authenticated) reverse proxy is highly recommended. +Siren is a webapp, you can access it like any other website. We don't recommend exposing it to the internet; if you require remote access a VPN or (authenticated) reverse proxy is highly recommended. +That being said, it is entirely possible to have it published over the internet, how to do that goes well beyond the scope of this document but we want to emphasize once more the need for *at least* SSL encryption if you choose to do so. ## 5. How can I use Siren to monitor my validators remotely when I am not at home? -Most contemporary home routers provide options for VPN access in various ways. A VPN permits a remote computer to establish a connection with internal computers within a home network. With a VPN configuration in place, connecting to the VPN enables you to treat your computer as if it is part of your local home network. The connection process involves following the setup steps for connecting via another machine on the same network on the Siren configuration page and [`connecting to clients section`](./ui-configuration.md#connecting-to-the-clients). +Most contemporary home routers provide options for VPN access in various ways. A VPN permits a remote computer to establish a connection with internal computers within a home network. With a VPN configuration in place, connecting to the VPN enables you to treat your computer as if it is part of your local home network. The connection process involves following the setup steps for connecting via another machine on the same network on the Siren configuration page and [`configuration`](./ui-configuration.md#configuration). ## 6. Does Siren support reverse proxy or DNS named addresses? diff --git a/wordlist.txt b/wordlist.txt index f06c278866..6287366cbc 100644 --- a/wordlist.txt +++ b/wordlist.txt @@ -194,7 +194,6 @@ rc reimport resync roadmap -runtime rustfmt rustup schemas From 42c64a2744759b7a0ef9852b0e8caf3b3cb4e7db Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Thu, 19 Dec 2024 14:09:44 +0700 Subject: [PATCH 057/254] Ensure non-zero bits for each committee bitfield comprising an aggregate (#6603) * add new validation --- .../src/common/get_attesting_indices.rs | 16 +++++++++++----- consensus/types/src/beacon_state.rs | 1 + 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/consensus/state_processing/src/common/get_attesting_indices.rs b/consensus/state_processing/src/common/get_attesting_indices.rs index b131f7679a..842adce431 100644 --- a/consensus/state_processing/src/common/get_attesting_indices.rs +++ b/consensus/state_processing/src/common/get_attesting_indices.rs @@ -103,14 +103,14 @@ pub mod attesting_indices_electra { let committee_count_per_slot = committees.len() as u64; let mut participant_count = 0; - for index in committee_indices { + for committee_index in committee_indices { let beacon_committee = committees - .get(index as usize) - .ok_or(Error::NoCommitteeFound(index))?; + .get(committee_index as usize) + .ok_or(Error::NoCommitteeFound(committee_index))?; // This check is new to the spec's `process_attestation` in Electra. - if index >= committee_count_per_slot { - return Err(BeaconStateError::InvalidCommitteeIndex(index)); + if committee_index >= committee_count_per_slot { + return Err(BeaconStateError::InvalidCommitteeIndex(committee_index)); } participant_count.safe_add_assign(beacon_committee.committee.len() as u64)?; let committee_attesters = beacon_committee @@ -127,6 +127,12 @@ pub mod attesting_indices_electra { }) .collect::>(); + // Require at least a single non-zero bit for each attesting committee bitfield. + // This check is new to the spec's `process_attestation` in Electra. + if committee_attesters.is_empty() { + return Err(BeaconStateError::EmptyCommittee); + } + attesting_indices.extend(committee_attesters); committee_offset.safe_add_assign(beacon_committee.committee.len())?; } diff --git a/consensus/types/src/beacon_state.rs b/consensus/types/src/beacon_state.rs index 77b72b209c..ad4484b86a 100644 --- a/consensus/types/src/beacon_state.rs +++ b/consensus/types/src/beacon_state.rs @@ -59,6 +59,7 @@ pub enum Error { UnknownValidator(usize), UnableToDetermineProducer, InvalidBitfield, + EmptyCommittee, ValidatorIsWithdrawable, ValidatorIsInactive { val_index: usize, From 7e0cddef321c2a069582c65b58e5f46590d60c49 Mon Sep 17 00:00:00 2001 From: Akihito Nakano Date: Tue, 24 Dec 2024 10:38:56 +0900 Subject: [PATCH 058/254] Make sure we have fanout peers when publish (#6738) * Ensure that `fanout_peers` is always non-empty if it's `Some` --- .../lighthouse_network/gossipsub/src/behaviour.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/beacon_node/lighthouse_network/gossipsub/src/behaviour.rs b/beacon_node/lighthouse_network/gossipsub/src/behaviour.rs index aafd869bee..c4e20e4397 100644 --- a/beacon_node/lighthouse_network/gossipsub/src/behaviour.rs +++ b/beacon_node/lighthouse_network/gossipsub/src/behaviour.rs @@ -679,9 +679,15 @@ where // Gossipsub peers None => { tracing::debug!(topic=%topic_hash, "Topic not in the mesh"); + // `fanout_peers` is always non-empty if it's `Some`. + let fanout_peers = self + .fanout + .get(&topic_hash) + .map(|peers| if peers.is_empty() { None } else { Some(peers) }) + .unwrap_or(None); // If we have fanout peers add them to the map. - if self.fanout.contains_key(&topic_hash) { - for peer in self.fanout.get(&topic_hash).expect("Topic must exist") { + if let Some(peers) = fanout_peers { + for peer in peers { recipient_peers.insert(*peer); } } else { From f51a292f77575a1786af34271fb44954f141c377 Mon Sep 17 00:00:00 2001 From: Daniel Knopik <107140945+dknopik@users.noreply.github.com> Date: Fri, 3 Jan 2025 20:27:21 +0100 Subject: [PATCH 059/254] fully lint only explicitly to avoid unnecessary rebuilds (#6753) * fully lint only explicitly to avoid unnecessary rebuilds --- .github/workflows/test-suite.yml | 2 +- Makefile | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index 65663e0cf4..45f3b757e7 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -350,7 +350,7 @@ jobs: - name: Check formatting with cargo fmt run: make cargo-fmt - name: Lint code for quality and style with Clippy - run: make lint + run: make lint-full - name: Certify Cargo.lock freshness run: git diff --exit-code Cargo.lock - name: Typecheck benchmark code without running it diff --git a/Makefile b/Makefile index 958abf8705..8faf8a2e54 100644 --- a/Makefile +++ b/Makefile @@ -204,7 +204,7 @@ test-full: cargo-fmt test-release test-debug test-ef test-exec-engine # Lints the code for bad style and potentially unsafe arithmetic using Clippy. # Clippy lints are opt-in per-crate for now. By default, everything is allowed except for performance and correctness lints. lint: - RUSTFLAGS="-C debug-assertions=no $(RUSTFLAGS)" cargo clippy --workspace --benches --tests $(EXTRA_CLIPPY_OPTS) --features "$(TEST_FEATURES)" -- \ + cargo clippy --workspace --benches --tests $(EXTRA_CLIPPY_OPTS) --features "$(TEST_FEATURES)" -- \ -D clippy::fn_to_numeric_cast_any \ -D clippy::manual_let_else \ -D clippy::large_stack_frames \ @@ -220,6 +220,10 @@ lint: lint-fix: EXTRA_CLIPPY_OPTS="--fix --allow-staged --allow-dirty" $(MAKE) lint +# Also run the lints on the optimized-only tests +lint-full: + RUSTFLAGS="-C debug-assertions=no $(RUSTFLAGS)" $(MAKE) lint + # Runs the makefile in the `ef_tests` repo. # # May download and extract an archive of test vectors from the ethereum From 84519010f29b7f8ac50cb2e68ec6ffed69a6e6f2 Mon Sep 17 00:00:00 2001 From: realbigsean Date: Tue, 7 Jan 2025 16:39:48 -0800 Subject: [PATCH 060/254] add joao CODEOWNERS (#6762) * add joao CODEOWNERS --- .github/CODEOWNERS | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000000..f9478d1369 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +beacon_node/network/ @jxs +beacon_node/lighthouse_network/ @jxs From 57141d8b4bab2d8281a01629de04d9a935f00d1c Mon Sep 17 00:00:00 2001 From: Ekaterina Riazantseva Date: Wed, 8 Jan 2025 01:39:53 +0100 Subject: [PATCH 061/254] Add 'beacon_' prefix to PeerDAS metrics names (#6537) * Add 'beacon_' prefix to PeerDAS metrics names * Merge remote-tracking branch 'origin/unstable' into peerdas-metrics * Merge 'origin/unstable' into peerdas-metrics * Merge remote-tracking branch 'origin/unstable/ into peerdas-metrics * Add 'beacon_' prefix to 'kzg_data_column' metrics --- beacon_node/beacon_chain/src/metrics.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/beacon_node/beacon_chain/src/metrics.rs b/beacon_node/beacon_chain/src/metrics.rs index c6aa9fbcac..8d71e895c9 100644 --- a/beacon_node/beacon_chain/src/metrics.rs +++ b/beacon_node/beacon_chain/src/metrics.rs @@ -1656,7 +1656,7 @@ pub static BLOB_SIDECAR_INCLUSION_PROOF_COMPUTATION: LazyLock> }); pub static DATA_COLUMN_SIDECAR_COMPUTATION: LazyLock> = LazyLock::new(|| { try_create_histogram_vec_with_buckets( - "data_column_sidecar_computation_seconds", + "beacon_data_column_sidecar_computation_seconds", "Time taken to compute data column sidecar, including cells, proofs and inclusion proof", Ok(vec![0.1, 0.15, 0.25, 0.35, 0.5, 0.7, 1.0, 2.5, 5.0, 10.0]), &["blob_count"], @@ -1665,7 +1665,7 @@ pub static DATA_COLUMN_SIDECAR_COMPUTATION: LazyLock> = Laz pub static DATA_COLUMN_SIDECAR_INCLUSION_PROOF_VERIFICATION: LazyLock> = LazyLock::new(|| { try_create_histogram( - "data_column_sidecar_inclusion_proof_verification_seconds", + "beacon_data_column_sidecar_inclusion_proof_verification_seconds", "Time taken to verify data_column sidecar inclusion proof", ) }); @@ -1847,7 +1847,7 @@ pub static KZG_VERIFICATION_BATCH_TIMES: LazyLock> = LazyLock: pub static KZG_VERIFICATION_DATA_COLUMN_SINGLE_TIMES: LazyLock> = LazyLock::new(|| { try_create_histogram_with_buckets( - "kzg_verification_data_column_single_seconds", + "beacon_kzg_verification_data_column_single_seconds", "Runtime of single data column kzg verification", Ok(vec![ 0.0005, 0.001, 0.0015, 0.002, 0.003, 0.004, 0.005, 0.007, 0.01, 0.02, 0.05, @@ -1857,7 +1857,7 @@ pub static KZG_VERIFICATION_DATA_COLUMN_SINGLE_TIMES: LazyLock pub static KZG_VERIFICATION_DATA_COLUMN_BATCH_TIMES: LazyLock> = LazyLock::new(|| { try_create_histogram_with_buckets( - "kzg_verification_data_column_batch_seconds", + "beacon_kzg_verification_data_column_batch_seconds", "Runtime of batched data column kzg verification", Ok(vec![ 0.002, 0.004, 0.006, 0.008, 0.01, 0.012, 0.015, 0.02, 0.03, 0.05, 0.07, @@ -1910,14 +1910,14 @@ pub static DATA_AVAILABILITY_OVERFLOW_STORE_CACHE_SIZE: LazyLock> = LazyLock::new(|| { try_create_histogram( - "data_availability_reconstruction_time_seconds", + "beacon_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", + "beacon_data_availability_reconstructed_columns_total", "Total count of reconstructed columns", ) }); @@ -1925,7 +1925,7 @@ pub static DATA_AVAILABILITY_RECONSTRUCTED_COLUMNS: LazyLock> pub static KZG_DATA_COLUMN_RECONSTRUCTION_ATTEMPTS: LazyLock> = LazyLock::new(|| { try_create_int_counter( - "kzg_data_column_reconstruction_attempts", + "beacon_kzg_data_column_reconstruction_attempts", "Count of times data column reconstruction has been attempted", ) }); @@ -1933,7 +1933,7 @@ pub static KZG_DATA_COLUMN_RECONSTRUCTION_ATTEMPTS: LazyLock> pub static KZG_DATA_COLUMN_RECONSTRUCTION_FAILURES: LazyLock> = LazyLock::new(|| { try_create_int_counter( - "kzg_data_column_reconstruction_failures", + "beacon_kzg_data_column_reconstruction_failures", "Count of times data column reconstruction has failed", ) }); @@ -1941,7 +1941,7 @@ pub static KZG_DATA_COLUMN_RECONSTRUCTION_FAILURES: LazyLock> pub static KZG_DATA_COLUMN_RECONSTRUCTION_INCOMPLETE_TOTAL: LazyLock> = LazyLock::new(|| { try_create_int_counter_vec( - "kzg_data_column_reconstruction_incomplete_total", + "beacon_kzg_data_column_reconstruction_incomplete_total", "Count of times data column reconstruction attempts did not result in an import", &["reason"], ) From 7ec748a108bdef9fbe02ae9edb2f49f2682a555f Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Wed, 8 Jan 2025 14:12:34 +1100 Subject: [PATCH 062/254] Implement `getBlobSidecars` support for PeerDAS (#6755) * Implement getBlobSidecars endpoint for PeerDAS. * Merge branch 'unstable' into peerdas-get-blob-sidecars * Fix incorrect logging. * Replace `and_then` usage. --- beacon_node/beacon_chain/src/kzg_utils.rs | 143 ++++++++++++++++++++-- beacon_node/http_api/src/block_id.rs | 84 ++++++++++--- consensus/types/src/blob_sidecar.rs | 6 +- 3 files changed, 202 insertions(+), 31 deletions(-) diff --git a/beacon_node/beacon_chain/src/kzg_utils.rs b/beacon_node/beacon_chain/src/kzg_utils.rs index 1680c0298d..bd47e82215 100644 --- a/beacon_node/beacon_chain/src/kzg_utils.rs +++ b/beacon_node/beacon_chain/src/kzg_utils.rs @@ -7,8 +7,9 @@ use std::sync::Arc; use types::beacon_block_body::KzgCommitments; use types::data_column_sidecar::{Cell, DataColumn, DataColumnSidecarError}; use types::{ - Blob, ChainSpec, ColumnIndex, DataColumnSidecar, DataColumnSidecarList, EthSpec, Hash256, - KzgCommitment, KzgProof, KzgProofs, SignedBeaconBlock, SignedBeaconBlockHeader, + Blob, BlobSidecar, BlobSidecarList, ChainSpec, ColumnIndex, DataColumnSidecar, + DataColumnSidecarList, EthSpec, Hash256, KzgCommitment, KzgProof, KzgProofs, SignedBeaconBlock, + SignedBeaconBlockHeader, SignedBlindedBeaconBlock, }; /// Converts a blob ssz List object to an array to be used with the kzg @@ -243,6 +244,83 @@ fn build_data_column_sidecars( Ok(sidecars) } +/// Reconstruct blobs from a subset of data column sidecars (requires at least 50%). +/// +/// If `blob_indices_opt` is `None`, this function attempts to reconstruct all blobs associated +/// with the block. +pub fn reconstruct_blobs( + kzg: &Kzg, + data_columns: &[Arc>], + blob_indices_opt: Option>, + signed_block: &SignedBlindedBeaconBlock, +) -> Result, String> { + // The data columns are from the database, so we assume their correctness. + let first_data_column = data_columns + .first() + .ok_or("data_columns should have at least one element".to_string())?; + + let blob_indices: Vec = match blob_indices_opt { + Some(indices) => indices.into_iter().map(|i| i as usize).collect(), + None => { + let num_of_blobs = first_data_column.kzg_commitments.len(); + (0..num_of_blobs).collect() + } + }; + + let blob_sidecars = blob_indices + .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(format!("Missing data column at row index {row_index}")) + .and_then(|cell| { + ssz_cell_to_crypto_cell::(cell).map_err(|e| format!("{e:?}")) + })?; + + cells.push(cell); + cell_ids.push(data_column.index); + } + + let (cells, _kzg_proofs) = kzg + .recover_cells_and_compute_kzg_proofs(&cell_ids, &cells) + .map_err(|e| format!("Failed to recover cells and compute KZG proofs: {e:?}"))?; + + let num_cells_original_blob = cells.len() / 2; + let blob_bytes = cells + .into_iter() + .take(num_cells_original_blob) + .flat_map(|cell| cell.into_iter()) + .collect(); + + let blob = Blob::::new(blob_bytes).map_err(|e| format!("{e:?}"))?; + let kzg_commitment = first_data_column + .kzg_commitments + .get(row_index) + .ok_or(format!("Missing KZG commitment for blob {row_index}"))?; + let kzg_proof = compute_blob_kzg_proof::(kzg, &blob, *kzg_commitment) + .map_err(|e| format!("{e:?}"))?; + + BlobSidecar::::new_with_existing_proof( + row_index, + blob, + signed_block, + first_data_column.signed_block_header.clone(), + &first_data_column.kzg_commitments_inclusion_proof, + kzg_proof, + ) + .map(Arc::new) + .map_err(|e| format!("{e:?}")) + }) + .collect::, _>>()? + .into(); + + Ok(blob_sidecars) +} + /// Reconstruct all data columns from a subset of data column sidecars (requires at least 50%). pub fn reconstruct_data_columns( kzg: &Kzg, @@ -265,7 +343,7 @@ pub fn reconstruct_data_columns( 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}" + "Missing data column at row index {row_index}" )), )?; @@ -289,12 +367,16 @@ pub fn reconstruct_data_columns( #[cfg(test)] mod test { - use crate::kzg_utils::{blobs_to_data_column_sidecars, reconstruct_data_columns}; + use crate::kzg_utils::{ + blobs_to_data_column_sidecars, reconstruct_blobs, reconstruct_data_columns, + }; use bls::Signature; + use eth2::types::BlobsBundle; + use execution_layer::test_utils::generate_blobs; use kzg::{trusted_setup::get_trusted_setup, Kzg, KzgCommitment, TrustedSetup}; use types::{ - beacon_block_body::KzgCommitments, BeaconBlock, BeaconBlockDeneb, Blob, BlobsList, - ChainSpec, EmptyBlock, EthSpec, MainnetEthSpec, SignedBeaconBlock, + beacon_block_body::KzgCommitments, BeaconBlock, BeaconBlockDeneb, BlobsList, ChainSpec, + EmptyBlock, EthSpec, MainnetEthSpec, SignedBeaconBlock, }; type E = MainnetEthSpec; @@ -308,6 +390,7 @@ mod test { test_build_data_columns_empty(&kzg, &spec); test_build_data_columns(&kzg, &spec); test_reconstruct_data_columns(&kzg, &spec); + test_reconstruct_blobs_from_data_columns(&kzg, &spec); } #[track_caller] @@ -379,6 +462,36 @@ mod test { } } + #[track_caller] + fn test_reconstruct_blobs_from_data_columns(kzg: &Kzg, spec: &ChainSpec) { + let num_of_blobs = 6; + let (signed_block, blobs) = create_test_block_and_blobs::(num_of_blobs, spec); + let blob_refs = blobs.iter().collect::>(); + let column_sidecars = + blobs_to_data_column_sidecars(&blob_refs, &signed_block, kzg, spec).unwrap(); + + // Now reconstruct + let signed_blinded_block = signed_block.into(); + let blob_indices = vec![3, 4, 5]; + let reconstructed_blobs = reconstruct_blobs( + kzg, + &column_sidecars.iter().as_slice()[0..column_sidecars.len() / 2], + Some(blob_indices.clone()), + &signed_blinded_block, + ) + .unwrap(); + + for i in blob_indices { + let reconstructed_blob = &reconstructed_blobs + .iter() + .find(|sidecar| sidecar.index == i) + .map(|sidecar| sidecar.blob.clone()) + .expect("reconstructed blob should exist"); + let original_blob = blobs.get(i as usize).unwrap(); + assert_eq!(reconstructed_blob, original_blob, "{i}"); + } + } + fn get_kzg() -> Kzg { let trusted_setup: TrustedSetup = serde_json::from_reader(get_trusted_setup().as_slice()) .map_err(|e| format!("Unable to read trusted setup file: {}", e)) @@ -397,12 +510,20 @@ mod test { KzgCommitments::::new(vec![KzgCommitment::empty_for_testing(); num_of_blobs]) .unwrap(); - let signed_block = SignedBeaconBlock::from_block(block, Signature::empty()); + let mut signed_block = SignedBeaconBlock::from_block(block, Signature::empty()); - let blobs = (0..num_of_blobs) - .map(|_| Blob::::default()) - .collect::>() - .into(); + let (blobs_bundle, _) = generate_blobs::(num_of_blobs).unwrap(); + let BlobsBundle { + blobs, + commitments, + proofs: _, + } = blobs_bundle; + + *signed_block + .message_mut() + .body_mut() + .blob_kzg_commitments_mut() + .unwrap() = commitments; (signed_block, blobs) } diff --git a/beacon_node/http_api/src/block_id.rs b/beacon_node/http_api/src/block_id.rs index dba8eb1ef3..b9e4883318 100644 --- a/beacon_node/http_api/src/block_id.rs +++ b/beacon_node/http_api/src/block_id.rs @@ -1,4 +1,5 @@ use crate::{state_id::checkpoint_slot_and_execution_optimistic, ExecutionOptimistic}; +use beacon_chain::kzg_utils::reconstruct_blobs; use beacon_chain::{BeaconChain, BeaconChainError, BeaconChainTypes, WhenSlotSkipped}; use eth2::types::BlobIndicesQuery; use eth2::types::BlockId as CoreBlockId; @@ -9,6 +10,7 @@ use types::{ BlobSidecarList, EthSpec, FixedBytesExtended, Hash256, SignedBeaconBlock, SignedBlindedBeaconBlock, Slot, }; +use warp::Rejection; /// Wraps `eth2::types::BlockId` and provides a simple way to obtain a block or root for a given /// `BlockId`. @@ -261,7 +263,7 @@ impl BlockId { #[allow(clippy::type_complexity)] pub fn get_blinded_block_and_blob_list_filtered( &self, - indices: BlobIndicesQuery, + query: BlobIndicesQuery, chain: &BeaconChain, ) -> Result< ( @@ -286,20 +288,32 @@ impl BlockId { // Return the `BlobSidecarList` identified by `self`. let blob_sidecar_list = if !blob_kzg_commitments.is_empty() { - chain - .store - .get_blobs(&root) - .map_err(|e| warp_utils::reject::beacon_chain_error(e.into()))? - .ok_or_else(|| { - warp_utils::reject::custom_not_found(format!( - "no blobs stored for block {root}" - )) - })? + if chain.spec.is_peer_das_enabled_for_epoch(block.epoch()) { + Self::get_blobs_from_data_columns(chain, root, query.indices, &block)? + } else { + Self::get_blobs(chain, root, query.indices)? + } } else { BlobSidecarList::default() }; - let blob_sidecar_list_filtered = match indices.indices { + Ok((block, blob_sidecar_list, execution_optimistic, finalized)) + } + + fn get_blobs( + chain: &BeaconChain, + root: Hash256, + indices: Option>, + ) -> Result, Rejection> { + let blob_sidecar_list = chain + .store + .get_blobs(&root) + .map_err(|e| warp_utils::reject::beacon_chain_error(e.into()))? + .ok_or_else(|| { + warp_utils::reject::custom_not_found(format!("no blobs stored for block {root}")) + })?; + + let blob_sidecar_list_filtered = match indices { Some(vec) => { let list = blob_sidecar_list .into_iter() @@ -310,12 +324,48 @@ impl BlockId { } None => blob_sidecar_list, }; - Ok(( - block, - blob_sidecar_list_filtered, - execution_optimistic, - finalized, - )) + + Ok(blob_sidecar_list_filtered) + } + + fn get_blobs_from_data_columns( + chain: &BeaconChain, + root: Hash256, + blob_indices: Option>, + block: &SignedBlindedBeaconBlock<::EthSpec>, + ) -> Result, Rejection> { + let column_indices = chain.store.get_data_column_keys(root).map_err(|e| { + warp_utils::reject::custom_server_error(format!( + "Error fetching data columns keys: {e:?}" + )) + })?; + + let num_found_column_keys = column_indices.len(); + let num_required_columns = chain.spec.number_of_columns / 2; + let is_blob_available = num_found_column_keys >= num_required_columns; + + if is_blob_available { + let data_columns = column_indices + .into_iter() + .filter_map( + |column_index| match chain.get_data_column(&root, &column_index) { + Ok(Some(data_column)) => Some(Ok(data_column)), + Ok(None) => None, + Err(e) => Some(Err(warp_utils::reject::beacon_chain_error(e))), + }, + ) + .collect::, _>>()?; + + reconstruct_blobs(&chain.kzg, &data_columns, blob_indices, block).map_err(|e| { + warp_utils::reject::custom_server_error(format!( + "Error reconstructing data columns: {e:?}" + )) + }) + } else { + Err(warp_utils::reject::custom_server_error( + format!("Insufficient data columns to reconstruct blobs: required {num_required_columns}, but only {num_found_column_keys} were found.") + )) + } } } diff --git a/consensus/types/src/blob_sidecar.rs b/consensus/types/src/blob_sidecar.rs index 5a330388cc..302aa2a4c1 100644 --- a/consensus/types/src/blob_sidecar.rs +++ b/consensus/types/src/blob_sidecar.rs @@ -1,9 +1,9 @@ use crate::test_utils::TestRandom; -use crate::ForkName; use crate::{ beacon_block_body::BLOB_KZG_COMMITMENTS_INDEX, BeaconBlockHeader, BeaconStateError, Blob, Epoch, EthSpec, FixedVector, Hash256, SignedBeaconBlockHeader, Slot, VariableList, }; +use crate::{AbstractExecPayload, ForkName}; use crate::{ForkVersionDeserialize, KzgProofs, SignedBeaconBlock}; use bls::Signature; use derivative::Derivative; @@ -150,10 +150,10 @@ impl BlobSidecar { }) } - pub fn new_with_existing_proof( + pub fn new_with_existing_proof>( index: usize, blob: Blob, - signed_block: &SignedBeaconBlock, + signed_block: &SignedBeaconBlock, signed_block_header: SignedBeaconBlockHeader, kzg_commitments_inclusion_proof: &[Hash256], kzg_proof: KzgProof, From 80cfbea7fe4c78d90638b256b0cb7fc19652b31f Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Wed, 8 Jan 2025 14:12:37 +1100 Subject: [PATCH 063/254] Fix incorrect data column metric name (#6761) * Fix incorrect data column metric name. --- beacon_node/beacon_chain/src/metrics.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beacon_node/beacon_chain/src/metrics.rs b/beacon_node/beacon_chain/src/metrics.rs index 8d71e895c9..ae3add7f03 100644 --- a/beacon_node/beacon_chain/src/metrics.rs +++ b/beacon_node/beacon_chain/src/metrics.rs @@ -1693,7 +1693,7 @@ pub static DATA_COLUMN_SIDECAR_GOSSIP_VERIFICATION_TIMES: LazyLock> = LazyLock::new(|| { try_create_int_counter( - "beacon_blobs_column_sidecar_processing_successes_total", + "beacon_data_column_sidecar_processing_successes_total", "Number of data column sidecars verified for gossip", ) }); From 87b72dec21759acfbc749220be3aee11ac91cdf3 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Wed, 8 Jan 2025 14:12:39 +1100 Subject: [PATCH 064/254] Fix incorrect VC default HTTP token path when the `--datadir` flag is present (#6748) * Fix incorrect default http token path when datadir flag is present. --- lighthouse/tests/validator_client.rs | 15 ++++++++++++++- validator_client/http_api/src/lib.rs | 1 + validator_client/src/config.rs | 9 +++++---- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/lighthouse/tests/validator_client.rs b/lighthouse/tests/validator_client.rs index c5b303e4d1..1945399c86 100644 --- a/lighthouse/tests/validator_client.rs +++ b/lighthouse/tests/validator_client.rs @@ -345,7 +345,7 @@ fn http_store_keystore_passwords_in_secrets_dir_present() { } #[test] -fn http_token_path_flag() { +fn http_token_path_flag_present() { let dir = TempDir::new().expect("Unable to create temporary directory"); CommandLineTest::new() .flag("http", None) @@ -359,6 +359,19 @@ fn http_token_path_flag() { }); } +#[test] +fn http_token_path_default() { + CommandLineTest::new() + .flag("http", None) + .run() + .with_config(|config| { + assert_eq!( + config.http_api.http_token_path, + config.validator_dir.join("api-token.txt") + ); + }); +} + // Tests for Metrics flags. #[test] fn metrics_flag() { diff --git a/validator_client/http_api/src/lib.rs b/validator_client/http_api/src/lib.rs index f3dab3780c..73ebe717af 100644 --- a/validator_client/http_api/src/lib.rs +++ b/validator_client/http_api/src/lib.rs @@ -106,6 +106,7 @@ pub struct Config { impl Default for Config { fn default() -> Self { + // This value is always overridden when building config from CLI. let http_token_path = dirs::home_dir() .unwrap_or_else(|| PathBuf::from(".")) .join(DEFAULT_ROOT_DIR) diff --git a/validator_client/src/config.rs b/validator_client/src/config.rs index 0fecb5202d..bb72ef81c8 100644 --- a/validator_client/src/config.rs +++ b/validator_client/src/config.rs @@ -314,10 +314,11 @@ impl Config { config.http_api.store_passwords_in_secrets_dir = true; } - if cli_args.get_one::("http-token-path").is_some() { - config.http_api.http_token_path = parse_required(cli_args, "http-token-path") - // For backward compatibility, default to the path under the validator dir if not provided. - .unwrap_or_else(|_| config.validator_dir.join(PK_FILENAME)); + if let Some(http_token_path) = cli_args.get_one::("http-token-path") { + config.http_api.http_token_path = PathBuf::from(http_token_path); + } else { + // For backward compatibility, default to the path under the validator dir if not provided. + config.http_api.http_token_path = config.validator_dir.join(PK_FILENAME); } /* From 1f6850fae2807c1d3f0e281524e0b1b9ab230e67 Mon Sep 17 00:00:00 2001 From: Pawan Dhananjay Date: Fri, 10 Jan 2025 06:43:29 +0530 Subject: [PATCH 065/254] Rust 1.84 lints (#6781) * Fix few lints * Fix remaining lints * Use fully qualified syntax --- beacon_node/beacon_chain/src/beacon_chain.rs | 10 +++++----- beacon_node/beacon_chain/src/canonical_head.rs | 6 +----- .../beacon_chain/src/data_availability_checker.rs | 6 +++--- .../overflow_lru_cache.rs | 9 +++------ .../beacon_chain/src/early_attester_cache.rs | 2 +- beacon_node/beacon_chain/src/eth1_chain.rs | 2 +- beacon_node/beacon_chain/src/execution_payload.rs | 8 ++++---- .../beacon_chain/src/graffiti_calculator.rs | 7 ++----- .../beacon_chain/src/observed_aggregates.rs | 2 +- beacon_node/beacon_chain/src/observed_attesters.rs | 4 ++-- .../beacon_chain/src/observed_data_sidecars.rs | 2 +- beacon_node/beacon_chain/src/shuffling_cache.rs | 2 +- beacon_node/beacon_processor/src/lib.rs | 2 +- beacon_node/client/src/builder.rs | 2 +- beacon_node/client/src/notifier.rs | 8 +++----- beacon_node/execution_layer/src/engine_api/http.rs | 10 ++++------ beacon_node/execution_layer/src/lib.rs | 2 +- beacon_node/execution_layer/src/payload_status.rs | 2 +- .../src/test_utils/mock_execution_layer.rs | 2 +- beacon_node/genesis/src/eth1_genesis_service.rs | 2 +- beacon_node/http_api/src/lib.rs | 4 ++-- beacon_node/http_api/src/validator.rs | 2 +- beacon_node/http_api/tests/interactive_tests.rs | 2 +- beacon_node/http_api/tests/tests.rs | 2 +- .../lighthouse_network/gossipsub/src/backoff.rs | 2 +- .../lighthouse_network/gossipsub/src/behaviour.rs | 3 +-- beacon_node/lighthouse_network/src/config.rs | 6 +++--- .../src/discovery/subnet_predicate.rs | 4 ++-- .../lighthouse_network/src/peer_manager/peerdb.rs | 2 +- .../src/peer_manager/peerdb/peer_info.rs | 4 ++-- .../src/network_beacon_processor/gossip_methods.rs | 2 +- beacon_node/operation_pool/src/lib.rs | 14 +++++--------- beacon_node/store/src/forwards_iter.rs | 4 ++-- beacon_node/store/src/reconstruct.rs | 2 +- beacon_node/store/src/state_cache.rs | 8 ++------ common/account_utils/src/validator_definitions.rs | 2 +- common/logging/src/lib.rs | 2 +- consensus/proto_array/src/proto_array.rs | 10 +++++----- consensus/state_processing/src/genesis.rs | 12 ++++++------ .../per_block_processing/altair/sync_committee.rs | 2 +- .../epoch_processing_summary.rs | 12 ++++++------ consensus/types/src/beacon_block_body.rs | 2 +- consensus/types/src/beacon_state.rs | 2 +- .../src/beacon_state/progressive_balances_cache.rs | 2 +- consensus/types/src/chain_spec.rs | 10 ++++------ consensus/types/src/deposit_tree_snapshot.rs | 3 +-- consensus/types/src/graffiti.rs | 2 +- lcli/src/transition_blocks.rs | 4 ++-- slasher/src/database.rs | 2 +- testing/ef_tests/src/cases/fork_choice.rs | 2 +- testing/ef_tests/src/cases/operations.rs | 4 ++-- testing/ef_tests/src/decode.rs | 2 +- validator_client/doppelganger_service/src/lib.rs | 2 +- .../slashing_protection/src/slashing_database.rs | 4 +--- .../validator_services/src/preparation_service.rs | 2 +- validator_client/validator_services/src/sync.rs | 2 +- validator_manager/src/create_validators.rs | 4 ++-- validator_manager/src/delete_validators.rs | 2 +- validator_manager/src/import_validators.rs | 2 +- validator_manager/src/list_validators.rs | 2 +- validator_manager/src/move_validators.rs | 2 +- 61 files changed, 110 insertions(+), 138 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 80766d57b3..7bbb9ff74d 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -573,7 +573,7 @@ impl BeaconChain { .start_slot(T::EthSpec::slots_per_epoch()); let is_canonical = self .block_root_at_slot(block_slot, WhenSlotSkipped::None)? - .map_or(false, |canonical_root| block_root == &canonical_root); + .is_some_and(|canonical_root| block_root == &canonical_root); Ok(block_slot <= finalized_slot && is_canonical) } @@ -604,7 +604,7 @@ impl BeaconChain { let slot_is_finalized = state_slot <= finalized_slot; let canonical = self .state_root_at_slot(state_slot)? - .map_or(false, |canonical_root| state_root == &canonical_root); + .is_some_and(|canonical_root| state_root == &canonical_root); Ok(FinalizationAndCanonicity { slot_is_finalized, canonical, @@ -5118,9 +5118,9 @@ impl BeaconChain { .start_of(slot) .unwrap_or_else(|| Duration::from_secs(0)), ); - block_delays.observed.map_or(false, |delay| { - delay >= self.slot_clock.unagg_attestation_production_delay() - }) + block_delays + .observed + .is_some_and(|delay| delay >= self.slot_clock.unagg_attestation_production_delay()) } /// Produce a block for some `slot` upon the given `state`. diff --git a/beacon_node/beacon_chain/src/canonical_head.rs b/beacon_node/beacon_chain/src/canonical_head.rs index 4f92f5ec8f..4e21372efb 100644 --- a/beacon_node/beacon_chain/src/canonical_head.rs +++ b/beacon_node/beacon_chain/src/canonical_head.rs @@ -1254,11 +1254,7 @@ pub fn find_reorg_slot( ($state: ident, $block_root: ident) => { std::iter::once(Ok(($state.slot(), $block_root))) .chain($state.rev_iter_block_roots(spec)) - .skip_while(|result| { - result - .as_ref() - .map_or(false, |(slot, _)| *slot > lowest_slot) - }) + .skip_while(|result| result.as_ref().is_ok_and(|(slot, _)| *slot > lowest_slot)) }; } diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index 72806a74d2..f6002ea0ac 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -519,13 +519,13 @@ impl DataAvailabilityChecker { /// Returns true if the given epoch lies within the da boundary and false otherwise. pub fn da_check_required_for_epoch(&self, block_epoch: Epoch) -> bool { self.data_availability_boundary() - .map_or(false, |da_epoch| block_epoch >= da_epoch) + .is_some_and(|da_epoch| block_epoch >= da_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| { - self.spec.deneb_fork_epoch.map_or(false, |deneb_epoch| { + self.slot_clock.now().is_some_and(|slot| { + self.spec.deneb_fork_epoch.is_some_and(|deneb_epoch| { let now_epoch = slot.epoch(T::EthSpec::slots_per_epoch()); now_epoch >= deneb_epoch }) 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 40361574af..5ce023038d 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 @@ -228,13 +228,10 @@ impl PendingComponents { ); let all_blobs_received = block_kzg_commitments_count_opt - .map_or(false, |num_expected_blobs| { - num_expected_blobs == num_received_blobs - }); + .is_some_and(|num_expected_blobs| num_expected_blobs == num_received_blobs); - let all_columns_received = expected_columns_opt.map_or(false, |num_expected_columns| { - num_expected_columns == num_received_columns - }); + let all_columns_received = expected_columns_opt + .is_some_and(|num_expected_columns| num_expected_columns == num_received_columns); all_blobs_received || all_columns_received } diff --git a/beacon_node/beacon_chain/src/early_attester_cache.rs b/beacon_node/beacon_chain/src/early_attester_cache.rs index 606610a748..c94ea0e941 100644 --- a/beacon_node/beacon_chain/src/early_attester_cache.rs +++ b/beacon_node/beacon_chain/src/early_attester_cache.rs @@ -145,7 +145,7 @@ impl EarlyAttesterCache { self.item .read() .as_ref() - .map_or(false, |item| item.beacon_block_root == block_root) + .is_some_and(|item| item.beacon_block_root == block_root) } /// Returns the block, if `block_root` matches the cached item. diff --git a/beacon_node/beacon_chain/src/eth1_chain.rs b/beacon_node/beacon_chain/src/eth1_chain.rs index cb6e4c34f3..ad4f106517 100644 --- a/beacon_node/beacon_chain/src/eth1_chain.rs +++ b/beacon_node/beacon_chain/src/eth1_chain.rs @@ -153,7 +153,7 @@ fn get_sync_status( // Lighthouse is "cached and ready" when it has cached enough blocks to cover the start of the // current voting period. let lighthouse_is_cached_and_ready = - latest_cached_block_timestamp.map_or(false, |t| t >= voting_target_timestamp); + latest_cached_block_timestamp.is_some_and(|t| t >= voting_target_timestamp); Some(Eth1SyncStatusData { head_block_number, diff --git a/beacon_node/beacon_chain/src/execution_payload.rs b/beacon_node/beacon_chain/src/execution_payload.rs index 92d24c53c0..502a7918a1 100644 --- a/beacon_node/beacon_chain/src/execution_payload.rs +++ b/beacon_node/beacon_chain/src/execution_payload.rs @@ -127,9 +127,9 @@ impl PayloadNotifier { /// contains a few extra checks by running `partially_verify_execution_payload` first: /// /// https://github.com/ethereum/consensus-specs/blob/v1.1.9/specs/bellatrix/beacon-chain.md#notify_new_payload -async fn notify_new_payload<'a, T: BeaconChainTypes>( +async fn notify_new_payload( chain: &Arc>, - block: BeaconBlockRef<'a, T::EthSpec>, + block: BeaconBlockRef<'_, T::EthSpec>, ) -> Result { let execution_layer = chain .execution_layer @@ -230,9 +230,9 @@ async fn notify_new_payload<'a, T: BeaconChainTypes>( /// Equivalent to the `validate_merge_block` function in the merge Fork Choice Changes: /// /// https://github.com/ethereum/consensus-specs/blob/v1.1.5/specs/merge/fork-choice.md#validate_merge_block -pub async fn validate_merge_block<'a, T: BeaconChainTypes>( +pub async fn validate_merge_block( chain: &Arc>, - block: BeaconBlockRef<'a, T::EthSpec>, + block: BeaconBlockRef<'_, T::EthSpec>, allow_optimistic_import: AllowOptimisticImport, ) -> Result<(), BlockError> { let spec = &chain.spec; diff --git a/beacon_node/beacon_chain/src/graffiti_calculator.rs b/beacon_node/beacon_chain/src/graffiti_calculator.rs index 4373164d62..8692d374ed 100644 --- a/beacon_node/beacon_chain/src/graffiti_calculator.rs +++ b/beacon_node/beacon_chain/src/graffiti_calculator.rs @@ -293,10 +293,7 @@ mod tests { .await .unwrap(); - let version_bytes = std::cmp::min( - lighthouse_version::VERSION.as_bytes().len(), - GRAFFITI_BYTES_LEN, - ); + let version_bytes = std::cmp::min(lighthouse_version::VERSION.len(), GRAFFITI_BYTES_LEN); // grab the slice of the graffiti that corresponds to the lighthouse version let graffiti_slice = &harness.chain.graffiti_calculator.get_graffiti(None).await.0[..version_bytes]; @@ -361,7 +358,7 @@ mod tests { let graffiti_str = "nice graffiti bro"; let mut graffiti_bytes = [0u8; GRAFFITI_BYTES_LEN]; - graffiti_bytes[..graffiti_str.as_bytes().len()].copy_from_slice(graffiti_str.as_bytes()); + graffiti_bytes[..graffiti_str.len()].copy_from_slice(graffiti_str.as_bytes()); let found_graffiti = harness .chain diff --git a/beacon_node/beacon_chain/src/observed_aggregates.rs b/beacon_node/beacon_chain/src/observed_aggregates.rs index dec012fb92..20ed36ace7 100644 --- a/beacon_node/beacon_chain/src/observed_aggregates.rs +++ b/beacon_node/beacon_chain/src/observed_aggregates.rs @@ -293,7 +293,7 @@ impl SlotHashSet { Ok(self .map .get(&root) - .map_or(false, |agg| agg.iter().any(|val| item.is_subset(val)))) + .is_some_and(|agg| agg.iter().any(|val| item.is_subset(val)))) } /// The number of observed items in `self`. diff --git a/beacon_node/beacon_chain/src/observed_attesters.rs b/beacon_node/beacon_chain/src/observed_attesters.rs index efb95f57a9..5bba8e4d8e 100644 --- a/beacon_node/beacon_chain/src/observed_attesters.rs +++ b/beacon_node/beacon_chain/src/observed_attesters.rs @@ -130,7 +130,7 @@ impl Item<()> for EpochBitfield { fn get(&self, validator_index: usize) -> Option<()> { self.bitfield .get(validator_index) - .map_or(false, |bit| *bit) + .is_some_and(|bit| *bit) .then_some(()) } } @@ -336,7 +336,7 @@ impl, E: EthSpec> AutoPruningEpochContainer { let exists = self .items .get(&epoch) - .map_or(false, |item| item.get(validator_index).is_some()); + .is_some_and(|item| item.get(validator_index).is_some()); Ok(exists) } diff --git a/beacon_node/beacon_chain/src/observed_data_sidecars.rs b/beacon_node/beacon_chain/src/observed_data_sidecars.rs index 53f8c71f54..a9f4664064 100644 --- a/beacon_node/beacon_chain/src/observed_data_sidecars.rs +++ b/beacon_node/beacon_chain/src/observed_data_sidecars.rs @@ -118,7 +118,7 @@ impl ObservedDataSidecars { slot: data_sidecar.slot(), proposer: data_sidecar.block_proposer_index(), }) - .map_or(false, |indices| indices.contains(&data_sidecar.index())); + .is_some_and(|indices| indices.contains(&data_sidecar.index())); Ok(is_known) } diff --git a/beacon_node/beacon_chain/src/shuffling_cache.rs b/beacon_node/beacon_chain/src/shuffling_cache.rs index da1d60db17..67ca72254b 100644 --- a/beacon_node/beacon_chain/src/shuffling_cache.rs +++ b/beacon_node/beacon_chain/src/shuffling_cache.rs @@ -253,7 +253,7 @@ impl BlockShufflingIds { } else if self .previous .as_ref() - .map_or(false, |id| id.shuffling_epoch == epoch) + .is_some_and(|id| id.shuffling_epoch == epoch) { self.previous.clone() } else if epoch == self.next.shuffling_epoch { diff --git a/beacon_node/beacon_processor/src/lib.rs b/beacon_node/beacon_processor/src/lib.rs index 2a69b04c91..0edda2f95b 100644 --- a/beacon_node/beacon_processor/src/lib.rs +++ b/beacon_node/beacon_processor/src/lib.rs @@ -1022,7 +1022,7 @@ impl BeaconProcessor { let can_spawn = self.current_workers < self.config.max_workers; let drop_during_sync = work_event .as_ref() - .map_or(false, |event| event.drop_during_sync); + .is_some_and(|event| event.drop_during_sync); let idle_tx = idle_tx.clone(); let modified_queue_id = match work_event { diff --git a/beacon_node/client/src/builder.rs b/beacon_node/client/src/builder.rs index 7c6a253aca..24c6615822 100644 --- a/beacon_node/client/src/builder.rs +++ b/beacon_node/client/src/builder.rs @@ -910,7 +910,7 @@ where .forkchoice_update_parameters(); if params .head_hash - .map_or(false, |hash| hash != ExecutionBlockHash::zero()) + .is_some_and(|hash| hash != ExecutionBlockHash::zero()) { // Spawn a new task to update the EE without waiting for it to complete. let inner_chain = beacon_chain.clone(); diff --git a/beacon_node/client/src/notifier.rs b/beacon_node/client/src/notifier.rs index f686c2c650..e88803e94f 100644 --- a/beacon_node/client/src/notifier.rs +++ b/beacon_node/client/src/notifier.rs @@ -197,7 +197,7 @@ pub fn spawn_notifier( ); let speed = speedo.slots_per_second(); - let display_speed = speed.map_or(false, |speed| speed != 0.0); + let display_speed = speed.is_some_and(|speed| speed != 0.0); if display_speed { info!( @@ -233,7 +233,7 @@ pub fn spawn_notifier( ); let speed = speedo.slots_per_second(); - let display_speed = speed.map_or(false, |speed| speed != 0.0); + let display_speed = speed.is_some_and(|speed| speed != 0.0); if display_speed { info!( @@ -339,9 +339,7 @@ async fn bellatrix_readiness_logging( .message() .body() .execution_payload() - .map_or(false, |payload| { - payload.parent_hash() != ExecutionBlockHash::zero() - }); + .is_ok_and(|payload| payload.parent_hash() != ExecutionBlockHash::zero()); let has_execution_layer = beacon_chain.execution_layer.is_some(); diff --git a/beacon_node/execution_layer/src/engine_api/http.rs b/beacon_node/execution_layer/src/engine_api/http.rs index 33dc60d037..e2a81c072c 100644 --- a/beacon_node/execution_layer/src/engine_api/http.rs +++ b/beacon_node/execution_layer/src/engine_api/http.rs @@ -158,9 +158,7 @@ pub mod deposit_log { }; let signature_is_valid = deposit_pubkey_signature_message(&deposit_data, spec) - .map_or(false, |(public_key, signature, msg)| { - signature.verify(&public_key, msg) - }); + .is_some_and(|(public_key, signature, msg)| signature.verify(&public_key, msg)); Ok(DepositLog { deposit_data, @@ -592,7 +590,7 @@ impl CachedResponse { /// returns `true` if the entry's age is >= age_limit pub fn older_than(&self, age_limit: Option) -> bool { - age_limit.map_or(false, |limit| self.age() >= limit) + age_limit.is_some_and(|limit| self.age() >= limit) } } @@ -720,9 +718,9 @@ impl HttpJsonRpc { .await } - pub async fn get_block_by_number<'a>( + pub async fn get_block_by_number( &self, - query: BlockByNumberQuery<'a>, + query: BlockByNumberQuery<'_>, ) -> Result, Error> { let params = json!([query, RETURN_FULL_TRANSACTION_OBJECTS]); diff --git a/beacon_node/execution_layer/src/lib.rs b/beacon_node/execution_layer/src/lib.rs index ae0dca9833..f3b12b21d1 100644 --- a/beacon_node/execution_layer/src/lib.rs +++ b/beacon_node/execution_layer/src/lib.rs @@ -2095,7 +2095,7 @@ fn verify_builder_bid( payload: header.timestamp(), expected: payload_attributes.timestamp(), })) - } else if block_number.map_or(false, |n| n != header.block_number()) { + } else if block_number.is_some_and(|n| n != header.block_number()) { Err(Box::new(InvalidBuilderPayload::BlockNumber { payload: header.block_number(), expected: block_number, diff --git a/beacon_node/execution_layer/src/payload_status.rs b/beacon_node/execution_layer/src/payload_status.rs index 5405fd7009..cf0be8ed0d 100644 --- a/beacon_node/execution_layer/src/payload_status.rs +++ b/beacon_node/execution_layer/src/payload_status.rs @@ -41,7 +41,7 @@ pub fn process_payload_status( PayloadStatusV1Status::Valid => { if response .latest_valid_hash - .map_or(false, |h| h == head_block_hash) + .is_some_and(|h| h == head_block_hash) { // The response is only valid if `latest_valid_hash` is not `null` and // equal to the provided `block_hash`. diff --git a/beacon_node/execution_layer/src/test_utils/mock_execution_layer.rs b/beacon_node/execution_layer/src/test_utils/mock_execution_layer.rs index 48372a39be..dc90d91c0f 100644 --- a/beacon_node/execution_layer/src/test_utils/mock_execution_layer.rs +++ b/beacon_node/execution_layer/src/test_utils/mock_execution_layer.rs @@ -318,7 +318,7 @@ impl MockExecutionLayer { (self, block_hash) } - pub async fn with_terminal_block<'a, U, V>(self, func: U) -> Self + pub async fn with_terminal_block(self, func: U) -> Self where U: Fn(ChainSpec, ExecutionLayer, Option) -> V, V: Future, diff --git a/beacon_node/genesis/src/eth1_genesis_service.rs b/beacon_node/genesis/src/eth1_genesis_service.rs index 3981833a5c..b5f4bd50ee 100644 --- a/beacon_node/genesis/src/eth1_genesis_service.rs +++ b/beacon_node/genesis/src/eth1_genesis_service.rs @@ -270,7 +270,7 @@ impl Eth1GenesisService { // Ignore any block that has already been processed or update the highest processed // block. - if highest_processed_block.map_or(false, |highest| highest >= block.number) { + if highest_processed_block.is_some_and(|highest| highest >= block.number) { continue; } else { self.stats diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index 23d177da78..febdf69259 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -1164,7 +1164,7 @@ pub fn serve( .map_err(warp_utils::reject::beacon_chain_error)? // Ignore any skip-slots immediately following the parent. .find(|res| { - res.as_ref().map_or(false, |(root, _)| *root != parent_root) + res.as_ref().is_ok_and(|(root, _)| *root != parent_root) }) .transpose() .map_err(warp_utils::reject::beacon_chain_error)? @@ -1249,7 +1249,7 @@ pub fn serve( let canonical = chain .block_root_at_slot(block.slot(), WhenSlotSkipped::None) .map_err(warp_utils::reject::beacon_chain_error)? - .map_or(false, |canonical| root == canonical); + .is_some_and(|canonical| root == canonical); let data = api_types::BlockHeaderData { root, diff --git a/beacon_node/http_api/src/validator.rs b/beacon_node/http_api/src/validator.rs index 7f11ddd8f4..baa41e33ed 100644 --- a/beacon_node/http_api/src/validator.rs +++ b/beacon_node/http_api/src/validator.rs @@ -14,7 +14,7 @@ pub fn pubkey_to_validator_index( state .validators() .get(index) - .map_or(false, |v| v.pubkey == *pubkey) + .is_some_and(|v| v.pubkey == *pubkey) }) .map(Result::Ok) .transpose() diff --git a/beacon_node/http_api/tests/interactive_tests.rs b/beacon_node/http_api/tests/interactive_tests.rs index e45dcf221c..8cfcf5d93e 100644 --- a/beacon_node/http_api/tests/interactive_tests.rs +++ b/beacon_node/http_api/tests/interactive_tests.rs @@ -161,7 +161,7 @@ impl ForkChoiceUpdates { update .payload_attributes .as_ref() - .map_or(false, |payload_attributes| { + .is_some_and(|payload_attributes| { payload_attributes.timestamp() == proposal_timestamp }) }) diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 7007a14466..1efe44a613 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -1278,7 +1278,7 @@ impl ApiTester { .chain .block_root_at_slot(block.slot(), WhenSlotSkipped::None) .unwrap() - .map_or(false, |canonical| block_root == canonical); + .is_some_and(|canonical| block_root == canonical); assert_eq!(result.canonical, canonical, "{:?}", block_id); assert_eq!(result.root, block_root, "{:?}", block_id); diff --git a/beacon_node/lighthouse_network/gossipsub/src/backoff.rs b/beacon_node/lighthouse_network/gossipsub/src/backoff.rs index 537d2319c2..0d77e2cd0f 100644 --- a/beacon_node/lighthouse_network/gossipsub/src/backoff.rs +++ b/beacon_node/lighthouse_network/gossipsub/src/backoff.rs @@ -124,7 +124,7 @@ impl BackoffStorage { pub(crate) fn is_backoff_with_slack(&self, topic: &TopicHash, peer: &PeerId) -> bool { self.backoffs .get(topic) - .map_or(false, |m| m.contains_key(peer)) + .is_some_and(|m| m.contains_key(peer)) } pub(crate) fn get_backoff_time(&self, topic: &TopicHash, peer: &PeerId) -> Option { diff --git a/beacon_node/lighthouse_network/gossipsub/src/behaviour.rs b/beacon_node/lighthouse_network/gossipsub/src/behaviour.rs index c4e20e4397..6528e737a3 100644 --- a/beacon_node/lighthouse_network/gossipsub/src/behaviour.rs +++ b/beacon_node/lighthouse_network/gossipsub/src/behaviour.rs @@ -1770,8 +1770,7 @@ where // reject messages claiming to be from ourselves but not locally published let self_published = !self.config.allow_self_origin() && if let Some(own_id) = self.publish_config.get_own_id() { - own_id != propagation_source - && raw_message.source.as_ref().map_or(false, |s| s == own_id) + own_id != propagation_source && raw_message.source.as_ref() == Some(own_id) } else { self.published_message_ids.contains(msg_id) }; diff --git a/beacon_node/lighthouse_network/src/config.rs b/beacon_node/lighthouse_network/src/config.rs index 21f3dc830f..8a93b1185d 100644 --- a/beacon_node/lighthouse_network/src/config.rs +++ b/beacon_node/lighthouse_network/src/config.rs @@ -166,7 +166,7 @@ impl Config { tcp_port, }); self.discv5_config.listen_config = discv5::ListenConfig::from_ip(addr.into(), disc_port); - self.discv5_config.table_filter = |enr| enr.ip4().as_ref().map_or(false, is_global_ipv4) + self.discv5_config.table_filter = |enr| enr.ip4().as_ref().is_some_and(is_global_ipv4) } /// Sets the listening address to use an ipv6 address. The discv5 ip_mode and table filter is @@ -187,7 +187,7 @@ impl Config { }); self.discv5_config.listen_config = discv5::ListenConfig::from_ip(addr.into(), disc_port); - self.discv5_config.table_filter = |enr| enr.ip6().as_ref().map_or(false, is_global_ipv6) + self.discv5_config.table_filter = |enr| enr.ip6().as_ref().is_some_and(is_global_ipv6) } /// Sets the listening address to use both an ipv4 and ipv6 address. The discv5 ip_mode and @@ -317,7 +317,7 @@ impl Default for Config { .filter_rate_limiter(filter_rate_limiter) .filter_max_bans_per_ip(Some(5)) .filter_max_nodes_per_ip(Some(10)) - .table_filter(|enr| enr.ip4().map_or(false, |ip| is_global_ipv4(&ip))) // Filter non-global IPs + .table_filter(|enr| enr.ip4().is_some_and(|ip| is_global_ipv4(&ip))) // Filter non-global IPs .ban_duration(Some(Duration::from_secs(3600))) .ping_interval(Duration::from_secs(300)) .build(); diff --git a/beacon_node/lighthouse_network/src/discovery/subnet_predicate.rs b/beacon_node/lighthouse_network/src/discovery/subnet_predicate.rs index 02ff0cc3ca..751f8dbb83 100644 --- a/beacon_node/lighthouse_network/src/discovery/subnet_predicate.rs +++ b/beacon_node/lighthouse_network/src/discovery/subnet_predicate.rs @@ -35,7 +35,7 @@ where .unwrap_or(false), Subnet::SyncCommittee(s) => sync_committee_bitfield .as_ref() - .map_or(false, |b| b.get(*s.deref() as usize).unwrap_or(false)), + .is_ok_and(|b| b.get(*s.deref() as usize).unwrap_or(false)), Subnet::DataColumn(s) => { if let Ok(custody_subnet_count) = enr.custody_subnet_count::(&spec) { DataColumnSubnetId::compute_custody_subnets::( @@ -43,7 +43,7 @@ where custody_subnet_count, &spec, ) - .map_or(false, |mut subnets| subnets.contains(s)) + .is_ok_and(|mut subnets| subnets.contains(s)) } else { false } diff --git a/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs b/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs index d2effd4d03..22a3df1ae8 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs @@ -1305,7 +1305,7 @@ impl BannedPeersCount { pub fn ip_is_banned(&self, ip: &IpAddr) -> bool { self.banned_peers_per_ip .get(ip) - .map_or(false, |count| *count > BANNED_PEERS_PER_IP_THRESHOLD) + .is_some_and(|count| *count > BANNED_PEERS_PER_IP_THRESHOLD) } } 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 ee8c27f474..27c8463a55 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 @@ -99,7 +99,7 @@ impl PeerInfo { Subnet::SyncCommittee(id) => { return meta_data .syncnets() - .map_or(false, |s| s.get(**id as usize).unwrap_or(false)) + .is_ok_and(|s| s.get(**id as usize).unwrap_or(false)) } Subnet::DataColumn(column) => return self.custody_subnets.contains(column), } @@ -264,7 +264,7 @@ impl PeerInfo { /// Reports if this peer has some future validator duty in which case it is valuable to keep it. pub fn has_future_duty(&self) -> bool { - self.min_ttl.map_or(false, |i| i >= Instant::now()) + self.min_ttl.is_some_and(|i| i >= Instant::now()) } /// Returns score of the peer. 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 f3c48e42f0..6b5753e96a 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -3129,7 +3129,7 @@ impl NetworkBeaconProcessor { .chain .slot_clock .now() - .map_or(false, |current_slot| sync_message_slot == current_slot); + .is_some_and(|current_slot| sync_message_slot == current_slot); self.propagate_if_timely(is_timely, message_id, peer_id) } diff --git a/beacon_node/operation_pool/src/lib.rs b/beacon_node/operation_pool/src/lib.rs index d01c73118c..d8183de752 100644 --- a/beacon_node/operation_pool/src/lib.rs +++ b/beacon_node/operation_pool/src/lib.rs @@ -186,7 +186,7 @@ impl OperationPool { self.sync_contributions.write().retain(|_, contributions| { // All the contributions in this bucket have the same data, so we only need to // check the first one. - contributions.first().map_or(false, |contribution| { + contributions.first().is_some_and(|contribution| { current_slot <= contribution.slot.saturating_add(Slot::new(1)) }) }); @@ -401,7 +401,7 @@ impl OperationPool { && state .validators() .get(slashing.as_inner().signed_header_1.message.proposer_index as usize) - .map_or(false, |validator| !validator.slashed) + .is_some_and(|validator| !validator.slashed) }, |slashing| slashing.as_inner().clone(), E::MaxProposerSlashings::to_usize(), @@ -484,7 +484,7 @@ impl OperationPool { validator.exit_epoch > head_state.finalized_checkpoint().epoch }, ) - .map_or(false, |indices| !indices.is_empty()); + .is_ok_and(|indices| !indices.is_empty()); signature_ok && slashing_ok }); @@ -583,9 +583,7 @@ impl OperationPool { address_change.signature_is_still_valid(&state.fork()) && state .get_validator(address_change.as_inner().message.validator_index as usize) - .map_or(false, |validator| { - !validator.has_execution_withdrawal_credential(spec) - }) + .is_ok_and(|validator| !validator.has_execution_withdrawal_credential(spec)) }, |address_change| address_change.as_inner().clone(), E::MaxBlsToExecutionChanges::to_usize(), @@ -609,9 +607,7 @@ impl OperationPool { address_change.signature_is_still_valid(&state.fork()) && state .get_validator(address_change.as_inner().message.validator_index as usize) - .map_or(false, |validator| { - !validator.has_eth1_withdrawal_credential(spec) - }) + .is_ok_and(|validator| !validator.has_eth1_withdrawal_credential(spec)) }, |address_change| address_change.as_inner().clone(), usize::MAX, diff --git a/beacon_node/store/src/forwards_iter.rs b/beacon_node/store/src/forwards_iter.rs index 27769a310a..955bd33b30 100644 --- a/beacon_node/store/src/forwards_iter.rs +++ b/beacon_node/store/src/forwards_iter.rs @@ -265,7 +265,7 @@ impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> // `end_slot`. If it tries to continue further a `NoContinuationData` error will be // returned. let continuation_data = - if end_slot.map_or(false, |end_slot| end_slot < freezer_upper_bound) { + if end_slot.is_some_and(|end_slot| end_slot < freezer_upper_bound) { None } else { Some(Box::new(get_state()?)) @@ -306,7 +306,7 @@ impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> None => { // If the iterator has an end slot (inclusive) which has already been // covered by the (exclusive) frozen forwards iterator, then we're done! - if end_slot.map_or(false, |end_slot| iter.end_slot == end_slot + 1) { + if end_slot.is_some_and(|end_slot| iter.end_slot == end_slot + 1) { *self = Finished; return Ok(None); } diff --git a/beacon_node/store/src/reconstruct.rs b/beacon_node/store/src/reconstruct.rs index 9bec83a35c..2a3b208aae 100644 --- a/beacon_node/store/src/reconstruct.rs +++ b/beacon_node/store/src/reconstruct.rs @@ -111,7 +111,7 @@ where self.store_cold_state(&state_root, &state, &mut io_batch)?; let batch_complete = - num_blocks.map_or(false, |n_blocks| slot == lower_limit_slot + n_blocks as u64); + num_blocks.is_some_and(|n_blocks| slot == lower_limit_slot + n_blocks as u64); let reconstruction_complete = slot + 1 == upper_limit_slot; // Commit the I/O batch if: diff --git a/beacon_node/store/src/state_cache.rs b/beacon_node/store/src/state_cache.rs index 5c1faa7f2f..96e4de4639 100644 --- a/beacon_node/store/src/state_cache.rs +++ b/beacon_node/store/src/state_cache.rs @@ -77,9 +77,7 @@ impl StateCache { if self .finalized_state .as_ref() - .map_or(false, |finalized_state| { - state.slot() < finalized_state.state.slot() - }) + .is_some_and(|finalized_state| state.slot() < finalized_state.state.slot()) { return Err(Error::FinalizedStateDecreasingSlot); } @@ -127,9 +125,7 @@ impl StateCache { if self .finalized_state .as_ref() - .map_or(false, |finalized_state| { - finalized_state.state_root == state_root - }) + .is_some_and(|finalized_state| finalized_state.state_root == state_root) { return Ok(PutStateOutcome::Finalized); } diff --git a/common/account_utils/src/validator_definitions.rs b/common/account_utils/src/validator_definitions.rs index a4850fc1c6..24f6861daa 100644 --- a/common/account_utils/src/validator_definitions.rs +++ b/common/account_utils/src/validator_definitions.rs @@ -435,7 +435,7 @@ pub fn recursively_find_voting_keystores>( && dir_entry .file_name() .to_str() - .map_or(false, is_voting_keystore) + .is_some_and(is_voting_keystore) { matches.push(dir_entry.path()) } diff --git a/common/logging/src/lib.rs b/common/logging/src/lib.rs index 7fe7f79506..0ddd867c2f 100644 --- a/common/logging/src/lib.rs +++ b/common/logging/src/lib.rs @@ -204,7 +204,7 @@ impl TimeLatch { pub fn elapsed(&mut self) -> bool { let now = Instant::now(); - let is_elapsed = self.0.map_or(false, |elapse_time| now > elapse_time); + let is_elapsed = self.0.is_some_and(|elapse_time| now > elapse_time); if is_elapsed || self.0.is_none() { self.0 = Some(now + LOG_DEBOUNCE_INTERVAL); diff --git a/consensus/proto_array/src/proto_array.rs b/consensus/proto_array/src/proto_array.rs index 38ea141199..5d0bee4c85 100644 --- a/consensus/proto_array/src/proto_array.rs +++ b/consensus/proto_array/src/proto_array.rs @@ -468,7 +468,7 @@ impl ProtoArray { // 1. The `head_block_root` is a descendant of `latest_valid_ancestor_hash` // 2. The `latest_valid_ancestor_hash` is equal to or a descendant of the finalized block. let latest_valid_ancestor_is_descendant = - latest_valid_ancestor_root.map_or(false, |ancestor_root| { + latest_valid_ancestor_root.is_some_and(|ancestor_root| { self.is_descendant(ancestor_root, head_block_root) && self.is_finalized_checkpoint_or_descendant::(ancestor_root) }); @@ -505,13 +505,13 @@ impl ProtoArray { // head. if node .best_child - .map_or(false, |i| invalidated_indices.contains(&i)) + .is_some_and(|i| invalidated_indices.contains(&i)) { node.best_child = None } if node .best_descendant - .map_or(false, |i| invalidated_indices.contains(&i)) + .is_some_and(|i| invalidated_indices.contains(&i)) { node.best_descendant = None } @@ -999,7 +999,7 @@ impl ProtoArray { node.unrealized_finalized_checkpoint, node.unrealized_justified_checkpoint, ] { - if checkpoint.map_or(false, |cp| cp == self.finalized_checkpoint) { + if checkpoint.is_some_and(|cp| cp == self.finalized_checkpoint) { return true; } } @@ -1037,7 +1037,7 @@ impl ProtoArray { .find(|node| { node.execution_status .block_hash() - .map_or(false, |node_block_hash| node_block_hash == *block_hash) + .is_some_and(|node_block_hash| node_block_hash == *block_hash) }) .map(|node| node.root) } diff --git a/consensus/state_processing/src/genesis.rs b/consensus/state_processing/src/genesis.rs index 00697def5d..ccff3d80c0 100644 --- a/consensus/state_processing/src/genesis.rs +++ b/consensus/state_processing/src/genesis.rs @@ -53,7 +53,7 @@ pub fn initialize_beacon_state_from_eth1( // https://github.com/ethereum/eth2.0-specs/pull/2323 if spec .altair_fork_epoch - .map_or(false, |fork_epoch| fork_epoch == E::genesis_epoch()) + .is_some_and(|fork_epoch| fork_epoch == E::genesis_epoch()) { upgrade_to_altair(&mut state, spec)?; @@ -63,7 +63,7 @@ pub fn initialize_beacon_state_from_eth1( // Similarly, perform an upgrade to the merge if configured from genesis. if spec .bellatrix_fork_epoch - .map_or(false, |fork_epoch| fork_epoch == E::genesis_epoch()) + .is_some_and(|fork_epoch| fork_epoch == E::genesis_epoch()) { // this will set state.latest_execution_payload_header = ExecutionPayloadHeaderBellatrix::default() upgrade_to_bellatrix(&mut state, spec)?; @@ -81,7 +81,7 @@ pub fn initialize_beacon_state_from_eth1( // Upgrade to capella if configured from genesis if spec .capella_fork_epoch - .map_or(false, |fork_epoch| fork_epoch == E::genesis_epoch()) + .is_some_and(|fork_epoch| fork_epoch == E::genesis_epoch()) { upgrade_to_capella(&mut state, spec)?; @@ -98,7 +98,7 @@ pub fn initialize_beacon_state_from_eth1( // Upgrade to deneb if configured from genesis if spec .deneb_fork_epoch - .map_or(false, |fork_epoch| fork_epoch == E::genesis_epoch()) + .is_some_and(|fork_epoch| fork_epoch == E::genesis_epoch()) { upgrade_to_deneb(&mut state, spec)?; @@ -115,7 +115,7 @@ pub fn initialize_beacon_state_from_eth1( // Upgrade to electra if configured from genesis. if spec .electra_fork_epoch - .map_or(false, |fork_epoch| fork_epoch == E::genesis_epoch()) + .is_some_and(|fork_epoch| fork_epoch == E::genesis_epoch()) { let post = upgrade_state_to_electra(&mut state, Epoch::new(0), Epoch::new(0), spec)?; state = post; @@ -153,7 +153,7 @@ pub fn initialize_beacon_state_from_eth1( pub fn is_valid_genesis_state(state: &BeaconState, spec: &ChainSpec) -> bool { state .get_active_validator_indices(E::genesis_epoch(), spec) - .map_or(false, |active_validators| { + .is_ok_and(|active_validators| { state.genesis_time() >= spec.min_genesis_time && active_validators.len() as u64 >= spec.min_genesis_active_validator_count }) diff --git a/consensus/state_processing/src/per_block_processing/altair/sync_committee.rs b/consensus/state_processing/src/per_block_processing/altair/sync_committee.rs index 210db4c9c1..08cfd9cba8 100644 --- a/consensus/state_processing/src/per_block_processing/altair/sync_committee.rs +++ b/consensus/state_processing/src/per_block_processing/altair/sync_committee.rs @@ -38,7 +38,7 @@ pub fn process_sync_aggregate( )?; // If signature set is `None` then the signature is valid (infinity). - if signature_set.map_or(false, |signature| !signature.verify()) { + if signature_set.is_some_and(|signature| !signature.verify()) { return Err(SyncAggregateInvalid::SignatureInvalid.into()); } } diff --git a/consensus/state_processing/src/per_epoch_processing/epoch_processing_summary.rs b/consensus/state_processing/src/per_epoch_processing/epoch_processing_summary.rs index 952ab3f649..5508b80807 100644 --- a/consensus/state_processing/src/per_epoch_processing/epoch_processing_summary.rs +++ b/consensus/state_processing/src/per_epoch_processing/epoch_processing_summary.rs @@ -151,7 +151,7 @@ impl EpochProcessingSummary { match self { EpochProcessingSummary::Base { statuses, .. } => statuses .get(val_index) - .map_or(false, |s| s.is_active_in_current_epoch && !s.is_slashed), + .is_some_and(|s| s.is_active_in_current_epoch && !s.is_slashed), EpochProcessingSummary::Altair { participation, .. } => { participation.is_active_and_unslashed(val_index, participation.current_epoch) } @@ -176,7 +176,7 @@ impl EpochProcessingSummary { match self { EpochProcessingSummary::Base { statuses, .. } => Ok(statuses .get(val_index) - .map_or(false, |s| s.is_current_epoch_target_attester)), + .is_some_and(|s| s.is_current_epoch_target_attester)), EpochProcessingSummary::Altair { participation, .. } => participation .is_current_epoch_unslashed_participating_index( val_index, @@ -247,7 +247,7 @@ impl EpochProcessingSummary { match self { EpochProcessingSummary::Base { statuses, .. } => statuses .get(val_index) - .map_or(false, |s| s.is_active_in_previous_epoch && !s.is_slashed), + .is_some_and(|s| s.is_active_in_previous_epoch && !s.is_slashed), EpochProcessingSummary::Altair { participation, .. } => { participation.is_active_and_unslashed(val_index, participation.previous_epoch) } @@ -267,7 +267,7 @@ impl EpochProcessingSummary { match self { EpochProcessingSummary::Base { statuses, .. } => Ok(statuses .get(val_index) - .map_or(false, |s| s.is_previous_epoch_target_attester)), + .is_some_and(|s| s.is_previous_epoch_target_attester)), EpochProcessingSummary::Altair { participation, .. } => participation .is_previous_epoch_unslashed_participating_index( val_index, @@ -294,7 +294,7 @@ impl EpochProcessingSummary { match self { EpochProcessingSummary::Base { statuses, .. } => Ok(statuses .get(val_index) - .map_or(false, |s| s.is_previous_epoch_head_attester)), + .is_some_and(|s| s.is_previous_epoch_head_attester)), EpochProcessingSummary::Altair { participation, .. } => participation .is_previous_epoch_unslashed_participating_index(val_index, TIMELY_HEAD_FLAG_INDEX), } @@ -318,7 +318,7 @@ impl EpochProcessingSummary { match self { EpochProcessingSummary::Base { statuses, .. } => Ok(statuses .get(val_index) - .map_or(false, |s| s.is_previous_epoch_attester)), + .is_some_and(|s| s.is_previous_epoch_attester)), EpochProcessingSummary::Altair { participation, .. } => participation .is_previous_epoch_unslashed_participating_index( val_index, diff --git a/consensus/types/src/beacon_block_body.rs b/consensus/types/src/beacon_block_body.rs index b896dc4693..f7a701fed6 100644 --- a/consensus/types/src/beacon_block_body.rs +++ b/consensus/types/src/beacon_block_body.rs @@ -283,7 +283,7 @@ impl<'a, E: EthSpec, Payload: AbstractExecPayload> BeaconBlockBodyRef<'a, E, /// Return `true` if this block body has a non-zero number of blobs. pub fn has_blobs(self) -> bool { self.blob_kzg_commitments() - .map_or(false, |blobs| !blobs.is_empty()) + .is_ok_and(|blobs| !blobs.is_empty()) } pub fn attestations_len(&self) -> usize { diff --git a/consensus/types/src/beacon_state.rs b/consensus/types/src/beacon_state.rs index ad4484b86a..05f28744fa 100644 --- a/consensus/types/src/beacon_state.rs +++ b/consensus/types/src/beacon_state.rs @@ -1856,7 +1856,7 @@ impl BeaconState { pub fn committee_cache_is_initialized(&self, relative_epoch: RelativeEpoch) -> bool { let i = Self::committee_cache_index(relative_epoch); - self.committee_cache_at_index(i).map_or(false, |cache| { + self.committee_cache_at_index(i).is_ok_and(|cache| { cache.is_initialized_at(relative_epoch.into_epoch(self.current_epoch())) }) } diff --git a/consensus/types/src/beacon_state/progressive_balances_cache.rs b/consensus/types/src/beacon_state/progressive_balances_cache.rs index fd5e51313f..bc258ef68d 100644 --- a/consensus/types/src/beacon_state/progressive_balances_cache.rs +++ b/consensus/types/src/beacon_state/progressive_balances_cache.rs @@ -145,7 +145,7 @@ impl ProgressiveBalancesCache { pub fn is_initialized_at(&self, epoch: Epoch) -> bool { self.inner .as_ref() - .map_or(false, |inner| inner.current_epoch == epoch) + .is_some_and(|inner| inner.current_epoch == epoch) } /// When a new target attestation has been processed, we update the cached diff --git a/consensus/types/src/chain_spec.rs b/consensus/types/src/chain_spec.rs index 0b33a76ff1..9d3308cf23 100644 --- a/consensus/types/src/chain_spec.rs +++ b/consensus/types/src/chain_spec.rs @@ -420,16 +420,14 @@ 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 - }) + self.eip7594_fork_epoch + .is_some_and(|eip7594_fork_epoch| block_epoch >= eip7594_fork_epoch) } /// Returns true if `EIP7594_FORK_EPOCH` is set and is not set to `FAR_FUTURE_EPOCH`. pub fn is_peer_das_scheduled(&self) -> bool { - self.eip7594_fork_epoch.map_or(false, |eip7594_fork_epoch| { - eip7594_fork_epoch != self.far_future_epoch - }) + self.eip7594_fork_epoch + .is_some_and(|eip7594_fork_epoch| eip7594_fork_epoch != self.far_future_epoch) } /// Returns a full `Fork` struct for a given epoch. diff --git a/consensus/types/src/deposit_tree_snapshot.rs b/consensus/types/src/deposit_tree_snapshot.rs index df1064daba..2f9df8758b 100644 --- a/consensus/types/src/deposit_tree_snapshot.rs +++ b/consensus/types/src/deposit_tree_snapshot.rs @@ -72,8 +72,7 @@ impl DepositTreeSnapshot { Some(Hash256::from_slice(&deposit_root)) } pub fn is_valid(&self) -> bool { - self.calculate_root() - .map_or(false, |calculated| self.deposit_root == calculated) + self.calculate_root() == Some(self.deposit_root) } } diff --git a/consensus/types/src/graffiti.rs b/consensus/types/src/graffiti.rs index 08f8573c6d..f781aacabd 100644 --- a/consensus/types/src/graffiti.rs +++ b/consensus/types/src/graffiti.rs @@ -57,7 +57,7 @@ impl FromStr for GraffitiString { type Err = String; fn from_str(s: &str) -> Result { - if s.as_bytes().len() > GRAFFITI_BYTES_LEN { + if s.len() > GRAFFITI_BYTES_LEN { return Err(format!( "Graffiti exceeds max length {}", GRAFFITI_BYTES_LEN diff --git a/lcli/src/transition_blocks.rs b/lcli/src/transition_blocks.rs index 94d95a0d1c..ecfa04fc81 100644 --- a/lcli/src/transition_blocks.rs +++ b/lcli/src/transition_blocks.rs @@ -223,7 +223,7 @@ pub fn run( .update_tree_hash_cache() .map_err(|e| format!("Unable to build THC: {:?}", e))?; - if state_root_opt.map_or(false, |expected| expected != state_root) { + if state_root_opt.is_some_and(|expected| expected != state_root) { return Err(format!( "State root mismatch! Expected {}, computed {}", state_root_opt.unwrap(), @@ -331,7 +331,7 @@ fn do_transition( .map_err(|e| format!("Unable to build tree hash cache: {:?}", e))?; debug!("Initial tree hash: {:?}", t.elapsed()); - if state_root_opt.map_or(false, |expected| expected != state_root) { + if state_root_opt.is_some_and(|expected| expected != state_root) { return Err(format!( "State root mismatch! Expected {}, computed {}", state_root_opt.unwrap(), diff --git a/slasher/src/database.rs b/slasher/src/database.rs index 20b4a33771..e2b49dca29 100644 --- a/slasher/src/database.rs +++ b/slasher/src/database.rs @@ -406,7 +406,7 @@ impl SlasherDB { ) -> Result<(), Error> { // Don't update maximum if new target is less than or equal to previous. In the case of // no previous we *do* want to update. - if previous_max_target.map_or(false, |prev_max| max_target <= prev_max) { + if previous_max_target.is_some_and(|prev_max| max_target <= prev_max) { return Ok(()); } diff --git a/testing/ef_tests/src/cases/fork_choice.rs b/testing/ef_tests/src/cases/fork_choice.rs index 427bcf5e9c..a1c74389a7 100644 --- a/testing/ef_tests/src/cases/fork_choice.rs +++ b/testing/ef_tests/src/cases/fork_choice.rs @@ -523,7 +523,7 @@ impl Tester { || Ok(()), ))? .map(|avail: AvailabilityProcessingStatus| avail.try_into()); - let success = blob_success && result.as_ref().map_or(false, |inner| inner.is_ok()); + let success = blob_success && result.as_ref().is_ok_and(|inner| inner.is_ok()); if success != valid { return Err(Error::DidntFail(format!( "block with root {} was valid={} whilst test expects valid={}. result: {:?}", diff --git a/testing/ef_tests/src/cases/operations.rs b/testing/ef_tests/src/cases/operations.rs index 54ca52447f..d8cade296b 100644 --- a/testing/ef_tests/src/cases/operations.rs +++ b/testing/ef_tests/src/cases/operations.rs @@ -322,7 +322,7 @@ impl Operation for BeaconBlockBody> { let valid = extra .execution_metadata .as_ref() - .map_or(false, |e| e.execution_valid); + .is_some_and(|e| e.execution_valid); if valid { process_execution_payload::>(state, self.to_ref(), spec) } else { @@ -377,7 +377,7 @@ impl Operation for BeaconBlockBody> { let valid = extra .execution_metadata .as_ref() - .map_or(false, |e| e.execution_valid); + .is_some_and(|e| e.execution_valid); if valid { process_execution_payload::>(state, self.to_ref(), spec) } else { diff --git a/testing/ef_tests/src/decode.rs b/testing/ef_tests/src/decode.rs index 757b9bf3c4..eb88ac6af1 100644 --- a/testing/ef_tests/src/decode.rs +++ b/testing/ef_tests/src/decode.rs @@ -28,7 +28,7 @@ pub fn log_file_access>(file_accessed: P) { writeln!(&mut file, "{:?}", file_accessed.as_ref()).expect("should write to file"); - file.unlock().expect("unable to unlock file"); + fs2::FileExt::unlock(&file).expect("unable to unlock file"); } pub fn yaml_decode(string: &str) -> Result { diff --git a/validator_client/doppelganger_service/src/lib.rs b/validator_client/doppelganger_service/src/lib.rs index 35228fe354..4a593c2700 100644 --- a/validator_client/doppelganger_service/src/lib.rs +++ b/validator_client/doppelganger_service/src/lib.rs @@ -162,7 +162,7 @@ impl DoppelgangerState { /// If the BN fails to respond to either of these requests, simply return an empty response. /// This behaviour is to help prevent spurious failures on the BN from needlessly preventing /// doppelganger progression. -async fn beacon_node_liveness<'a, T: 'static + SlotClock, E: EthSpec>( +async fn beacon_node_liveness( beacon_nodes: Arc>, log: Logger, current_epoch: Epoch, diff --git a/validator_client/slashing_protection/src/slashing_database.rs b/validator_client/slashing_protection/src/slashing_database.rs index baaf930c68..71611339f9 100644 --- a/validator_client/slashing_protection/src/slashing_database.rs +++ b/validator_client/slashing_protection/src/slashing_database.rs @@ -1113,9 +1113,7 @@ fn max_or(opt_x: Option, y: T) -> T { /// /// If prev is `None` and `new` is `Some` then `true` is returned. fn monotonic(new: Option, prev: Option) -> bool { - new.map_or(false, |new_val| { - prev.map_or(true, |prev_val| new_val >= prev_val) - }) + new.is_some_and(|new_val| prev.map_or(true, |prev_val| new_val >= prev_val)) } /// The result of importing a single entry from an interchange file. diff --git a/validator_client/validator_services/src/preparation_service.rs b/validator_client/validator_services/src/preparation_service.rs index 480f4af2b3..fe6eab3a8a 100644 --- a/validator_client/validator_services/src/preparation_service.rs +++ b/validator_client/validator_services/src/preparation_service.rs @@ -258,7 +258,7 @@ impl PreparationService { .slot_clock .now() .map_or(E::genesis_epoch(), |slot| slot.epoch(E::slots_per_epoch())); - spec.bellatrix_fork_epoch.map_or(false, |fork_epoch| { + spec.bellatrix_fork_epoch.is_some_and(|fork_epoch| { current_epoch + PROPOSER_PREPARATION_LOOKAHEAD_EPOCHS >= fork_epoch }) } diff --git a/validator_client/validator_services/src/sync.rs b/validator_client/validator_services/src/sync.rs index af501326f4..dd3e05088e 100644 --- a/validator_client/validator_services/src/sync.rs +++ b/validator_client/validator_services/src/sync.rs @@ -94,7 +94,7 @@ impl SyncDutiesMap { self.committees .read() .get(&committee_period) - .map_or(false, |committee_duties| { + .is_some_and(|committee_duties| { let validator_duties = committee_duties.validators.read(); validator_indices .iter() diff --git a/validator_manager/src/create_validators.rs b/validator_manager/src/create_validators.rs index d4403b4613..b40fe61a82 100644 --- a/validator_manager/src/create_validators.rs +++ b/validator_manager/src/create_validators.rs @@ -286,7 +286,7 @@ struct ValidatorsAndDeposits { } impl ValidatorsAndDeposits { - async fn new<'a, E: EthSpec>(config: CreateConfig, spec: &ChainSpec) -> Result { + async fn new(config: CreateConfig, spec: &ChainSpec) -> Result { let CreateConfig { // The output path is handled upstream. output_path: _, @@ -545,7 +545,7 @@ pub async fn cli_run( } } -async fn run<'a, E: EthSpec>(config: CreateConfig, spec: &ChainSpec) -> Result<(), String> { +async fn run(config: CreateConfig, spec: &ChainSpec) -> Result<(), String> { let output_path = config.output_path.clone(); if !output_path.exists() { diff --git a/validator_manager/src/delete_validators.rs b/validator_manager/src/delete_validators.rs index a2d6c062fa..5ef647c5af 100644 --- a/validator_manager/src/delete_validators.rs +++ b/validator_manager/src/delete_validators.rs @@ -86,7 +86,7 @@ pub async fn cli_run(matches: &ArgMatches, dump_config: DumpConfig) -> Result<() } } -async fn run<'a>(config: DeleteConfig) -> Result<(), String> { +async fn run(config: DeleteConfig) -> Result<(), String> { let DeleteConfig { vc_url, vc_token_path, diff --git a/validator_manager/src/import_validators.rs b/validator_manager/src/import_validators.rs index 3cebc10bb3..63c7ca4596 100644 --- a/validator_manager/src/import_validators.rs +++ b/validator_manager/src/import_validators.rs @@ -209,7 +209,7 @@ pub async fn cli_run(matches: &ArgMatches, dump_config: DumpConfig) -> Result<() } } -async fn run<'a>(config: ImportConfig) -> Result<(), String> { +async fn run(config: ImportConfig) -> Result<(), String> { let ImportConfig { validators_file_path, keystore_file_path, diff --git a/validator_manager/src/list_validators.rs b/validator_manager/src/list_validators.rs index e3deb0b21a..a0a1c5fb40 100644 --- a/validator_manager/src/list_validators.rs +++ b/validator_manager/src/list_validators.rs @@ -58,7 +58,7 @@ pub async fn cli_run(matches: &ArgMatches, dump_config: DumpConfig) -> Result<() } } -async fn run<'a>(config: ListConfig) -> Result, String> { +async fn run(config: ListConfig) -> Result, String> { let ListConfig { vc_url, vc_token_path, diff --git a/validator_manager/src/move_validators.rs b/validator_manager/src/move_validators.rs index 4d0820f5a8..abac071673 100644 --- a/validator_manager/src/move_validators.rs +++ b/validator_manager/src/move_validators.rs @@ -277,7 +277,7 @@ pub async fn cli_run(matches: &ArgMatches, dump_config: DumpConfig) -> Result<() } } -async fn run<'a>(config: MoveConfig) -> Result<(), String> { +async fn run(config: MoveConfig) -> Result<(), String> { let MoveConfig { src_vc_url, src_vc_token_path, From a244aa3a6971572c65dd1c68c726547a1d38c033 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Fri, 10 Jan 2025 08:13:32 +0700 Subject: [PATCH 066/254] Add libssl install to udeps task (#6777) * Add libssl install to udeps task * Use HTTPS --- .github/workflows/test-suite.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index 45f3b757e7..0ee9dbb622 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -392,6 +392,10 @@ jobs: cache: false env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Fetch libssl1.1 + run: wget https://nz2.archive.ubuntu.com/ubuntu/pool/main/o/openssl/libssl1.1_1.1.1f-1ubuntu2_amd64.deb + - name: Install libssl1.1 + run: sudo dpkg -i libssl1.1_1.1.1f-1ubuntu2_amd64.deb - name: Create Cargo config dir run: mkdir -p .cargo - name: Install custom Cargo config From 722573f7ed102bda56f2778984989151980a50ff Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Fri, 10 Jan 2025 13:25:20 +0800 Subject: [PATCH 067/254] Use oldest_block_slot to break off pruning payloads (#6745) * Use oldest_block_slot to break of pruning payloads * Update beacon_node/store/src/hot_cold_store.rs Co-authored-by: Michael Sproul * Merge remote-tracking branch 'origin/unstable' into anchor_slot_pruning --- beacon_node/store/src/hot_cold_store.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/beacon_node/store/src/hot_cold_store.rs b/beacon_node/store/src/hot_cold_store.rs index da3e6d4ebc..c6148e5314 100644 --- a/beacon_node/store/src/hot_cold_store.rs +++ b/beacon_node/store/src/hot_cold_store.rs @@ -2629,7 +2629,7 @@ impl, Cold: ItemStore> HotColdDB "Pruning finalized payloads"; "info" => "you may notice degraded I/O performance while this runs" ); - let anchor_slot = self.get_anchor_info().anchor_slot; + let anchor_info = self.get_anchor_info(); let mut ops = vec![]; let mut last_pruned_block_root = None; @@ -2670,10 +2670,10 @@ impl, Cold: ItemStore> HotColdDB ops.push(StoreOp::DeleteExecutionPayload(block_root)); } - if slot == anchor_slot { + if slot <= anchor_info.oldest_block_slot { info!( self.log, - "Payload pruning reached anchor state"; + "Payload pruning reached anchor oldest block slot"; "slot" => slot ); break; From ecdf2d891fb78d84e500ca8cf3a9deb9706263cf Mon Sep 17 00:00:00 2001 From: Mac L Date: Fri, 10 Jan 2025 09:25:23 +0400 Subject: [PATCH 068/254] Add Fulu boilerplate (#6695) * Add Fulu boilerplate * Add more boilerplate * Change fulu_time to osaka_time * Merge branch 'unstable' into fulu-boilerplate * Fix tests * Merge branch 'unstable' into fulu-boilerplate * More test fixes * Apply suggestions * Remove `get_payload` boilerplate * Add lightclient fulu types and fix beacon-chain-tests * Disable Fulu in ef-tests * Reduce boilerplate for future forks * Small fixes * One more fix * Apply suggestions * Merge branch 'unstable' into fulu-boilerplate * Fix lints --- Makefile | 2 +- .../beacon_chain/src/attestation_rewards.rs | 11 +- .../beacon_chain/src/beacon_block_streamer.rs | 9 +- beacon_node/beacon_chain/src/beacon_chain.rs | 72 ++++++-- .../beacon_chain/src/execution_payload.rs | 20 +-- .../beacon_chain/src/fulu_readiness.rs | 114 ++++++++++++ beacon_node/beacon_chain/src/lib.rs | 1 + beacon_node/beacon_chain/src/test_utils.rs | 52 ++++-- .../beacon_chain/tests/block_verification.rs | 9 + beacon_node/beacon_chain/tests/store_tests.rs | 1 + .../beacon_chain/tests/validator_monitor.rs | 1 + beacon_node/client/src/notifier.rs | 58 ++++++ beacon_node/execution_layer/src/engine_api.rs | 60 ++++++- .../execution_layer/src/engine_api/http.rs | 90 ++++++++-- .../src/engine_api/json_structures.rs | 87 ++++++++- .../src/engine_api/new_payload_request.rs | 29 ++- beacon_node/execution_layer/src/lib.rs | 13 +- .../test_utils/execution_block_generator.rs | 49 +++++- .../src/test_utils/handle_rpc.rs | 99 ++++++++++- .../src/test_utils/mock_builder.rs | 54 +++++- .../src/test_utils/mock_execution_layer.rs | 3 + .../execution_layer/src/test_utils/mod.rs | 9 + .../tests/broadcast_validation_tests.rs | 36 ++-- beacon_node/http_api/tests/tests.rs | 4 +- .../lighthouse_network/src/rpc/codec.rs | 106 ++++++++--- .../lighthouse_network/src/rpc/protocol.rs | 38 +++- .../lighthouse_network/src/types/pubsub.rs | 9 +- .../lighthouse_network/src/types/topics.rs | 1 + .../lighthouse_network/tests/common.rs | 3 + beacon_node/network/src/sync/tests/lookups.rs | 2 +- beacon_node/operation_pool/src/lib.rs | 71 +++----- beacon_node/src/lib.rs | 1 + .../store/src/impls/execution_payload.rs | 23 ++- beacon_node/store/src/partial_beacon_state.rs | 66 +++++-- common/eth2/src/types.rs | 33 ++-- .../chiado/config.yaml | 9 +- .../gnosis/config.yaml | 9 +- .../holesky/config.yaml | 7 +- .../mainnet/config.yaml | 20 +-- .../sepolia/config.yaml | 2 +- consensus/fork_choice/src/fork_choice.rs | 14 +- consensus/fork_choice/tests/tests.rs | 2 +- .../common/get_attestation_participation.rs | 21 +-- .../src/common/slash_validator.rs | 13 +- consensus/state_processing/src/genesis.rs | 20 ++- .../src/per_block_processing.rs | 119 +++++++------ .../process_operations.rs | 39 ++--- .../per_block_processing/signature_sets.rs | 22 ++- .../verify_attestation.rs | 23 +-- .../src/per_epoch_processing.rs | 11 +- .../src/per_slot_processing.rs | 7 +- consensus/state_processing/src/upgrade.rs | 2 + .../state_processing/src/upgrade/fulu.rs | 94 ++++++++++ consensus/types/presets/gnosis/fulu.yaml | 3 + consensus/types/presets/mainnet/fulu.yaml | 3 + consensus/types/presets/minimal/fulu.yaml | 3 + consensus/types/src/beacon_block.rs | 163 ++++++++++++++++- consensus/types/src/beacon_block_body.rs | 124 ++++++++++++- consensus/types/src/beacon_state.rs | 165 ++++++++++++++---- .../progressive_balances_cache.rs | 9 +- consensus/types/src/builder_bid.rs | 12 +- consensus/types/src/chain_spec.rs | 74 ++++++-- consensus/types/src/config_and_preset.rs | 51 +++--- consensus/types/src/execution_payload.rs | 24 ++- .../types/src/execution_payload_header.rs | 91 ++++++++-- consensus/types/src/fork_context.rs | 7 + consensus/types/src/fork_name.rs | 32 +++- consensus/types/src/lib.rs | 36 ++-- consensus/types/src/light_client_bootstrap.rs | 27 ++- .../types/src/light_client_finality_update.rs | 33 +++- consensus/types/src/light_client_header.rs | 60 ++++++- .../src/light_client_optimistic_update.rs | 23 ++- consensus/types/src/light_client_update.rs | 47 ++++- consensus/types/src/payload.rs | 44 ++++- consensus/types/src/preset.rs | 18 ++ consensus/types/src/signed_beacon_block.rs | 75 +++++++- lcli/src/mock_el.rs | 2 + testing/ef_tests/src/cases/common.rs | 1 + .../ef_tests/src/cases/epoch_processing.rs | 129 ++++++-------- testing/ef_tests/src/cases/fork.rs | 3 +- .../src/cases/merkle_proof_validity.rs | 8 +- testing/ef_tests/src/cases/operations.rs | 49 +++--- testing/ef_tests/src/cases/transition.rs | 8 + testing/ef_tests/src/handler.rs | 10 +- testing/simulator/src/basic_sim.rs | 4 +- testing/simulator/src/fallback_sim.rs | 4 +- testing/simulator/src/local_network.rs | 5 + .../beacon_node_fallback/src/lib.rs | 8 + validator_client/http_api/src/test_utils.rs | 4 +- validator_client/http_api/src/tests.rs | 4 +- .../signing_method/src/web3signer.rs | 6 + 91 files changed, 2365 insertions(+), 674 deletions(-) create mode 100644 beacon_node/beacon_chain/src/fulu_readiness.rs create mode 100644 consensus/state_processing/src/upgrade/fulu.rs create mode 100644 consensus/types/presets/gnosis/fulu.yaml create mode 100644 consensus/types/presets/mainnet/fulu.yaml create mode 100644 consensus/types/presets/minimal/fulu.yaml diff --git a/Makefile b/Makefile index 8faf8a2e54..4d95f50c5c 100644 --- a/Makefile +++ b/Makefile @@ -30,7 +30,7 @@ PROFILE ?= release # List of all hard forks. This list is used to set env variables for several tests so that # they run for different forks. -FORKS=phase0 altair bellatrix capella deneb electra +FORKS=phase0 altair bellatrix capella deneb electra fulu # Extra flags for Cargo CARGO_INSTALL_EXTRA_FLAGS?= diff --git a/beacon_node/beacon_chain/src/attestation_rewards.rs b/beacon_node/beacon_chain/src/attestation_rewards.rs index 87b7384ea6..3b37b09e40 100644 --- a/beacon_node/beacon_chain/src/attestation_rewards.rs +++ b/beacon_node/beacon_chain/src/attestation_rewards.rs @@ -51,13 +51,10 @@ impl BeaconChain { .get_state(&state_root, Some(state_slot))? .ok_or(BeaconChainError::MissingBeaconState(state_root))?; - match state { - BeaconState::Base(_) => self.compute_attestation_rewards_base(state, validators), - BeaconState::Altair(_) - | BeaconState::Bellatrix(_) - | BeaconState::Capella(_) - | BeaconState::Deneb(_) - | BeaconState::Electra(_) => self.compute_attestation_rewards_altair(state, validators), + if state.fork_name_unchecked().altair_enabled() { + self.compute_attestation_rewards_altair(state, validators) + } else { + self.compute_attestation_rewards_base(state, validators) } } diff --git a/beacon_node/beacon_chain/src/beacon_block_streamer.rs b/beacon_node/beacon_chain/src/beacon_block_streamer.rs index b76dba88fd..32ec776868 100644 --- a/beacon_node/beacon_chain/src/beacon_block_streamer.rs +++ b/beacon_node/beacon_chain/src/beacon_block_streamer.rs @@ -15,7 +15,7 @@ use types::{ }; use types::{ ExecutionPayload, ExecutionPayloadBellatrix, ExecutionPayloadCapella, ExecutionPayloadElectra, - ExecutionPayloadHeader, + ExecutionPayloadFulu, ExecutionPayloadHeader, }; #[derive(PartialEq)] @@ -99,6 +99,7 @@ fn reconstruct_default_header_block( ForkName::Capella => ExecutionPayloadCapella::default().into(), ForkName::Deneb => ExecutionPayloadDeneb::default().into(), ForkName::Electra => ExecutionPayloadElectra::default().into(), + ForkName::Fulu => ExecutionPayloadFulu::default().into(), ForkName::Base | ForkName::Altair => { return Err(Error::PayloadReconstruction(format!( "Block with fork variant {} has execution payload", @@ -742,13 +743,14 @@ mod tests { } #[tokio::test] - async fn check_all_blocks_from_altair_to_electra() { + async fn check_all_blocks_from_altair_to_fulu() { let slots_per_epoch = MinimalEthSpec::slots_per_epoch() as usize; - let num_epochs = 10; + let num_epochs = 12; let bellatrix_fork_epoch = 2usize; let capella_fork_epoch = 4usize; let deneb_fork_epoch = 6usize; let electra_fork_epoch = 8usize; + let fulu_fork_epoch = 10usize; let num_blocks_produced = num_epochs * slots_per_epoch; let mut spec = test_spec::(); @@ -757,6 +759,7 @@ mod tests { spec.capella_fork_epoch = Some(Epoch::new(capella_fork_epoch as u64)); spec.deneb_fork_epoch = Some(Epoch::new(deneb_fork_epoch as u64)); spec.electra_fork_epoch = Some(Epoch::new(electra_fork_epoch as u64)); + spec.fulu_fork_epoch = Some(Epoch::new(fulu_fork_epoch as u64)); let spec = Arc::new(spec); let harness = get_harness(VALIDATOR_COUNT, spec.clone()); diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 7bbb9ff74d..d84cd9615a 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -5317,23 +5317,19 @@ impl BeaconChain { // If required, start the process of loading an execution payload from the EL early. This // allows it to run concurrently with things like attestation packing. - let prepare_payload_handle = match &state { - BeaconState::Base(_) | BeaconState::Altair(_) => None, - BeaconState::Bellatrix(_) - | BeaconState::Capella(_) - | BeaconState::Deneb(_) - | BeaconState::Electra(_) => { - let prepare_payload_handle = get_execution_payload( - self.clone(), - &state, - parent_root, - proposer_index, - builder_params, - builder_boost_factor, - block_production_version, - )?; - Some(prepare_payload_handle) - } + let prepare_payload_handle = if state.fork_name_unchecked().bellatrix_enabled() { + let prepare_payload_handle = get_execution_payload( + self.clone(), + &state, + parent_root, + proposer_index, + builder_params, + builder_boost_factor, + block_production_version, + )?; + Some(prepare_payload_handle) + } else { + None }; let (mut proposer_slashings, mut attester_slashings, mut voluntary_exits) = @@ -5751,6 +5747,48 @@ impl BeaconChain { execution_payload_value, ) } + BeaconState::Fulu(_) => { + let ( + payload, + kzg_commitments, + maybe_blobs_and_proofs, + maybe_requests, + execution_payload_value, + ) = block_contents + .ok_or(BlockProductionError::MissingExecutionPayload)? + .deconstruct(); + + ( + BeaconBlock::Fulu(BeaconBlockFulu { + slot, + proposer_index, + parent_root, + state_root: Hash256::zero(), + body: BeaconBlockBodyFulu { + randao_reveal, + eth1_data, + graffiti, + proposer_slashings: proposer_slashings.into(), + attester_slashings: attester_slashings_electra.into(), + attestations: attestations_electra.into(), + deposits: deposits.into(), + voluntary_exits: voluntary_exits.into(), + sync_aggregate: sync_aggregate + .ok_or(BlockProductionError::MissingSyncAggregate)?, + execution_payload: payload + .try_into() + .map_err(|_| BlockProductionError::InvalidPayloadFork)?, + bls_to_execution_changes: bls_to_execution_changes.into(), + blob_kzg_commitments: kzg_commitments + .ok_or(BlockProductionError::InvalidPayloadFork)?, + execution_requests: maybe_requests + .ok_or(BlockProductionError::MissingExecutionRequests)?, + }, + }), + maybe_blobs_and_proofs, + execution_payload_value, + ) + } }; let block = SignedBeaconBlock::from_block( diff --git a/beacon_node/beacon_chain/src/execution_payload.rs b/beacon_node/beacon_chain/src/execution_payload.rs index 502a7918a1..720f98e298 100644 --- a/beacon_node/beacon_chain/src/execution_payload.rs +++ b/beacon_node/beacon_chain/src/execution_payload.rs @@ -374,19 +374,15 @@ pub fn get_execution_payload( let latest_execution_payload_header = state.latest_execution_payload_header()?; let latest_execution_payload_header_block_hash = latest_execution_payload_header.block_hash(); let latest_execution_payload_header_gas_limit = latest_execution_payload_header.gas_limit(); - let withdrawals = match state { - &BeaconState::Capella(_) | &BeaconState::Deneb(_) | &BeaconState::Electra(_) => { - Some(get_expected_withdrawals(state, spec)?.0.into()) - } - &BeaconState::Bellatrix(_) => None, - // These shouldn't happen but they're here to make the pattern irrefutable - &BeaconState::Base(_) | &BeaconState::Altair(_) => None, + let withdrawals = if state.fork_name_unchecked().capella_enabled() { + Some(get_expected_withdrawals(state, spec)?.0.into()) + } else { + None }; - let parent_beacon_block_root = match state { - BeaconState::Deneb(_) | BeaconState::Electra(_) => Some(parent_block_root), - BeaconState::Bellatrix(_) | BeaconState::Capella(_) => None, - // These shouldn't happen but they're here to make the pattern irrefutable - BeaconState::Base(_) | BeaconState::Altair(_) => None, + let parent_beacon_block_root = if state.fork_name_unchecked().deneb_enabled() { + Some(parent_block_root) + } else { + None }; // Spawn a task to obtain the execution payload from the EL via a series of async calls. The diff --git a/beacon_node/beacon_chain/src/fulu_readiness.rs b/beacon_node/beacon_chain/src/fulu_readiness.rs new file mode 100644 index 0000000000..71494623f8 --- /dev/null +++ b/beacon_node/beacon_chain/src/fulu_readiness.rs @@ -0,0 +1,114 @@ +//! Provides tools for checking if a node is ready for the Fulu upgrade. + +use crate::{BeaconChain, BeaconChainTypes}; +use execution_layer::http::{ENGINE_GET_PAYLOAD_V5, ENGINE_NEW_PAYLOAD_V5}; +use serde::{Deserialize, Serialize}; +use std::fmt; +use std::time::Duration; +use types::*; + +/// The time before the Fulu fork when we will start issuing warnings about preparation. +use super::bellatrix_readiness::SECONDS_IN_A_WEEK; +pub const FULU_READINESS_PREPARATION_SECONDS: u64 = SECONDS_IN_A_WEEK * 2; +pub const ENGINE_CAPABILITIES_REFRESH_INTERVAL: u64 = 300; + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +#[serde(tag = "type")] +pub enum FuluReadiness { + /// The execution engine is fulu-enabled (as far as we can tell) + Ready, + /// We are connected to an execution engine which doesn't support the V5 engine api methods + V5MethodsNotSupported { error: String }, + /// The transition configuration with the EL failed, there might be a problem with + /// connectivity, authentication or a difference in configuration. + ExchangeCapabilitiesFailed { error: String }, + /// The user has not configured an execution endpoint + NoExecutionEndpoint, +} + +impl fmt::Display for FuluReadiness { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + FuluReadiness::Ready => { + write!(f, "This node appears ready for Fulu.") + } + FuluReadiness::ExchangeCapabilitiesFailed { error } => write!( + f, + "Could not exchange capabilities with the \ + execution endpoint: {}", + error + ), + FuluReadiness::NoExecutionEndpoint => write!( + f, + "The --execution-endpoint flag is not specified, this is a \ + requirement post-merge" + ), + FuluReadiness::V5MethodsNotSupported { error } => write!( + f, + "Execution endpoint does not support Fulu methods: {}", + error + ), + } + } +} + +impl BeaconChain { + /// Returns `true` if fulu epoch is set and Fulu fork has occurred or will + /// occur within `FULU_READINESS_PREPARATION_SECONDS` + pub fn is_time_to_prepare_for_fulu(&self, current_slot: Slot) -> bool { + if let Some(fulu_epoch) = self.spec.fulu_fork_epoch { + let fulu_slot = fulu_epoch.start_slot(T::EthSpec::slots_per_epoch()); + let fulu_readiness_preparation_slots = + FULU_READINESS_PREPARATION_SECONDS / self.spec.seconds_per_slot; + // Return `true` if Fulu has happened or is within the preparation time. + current_slot + fulu_readiness_preparation_slots > fulu_slot + } else { + // The Fulu fork epoch has not been defined yet, no need to prepare. + false + } + } + + /// Attempts to connect to the EL and confirm that it is ready for fulu. + pub async fn check_fulu_readiness(&self) -> FuluReadiness { + if let Some(el) = self.execution_layer.as_ref() { + match el + .get_engine_capabilities(Some(Duration::from_secs( + ENGINE_CAPABILITIES_REFRESH_INTERVAL, + ))) + .await + { + Err(e) => { + // The EL was either unreachable or responded with an error + FuluReadiness::ExchangeCapabilitiesFailed { + error: format!("{:?}", e), + } + } + Ok(capabilities) => { + let mut missing_methods = String::from("Required Methods Unsupported:"); + let mut all_good = true; + if !capabilities.get_payload_v5 { + missing_methods.push(' '); + missing_methods.push_str(ENGINE_GET_PAYLOAD_V5); + all_good = false; + } + if !capabilities.new_payload_v5 { + missing_methods.push(' '); + missing_methods.push_str(ENGINE_NEW_PAYLOAD_V5); + all_good = false; + } + + if all_good { + FuluReadiness::Ready + } else { + FuluReadiness::V5MethodsNotSupported { + error: missing_methods, + } + } + } + } + } else { + FuluReadiness::NoExecutionEndpoint + } + } +} diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index d9728b9fd4..4783945eb1 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -31,6 +31,7 @@ pub mod execution_payload; pub mod fetch_blobs; pub mod fork_choice_signal; pub mod fork_revert; +pub mod fulu_readiness; pub mod graffiti_calculator; mod head_tracker; pub mod historical_blocks; diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index 093ee0c44b..d37398e4e0 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -501,6 +501,9 @@ where spec.electra_fork_epoch.map(|epoch| { genesis_time + spec.seconds_per_slot * E::slots_per_epoch() * epoch.as_u64() }); + mock.server.execution_block_generator().osaka_time = spec.fulu_fork_epoch.map(|epoch| { + genesis_time + spec.seconds_per_slot * E::slots_per_epoch() * epoch.as_u64() + }); self } @@ -623,6 +626,9 @@ pub fn mock_execution_layer_from_parts( let prague_time = spec.electra_fork_epoch.map(|epoch| { HARNESS_GENESIS_TIME + spec.seconds_per_slot * E::slots_per_epoch() * epoch.as_u64() }); + let osaka_time = spec.fulu_fork_epoch.map(|epoch| { + HARNESS_GENESIS_TIME + spec.seconds_per_slot * E::slots_per_epoch() * epoch.as_u64() + }); let kzg = get_kzg(spec); @@ -632,6 +638,7 @@ pub fn mock_execution_layer_from_parts( shanghai_time, cancun_time, prague_time, + osaka_time, Some(JwtKey::from_slice(&DEFAULT_JWT_SECRET).unwrap()), spec.clone(), Some(kzg), @@ -913,15 +920,12 @@ where &self.spec, )); - let block_contents: SignedBlockContentsTuple = match *signed_block { - SignedBeaconBlock::Base(_) - | SignedBeaconBlock::Altair(_) - | SignedBeaconBlock::Bellatrix(_) - | SignedBeaconBlock::Capella(_) => (signed_block, None), - SignedBeaconBlock::Deneb(_) | SignedBeaconBlock::Electra(_) => { + let block_contents: SignedBlockContentsTuple = + if signed_block.fork_name_unchecked().deneb_enabled() { (signed_block, block_response.blob_items) - } - }; + } else { + (signed_block, None) + }; (block_contents, block_response.state) } @@ -977,15 +981,12 @@ where &self.spec, )); - let block_contents: SignedBlockContentsTuple = match *signed_block { - SignedBeaconBlock::Base(_) - | SignedBeaconBlock::Altair(_) - | SignedBeaconBlock::Bellatrix(_) - | SignedBeaconBlock::Capella(_) => (signed_block, None), - SignedBeaconBlock::Deneb(_) | SignedBeaconBlock::Electra(_) => { + let block_contents: SignedBlockContentsTuple = + if signed_block.fork_name_unchecked().deneb_enabled() { (signed_block, block_response.blob_items) - } - }; + } else { + (signed_block, None) + }; (block_contents, pre_state) } @@ -2863,6 +2864,25 @@ pub fn generate_rand_block_and_blobs( message.body.blob_kzg_commitments = bundle.commitments.clone(); bundle } + SignedBeaconBlock::Fulu(SignedBeaconBlockFulu { + ref mut message, .. + }) => { + // Get either zero blobs or a random number of blobs between 1 and Max Blobs. + let payload: &mut FullPayloadFulu = &mut message.body.execution_payload; + let num_blobs = match num_blobs { + NumBlobs::Random => rng.gen_range(1..=E::max_blobs_per_block()), + NumBlobs::Number(n) => n, + NumBlobs::None => 0, + }; + let (bundle, transactions) = + execution_layer::test_utils::generate_blobs::(num_blobs).unwrap(); + payload.execution_payload.transactions = <_>::default(); + for tx in Vec::from(transactions) { + payload.execution_payload.transactions.push(tx).unwrap(); + } + message.body.blob_kzg_commitments = bundle.commitments.clone(); + bundle + } _ => return (block, blob_sidecars), }; diff --git a/beacon_node/beacon_chain/tests/block_verification.rs b/beacon_node/beacon_chain/tests/block_verification.rs index f094a173ee..103734b224 100644 --- a/beacon_node/beacon_chain/tests/block_verification.rs +++ b/beacon_node/beacon_chain/tests/block_verification.rs @@ -754,6 +754,11 @@ async fn invalid_signature_attester_slashing() { .push(attester_slashing.as_electra().unwrap().clone()) .expect("should update attester slashing"); } + BeaconBlockBodyRefMut::Fulu(ref mut blk) => { + blk.attester_slashings + .push(attester_slashing.as_electra().unwrap().clone()) + .expect("should update attester slashing"); + } } snapshots[block_index].beacon_block = Arc::new(SignedBeaconBlock::from_block(block, signature)); @@ -809,6 +814,10 @@ async fn invalid_signature_attestation() { .attestations .get_mut(0) .map(|att| att.signature = junk_aggregate_signature()), + BeaconBlockBodyRefMut::Fulu(ref mut blk) => blk + .attestations + .get_mut(0) + .map(|att| att.signature = junk_aggregate_signature()), }; if block.body().attestations_len() > 0 { diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index e1258ccdea..ed97b8d634 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -147,6 +147,7 @@ async fn light_client_bootstrap_test() { LightClientBootstrap::Capella(lc_bootstrap) => lc_bootstrap.header.beacon.slot, LightClientBootstrap::Deneb(lc_bootstrap) => lc_bootstrap.header.beacon.slot, LightClientBootstrap::Electra(lc_bootstrap) => lc_bootstrap.header.beacon.slot, + LightClientBootstrap::Fulu(lc_bootstrap) => lc_bootstrap.header.beacon.slot, }; assert_eq!( diff --git a/beacon_node/beacon_chain/tests/validator_monitor.rs b/beacon_node/beacon_chain/tests/validator_monitor.rs index b4a54d2667..91de4fe270 100644 --- a/beacon_node/beacon_chain/tests/validator_monitor.rs +++ b/beacon_node/beacon_chain/tests/validator_monitor.rs @@ -214,6 +214,7 @@ async fn produces_missed_blocks() { ForkName::Capella => 11, ForkName::Deneb => 3, ForkName::Electra => 1, + ForkName::Fulu => 6, }; let harness2 = get_harness(validator_count, vec![validator_index_to_monitor]); diff --git a/beacon_node/client/src/notifier.rs b/beacon_node/client/src/notifier.rs index e88803e94f..0c3b1578d6 100644 --- a/beacon_node/client/src/notifier.rs +++ b/beacon_node/client/src/notifier.rs @@ -4,6 +4,7 @@ use beacon_chain::{ capella_readiness::CapellaReadiness, deneb_readiness::DenebReadiness, electra_readiness::ElectraReadiness, + fulu_readiness::FuluReadiness, BeaconChain, BeaconChainTypes, ExecutionStatus, }; use lighthouse_network::{types::SyncState, NetworkGlobals}; @@ -315,6 +316,7 @@ pub fn spawn_notifier( capella_readiness_logging(current_slot, &beacon_chain, &log).await; deneb_readiness_logging(current_slot, &beacon_chain, &log).await; electra_readiness_logging(current_slot, &beacon_chain, &log).await; + fulu_readiness_logging(current_slot, &beacon_chain, &log).await; } }; @@ -586,6 +588,62 @@ async fn electra_readiness_logging( } } +/// Provides some helpful logging to users to indicate if their node is ready for Fulu. +async fn fulu_readiness_logging( + current_slot: Slot, + beacon_chain: &BeaconChain, + log: &Logger, +) { + let fulu_completed = beacon_chain + .canonical_head + .cached_head() + .snapshot + .beacon_state + .fork_name_unchecked() + .fulu_enabled(); + + let has_execution_layer = beacon_chain.execution_layer.is_some(); + + if fulu_completed && has_execution_layer + || !beacon_chain.is_time_to_prepare_for_fulu(current_slot) + { + return; + } + + if fulu_completed && !has_execution_layer { + error!( + log, + "Execution endpoint required"; + "info" => "you need a Fulu enabled execution engine to validate blocks." + ); + return; + } + + match beacon_chain.check_fulu_readiness().await { + FuluReadiness::Ready => { + info!( + log, + "Ready for Fulu"; + "info" => "ensure the execution endpoint is updated to the latest Fulu release" + ) + } + readiness @ FuluReadiness::ExchangeCapabilitiesFailed { error: _ } => { + error!( + log, + "Not ready for Fulu"; + "hint" => "the execution endpoint may be offline", + "info" => %readiness, + ) + } + readiness => warn!( + log, + "Not ready for Fulu"; + "hint" => "try updating the execution endpoint", + "info" => %readiness, + ), + } +} + async fn genesis_execution_payload_logging( beacon_chain: &BeaconChain, log: &Logger, diff --git a/beacon_node/execution_layer/src/engine_api.rs b/beacon_node/execution_layer/src/engine_api.rs index 083aaf2e25..b9d878b1f8 100644 --- a/beacon_node/execution_layer/src/engine_api.rs +++ b/beacon_node/execution_layer/src/engine_api.rs @@ -3,8 +3,8 @@ use crate::http::{ ENGINE_FORKCHOICE_UPDATED_V1, ENGINE_FORKCHOICE_UPDATED_V2, ENGINE_FORKCHOICE_UPDATED_V3, ENGINE_GET_BLOBS_V1, ENGINE_GET_CLIENT_VERSION_V1, ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1, ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1, ENGINE_GET_PAYLOAD_V1, ENGINE_GET_PAYLOAD_V2, - ENGINE_GET_PAYLOAD_V3, ENGINE_GET_PAYLOAD_V4, ENGINE_NEW_PAYLOAD_V1, ENGINE_NEW_PAYLOAD_V2, - ENGINE_NEW_PAYLOAD_V3, ENGINE_NEW_PAYLOAD_V4, + ENGINE_GET_PAYLOAD_V3, ENGINE_GET_PAYLOAD_V4, ENGINE_GET_PAYLOAD_V5, ENGINE_NEW_PAYLOAD_V1, + ENGINE_NEW_PAYLOAD_V2, ENGINE_NEW_PAYLOAD_V3, ENGINE_NEW_PAYLOAD_V4, ENGINE_NEW_PAYLOAD_V5, }; use eth2::types::{ BlobsBundle, SsePayloadAttributes, SsePayloadAttributesV1, SsePayloadAttributesV2, @@ -24,7 +24,7 @@ pub use types::{ }; use types::{ ExecutionPayloadBellatrix, ExecutionPayloadCapella, ExecutionPayloadDeneb, - ExecutionPayloadElectra, ExecutionRequests, KzgProofs, + ExecutionPayloadElectra, ExecutionPayloadFulu, ExecutionRequests, KzgProofs, }; use types::{Graffiti, GRAFFITI_BYTES_LEN}; @@ -35,7 +35,7 @@ mod new_payload_request; pub use new_payload_request::{ NewPayloadRequest, NewPayloadRequestBellatrix, NewPayloadRequestCapella, - NewPayloadRequestDeneb, NewPayloadRequestElectra, + NewPayloadRequestDeneb, NewPayloadRequestElectra, NewPayloadRequestFulu, }; pub const LATEST_TAG: &str = "latest"; @@ -261,7 +261,7 @@ pub struct ProposeBlindedBlockResponse { } #[superstruct( - variants(Bellatrix, Capella, Deneb, Electra), + variants(Bellatrix, Capella, Deneb, Electra, Fulu), variant_attributes(derive(Clone, Debug, PartialEq),), map_into(ExecutionPayload), map_ref_into(ExecutionPayloadRef), @@ -281,12 +281,14 @@ pub struct GetPayloadResponse { pub execution_payload: ExecutionPayloadDeneb, #[superstruct(only(Electra), partial_getter(rename = "execution_payload_electra"))] pub execution_payload: ExecutionPayloadElectra, + #[superstruct(only(Fulu), partial_getter(rename = "execution_payload_fulu"))] + pub execution_payload: ExecutionPayloadFulu, pub block_value: Uint256, - #[superstruct(only(Deneb, Electra))] + #[superstruct(only(Deneb, Electra, Fulu))] pub blobs_bundle: BlobsBundle, - #[superstruct(only(Deneb, Electra), partial_getter(copy))] + #[superstruct(only(Deneb, Electra, Fulu), partial_getter(copy))] pub should_override_builder: bool, - #[superstruct(only(Electra))] + #[superstruct(only(Electra, Fulu))] pub requests: ExecutionRequests, } @@ -354,6 +356,12 @@ impl From> Some(inner.blobs_bundle), Some(inner.requests), ), + GetPayloadResponse::Fulu(inner) => ( + ExecutionPayload::Fulu(inner.execution_payload), + inner.block_value, + Some(inner.blobs_bundle), + Some(inner.requests), + ), } } } @@ -487,6 +495,34 @@ impl ExecutionPayloadBodyV1 { )) } } + ExecutionPayloadHeader::Fulu(header) => { + if let Some(withdrawals) = self.withdrawals { + Ok(ExecutionPayload::Fulu(ExecutionPayloadFulu { + parent_hash: header.parent_hash, + fee_recipient: header.fee_recipient, + state_root: header.state_root, + receipts_root: header.receipts_root, + logs_bloom: header.logs_bloom, + prev_randao: header.prev_randao, + block_number: header.block_number, + gas_limit: header.gas_limit, + gas_used: header.gas_used, + timestamp: header.timestamp, + extra_data: header.extra_data, + base_fee_per_gas: header.base_fee_per_gas, + block_hash: header.block_hash, + transactions: self.transactions, + withdrawals, + blob_gas_used: header.blob_gas_used, + excess_blob_gas: header.excess_blob_gas, + })) + } else { + Err(format!( + "block {} is post capella but payload body doesn't have withdrawals", + header.block_hash + )) + } + } } } } @@ -497,6 +533,7 @@ pub struct EngineCapabilities { pub new_payload_v2: bool, pub new_payload_v3: bool, pub new_payload_v4: bool, + pub new_payload_v5: bool, pub forkchoice_updated_v1: bool, pub forkchoice_updated_v2: bool, pub forkchoice_updated_v3: bool, @@ -506,6 +543,7 @@ pub struct EngineCapabilities { pub get_payload_v2: bool, pub get_payload_v3: bool, pub get_payload_v4: bool, + pub get_payload_v5: bool, pub get_client_version_v1: bool, pub get_blobs_v1: bool, } @@ -525,6 +563,9 @@ impl EngineCapabilities { if self.new_payload_v4 { response.push(ENGINE_NEW_PAYLOAD_V4); } + if self.new_payload_v5 { + response.push(ENGINE_NEW_PAYLOAD_V5); + } if self.forkchoice_updated_v1 { response.push(ENGINE_FORKCHOICE_UPDATED_V1); } @@ -552,6 +593,9 @@ impl EngineCapabilities { if self.get_payload_v4 { response.push(ENGINE_GET_PAYLOAD_V4); } + if self.get_payload_v5 { + response.push(ENGINE_GET_PAYLOAD_V5); + } if self.get_client_version_v1 { response.push(ENGINE_GET_CLIENT_VERSION_V1); } diff --git a/beacon_node/execution_layer/src/engine_api/http.rs b/beacon_node/execution_layer/src/engine_api/http.rs index e2a81c072c..1fd9f81d46 100644 --- a/beacon_node/execution_layer/src/engine_api/http.rs +++ b/beacon_node/execution_layer/src/engine_api/http.rs @@ -35,12 +35,14 @@ pub const ENGINE_NEW_PAYLOAD_V1: &str = "engine_newPayloadV1"; pub const ENGINE_NEW_PAYLOAD_V2: &str = "engine_newPayloadV2"; pub const ENGINE_NEW_PAYLOAD_V3: &str = "engine_newPayloadV3"; pub const ENGINE_NEW_PAYLOAD_V4: &str = "engine_newPayloadV4"; +pub const ENGINE_NEW_PAYLOAD_V5: &str = "engine_newPayloadV5"; pub const ENGINE_NEW_PAYLOAD_TIMEOUT: Duration = Duration::from_secs(8); pub const ENGINE_GET_PAYLOAD_V1: &str = "engine_getPayloadV1"; pub const ENGINE_GET_PAYLOAD_V2: &str = "engine_getPayloadV2"; pub const ENGINE_GET_PAYLOAD_V3: &str = "engine_getPayloadV3"; pub const ENGINE_GET_PAYLOAD_V4: &str = "engine_getPayloadV4"; +pub const ENGINE_GET_PAYLOAD_V5: &str = "engine_getPayloadV5"; pub const ENGINE_GET_PAYLOAD_TIMEOUT: Duration = Duration::from_secs(2); pub const ENGINE_FORKCHOICE_UPDATED_V1: &str = "engine_forkchoiceUpdatedV1"; @@ -72,10 +74,12 @@ pub static LIGHTHOUSE_CAPABILITIES: &[&str] = &[ ENGINE_NEW_PAYLOAD_V2, ENGINE_NEW_PAYLOAD_V3, ENGINE_NEW_PAYLOAD_V4, + ENGINE_NEW_PAYLOAD_V5, ENGINE_GET_PAYLOAD_V1, ENGINE_GET_PAYLOAD_V2, ENGINE_GET_PAYLOAD_V3, ENGINE_GET_PAYLOAD_V4, + ENGINE_GET_PAYLOAD_V5, ENGINE_FORKCHOICE_UPDATED_V1, ENGINE_FORKCHOICE_UPDATED_V2, ENGINE_FORKCHOICE_UPDATED_V3, @@ -825,6 +829,30 @@ impl HttpJsonRpc { Ok(response.into()) } + pub async fn new_payload_v5_fulu( + &self, + new_payload_request_fulu: NewPayloadRequestFulu<'_, E>, + ) -> Result { + let params = json!([ + JsonExecutionPayload::V5(new_payload_request_fulu.execution_payload.clone().into()), + new_payload_request_fulu.versioned_hashes, + new_payload_request_fulu.parent_beacon_block_root, + new_payload_request_fulu + .execution_requests + .get_execution_requests_list(), + ]); + + let response: JsonPayloadStatusV1 = self + .rpc_request( + ENGINE_NEW_PAYLOAD_V5, + params, + ENGINE_NEW_PAYLOAD_TIMEOUT * self.execution_timeout_multiplier, + ) + .await?; + + Ok(response.into()) + } + pub async fn get_payload_v1( &self, payload_id: PayloadId, @@ -880,9 +908,10 @@ impl HttpJsonRpc { .try_into() .map_err(Error::BadResponse) } - ForkName::Base | ForkName::Altair | ForkName::Deneb | ForkName::Electra => Err( - Error::UnsupportedForkVariant(format!("called get_payload_v2 with {}", fork_name)), - ), + _ => Err(Error::UnsupportedForkVariant(format!( + "called get_payload_v2 with {}", + fork_name + ))), } } @@ -906,11 +935,7 @@ impl HttpJsonRpc { .try_into() .map_err(Error::BadResponse) } - ForkName::Base - | ForkName::Altair - | ForkName::Bellatrix - | ForkName::Capella - | ForkName::Electra => Err(Error::UnsupportedForkVariant(format!( + _ => Err(Error::UnsupportedForkVariant(format!( "called get_payload_v3 with {}", fork_name ))), @@ -937,17 +962,40 @@ impl HttpJsonRpc { .try_into() .map_err(Error::BadResponse) } - ForkName::Base - | ForkName::Altair - | ForkName::Bellatrix - | ForkName::Capella - | ForkName::Deneb => Err(Error::UnsupportedForkVariant(format!( + _ => Err(Error::UnsupportedForkVariant(format!( "called get_payload_v4 with {}", fork_name ))), } } + pub async fn get_payload_v5( + &self, + fork_name: ForkName, + payload_id: PayloadId, + ) -> Result, Error> { + let params = json!([JsonPayloadIdRequest::from(payload_id)]); + + match fork_name { + ForkName::Fulu => { + let response: JsonGetPayloadResponseV5 = self + .rpc_request( + ENGINE_GET_PAYLOAD_V5, + params, + ENGINE_GET_PAYLOAD_TIMEOUT * self.execution_timeout_multiplier, + ) + .await?; + JsonGetPayloadResponse::V5(response) + .try_into() + .map_err(Error::BadResponse) + } + _ => Err(Error::UnsupportedForkVariant(format!( + "called get_payload_v5 with {}", + fork_name + ))), + } + } + pub async fn forkchoice_updated_v1( &self, forkchoice_state: ForkchoiceState, @@ -1071,6 +1119,7 @@ impl HttpJsonRpc { new_payload_v2: capabilities.contains(ENGINE_NEW_PAYLOAD_V2), new_payload_v3: capabilities.contains(ENGINE_NEW_PAYLOAD_V3), new_payload_v4: capabilities.contains(ENGINE_NEW_PAYLOAD_V4), + new_payload_v5: capabilities.contains(ENGINE_NEW_PAYLOAD_V5), forkchoice_updated_v1: capabilities.contains(ENGINE_FORKCHOICE_UPDATED_V1), forkchoice_updated_v2: capabilities.contains(ENGINE_FORKCHOICE_UPDATED_V2), forkchoice_updated_v3: capabilities.contains(ENGINE_FORKCHOICE_UPDATED_V3), @@ -1082,6 +1131,7 @@ impl HttpJsonRpc { get_payload_v2: capabilities.contains(ENGINE_GET_PAYLOAD_V2), get_payload_v3: capabilities.contains(ENGINE_GET_PAYLOAD_V3), get_payload_v4: capabilities.contains(ENGINE_GET_PAYLOAD_V4), + get_payload_v5: capabilities.contains(ENGINE_GET_PAYLOAD_V5), get_client_version_v1: capabilities.contains(ENGINE_GET_CLIENT_VERSION_V1), get_blobs_v1: capabilities.contains(ENGINE_GET_BLOBS_V1), }) @@ -1212,6 +1262,13 @@ impl HttpJsonRpc { Err(Error::RequiredMethodUnsupported("engine_newPayloadV4")) } } + NewPayloadRequest::Fulu(new_payload_request_fulu) => { + if engine_capabilities.new_payload_v5 { + self.new_payload_v5_fulu(new_payload_request_fulu).await + } else { + Err(Error::RequiredMethodUnsupported("engine_newPayloadV5")) + } + } } } @@ -1247,6 +1304,13 @@ impl HttpJsonRpc { Err(Error::RequiredMethodUnsupported("engine_getPayloadv4")) } } + ForkName::Fulu => { + if engine_capabilities.get_payload_v5 { + self.get_payload_v5(fork_name, payload_id).await + } else { + Err(Error::RequiredMethodUnsupported("engine_getPayloadv5")) + } + } ForkName::Base | ForkName::Altair => Err(Error::UnsupportedForkVariant(format!( "called get_payload with {}", fork_name diff --git a/beacon_node/execution_layer/src/engine_api/json_structures.rs b/beacon_node/execution_layer/src/engine_api/json_structures.rs index 1c6639804e..86acaaaf3b 100644 --- a/beacon_node/execution_layer/src/engine_api/json_structures.rs +++ b/beacon_node/execution_layer/src/engine_api/json_structures.rs @@ -65,7 +65,7 @@ pub struct JsonPayloadIdResponse { } #[superstruct( - variants(V1, V2, V3, V4), + variants(V1, V2, V3, V4, V5), variant_attributes( derive(Debug, PartialEq, Default, Serialize, Deserialize,), serde(bound = "E: EthSpec", rename_all = "camelCase"), @@ -100,12 +100,12 @@ pub struct JsonExecutionPayload { pub block_hash: ExecutionBlockHash, #[serde(with = "ssz_types::serde_utils::list_of_hex_var_list")] pub transactions: Transactions, - #[superstruct(only(V2, V3, V4))] + #[superstruct(only(V2, V3, V4, V5))] pub withdrawals: VariableList, - #[superstruct(only(V3, V4))] + #[superstruct(only(V3, V4, V5))] #[serde(with = "serde_utils::u64_hex_be")] pub blob_gas_used: u64, - #[superstruct(only(V3, V4))] + #[superstruct(only(V3, V4, V5))] #[serde(with = "serde_utils::u64_hex_be")] pub excess_blob_gas: u64, } @@ -214,6 +214,35 @@ impl From> for JsonExecutionPayloadV4 } } +impl From> for JsonExecutionPayloadV5 { + fn from(payload: ExecutionPayloadFulu) -> Self { + JsonExecutionPayloadV5 { + parent_hash: payload.parent_hash, + fee_recipient: payload.fee_recipient, + state_root: payload.state_root, + receipts_root: payload.receipts_root, + logs_bloom: payload.logs_bloom, + prev_randao: payload.prev_randao, + block_number: payload.block_number, + gas_limit: payload.gas_limit, + gas_used: payload.gas_used, + timestamp: payload.timestamp, + extra_data: payload.extra_data, + base_fee_per_gas: payload.base_fee_per_gas, + block_hash: payload.block_hash, + transactions: payload.transactions, + withdrawals: payload + .withdrawals + .into_iter() + .map(Into::into) + .collect::>() + .into(), + blob_gas_used: payload.blob_gas_used, + excess_blob_gas: payload.excess_blob_gas, + } + } +} + impl From> for JsonExecutionPayload { fn from(execution_payload: ExecutionPayload) -> Self { match execution_payload { @@ -221,6 +250,7 @@ impl From> for JsonExecutionPayload { ExecutionPayload::Capella(payload) => JsonExecutionPayload::V2(payload.into()), ExecutionPayload::Deneb(payload) => JsonExecutionPayload::V3(payload.into()), ExecutionPayload::Electra(payload) => JsonExecutionPayload::V4(payload.into()), + ExecutionPayload::Fulu(payload) => JsonExecutionPayload::V5(payload.into()), } } } @@ -330,6 +360,35 @@ impl From> for ExecutionPayloadElectra } } +impl From> for ExecutionPayloadFulu { + fn from(payload: JsonExecutionPayloadV5) -> Self { + ExecutionPayloadFulu { + parent_hash: payload.parent_hash, + fee_recipient: payload.fee_recipient, + state_root: payload.state_root, + receipts_root: payload.receipts_root, + logs_bloom: payload.logs_bloom, + prev_randao: payload.prev_randao, + block_number: payload.block_number, + gas_limit: payload.gas_limit, + gas_used: payload.gas_used, + timestamp: payload.timestamp, + extra_data: payload.extra_data, + base_fee_per_gas: payload.base_fee_per_gas, + block_hash: payload.block_hash, + transactions: payload.transactions, + withdrawals: payload + .withdrawals + .into_iter() + .map(Into::into) + .collect::>() + .into(), + blob_gas_used: payload.blob_gas_used, + excess_blob_gas: payload.excess_blob_gas, + } + } +} + impl From> for ExecutionPayload { fn from(json_execution_payload: JsonExecutionPayload) -> Self { match json_execution_payload { @@ -337,6 +396,7 @@ impl From> for ExecutionPayload { JsonExecutionPayload::V2(payload) => ExecutionPayload::Capella(payload.into()), JsonExecutionPayload::V3(payload) => ExecutionPayload::Deneb(payload.into()), JsonExecutionPayload::V4(payload) => ExecutionPayload::Electra(payload.into()), + JsonExecutionPayload::V5(payload) => ExecutionPayload::Fulu(payload.into()), } } } @@ -389,7 +449,7 @@ impl TryFrom for ExecutionRequests { } #[superstruct( - variants(V1, V2, V3, V4), + variants(V1, V2, V3, V4, V5), variant_attributes( derive(Debug, PartialEq, Serialize, Deserialize), serde(bound = "E: EthSpec", rename_all = "camelCase") @@ -408,13 +468,15 @@ pub struct JsonGetPayloadResponse { pub execution_payload: JsonExecutionPayloadV3, #[superstruct(only(V4), partial_getter(rename = "execution_payload_v4"))] pub execution_payload: JsonExecutionPayloadV4, + #[superstruct(only(V5), partial_getter(rename = "execution_payload_v5"))] + pub execution_payload: JsonExecutionPayloadV5, #[serde(with = "serde_utils::u256_hex_be")] pub block_value: Uint256, - #[superstruct(only(V3, V4))] + #[superstruct(only(V3, V4, V5))] pub blobs_bundle: JsonBlobsBundleV1, - #[superstruct(only(V3, V4))] + #[superstruct(only(V3, V4, V5))] pub should_override_builder: bool, - #[superstruct(only(V4))] + #[superstruct(only(V4, V5))] pub execution_requests: JsonExecutionRequests, } @@ -451,6 +513,15 @@ impl TryFrom> for GetPayloadResponse { requests: response.execution_requests.try_into()?, })) } + JsonGetPayloadResponse::V5(response) => { + Ok(GetPayloadResponse::Fulu(GetPayloadResponseFulu { + execution_payload: response.execution_payload.into(), + block_value: response.block_value, + blobs_bundle: response.blobs_bundle.into(), + should_override_builder: response.should_override_builder, + requests: response.execution_requests.try_into()?, + })) + } } } } diff --git a/beacon_node/execution_layer/src/engine_api/new_payload_request.rs b/beacon_node/execution_layer/src/engine_api/new_payload_request.rs index 60bc848974..a86b2fd9bb 100644 --- a/beacon_node/execution_layer/src/engine_api/new_payload_request.rs +++ b/beacon_node/execution_layer/src/engine_api/new_payload_request.rs @@ -9,11 +9,11 @@ use types::{ }; use types::{ ExecutionPayloadBellatrix, ExecutionPayloadCapella, ExecutionPayloadDeneb, - ExecutionPayloadElectra, ExecutionRequests, + ExecutionPayloadElectra, ExecutionPayloadFulu, ExecutionRequests, }; #[superstruct( - variants(Bellatrix, Capella, Deneb, Electra), + variants(Bellatrix, Capella, Deneb, Electra, Fulu), variant_attributes(derive(Clone, Debug, PartialEq),), map_into(ExecutionPayload), map_ref_into(ExecutionPayloadRef), @@ -39,11 +39,13 @@ pub struct NewPayloadRequest<'block, E: EthSpec> { pub execution_payload: &'block ExecutionPayloadDeneb, #[superstruct(only(Electra), partial_getter(rename = "execution_payload_electra"))] pub execution_payload: &'block ExecutionPayloadElectra, - #[superstruct(only(Deneb, Electra))] + #[superstruct(only(Fulu), partial_getter(rename = "execution_payload_fulu"))] + pub execution_payload: &'block ExecutionPayloadFulu, + #[superstruct(only(Deneb, Electra, Fulu))] pub versioned_hashes: Vec, - #[superstruct(only(Deneb, Electra))] + #[superstruct(only(Deneb, Electra, Fulu))] pub parent_beacon_block_root: Hash256, - #[superstruct(only(Electra))] + #[superstruct(only(Electra, Fulu))] pub execution_requests: &'block ExecutionRequests, } @@ -54,6 +56,7 @@ impl<'block, E: EthSpec> NewPayloadRequest<'block, E> { Self::Capella(payload) => payload.execution_payload.parent_hash, Self::Deneb(payload) => payload.execution_payload.parent_hash, Self::Electra(payload) => payload.execution_payload.parent_hash, + Self::Fulu(payload) => payload.execution_payload.parent_hash, } } @@ -63,6 +66,7 @@ impl<'block, E: EthSpec> NewPayloadRequest<'block, E> { Self::Capella(payload) => payload.execution_payload.block_hash, Self::Deneb(payload) => payload.execution_payload.block_hash, Self::Electra(payload) => payload.execution_payload.block_hash, + Self::Fulu(payload) => payload.execution_payload.block_hash, } } @@ -72,6 +76,7 @@ impl<'block, E: EthSpec> NewPayloadRequest<'block, E> { Self::Capella(payload) => payload.execution_payload.block_number, Self::Deneb(payload) => payload.execution_payload.block_number, Self::Electra(payload) => payload.execution_payload.block_number, + Self::Fulu(payload) => payload.execution_payload.block_number, } } @@ -81,6 +86,7 @@ impl<'block, E: EthSpec> NewPayloadRequest<'block, E> { Self::Capella(request) => ExecutionPayloadRef::Capella(request.execution_payload), Self::Deneb(request) => ExecutionPayloadRef::Deneb(request.execution_payload), Self::Electra(request) => ExecutionPayloadRef::Electra(request.execution_payload), + Self::Fulu(request) => ExecutionPayloadRef::Fulu(request.execution_payload), } } @@ -92,6 +98,7 @@ impl<'block, E: EthSpec> NewPayloadRequest<'block, E> { Self::Capella(request) => ExecutionPayload::Capella(request.execution_payload.clone()), Self::Deneb(request) => ExecutionPayload::Deneb(request.execution_payload.clone()), Self::Electra(request) => ExecutionPayload::Electra(request.execution_payload.clone()), + Self::Fulu(request) => ExecutionPayload::Fulu(request.execution_payload.clone()), } } @@ -190,6 +197,17 @@ impl<'a, E: EthSpec> TryFrom> for NewPayloadRequest<'a, E> parent_beacon_block_root: block_ref.parent_root, execution_requests: &block_ref.body.execution_requests, })), + BeaconBlockRef::Fulu(block_ref) => Ok(Self::Fulu(NewPayloadRequestFulu { + execution_payload: &block_ref.body.execution_payload.execution_payload, + versioned_hashes: block_ref + .body + .blob_kzg_commitments + .iter() + .map(kzg_commitment_to_versioned_hash) + .collect(), + parent_beacon_block_root: block_ref.parent_root, + execution_requests: &block_ref.body.execution_requests, + })), } } } @@ -209,6 +227,7 @@ impl<'a, E: EthSpec> TryFrom> for NewPayloadRequest<' })), ExecutionPayloadRef::Deneb(_) => Err(Self::Error::IncorrectStateVariant), ExecutionPayloadRef::Electra(_) => Err(Self::Error::IncorrectStateVariant), + ExecutionPayloadRef::Fulu(_) => Err(Self::Error::IncorrectStateVariant), } } } diff --git a/beacon_node/execution_layer/src/lib.rs b/beacon_node/execution_layer/src/lib.rs index f3b12b21d1..118d7adfca 100644 --- a/beacon_node/execution_layer/src/lib.rs +++ b/beacon_node/execution_layer/src/lib.rs @@ -54,8 +54,8 @@ use types::{ }; use types::{ BeaconStateError, BlindedPayload, ChainSpec, Epoch, ExecPayload, ExecutionPayloadBellatrix, - ExecutionPayloadCapella, ExecutionPayloadElectra, FullPayload, ProposerPreparationData, - PublicKeyBytes, Signature, Slot, + ExecutionPayloadCapella, ExecutionPayloadElectra, ExecutionPayloadFulu, FullPayload, + ProposerPreparationData, PublicKeyBytes, Signature, Slot, }; mod block_hash; @@ -124,6 +124,14 @@ impl TryFrom> for ProvenancedPayload BlockProposalContents::PayloadAndBlobs { + payload: ExecutionPayloadHeader::Fulu(builder_bid.header).into(), + block_value: builder_bid.value, + kzg_commitments: builder_bid.blob_kzg_commitments, + blobs_and_proofs: None, + // TODO(fulu): update this with builder api returning the requests + requests: None, + }, }; Ok(ProvenancedPayload::Builder( BlockProposalContentsType::Blinded(block_proposal_contents), @@ -1821,6 +1829,7 @@ impl ExecutionLayer { ForkName::Capella => ExecutionPayloadCapella::default().into(), ForkName::Deneb => ExecutionPayloadDeneb::default().into(), ForkName::Electra => ExecutionPayloadElectra::default().into(), + ForkName::Fulu => ExecutionPayloadFulu::default().into(), ForkName::Base | ForkName::Altair => { return Err(Error::InvalidForkForPayload); } diff --git a/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs b/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs index 4fab7150ce..2a39796707 100644 --- a/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs +++ b/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs @@ -19,7 +19,7 @@ use tree_hash::TreeHash; use tree_hash_derive::TreeHash; use types::{ Blob, ChainSpec, EthSpec, ExecutionBlockHash, ExecutionPayload, ExecutionPayloadBellatrix, - ExecutionPayloadCapella, ExecutionPayloadDeneb, ExecutionPayloadElectra, + ExecutionPayloadCapella, ExecutionPayloadDeneb, ExecutionPayloadElectra, ExecutionPayloadFulu, ExecutionPayloadHeader, FixedBytesExtended, ForkName, Hash256, Transaction, Transactions, Uint256, }; @@ -147,6 +147,7 @@ pub struct ExecutionBlockGenerator { pub shanghai_time: Option, // capella pub cancun_time: Option, // deneb pub prague_time: Option, // electra + pub osaka_time: Option, // fulu /* * deneb stuff */ @@ -162,6 +163,7 @@ fn make_rng() -> Arc> { } impl ExecutionBlockGenerator { + #[allow(clippy::too_many_arguments)] pub fn new( terminal_total_difficulty: Uint256, terminal_block_number: u64, @@ -169,6 +171,7 @@ impl ExecutionBlockGenerator { shanghai_time: Option, cancun_time: Option, prague_time: Option, + osaka_time: Option, kzg: Option>, ) -> Self { let mut gen = Self { @@ -185,6 +188,7 @@ impl ExecutionBlockGenerator { shanghai_time, cancun_time, prague_time, + osaka_time, blobs_bundles: <_>::default(), kzg, rng: make_rng(), @@ -233,13 +237,16 @@ impl ExecutionBlockGenerator { } pub fn get_fork_at_timestamp(&self, timestamp: u64) -> ForkName { - match self.prague_time { - Some(fork_time) if timestamp >= fork_time => ForkName::Electra, - _ => match self.cancun_time { - Some(fork_time) if timestamp >= fork_time => ForkName::Deneb, - _ => match self.shanghai_time { - Some(fork_time) if timestamp >= fork_time => ForkName::Capella, - _ => ForkName::Bellatrix, + match self.osaka_time { + Some(fork_time) if timestamp >= fork_time => ForkName::Fulu, + _ => match self.prague_time { + Some(fork_time) if timestamp >= fork_time => ForkName::Electra, + _ => match self.cancun_time { + Some(fork_time) if timestamp >= fork_time => ForkName::Deneb, + _ => match self.shanghai_time { + Some(fork_time) if timestamp >= fork_time => ForkName::Capella, + _ => ForkName::Bellatrix, + }, }, }, } @@ -664,6 +671,25 @@ impl ExecutionBlockGenerator { blob_gas_used: 0, excess_blob_gas: 0, }), + ForkName::Fulu => ExecutionPayload::Fulu(ExecutionPayloadFulu { + parent_hash: head_block_hash, + fee_recipient: pa.suggested_fee_recipient, + receipts_root: Hash256::repeat_byte(42), + state_root: Hash256::repeat_byte(43), + logs_bloom: vec![0; 256].into(), + prev_randao: pa.prev_randao, + block_number: parent.block_number() + 1, + gas_limit: DEFAULT_GAS_LIMIT, + gas_used: GAS_USED, + timestamp: pa.timestamp, + extra_data: "block gen was here".as_bytes().to_vec().into(), + base_fee_per_gas: Uint256::from(1u64), + block_hash: ExecutionBlockHash::zero(), + transactions: vec![].into(), + withdrawals: pa.withdrawals.clone().into(), + blob_gas_used: 0, + excess_blob_gas: 0, + }), _ => unreachable!(), }, }; @@ -811,6 +837,12 @@ pub fn generate_genesis_header( *header.transactions_root_mut() = empty_transactions_root; Some(header) } + ForkName::Fulu => { + let mut header = ExecutionPayloadHeader::Fulu(<_>::default()); + *header.block_hash_mut() = genesis_block_hash.unwrap_or_default(); + *header.transactions_root_mut() = empty_transactions_root; + Some(header) + } } } @@ -883,6 +915,7 @@ mod test { None, None, None, + None, ); for i in 0..=TERMINAL_BLOCK { diff --git a/beacon_node/execution_layer/src/test_utils/handle_rpc.rs b/beacon_node/execution_layer/src/test_utils/handle_rpc.rs index 9365024ffb..0babb9d1a3 100644 --- a/beacon_node/execution_layer/src/test_utils/handle_rpc.rs +++ b/beacon_node/execution_layer/src/test_utils/handle_rpc.rs @@ -99,7 +99,8 @@ pub async fn handle_rpc( ENGINE_NEW_PAYLOAD_V1 | ENGINE_NEW_PAYLOAD_V2 | ENGINE_NEW_PAYLOAD_V3 - | ENGINE_NEW_PAYLOAD_V4 => { + | ENGINE_NEW_PAYLOAD_V4 + | ENGINE_NEW_PAYLOAD_V5 => { let request = match method { ENGINE_NEW_PAYLOAD_V1 => JsonExecutionPayload::V1( get_param::>(params, 0) @@ -121,6 +122,9 @@ pub async fn handle_rpc( ENGINE_NEW_PAYLOAD_V4 => get_param::>(params, 0) .map(|jep| JsonExecutionPayload::V4(jep)) .map_err(|s| (s, BAD_PARAMS_ERROR_CODE))?, + ENGINE_NEW_PAYLOAD_V5 => get_param::>(params, 0) + .map(|jep| JsonExecutionPayload::V5(jep)) + .map_err(|s| (s, BAD_PARAMS_ERROR_CODE))?, _ => unreachable!(), }; @@ -222,6 +226,54 @@ pub async fn handle_rpc( )); } } + ForkName::Fulu => { + if method == ENGINE_NEW_PAYLOAD_V1 + || method == ENGINE_NEW_PAYLOAD_V2 + || method == ENGINE_NEW_PAYLOAD_V3 + || method == ENGINE_NEW_PAYLOAD_V4 + { + return Err(( + format!("{} called after Fulu fork!", method), + GENERIC_ERROR_CODE, + )); + } + if matches!(request, JsonExecutionPayload::V1(_)) { + return Err(( + format!( + "{} called with `ExecutionPayloadV1` after Fulu fork!", + method + ), + GENERIC_ERROR_CODE, + )); + } + if matches!(request, JsonExecutionPayload::V2(_)) { + return Err(( + format!( + "{} called with `ExecutionPayloadV2` after Fulu fork!", + method + ), + GENERIC_ERROR_CODE, + )); + } + if matches!(request, JsonExecutionPayload::V3(_)) { + return Err(( + format!( + "{} called with `ExecutionPayloadV3` after Fulu fork!", + method + ), + GENERIC_ERROR_CODE, + )); + } + if matches!(request, JsonExecutionPayload::V4(_)) { + return Err(( + format!( + "{} called with `ExecutionPayloadV4` after Fulu fork!", + method + ), + GENERIC_ERROR_CODE, + )); + } + } _ => unreachable!(), }; @@ -260,7 +312,8 @@ pub async fn handle_rpc( ENGINE_GET_PAYLOAD_V1 | ENGINE_GET_PAYLOAD_V2 | ENGINE_GET_PAYLOAD_V3 - | ENGINE_GET_PAYLOAD_V4 => { + | ENGINE_GET_PAYLOAD_V4 + | ENGINE_GET_PAYLOAD_V5 => { let request: JsonPayloadIdRequest = get_param(params, 0).map_err(|s| (s, BAD_PARAMS_ERROR_CODE))?; let id = request.into(); @@ -320,6 +373,23 @@ pub async fn handle_rpc( )); } + // validate method called correctly according to fulu fork time + if ctx + .execution_block_generator + .read() + .get_fork_at_timestamp(response.timestamp()) + == ForkName::Fulu + && (method == ENGINE_GET_PAYLOAD_V1 + || method == ENGINE_GET_PAYLOAD_V2 + || method == ENGINE_GET_PAYLOAD_V3 + || method == ENGINE_GET_PAYLOAD_V4) + { + return Err(( + format!("{} called after Fulu fork!", method), + FORK_REQUEST_MISMATCH_ERROR_CODE, + )); + } + match method { ENGINE_GET_PAYLOAD_V1 => { Ok(serde_json::to_value(JsonExecutionPayload::from(response)).unwrap()) @@ -380,6 +450,24 @@ pub async fn handle_rpc( } _ => unreachable!(), }), + ENGINE_GET_PAYLOAD_V5 => Ok(match JsonExecutionPayload::from(response) { + JsonExecutionPayload::V5(execution_payload) => { + serde_json::to_value(JsonGetPayloadResponseV5 { + execution_payload, + block_value: Uint256::from(DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI), + blobs_bundle: maybe_blobs + .ok_or(( + "No blobs returned despite V5 Payload".to_string(), + GENERIC_ERROR_CODE, + ))? + .into(), + should_override_builder: false, + execution_requests: Default::default(), + }) + .unwrap() + } + _ => unreachable!(), + }), _ => unreachable!(), } } @@ -411,7 +499,10 @@ pub async fn handle_rpc( .map(|opt| opt.map(JsonPayloadAttributes::V1)) .transpose() } - ForkName::Capella | ForkName::Deneb | ForkName::Electra => { + ForkName::Capella + | ForkName::Deneb + | ForkName::Electra + | ForkName::Fulu => { get_param::>(params, 1) .map(|opt| opt.map(JsonPayloadAttributes::V2)) .transpose() @@ -475,7 +566,7 @@ pub async fn handle_rpc( )); } } - ForkName::Deneb | ForkName::Electra => { + ForkName::Deneb | ForkName::Electra | ForkName::Fulu => { if method == ENGINE_FORKCHOICE_UPDATED_V1 { return Err(( format!("{} called after Deneb fork!", method), diff --git a/beacon_node/execution_layer/src/test_utils/mock_builder.rs b/beacon_node/execution_layer/src/test_utils/mock_builder.rs index 879b54eb07..65181dcf4f 100644 --- a/beacon_node/execution_layer/src/test_utils/mock_builder.rs +++ b/beacon_node/execution_layer/src/test_utils/mock_builder.rs @@ -16,7 +16,7 @@ use tempfile::NamedTempFile; use tree_hash::TreeHash; use types::builder_bid::{ BuilderBid, BuilderBidBellatrix, BuilderBidCapella, BuilderBidDeneb, BuilderBidElectra, - SignedBuilderBid, + BuilderBidFulu, SignedBuilderBid, }; use types::{ Address, BeaconState, ChainSpec, EthSpec, ExecPayload, ExecutionPayload, @@ -95,6 +95,9 @@ impl BidStuff for BuilderBid { ExecutionPayloadHeaderRefMut::Electra(header) => { header.fee_recipient = fee_recipient; } + ExecutionPayloadHeaderRefMut::Fulu(header) => { + header.fee_recipient = fee_recipient; + } } } @@ -112,6 +115,9 @@ impl BidStuff for BuilderBid { ExecutionPayloadHeaderRefMut::Electra(header) => { header.gas_limit = gas_limit; } + ExecutionPayloadHeaderRefMut::Fulu(header) => { + header.gas_limit = gas_limit; + } } } @@ -133,6 +139,9 @@ impl BidStuff for BuilderBid { ExecutionPayloadHeaderRefMut::Electra(header) => { header.parent_hash = ExecutionBlockHash::from_root(parent_hash); } + ExecutionPayloadHeaderRefMut::Fulu(header) => { + header.parent_hash = ExecutionBlockHash::from_root(parent_hash); + } } } @@ -150,6 +159,9 @@ impl BidStuff for BuilderBid { ExecutionPayloadHeaderRefMut::Electra(header) => { header.prev_randao = prev_randao; } + ExecutionPayloadHeaderRefMut::Fulu(header) => { + header.prev_randao = prev_randao; + } } } @@ -167,6 +179,9 @@ impl BidStuff for BuilderBid { ExecutionPayloadHeaderRefMut::Electra(header) => { header.block_number = block_number; } + ExecutionPayloadHeaderRefMut::Fulu(header) => { + header.block_number = block_number; + } } } @@ -184,6 +199,9 @@ impl BidStuff for BuilderBid { ExecutionPayloadHeaderRefMut::Electra(header) => { header.timestamp = timestamp; } + ExecutionPayloadHeaderRefMut::Fulu(header) => { + header.timestamp = timestamp; + } } } @@ -201,6 +219,9 @@ impl BidStuff for BuilderBid { ExecutionPayloadHeaderRefMut::Electra(header) => { header.withdrawals_root = withdrawals_root; } + ExecutionPayloadHeaderRefMut::Fulu(header) => { + header.withdrawals_root = withdrawals_root; + } } } @@ -230,6 +251,10 @@ impl BidStuff for BuilderBid { header.extra_data = extra_data; header.block_hash = ExecutionBlockHash::from_root(header.tree_hash_root()); } + ExecutionPayloadHeaderRefMut::Fulu(header) => { + header.extra_data = extra_data; + header.block_hash = ExecutionBlockHash::from_root(header.tree_hash_root()); + } } } } @@ -378,6 +403,9 @@ pub fn serve( SignedBlindedBeaconBlock::Electra(block) => { block.message.body.execution_payload.tree_hash_root() } + SignedBlindedBeaconBlock::Fulu(block) => { + block.message.body.execution_payload.tree_hash_root() + } }; let payload = builder .el @@ -536,7 +564,7 @@ pub fn serve( expected_withdrawals, None, ), - ForkName::Deneb | ForkName::Electra => PayloadAttributes::new( + ForkName::Deneb | ForkName::Electra | ForkName::Fulu => PayloadAttributes::new( timestamp, *prev_randao, fee_recipient, @@ -592,6 +620,17 @@ pub fn serve( ) = payload_response.into(); match fork { + ForkName::Fulu => BuilderBid::Fulu(BuilderBidFulu { + header: payload + .as_fulu() + .map_err(|_| reject("incorrect payload variant"))? + .into(), + blob_kzg_commitments: maybe_blobs_bundle + .map(|b| b.commitments) + .unwrap_or_default(), + value: Uint256::from(DEFAULT_BUILDER_PAYLOAD_VALUE_WEI), + pubkey: builder.builder_sk.public_key().compress(), + }), ForkName::Electra => BuilderBid::Electra(BuilderBidElectra { header: payload .as_electra() @@ -644,6 +683,17 @@ pub fn serve( Option>, ) = payload_response.into(); match fork { + ForkName::Fulu => BuilderBid::Fulu(BuilderBidFulu { + header: payload + .as_fulu() + .map_err(|_| reject("incorrect payload variant"))? + .into(), + blob_kzg_commitments: maybe_blobs_bundle + .map(|b| b.commitments) + .unwrap_or_default(), + value: Uint256::from(DEFAULT_BUILDER_PAYLOAD_VALUE_WEI), + pubkey: builder.builder_sk.public_key().compress(), + }), ForkName::Electra => BuilderBid::Electra(BuilderBidElectra { header: payload .as_electra() diff --git a/beacon_node/execution_layer/src/test_utils/mock_execution_layer.rs b/beacon_node/execution_layer/src/test_utils/mock_execution_layer.rs index dc90d91c0f..9df8d9cc5c 100644 --- a/beacon_node/execution_layer/src/test_utils/mock_execution_layer.rs +++ b/beacon_node/execution_layer/src/test_utils/mock_execution_layer.rs @@ -28,6 +28,7 @@ impl MockExecutionLayer { None, None, None, + None, Some(JwtKey::from_slice(&DEFAULT_JWT_SECRET).unwrap()), spec, None, @@ -41,6 +42,7 @@ impl MockExecutionLayer { shanghai_time: Option, cancun_time: Option, prague_time: Option, + osaka_time: Option, jwt_key: Option, spec: ChainSpec, kzg: Option>, @@ -57,6 +59,7 @@ impl MockExecutionLayer { shanghai_time, cancun_time, prague_time, + osaka_time, kzg, ); diff --git a/beacon_node/execution_layer/src/test_utils/mod.rs b/beacon_node/execution_layer/src/test_utils/mod.rs index faf6d4ef0b..5934c069a2 100644 --- a/beacon_node/execution_layer/src/test_utils/mod.rs +++ b/beacon_node/execution_layer/src/test_utils/mod.rs @@ -44,6 +44,7 @@ pub const DEFAULT_ENGINE_CAPABILITIES: EngineCapabilities = EngineCapabilities { new_payload_v2: true, new_payload_v3: true, new_payload_v4: true, + new_payload_v5: true, forkchoice_updated_v1: true, forkchoice_updated_v2: true, forkchoice_updated_v3: true, @@ -53,6 +54,7 @@ pub const DEFAULT_ENGINE_CAPABILITIES: EngineCapabilities = EngineCapabilities { get_payload_v2: true, get_payload_v3: true, get_payload_v4: true, + get_payload_v5: true, get_client_version_v1: true, get_blobs_v1: true, }; @@ -82,6 +84,7 @@ pub struct MockExecutionConfig { pub shanghai_time: Option, pub cancun_time: Option, pub prague_time: Option, + pub osaka_time: Option, } impl Default for MockExecutionConfig { @@ -95,6 +98,7 @@ impl Default for MockExecutionConfig { shanghai_time: None, cancun_time: None, prague_time: None, + osaka_time: None, } } } @@ -117,6 +121,7 @@ impl MockServer { None, // FIXME(capella): should this be the default? None, // FIXME(deneb): should this be the default? None, // FIXME(electra): should this be the default? + None, // FIXME(fulu): should this be the default? None, ) } @@ -135,6 +140,7 @@ impl MockServer { shanghai_time, cancun_time, prague_time, + osaka_time, } = config; let last_echo_request = Arc::new(RwLock::new(None)); let preloaded_responses = Arc::new(Mutex::new(vec![])); @@ -145,6 +151,7 @@ impl MockServer { shanghai_time, cancun_time, prague_time, + osaka_time, kzg, ); @@ -208,6 +215,7 @@ impl MockServer { shanghai_time: Option, cancun_time: Option, prague_time: Option, + osaka_time: Option, kzg: Option>, ) -> Self { Self::new_with_config( @@ -221,6 +229,7 @@ impl MockServer { shanghai_time, cancun_time, prague_time, + osaka_time, }, kzg, ) diff --git a/beacon_node/http_api/tests/broadcast_validation_tests.rs b/beacon_node/http_api/tests/broadcast_validation_tests.rs index e1ecf2d4fc..8e0a51a32a 100644 --- a/beacon_node/http_api/tests/broadcast_validation_tests.rs +++ b/beacon_node/http_api/tests/broadcast_validation_tests.rs @@ -75,7 +75,7 @@ pub async fn gossip_invalid() { let response: Result<(), eth2::Error> = tester .client - .post_beacon_blocks_v2(&PublishBlockRequest::new(block, blobs), validation_level) + .post_beacon_blocks_v2_ssz(&PublishBlockRequest::new(block, blobs), validation_level) .await; assert!(response.is_err()); @@ -124,7 +124,7 @@ pub async fn gossip_partial_pass() { let response: Result<(), eth2::Error> = tester .client - .post_beacon_blocks_v2(&PublishBlockRequest::new(block, blobs), validation_level) + .post_beacon_blocks_v2_ssz(&PublishBlockRequest::new(block, blobs), validation_level) .await; assert!(response.is_err()); @@ -165,7 +165,7 @@ pub async fn gossip_full_pass() { let response: Result<(), eth2::Error> = tester .client - .post_beacon_blocks_v2( + .post_beacon_blocks_v2_ssz( &PublishBlockRequest::new(block.clone(), blobs), validation_level, ) @@ -261,7 +261,7 @@ pub async fn consensus_invalid() { let response: Result<(), eth2::Error> = tester .client - .post_beacon_blocks_v2(&PublishBlockRequest::new(block, blobs), validation_level) + .post_beacon_blocks_v2_ssz(&PublishBlockRequest::new(block, blobs), validation_level) .await; assert!(response.is_err()); @@ -307,7 +307,7 @@ pub async fn consensus_gossip() { let response: Result<(), eth2::Error> = tester .client - .post_beacon_blocks_v2(&PublishBlockRequest::new(block, blobs), validation_level) + .post_beacon_blocks_v2_ssz(&PublishBlockRequest::new(block, blobs), validation_level) .await; assert!(response.is_err()); @@ -423,7 +423,7 @@ pub async fn consensus_full_pass() { let response: Result<(), eth2::Error> = tester .client - .post_beacon_blocks_v2( + .post_beacon_blocks_v2_ssz( &PublishBlockRequest::new(block.clone(), blobs), validation_level, ) @@ -475,7 +475,7 @@ pub async fn equivocation_invalid() { let response: Result<(), eth2::Error> = tester .client - .post_beacon_blocks_v2(&PublishBlockRequest::new(block, blobs), validation_level) + .post_beacon_blocks_v2_ssz(&PublishBlockRequest::new(block, blobs), validation_level) .await; assert!(response.is_err()); @@ -533,7 +533,7 @@ pub async fn equivocation_consensus_early_equivocation() { /* submit `block_a` as valid */ assert!(tester .client - .post_beacon_blocks_v2( + .post_beacon_blocks_v2_ssz( &PublishBlockRequest::new(block_a.clone(), blobs_a), validation_level ) @@ -547,7 +547,7 @@ pub async fn equivocation_consensus_early_equivocation() { /* submit `block_b` which should induce equivocation */ let response: Result<(), eth2::Error> = tester .client - .post_beacon_blocks_v2( + .post_beacon_blocks_v2_ssz( &PublishBlockRequest::new(block_b.clone(), blobs_b), validation_level, ) @@ -596,7 +596,7 @@ pub async fn equivocation_gossip() { let response: Result<(), eth2::Error> = tester .client - .post_beacon_blocks_v2(&PublishBlockRequest::new(block, blobs), validation_level) + .post_beacon_blocks_v2_ssz(&PublishBlockRequest::new(block, blobs), validation_level) .await; assert!(response.is_err()); @@ -721,7 +721,7 @@ pub async fn equivocation_full_pass() { let response: Result<(), eth2::Error> = tester .client - .post_beacon_blocks_v2( + .post_beacon_blocks_v2_ssz( &PublishBlockRequest::new(block.clone(), blobs), validation_level, ) @@ -1413,7 +1413,7 @@ pub async fn block_seen_on_gossip_without_blobs() { // Post the block *and* blobs to the HTTP API. let response: Result<(), eth2::Error> = tester .client - .post_beacon_blocks_v2( + .post_beacon_blocks_v2_ssz( &PublishBlockRequest::new(block.clone(), Some(blobs)), validation_level, ) @@ -1498,7 +1498,7 @@ pub async fn block_seen_on_gossip_with_some_blobs() { // Post the block *and* all blobs to the HTTP API. let response: Result<(), eth2::Error> = tester .client - .post_beacon_blocks_v2( + .post_beacon_blocks_v2_ssz( &PublishBlockRequest::new(block.clone(), Some(blobs)), validation_level, ) @@ -1571,7 +1571,7 @@ pub async fn blobs_seen_on_gossip_without_block() { // Post the block *and* all blobs to the HTTP API. let response: Result<(), eth2::Error> = tester .client - .post_beacon_blocks_v2( + .post_beacon_blocks_v2_ssz( &PublishBlockRequest::new(block.clone(), Some((kzg_proofs, blobs))), validation_level, ) @@ -1645,7 +1645,7 @@ pub async fn blobs_seen_on_gossip_without_block_and_no_http_blobs() { // Post just the block to the HTTP API (blob lists are empty). let response: Result<(), eth2::Error> = tester .client - .post_beacon_blocks_v2( + .post_beacon_blocks_v2_ssz( &PublishBlockRequest::new( block.clone(), Some((Default::default(), Default::default())), @@ -1717,7 +1717,7 @@ pub async fn slashable_blobs_seen_on_gossip_cause_failure() { // Post block A *and* all its blobs to the HTTP API. let response: Result<(), eth2::Error> = tester .client - .post_beacon_blocks_v2( + .post_beacon_blocks_v2_ssz( &PublishBlockRequest::new(block_a.clone(), Some((kzg_proofs_a, blobs_a))), validation_level, ) @@ -1778,7 +1778,7 @@ pub async fn duplicate_block_status_code() { let block_request = PublishBlockRequest::new(block.clone(), Some((kzg_proofs, blobs))); let response: Result<(), eth2::Error> = tester .client - .post_beacon_blocks_v2(&block_request, validation_level) + .post_beacon_blocks_v2_ssz(&block_request, validation_level) .await; // This should result in the block being fully imported. @@ -1791,7 +1791,7 @@ pub async fn duplicate_block_status_code() { // Post again. let duplicate_response: Result<(), eth2::Error> = tester .client - .post_beacon_blocks_v2(&block_request, validation_level) + .post_beacon_blocks_v2_ssz(&block_request, validation_level) .await; let err = duplicate_response.unwrap_err(); assert_eq!(err.status().unwrap(), duplicate_block_status_code); diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 1efe44a613..85d3b4e9ba 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -2232,9 +2232,9 @@ impl ApiTester { pub async fn test_get_config_spec(self) -> Self { let result = self .client - .get_config_spec::() + .get_config_spec::() .await - .map(|res| ConfigAndPreset::Electra(res.data)) + .map(|res| ConfigAndPreset::Fulu(res.data)) .unwrap(); let expected = ConfigAndPreset::from_chain_spec::(&self.chain.spec, None); diff --git a/beacon_node/lighthouse_network/src/rpc/codec.rs b/beacon_node/lighthouse_network/src/rpc/codec.rs index 5d86936d41..c3d20bbfb1 100644 --- a/beacon_node/lighthouse_network/src/rpc/codec.rs +++ b/beacon_node/lighthouse_network/src/rpc/codec.rs @@ -20,7 +20,7 @@ use types::{ LightClientBootstrap, LightClientFinalityUpdate, LightClientOptimisticUpdate, LightClientUpdate, RuntimeVariableList, SignedBeaconBlock, SignedBeaconBlockAltair, SignedBeaconBlockBase, SignedBeaconBlockBellatrix, SignedBeaconBlockCapella, - SignedBeaconBlockDeneb, SignedBeaconBlockElectra, + SignedBeaconBlockDeneb, SignedBeaconBlockElectra, SignedBeaconBlockFulu, }; use unsigned_varint::codec::Uvi; @@ -458,6 +458,9 @@ fn context_bytes( return match **ref_box_block { // NOTE: If you are adding another fork type here, be sure to modify the // `fork_context.to_context_bytes()` function to support it as well! + SignedBeaconBlock::Fulu { .. } => { + fork_context.to_context_bytes(ForkName::Fulu) + } SignedBeaconBlock::Electra { .. } => { fork_context.to_context_bytes(ForkName::Electra) } @@ -682,18 +685,18 @@ fn handle_rpc_response( SignedBeaconBlock::Base(SignedBeaconBlockBase::from_ssz_bytes(decoded_buffer)?), )))), SupportedProtocol::BlobsByRangeV1 => match fork_name { - Some(ForkName::Deneb) | Some(ForkName::Electra) => { - Ok(Some(RpcSuccessResponse::BlobsByRange(Arc::new( - BlobSidecar::from_ssz_bytes(decoded_buffer)?, - )))) + Some(fork_name) => { + if fork_name.deneb_enabled() { + Ok(Some(RpcSuccessResponse::BlobsByRange(Arc::new( + BlobSidecar::from_ssz_bytes(decoded_buffer)?, + )))) + } else { + Err(RPCError::ErrorResponse( + RpcErrorResponse::InvalidRequest, + "Invalid fork name for blobs by range".to_string(), + )) + } } - Some(ForkName::Base) - | Some(ForkName::Altair) - | Some(ForkName::Bellatrix) - | Some(ForkName::Capella) => Err(RPCError::ErrorResponse( - RpcErrorResponse::InvalidRequest, - "Invalid fork name for blobs by range".to_string(), - )), None => Err(RPCError::ErrorResponse( RpcErrorResponse::InvalidRequest, format!( @@ -703,18 +706,18 @@ fn handle_rpc_response( )), }, SupportedProtocol::BlobsByRootV1 => match fork_name { - Some(ForkName::Deneb) | Some(ForkName::Electra) => { - Ok(Some(RpcSuccessResponse::BlobsByRoot(Arc::new( - BlobSidecar::from_ssz_bytes(decoded_buffer)?, - )))) + Some(fork_name) => { + if fork_name.deneb_enabled() { + Ok(Some(RpcSuccessResponse::BlobsByRoot(Arc::new( + BlobSidecar::from_ssz_bytes(decoded_buffer)?, + )))) + } else { + Err(RPCError::ErrorResponse( + RpcErrorResponse::InvalidRequest, + "Invalid fork name for blobs by root".to_string(), + )) + } } - Some(ForkName::Base) - | Some(ForkName::Altair) - | Some(ForkName::Bellatrix) - | Some(ForkName::Capella) => Err(RPCError::ErrorResponse( - RpcErrorResponse::InvalidRequest, - "Invalid fork name for blobs by root".to_string(), - )), None => Err(RPCError::ErrorResponse( RpcErrorResponse::InvalidRequest, format!( @@ -864,6 +867,9 @@ fn handle_rpc_response( decoded_buffer, )?), )))), + Some(ForkName::Fulu) => Ok(Some(RpcSuccessResponse::BlocksByRange(Arc::new( + SignedBeaconBlock::Fulu(SignedBeaconBlockFulu::from_ssz_bytes(decoded_buffer)?), + )))), None => Err(RPCError::ErrorResponse( RpcErrorResponse::InvalidRequest, format!( @@ -897,6 +903,9 @@ fn handle_rpc_response( decoded_buffer, )?), )))), + Some(ForkName::Fulu) => Ok(Some(RpcSuccessResponse::BlocksByRoot(Arc::new( + SignedBeaconBlock::Fulu(SignedBeaconBlockFulu::from_ssz_bytes(decoded_buffer)?), + )))), None => Err(RPCError::ErrorResponse( RpcErrorResponse::InvalidRequest, format!( @@ -948,12 +957,14 @@ mod tests { let capella_fork_epoch = Epoch::new(3); let deneb_fork_epoch = Epoch::new(4); let electra_fork_epoch = Epoch::new(5); + let fulu_fork_epoch = Epoch::new(6); chain_spec.altair_fork_epoch = Some(altair_fork_epoch); chain_spec.bellatrix_fork_epoch = Some(bellatrix_fork_epoch); chain_spec.capella_fork_epoch = Some(capella_fork_epoch); chain_spec.deneb_fork_epoch = Some(deneb_fork_epoch); chain_spec.electra_fork_epoch = Some(electra_fork_epoch); + chain_spec.fulu_fork_epoch = Some(fulu_fork_epoch); let current_slot = match fork_name { ForkName::Base => Slot::new(0), @@ -962,6 +973,7 @@ mod tests { ForkName::Capella => capella_fork_epoch.start_slot(Spec::slots_per_epoch()), ForkName::Deneb => deneb_fork_epoch.start_slot(Spec::slots_per_epoch()), ForkName::Electra => electra_fork_epoch.start_slot(Spec::slots_per_epoch()), + ForkName::Fulu => fulu_fork_epoch.start_slot(Spec::slots_per_epoch()), }; ForkContext::new::(current_slot, Hash256::zero(), &chain_spec) } @@ -1396,6 +1408,16 @@ mod tests { Ok(Some(RpcSuccessResponse::BlobsByRange(empty_blob_sidecar()))), ); + assert_eq!( + encode_then_decode_response( + SupportedProtocol::BlobsByRangeV1, + RpcResponse::Success(RpcSuccessResponse::BlobsByRange(empty_blob_sidecar())), + ForkName::Fulu, + &chain_spec + ), + Ok(Some(RpcSuccessResponse::BlobsByRange(empty_blob_sidecar()))), + ); + assert_eq!( encode_then_decode_response( SupportedProtocol::BlobsByRootV1, @@ -1416,6 +1438,16 @@ mod tests { Ok(Some(RpcSuccessResponse::BlobsByRoot(empty_blob_sidecar()))), ); + assert_eq!( + encode_then_decode_response( + SupportedProtocol::BlobsByRootV1, + RpcResponse::Success(RpcSuccessResponse::BlobsByRoot(empty_blob_sidecar())), + ForkName::Fulu, + &chain_spec + ), + Ok(Some(RpcSuccessResponse::BlobsByRoot(empty_blob_sidecar()))), + ); + assert_eq!( encode_then_decode_response( SupportedProtocol::DataColumnsByRangeV1, @@ -1444,6 +1476,20 @@ mod tests { ))), ); + assert_eq!( + encode_then_decode_response( + SupportedProtocol::DataColumnsByRangeV1, + RpcResponse::Success(RpcSuccessResponse::DataColumnsByRange( + empty_data_column_sidecar() + )), + ForkName::Fulu, + &chain_spec + ), + Ok(Some(RpcSuccessResponse::DataColumnsByRange( + empty_data_column_sidecar() + ))), + ); + assert_eq!( encode_then_decode_response( SupportedProtocol::DataColumnsByRootV1, @@ -1471,6 +1517,20 @@ mod tests { empty_data_column_sidecar() ))), ); + + assert_eq!( + encode_then_decode_response( + SupportedProtocol::DataColumnsByRootV1, + RpcResponse::Success(RpcSuccessResponse::DataColumnsByRoot( + empty_data_column_sidecar() + )), + ForkName::Fulu, + &chain_spec + ), + Ok(Some(RpcSuccessResponse::DataColumnsByRoot( + empty_data_column_sidecar() + ))), + ); } // Test RPCResponse encoding/decoding for V1 messages diff --git a/beacon_node/lighthouse_network/src/rpc/protocol.rs b/beacon_node/lighthouse_network/src/rpc/protocol.rs index 57c2795b04..87bde58292 100644 --- a/beacon_node/lighthouse_network/src/rpc/protocol.rs +++ b/beacon_node/lighthouse_network/src/rpc/protocol.rs @@ -18,9 +18,9 @@ use tokio_util::{ }; use types::{ BeaconBlock, BeaconBlockAltair, BeaconBlockBase, BeaconBlockCapella, BeaconBlockElectra, - BlobSidecar, ChainSpec, DataColumnSidecar, EmptyBlock, EthSpec, EthSpecId, ForkContext, - ForkName, LightClientBootstrap, LightClientBootstrapAltair, LightClientFinalityUpdate, - LightClientFinalityUpdateAltair, LightClientOptimisticUpdate, + BeaconBlockFulu, BlobSidecar, ChainSpec, DataColumnSidecar, EmptyBlock, EthSpec, EthSpecId, + ForkContext, ForkName, LightClientBootstrap, LightClientBootstrapAltair, + LightClientFinalityUpdate, LightClientFinalityUpdateAltair, LightClientOptimisticUpdate, LightClientOptimisticUpdateAltair, LightClientUpdate, MainnetEthSpec, MinimalEthSpec, Signature, SignedBeaconBlock, }; @@ -73,6 +73,15 @@ pub static SIGNED_BEACON_BLOCK_ELECTRA_MAX_WITHOUT_PAYLOAD: LazyLock = La .len() }); +pub static SIGNED_BEACON_BLOCK_FULU_MAX_WITHOUT_PAYLOAD: LazyLock = LazyLock::new(|| { + SignedBeaconBlock::::from_block( + BeaconBlock::Fulu(BeaconBlockFulu::full(&MainnetEthSpec::default_spec())), + Signature::empty(), + ) + .as_ssz_bytes() + .len() +}); + /// The `BeaconBlockBellatrix` block has an `ExecutionPayload` field which has a max size ~16 GiB for future proofing. /// We calculate the value from its fields instead of constructing the block and checking the length. /// Note: This is only the theoretical upper bound. We further bound the max size we receive over the network @@ -105,6 +114,15 @@ pub static SIGNED_BEACON_BLOCK_ELECTRA_MAX: LazyLock = LazyLock::new(|| { + ssz::BYTES_PER_LENGTH_OFFSET }); // Length offset for the blob commitments field. +pub static SIGNED_BEACON_BLOCK_FULU_MAX: LazyLock = LazyLock::new(|| { + *SIGNED_BEACON_BLOCK_FULU_MAX_WITHOUT_PAYLOAD + + types::ExecutionPayload::::max_execution_payload_fulu_size() + + ssz::BYTES_PER_LENGTH_OFFSET + + (::ssz_fixed_len() + * ::max_blobs_per_block()) + + ssz::BYTES_PER_LENGTH_OFFSET +}); + pub static BLOB_SIDECAR_SIZE: LazyLock = LazyLock::new(BlobSidecar::::max_size); @@ -209,6 +227,10 @@ pub fn rpc_block_limits_by_fork(current_fork: ForkName) -> RpcLimits { *SIGNED_BEACON_BLOCK_BASE_MIN, // Base block is smaller than altair and bellatrix blocks *SIGNED_BEACON_BLOCK_ELECTRA_MAX, // Electra block is larger than Deneb block ), + ForkName::Fulu => RpcLimits::new( + *SIGNED_BEACON_BLOCK_BASE_MIN, // Base block is smaller than all other blocks + *SIGNED_BEACON_BLOCK_FULU_MAX, // Fulu block is largest + ), } } @@ -226,7 +248,7 @@ fn rpc_light_client_updates_by_range_limits_by_fork(current_fork: ForkName) -> R ForkName::Deneb => { RpcLimits::new(altair_fixed_len, *LIGHT_CLIENT_UPDATES_BY_RANGE_DENEB_MAX) } - ForkName::Electra => { + ForkName::Electra | ForkName::Fulu => { RpcLimits::new(altair_fixed_len, *LIGHT_CLIENT_UPDATES_BY_RANGE_ELECTRA_MAX) } } @@ -246,7 +268,7 @@ fn rpc_light_client_finality_update_limits_by_fork(current_fork: ForkName) -> Rp ForkName::Deneb => { RpcLimits::new(altair_fixed_len, *LIGHT_CLIENT_FINALITY_UPDATE_DENEB_MAX) } - ForkName::Electra => { + ForkName::Electra | ForkName::Fulu => { RpcLimits::new(altair_fixed_len, *LIGHT_CLIENT_FINALITY_UPDATE_ELECTRA_MAX) } } @@ -267,7 +289,7 @@ fn rpc_light_client_optimistic_update_limits_by_fork(current_fork: ForkName) -> ForkName::Deneb => { RpcLimits::new(altair_fixed_len, *LIGHT_CLIENT_OPTIMISTIC_UPDATE_DENEB_MAX) } - ForkName::Electra => RpcLimits::new( + ForkName::Electra | ForkName::Fulu => RpcLimits::new( altair_fixed_len, *LIGHT_CLIENT_OPTIMISTIC_UPDATE_ELECTRA_MAX, ), @@ -284,7 +306,9 @@ fn rpc_light_client_bootstrap_limits_by_fork(current_fork: ForkName) -> RpcLimit } ForkName::Capella => RpcLimits::new(altair_fixed_len, *LIGHT_CLIENT_BOOTSTRAP_CAPELLA_MAX), ForkName::Deneb => RpcLimits::new(altair_fixed_len, *LIGHT_CLIENT_BOOTSTRAP_DENEB_MAX), - ForkName::Electra => RpcLimits::new(altair_fixed_len, *LIGHT_CLIENT_BOOTSTRAP_ELECTRA_MAX), + ForkName::Electra | ForkName::Fulu => { + RpcLimits::new(altair_fixed_len, *LIGHT_CLIENT_BOOTSTRAP_ELECTRA_MAX) + } } } diff --git a/beacon_node/lighthouse_network/src/types/pubsub.rs b/beacon_node/lighthouse_network/src/types/pubsub.rs index 9f68278e28..c976959470 100644 --- a/beacon_node/lighthouse_network/src/types/pubsub.rs +++ b/beacon_node/lighthouse_network/src/types/pubsub.rs @@ -13,8 +13,9 @@ use types::{ ProposerSlashing, SignedAggregateAndProof, SignedAggregateAndProofBase, SignedAggregateAndProofElectra, SignedBeaconBlock, SignedBeaconBlockAltair, SignedBeaconBlockBase, SignedBeaconBlockBellatrix, SignedBeaconBlockCapella, - SignedBeaconBlockDeneb, SignedBeaconBlockElectra, SignedBlsToExecutionChange, - SignedContributionAndProof, SignedVoluntaryExit, SubnetId, SyncCommitteeMessage, SyncSubnetId, + SignedBeaconBlockDeneb, SignedBeaconBlockElectra, SignedBeaconBlockFulu, + SignedBlsToExecutionChange, SignedContributionAndProof, SignedVoluntaryExit, SubnetId, + SyncCommitteeMessage, SyncSubnetId, }; #[derive(Debug, Clone, PartialEq)] @@ -242,6 +243,10 @@ impl PubsubMessage { SignedBeaconBlockElectra::from_ssz_bytes(data) .map_err(|e| format!("{:?}", e))?, ), + Some(ForkName::Fulu) => SignedBeaconBlock::::Fulu( + SignedBeaconBlockFulu::from_ssz_bytes(data) + .map_err(|e| format!("{:?}", e))?, + ), None => { return Err(format!( "Unknown gossipsub fork digest: {:?}", diff --git a/beacon_node/lighthouse_network/src/types/topics.rs b/beacon_node/lighthouse_network/src/types/topics.rs index 174787f999..8cdecc6bfa 100644 --- a/beacon_node/lighthouse_network/src/types/topics.rs +++ b/beacon_node/lighthouse_network/src/types/topics.rs @@ -61,6 +61,7 @@ pub fn fork_core_topics(fork_name: &ForkName, spec: &ChainSpec) -> V deneb_topics } ForkName::Electra => vec![], + ForkName::Fulu => vec![], } } diff --git a/beacon_node/lighthouse_network/tests/common.rs b/beacon_node/lighthouse_network/tests/common.rs index 84e19c81d0..6a3ec6dd32 100644 --- a/beacon_node/lighthouse_network/tests/common.rs +++ b/beacon_node/lighthouse_network/tests/common.rs @@ -25,12 +25,14 @@ pub fn fork_context(fork_name: ForkName) -> ForkContext { let capella_fork_epoch = Epoch::new(3); let deneb_fork_epoch = Epoch::new(4); let electra_fork_epoch = Epoch::new(5); + let fulu_fork_epoch = Epoch::new(6); chain_spec.altair_fork_epoch = Some(altair_fork_epoch); chain_spec.bellatrix_fork_epoch = Some(bellatrix_fork_epoch); chain_spec.capella_fork_epoch = Some(capella_fork_epoch); chain_spec.deneb_fork_epoch = Some(deneb_fork_epoch); chain_spec.electra_fork_epoch = Some(electra_fork_epoch); + chain_spec.fulu_fork_epoch = Some(fulu_fork_epoch); let current_slot = match fork_name { ForkName::Base => Slot::new(0), @@ -39,6 +41,7 @@ pub fn fork_context(fork_name: ForkName) -> ForkContext { ForkName::Capella => capella_fork_epoch.start_slot(E::slots_per_epoch()), ForkName::Deneb => deneb_fork_epoch.start_slot(E::slots_per_epoch()), ForkName::Electra => electra_fork_epoch.start_slot(E::slots_per_epoch()), + ForkName::Fulu => fulu_fork_epoch.start_slot(E::slots_per_epoch()), }; ForkContext::new::(current_slot, Hash256::zero(), &chain_spec) } diff --git a/beacon_node/network/src/sync/tests/lookups.rs b/beacon_node/network/src/sync/tests/lookups.rs index 94aacad3e8..a43b3bd022 100644 --- a/beacon_node/network/src/sync/tests/lookups.rs +++ b/beacon_node/network/src/sync/tests/lookups.rs @@ -174,7 +174,7 @@ impl TestRig { } pub fn after_deneb(&self) -> bool { - matches!(self.fork_name, ForkName::Deneb | ForkName::Electra) + self.fork_name.deneb_enabled() } fn trigger_unknown_parent_block(&mut self, peer_id: PeerId, block: Arc>) { diff --git a/beacon_node/operation_pool/src/lib.rs b/beacon_node/operation_pool/src/lib.rs index d8183de752..835133a059 100644 --- a/beacon_node/operation_pool/src/lib.rs +++ b/beacon_node/operation_pool/src/lib.rs @@ -1239,14 +1239,11 @@ mod release_tests { let stats = op_pool.attestation_stats(); let fork_name = state.fork_name_unchecked(); - match fork_name { - ForkName::Electra => { - assert_eq!(stats.num_attestation_data, 1); - } - _ => { - assert_eq!(stats.num_attestation_data, committees.len()); - } - }; + if fork_name.electra_enabled() { + assert_eq!(stats.num_attestation_data, 1); + } else { + assert_eq!(stats.num_attestation_data, committees.len()); + } assert_eq!( stats.num_attestations, @@ -1258,25 +1255,19 @@ mod release_tests { let best_attestations = op_pool .get_attestations(&state, |_| true, |_| true, spec) .expect("should have best attestations"); - match fork_name { - ForkName::Electra => { - assert_eq!(best_attestations.len(), 8); - } - _ => { - assert_eq!(best_attestations.len(), max_attestations); - } - }; + if fork_name.electra_enabled() { + assert_eq!(best_attestations.len(), 8); + } else { + assert_eq!(best_attestations.len(), max_attestations); + } // All the best attestations should be signed by at least `big_step_size` (4) validators. for att in &best_attestations { - match fork_name { - ForkName::Electra => { - assert!(att.num_set_aggregation_bits() >= small_step_size); - } - _ => { - assert!(att.num_set_aggregation_bits() >= big_step_size); - } - }; + if fork_name.electra_enabled() { + assert!(att.num_set_aggregation_bits() >= small_step_size); + } else { + assert!(att.num_set_aggregation_bits() >= big_step_size); + } } } @@ -1357,17 +1348,14 @@ mod release_tests { let num_big = target_committee_size / big_step_size; let fork_name = state.fork_name_unchecked(); - match fork_name { - ForkName::Electra => { - assert_eq!(op_pool.attestation_stats().num_attestation_data, 1); - } - _ => { - assert_eq!( - op_pool.attestation_stats().num_attestation_data, - committees.len() - ); - } - }; + if fork_name.electra_enabled() { + assert_eq!(op_pool.attestation_stats().num_attestation_data, 1); + } else { + assert_eq!( + op_pool.attestation_stats().num_attestation_data, + committees.len() + ); + } assert_eq!( op_pool.num_attestations(), @@ -1380,14 +1368,11 @@ mod release_tests { .get_attestations(&state, |_| true, |_| true, spec) .expect("should have valid best attestations"); - match fork_name { - ForkName::Electra => { - assert_eq!(best_attestations.len(), 8); - } - _ => { - assert_eq!(best_attestations.len(), max_attestations); - } - }; + if fork_name.electra_enabled() { + assert_eq!(best_attestations.len(), 8); + } else { + assert_eq!(best_attestations.len(), max_attestations); + } let total_active_balance = state.get_total_active_balance().unwrap(); diff --git a/beacon_node/src/lib.rs b/beacon_node/src/lib.rs index cca617d8c6..0c4cbf0f57 100644 --- a/beacon_node/src/lib.rs +++ b/beacon_node/src/lib.rs @@ -238,6 +238,7 @@ mod test { spec.bellatrix_fork_epoch = Some(Epoch::new(256)); spec.deneb_fork_epoch = Some(Epoch::new(257)); spec.electra_fork_epoch = None; + spec.fulu_fork_epoch = None; let result = validator_fork_epochs(&spec); assert_eq!( result, diff --git a/beacon_node/store/src/impls/execution_payload.rs b/beacon_node/store/src/impls/execution_payload.rs index 14fc10ad6d..5c60aa8d7e 100644 --- a/beacon_node/store/src/impls/execution_payload.rs +++ b/beacon_node/store/src/impls/execution_payload.rs @@ -2,7 +2,7 @@ use crate::{DBColumn, Error, StoreItem}; use ssz::{Decode, Encode}; use types::{ BlobSidecarList, EthSpec, ExecutionPayload, ExecutionPayloadBellatrix, ExecutionPayloadCapella, - ExecutionPayloadDeneb, ExecutionPayloadElectra, + ExecutionPayloadDeneb, ExecutionPayloadElectra, ExecutionPayloadFulu, }; macro_rules! impl_store_item { @@ -26,6 +26,7 @@ impl_store_item!(ExecutionPayloadBellatrix); impl_store_item!(ExecutionPayloadCapella); impl_store_item!(ExecutionPayloadDeneb); impl_store_item!(ExecutionPayloadElectra); +impl_store_item!(ExecutionPayloadFulu); impl_store_item!(BlobSidecarList); /// This fork-agnostic implementation should be only used for writing. @@ -42,17 +43,21 @@ impl StoreItem for ExecutionPayload { } fn from_store_bytes(bytes: &[u8]) -> Result { - ExecutionPayloadElectra::from_ssz_bytes(bytes) - .map(Self::Electra) + ExecutionPayloadFulu::from_ssz_bytes(bytes) + .map(Self::Fulu) .or_else(|_| { - ExecutionPayloadDeneb::from_ssz_bytes(bytes) - .map(Self::Deneb) + ExecutionPayloadElectra::from_ssz_bytes(bytes) + .map(Self::Electra) .or_else(|_| { - ExecutionPayloadCapella::from_ssz_bytes(bytes) - .map(Self::Capella) + ExecutionPayloadDeneb::from_ssz_bytes(bytes) + .map(Self::Deneb) .or_else(|_| { - ExecutionPayloadBellatrix::from_ssz_bytes(bytes) - .map(Self::Bellatrix) + ExecutionPayloadCapella::from_ssz_bytes(bytes) + .map(Self::Capella) + .or_else(|_| { + ExecutionPayloadBellatrix::from_ssz_bytes(bytes) + .map(Self::Bellatrix) + }) }) }) }) diff --git a/beacon_node/store/src/partial_beacon_state.rs b/beacon_node/store/src/partial_beacon_state.rs index 22eecdcc60..0b8bc2e0d4 100644 --- a/beacon_node/store/src/partial_beacon_state.rs +++ b/beacon_node/store/src/partial_beacon_state.rs @@ -16,7 +16,7 @@ use types::*; /// /// This can be deleted once schema versions prior to V22 are no longer supported. #[superstruct( - variants(Base, Altair, Bellatrix, Capella, Deneb, Electra), + variants(Base, Altair, Bellatrix, Capella, Deneb, Electra, Fulu), variant_attributes(derive(Debug, PartialEq, Clone, Encode, Decode)) )] #[derive(Debug, PartialEq, Clone, Encode)] @@ -68,9 +68,9 @@ where pub current_epoch_attestations: List, E::MaxPendingAttestations>, // Participation (Altair and later) - #[superstruct(only(Altair, Bellatrix, Capella, Deneb, Electra))] + #[superstruct(only(Altair, Bellatrix, Capella, Deneb, Electra, Fulu))] pub previous_epoch_participation: List, - #[superstruct(only(Altair, Bellatrix, Capella, Deneb, Electra))] + #[superstruct(only(Altair, Bellatrix, Capella, Deneb, Electra, Fulu))] pub current_epoch_participation: List, // Finality @@ -80,13 +80,13 @@ where pub finalized_checkpoint: Checkpoint, // Inactivity - #[superstruct(only(Altair, Bellatrix, Capella, Deneb, Electra))] + #[superstruct(only(Altair, Bellatrix, Capella, Deneb, Electra, Fulu))] pub inactivity_scores: List, // Light-client sync committees - #[superstruct(only(Altair, Bellatrix, Capella, Deneb, Electra))] + #[superstruct(only(Altair, Bellatrix, Capella, Deneb, Electra, Fulu))] pub current_sync_committee: Arc>, - #[superstruct(only(Altair, Bellatrix, Capella, Deneb, Electra))] + #[superstruct(only(Altair, Bellatrix, Capella, Deneb, Electra, Fulu))] pub next_sync_committee: Arc>, // Execution @@ -110,37 +110,42 @@ where partial_getter(rename = "latest_execution_payload_header_electra") )] pub latest_execution_payload_header: ExecutionPayloadHeaderElectra, + #[superstruct( + only(Fulu), + partial_getter(rename = "latest_execution_payload_header_fulu") + )] + pub latest_execution_payload_header: ExecutionPayloadHeaderFulu, // Capella - #[superstruct(only(Capella, Deneb, Electra))] + #[superstruct(only(Capella, Deneb, Electra, Fulu))] pub next_withdrawal_index: u64, - #[superstruct(only(Capella, Deneb, Electra))] + #[superstruct(only(Capella, Deneb, Electra, Fulu))] pub next_withdrawal_validator_index: u64, #[ssz(skip_serializing, skip_deserializing)] - #[superstruct(only(Capella, Deneb, Electra))] + #[superstruct(only(Capella, Deneb, Electra, Fulu))] pub historical_summaries: Option>, // Electra - #[superstruct(only(Electra))] + #[superstruct(only(Electra, Fulu))] pub deposit_requests_start_index: u64, - #[superstruct(only(Electra))] + #[superstruct(only(Electra, Fulu))] pub deposit_balance_to_consume: u64, - #[superstruct(only(Electra))] + #[superstruct(only(Electra, Fulu))] pub exit_balance_to_consume: u64, - #[superstruct(only(Electra))] + #[superstruct(only(Electra, Fulu))] pub earliest_exit_epoch: Epoch, - #[superstruct(only(Electra))] + #[superstruct(only(Electra, Fulu))] pub consolidation_balance_to_consume: u64, - #[superstruct(only(Electra))] + #[superstruct(only(Electra, Fulu))] pub earliest_consolidation_epoch: Epoch, - #[superstruct(only(Electra))] + #[superstruct(only(Electra, Fulu))] pub pending_deposits: List, - #[superstruct(only(Electra))] + #[superstruct(only(Electra, Fulu))] pub pending_partial_withdrawals: List, - #[superstruct(only(Electra))] + #[superstruct(only(Electra, Fulu))] pub pending_consolidations: List, } @@ -409,6 +414,31 @@ impl TryInto> for PartialBeaconState { ], [historical_summaries] ), + PartialBeaconState::Fulu(inner) => impl_try_into_beacon_state!( + inner, + Fulu, + BeaconStateFulu, + [ + previous_epoch_participation, + current_epoch_participation, + current_sync_committee, + next_sync_committee, + inactivity_scores, + latest_execution_payload_header, + next_withdrawal_index, + next_withdrawal_validator_index, + deposit_requests_start_index, + deposit_balance_to_consume, + exit_balance_to_consume, + earliest_exit_epoch, + consolidation_balance_to_consume, + earliest_consolidation_epoch, + pending_deposits, + pending_partial_withdrawals, + pending_consolidations + ], + [historical_summaries] + ), }; Ok(state) } diff --git a/common/eth2/src/types.rs b/common/eth2/src/types.rs index a303953a86..695d536944 100644 --- a/common/eth2/src/types.rs +++ b/common/eth2/src/types.rs @@ -1078,6 +1078,9 @@ impl ForkVersionDeserialize for SsePayloadAttributes { ForkName::Electra => serde_json::from_value(value) .map(Self::V3) .map_err(serde::de::Error::custom), + ForkName::Fulu => serde_json::from_value(value) + .map(Self::V3) + .map_err(serde::de::Error::custom), ForkName::Base | ForkName::Altair => Err(serde::de::Error::custom(format!( "SsePayloadAttributes deserialization for {fork_name} not implemented" ))), @@ -1861,14 +1864,10 @@ impl PublishBlockRequest { impl TryFrom>> for PublishBlockRequest { type Error = &'static str; fn try_from(block: Arc>) -> Result { - match *block { - SignedBeaconBlock::Base(_) - | SignedBeaconBlock::Altair(_) - | SignedBeaconBlock::Bellatrix(_) - | SignedBeaconBlock::Capella(_) => Ok(PublishBlockRequest::Block(block)), - SignedBeaconBlock::Deneb(_) | SignedBeaconBlock::Electra(_) => Err( - "post-Deneb block contents cannot be fully constructed from just the signed block", - ), + if block.message().fork_name_unchecked().deneb_enabled() { + Err("post-Deneb block contents cannot be fully constructed from just the signed block") + } else { + Ok(PublishBlockRequest::Block(block)) } } } @@ -1972,16 +1971,18 @@ impl ForkVersionDeserialize for FullPayloadContents { value: Value, fork_name: ForkName, ) -> Result { - match fork_name { - ForkName::Bellatrix | ForkName::Capella => serde_json::from_value(value) - .map(Self::Payload) - .map_err(serde::de::Error::custom), - ForkName::Deneb | ForkName::Electra => serde_json::from_value(value) + if fork_name.deneb_enabled() { + serde_json::from_value(value) .map(Self::PayloadAndBlobs) - .map_err(serde::de::Error::custom), - ForkName::Base | ForkName::Altair => Err(serde::de::Error::custom(format!( + .map_err(serde::de::Error::custom) + } else if fork_name.bellatrix_enabled() { + serde_json::from_value(value) + .map(Self::Payload) + .map_err(serde::de::Error::custom) + } else { + Err(serde::de::Error::custom(format!( "FullPayloadContents deserialization for {fork_name} not implemented" - ))), + ))) } } } 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 1eca01bbee..a107f6147a 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 @@ -15,7 +15,6 @@ TERMINAL_TOTAL_DIFFICULTY: 231707791542740786049188744689299064356246512 TERMINAL_BLOCK_HASH: 0x0000000000000000000000000000000000000000000000000000000000000000 TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH: 18446744073709551615 - # Genesis # --------------------------------------------------------------- # *CUSTOM @@ -27,7 +26,6 @@ GENESIS_FORK_VERSION: 0x0000006f # *CUSTOM GENESIS_DELAY: 300 - # Forking # --------------------------------------------------------------- # Some forks are disabled for now: @@ -49,6 +47,9 @@ DENEB_FORK_EPOCH: 516608 # Wed Jan 31 2024 18:15:40 GMT+0000 # Electra ELECTRA_FORK_VERSION: 0x0500006f ELECTRA_FORK_EPOCH: 18446744073709551615 +# Fulu +FULU_FORK_VERSION: 0x0600006f +FULU_FORK_EPOCH: 18446744073709551615 # Time parameters # --------------------------------------------------------------- @@ -63,7 +64,6 @@ SHARD_COMMITTEE_PERIOD: 256 # 2**10 (= 1024) ~1.4 hour ETH1_FOLLOW_DISTANCE: 1024 - # Validator cycle # --------------------------------------------------------------- # 2**2 (= 4) @@ -90,7 +90,6 @@ REORG_PARENT_WEIGHT_THRESHOLD: 160 # `2` epochs REORG_MAX_EPOCHS_SINCE_FINALIZATION: 2 - # Deposit contract # --------------------------------------------------------------- # xDai Mainnet @@ -141,4 +140,4 @@ BLOB_SIDECAR_SUBNET_COUNT: 6 CUSTODY_REQUIREMENT: 4 DATA_COLUMN_SIDECAR_SUBNET_COUNT: 128 NUMBER_OF_COLUMNS: 128 -SAMPLES_PER_SLOT: 8 \ No newline at end of file +SAMPLES_PER_SLOT: 8 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 500555a269..f71984059a 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 @@ -23,7 +23,6 @@ GENESIS_FORK_VERSION: 0x00000064 # 6000 seconds (100 minutes) GENESIS_DELAY: 6000 - # Forking # --------------------------------------------------------------- # Some forks are disabled for now: @@ -45,7 +44,9 @@ DENEB_FORK_EPOCH: 889856 # 2024-03-11T18:30:20.000Z # Electra ELECTRA_FORK_VERSION: 0x05000064 ELECTRA_FORK_EPOCH: 18446744073709551615 - +# Fulu +FULU_FORK_VERSION: 0x06000064 +FULU_FORK_EPOCH: 18446744073709551615 # Time parameters # --------------------------------------------------------------- @@ -60,7 +61,6 @@ SHARD_COMMITTEE_PERIOD: 256 # 2**10 (= 1024) ~1.4 hour ETH1_FOLLOW_DISTANCE: 1024 - # Validator cycle # --------------------------------------------------------------- # 2**2 (= 4) @@ -76,7 +76,6 @@ MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT: 2 # 2**12 (= 4096) CHURN_LIMIT_QUOTIENT: 4096 - # Fork choice # --------------------------------------------------------------- # 40% @@ -124,4 +123,4 @@ BLOB_SIDECAR_SUBNET_COUNT: 6 CUSTODY_REQUIREMENT: 4 DATA_COLUMN_SIDECAR_SUBNET_COUNT: 128 NUMBER_OF_COLUMNS: 128 -SAMPLES_PER_SLOT: 8 \ No newline at end of file +SAMPLES_PER_SLOT: 8 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 d67d77d3be..6d344b5b52 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 @@ -12,7 +12,6 @@ GENESIS_FORK_VERSION: 0x01017000 # Genesis delay 5 mins GENESIS_DELAY: 300 - # Forking # --------------------------------------------------------------- # Some forks are disabled for now: @@ -37,6 +36,9 @@ DENEB_FORK_EPOCH: 29696 # Electra ELECTRA_FORK_VERSION: 0x06017000 ELECTRA_FORK_EPOCH: 18446744073709551615 +# Fulu +FULU_FORK_VERSION: 0x07017000 +FULU_FORK_EPOCH: 18446744073709551615 # Time parameters # --------------------------------------------------------------- @@ -51,7 +53,6 @@ SHARD_COMMITTEE_PERIOD: 256 # 2**11 (= 2,048) Eth1 blocks ~8 hours ETH1_FOLLOW_DISTANCE: 2048 - # Validator cycle # --------------------------------------------------------------- # 2**2 (= 4) @@ -128,4 +129,4 @@ BLOB_SIDECAR_SUBNET_COUNT: 6 CUSTODY_REQUIREMENT: 4 DATA_COLUMN_SIDECAR_SUBNET_COUNT: 128 NUMBER_OF_COLUMNS: 128 -SAMPLES_PER_SLOT: 8 \ No newline at end of file +SAMPLES_PER_SLOT: 8 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 18591fecdc..244ddd564d 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 @@ -18,8 +18,6 @@ TERMINAL_TOTAL_DIFFICULTY: 58750000000000000000000 TERMINAL_BLOCK_HASH: 0x0000000000000000000000000000000000000000000000000000000000000000 TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH: 18446744073709551615 - - # Genesis # --------------------------------------------------------------- # `2**14` (= 16,384) @@ -31,7 +29,6 @@ GENESIS_FORK_VERSION: 0x00000000 # 604800 seconds (7 days) GENESIS_DELAY: 604800 - # Forking # --------------------------------------------------------------- # Some forks are disabled for now: @@ -40,23 +37,25 @@ GENESIS_DELAY: 604800 # Altair ALTAIR_FORK_VERSION: 0x01000000 -ALTAIR_FORK_EPOCH: 74240 # Oct 27, 2021, 10:56:23am UTC +ALTAIR_FORK_EPOCH: 74240 # Oct 27, 2021, 10:56:23am UTC # Bellatrix BELLATRIX_FORK_VERSION: 0x02000000 -BELLATRIX_FORK_EPOCH: 144896 # Sept 6, 2022, 11:34:47am UTC +BELLATRIX_FORK_EPOCH: 144896 # Sept 6, 2022, 11:34:47am UTC # Capella CAPELLA_FORK_VERSION: 0x03000000 -CAPELLA_FORK_EPOCH: 194048 # April 12, 2023, 10:27:35pm UTC +CAPELLA_FORK_EPOCH: 194048 # April 12, 2023, 10:27:35pm UTC # Deneb DENEB_FORK_VERSION: 0x04000000 -DENEB_FORK_EPOCH: 269568 # March 13, 2024, 01:55:35pm UTC +DENEB_FORK_EPOCH: 269568 # March 13, 2024, 01:55:35pm UTC # Electra ELECTRA_FORK_VERSION: 0x05000000 ELECTRA_FORK_EPOCH: 18446744073709551615 +# Fulu +FULU_FORK_VERSION: 0x06000000 +FULU_FORK_EPOCH: 18446744073709551615 # PeerDAS EIP7594_FORK_EPOCH: 18446744073709551615 - # Time parameters # --------------------------------------------------------------- # 12 seconds @@ -70,7 +69,6 @@ SHARD_COMMITTEE_PERIOD: 256 # 2**11 (= 2,048) Eth1 blocks ~8 hours ETH1_FOLLOW_DISTANCE: 2048 - # Validator cycle # --------------------------------------------------------------- # 2**2 (= 4) @@ -97,7 +95,6 @@ REORG_PARENT_WEIGHT_THRESHOLD: 160 # `2` epochs REORG_MAX_EPOCHS_SINCE_FINALIZATION: 2 - # Deposit contract # --------------------------------------------------------------- # Ethereum PoW Mainnet @@ -105,7 +102,6 @@ DEPOSIT_CHAIN_ID: 1 DEPOSIT_NETWORK_ID: 1 DEPOSIT_CONTRACT_ADDRESS: 0x00000000219ab540356cBB839Cbe05303d7705Fa - # Networking # --------------------------------------------------------------- # `10 * 2**20` (= 10485760, 10 MiB) @@ -150,4 +146,4 @@ BLOB_SIDECAR_SUBNET_COUNT: 6 CUSTODY_REQUIREMENT: 4 DATA_COLUMN_SIDECAR_SUBNET_COUNT: 128 NUMBER_OF_COLUMNS: 128 -SAMPLES_PER_SLOT: 8 \ No newline at end of file +SAMPLES_PER_SLOT: 8 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 b08a6180bf..88f8359bd1 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 @@ -124,4 +124,4 @@ BLOB_SIDECAR_SUBNET_COUNT: 6 CUSTODY_REQUIREMENT: 4 DATA_COLUMN_SIDECAR_SUBNET_COUNT: 128 NUMBER_OF_COLUMNS: 128 -SAMPLES_PER_SLOT: 8 \ No newline at end of file +SAMPLES_PER_SLOT: 8 diff --git a/consensus/fork_choice/src/fork_choice.rs b/consensus/fork_choice/src/fork_choice.rs index 85704042df..4c25be950b 100644 --- a/consensus/fork_choice/src/fork_choice.rs +++ b/consensus/fork_choice/src/fork_choice.rs @@ -755,20 +755,15 @@ where if let Some((parent_justified, parent_finalized)) = parent_checkpoints { (parent_justified, parent_finalized) } else { - let justification_and_finalization_state = match block { - BeaconBlockRef::Electra(_) - | BeaconBlockRef::Deneb(_) - | BeaconBlockRef::Capella(_) - | BeaconBlockRef::Bellatrix(_) - | BeaconBlockRef::Altair(_) => { + let justification_and_finalization_state = + if block.fork_name_unchecked().altair_enabled() { // NOTE: Processing justification & finalization requires the progressive // balances cache, but we cannot initialize it here as we only have an // immutable reference. The state *should* have come straight from block // processing, which initialises the cache, but if we add other `on_block` // calls in future it could be worth passing a mutable reference. per_epoch_processing::altair::process_justification_and_finalization(state)? - } - BeaconBlockRef::Base(_) => { + } else { let mut validator_statuses = per_epoch_processing::base::ValidatorStatuses::new(state, spec) .map_err(Error::ValidatorStatuses)?; @@ -780,8 +775,7 @@ where &validator_statuses.total_balances, spec, )? - } - }; + }; ( justification_and_finalization_state.current_justified_checkpoint(), diff --git a/consensus/fork_choice/tests/tests.rs b/consensus/fork_choice/tests/tests.rs index ef017159a0..001b80fe11 100644 --- a/consensus/fork_choice/tests/tests.rs +++ b/consensus/fork_choice/tests/tests.rs @@ -1263,7 +1263,7 @@ async fn progressive_balances_cache_proposer_slashing() { // (`HeaderInvalid::ProposerSlashed`). The harness should be re-worked to successfully skip // the slot in this scenario rather than panic-ing. The same applies to // `progressive_balances_cache_attester_slashing`. - .apply_blocks(2) + .apply_blocks(1) .await .add_previous_epoch_proposer_slashing(MainnetEthSpec::slots_per_epoch()) .await diff --git a/consensus/state_processing/src/common/get_attestation_participation.rs b/consensus/state_processing/src/common/get_attestation_participation.rs index fc09dad1f4..2c6fd3b215 100644 --- a/consensus/state_processing/src/common/get_attestation_participation.rs +++ b/consensus/state_processing/src/common/get_attestation_participation.rs @@ -44,22 +44,15 @@ pub fn get_attestation_participation_flag_indices( if is_matching_source && inclusion_delay <= E::slots_per_epoch().integer_sqrt() { participation_flag_indices.push(TIMELY_SOURCE_FLAG_INDEX); } - match state { - &BeaconState::Base(_) - | &BeaconState::Altair(_) - | &BeaconState::Bellatrix(_) - | &BeaconState::Capella(_) => { - if is_matching_target && inclusion_delay <= E::slots_per_epoch() { - participation_flag_indices.push(TIMELY_TARGET_FLAG_INDEX); - } - } - &BeaconState::Deneb(_) | &BeaconState::Electra(_) => { - if is_matching_target { - // [Modified in Deneb:EIP7045] - participation_flag_indices.push(TIMELY_TARGET_FLAG_INDEX); - } + if state.fork_name_unchecked().deneb_enabled() { + if is_matching_target { + // [Modified in Deneb:EIP7045] + participation_flag_indices.push(TIMELY_TARGET_FLAG_INDEX); } + } else if is_matching_target && inclusion_delay <= E::slots_per_epoch() { + participation_flag_indices.push(TIMELY_TARGET_FLAG_INDEX); } + if is_matching_head && inclusion_delay == spec.min_attestation_inclusion_delay { participation_flag_indices.push(TIMELY_HEAD_FLAG_INDEX); } diff --git a/consensus/state_processing/src/common/slash_validator.rs b/consensus/state_processing/src/common/slash_validator.rs index 80d857cc00..bd60f16014 100644 --- a/consensus/state_processing/src/common/slash_validator.rs +++ b/consensus/state_processing/src/common/slash_validator.rs @@ -55,15 +55,12 @@ pub fn slash_validator( let whistleblower_index = opt_whistleblower_index.unwrap_or(proposer_index); let whistleblower_reward = validator_effective_balance .safe_div(spec.whistleblower_reward_quotient_for_state(state))?; - let proposer_reward = match state { - BeaconState::Base(_) => whistleblower_reward.safe_div(spec.proposer_reward_quotient)?, - BeaconState::Altair(_) - | BeaconState::Bellatrix(_) - | BeaconState::Capella(_) - | BeaconState::Deneb(_) - | BeaconState::Electra(_) => whistleblower_reward + let proposer_reward = if state.fork_name_unchecked().altair_enabled() { + whistleblower_reward .safe_mul(PROPOSER_WEIGHT)? - .safe_div(WEIGHT_DENOMINATOR)?, + .safe_div(WEIGHT_DENOMINATOR)? + } else { + whistleblower_reward.safe_div(spec.proposer_reward_quotient)? }; // Ensure the whistleblower index is in the validator registry. diff --git a/consensus/state_processing/src/genesis.rs b/consensus/state_processing/src/genesis.rs index ccff3d80c0..10723ecc51 100644 --- a/consensus/state_processing/src/genesis.rs +++ b/consensus/state_processing/src/genesis.rs @@ -4,7 +4,7 @@ use super::per_block_processing::{ use crate::common::DepositDataTree; use crate::upgrade::electra::upgrade_state_to_electra; use crate::upgrade::{ - upgrade_to_altair, upgrade_to_bellatrix, upgrade_to_capella, upgrade_to_deneb, + upgrade_to_altair, upgrade_to_bellatrix, upgrade_to_capella, upgrade_to_deneb, upgrade_to_fulu, }; use safe_arith::{ArithError, SafeArith}; use std::sync::Arc; @@ -135,11 +135,27 @@ pub fn initialize_beacon_state_from_eth1( // Override latest execution payload header. // See https://github.com/ethereum/consensus-specs/blob/dev/specs/capella/beacon-chain.md#testing - if let Some(ExecutionPayloadHeader::Electra(header)) = execution_payload_header { + if let Some(ExecutionPayloadHeader::Electra(ref header)) = execution_payload_header { *state.latest_execution_payload_header_electra_mut()? = header.clone(); } } + // Upgrade to fulu if configured from genesis. + if spec + .fulu_fork_epoch + .is_some_and(|fork_epoch| fork_epoch == E::genesis_epoch()) + { + upgrade_to_fulu(&mut state, spec)?; + + // Remove intermediate Electra fork from `state.fork`. + state.fork_mut().previous_version = spec.fulu_fork_version; + + // Override latest execution payload header. + if let Some(ExecutionPayloadHeader::Fulu(header)) = execution_payload_header { + *state.latest_execution_payload_header_fulu_mut()? = header.clone(); + } + } + // Now that we have our validators, initialize the caches (including the committees) state.build_caches(spec)?; diff --git a/consensus/state_processing/src/per_block_processing.rs b/consensus/state_processing/src/per_block_processing.rs index 436f4934b9..22e0a5eab3 100644 --- a/consensus/state_processing/src/per_block_processing.rs +++ b/consensus/state_processing/src/per_block_processing.rs @@ -442,6 +442,12 @@ pub fn process_execution_payload>( _ => return Err(BlockProcessingError::IncorrectStateType), } } + ExecutionPayloadHeaderRefMut::Fulu(header_mut) => { + match payload.to_execution_payload_header() { + ExecutionPayloadHeader::Fulu(header) => *header_mut = header, + _ => return Err(BlockProcessingError::IncorrectStateType), + } + } } Ok(()) @@ -453,15 +459,17 @@ pub fn process_execution_payload>( /// repeatedly write code to treat these errors as false. /// https://github.com/ethereum/consensus-specs/blob/dev/specs/bellatrix/beacon-chain.md#is_merge_transition_complete pub fn is_merge_transition_complete(state: &BeaconState) -> bool { - match state { + if state.fork_name_unchecked().capella_enabled() { + true + } else if state.fork_name_unchecked().bellatrix_enabled() { // We must check defaultness against the payload header with 0x0 roots, as that's what's meant // by `ExecutionPayloadHeader()` in the spec. - BeaconState::Bellatrix(_) => state + state .latest_execution_payload_header() .map(|header| !header.is_default_with_zero_roots()) - .unwrap_or(false), - BeaconState::Electra(_) | BeaconState::Deneb(_) | BeaconState::Capella(_) => true, - BeaconState::Base(_) | BeaconState::Altair(_) => false, + .unwrap_or(false) + } else { + false } } /// https://github.com/ethereum/consensus-specs/blob/dev/specs/bellatrix/beacon-chain.md#is_merge_transition_block @@ -603,66 +611,65 @@ pub fn process_withdrawals>( payload: Payload::Ref<'_>, spec: &ChainSpec, ) -> Result<(), BlockProcessingError> { - match state { - BeaconState::Capella(_) | BeaconState::Deneb(_) | BeaconState::Electra(_) => { - let (expected_withdrawals, partial_withdrawals_count) = - get_expected_withdrawals(state, spec)?; - let expected_root = expected_withdrawals.tree_hash_root(); - let withdrawals_root = payload.withdrawals_root()?; + if state.fork_name_unchecked().capella_enabled() { + let (expected_withdrawals, partial_withdrawals_count) = + get_expected_withdrawals(state, spec)?; + let expected_root = expected_withdrawals.tree_hash_root(); + let withdrawals_root = payload.withdrawals_root()?; - if expected_root != withdrawals_root { - return Err(BlockProcessingError::WithdrawalsRootMismatch { - expected: expected_root, - found: withdrawals_root, - }); - } + if expected_root != withdrawals_root { + return Err(BlockProcessingError::WithdrawalsRootMismatch { + expected: expected_root, + found: withdrawals_root, + }); + } - for withdrawal in expected_withdrawals.iter() { - decrease_balance( - state, - withdrawal.validator_index as usize, - withdrawal.amount, - )?; - } + for withdrawal in expected_withdrawals.iter() { + decrease_balance( + state, + withdrawal.validator_index as usize, + withdrawal.amount, + )?; + } - // Update pending partial withdrawals [New in Electra:EIP7251] - if let Some(partial_withdrawals_count) = partial_withdrawals_count { - // TODO(electra): Use efficient pop_front after milhouse release https://github.com/sigp/milhouse/pull/38 - let new_partial_withdrawals = state - .pending_partial_withdrawals()? - .iter_from(partial_withdrawals_count)? - .cloned() - .collect::>(); - *state.pending_partial_withdrawals_mut()? = List::new(new_partial_withdrawals)?; - } + // Update pending partial withdrawals [New in Electra:EIP7251] + if let Some(partial_withdrawals_count) = partial_withdrawals_count { + // TODO(electra): Use efficient pop_front after milhouse release https://github.com/sigp/milhouse/pull/38 + let new_partial_withdrawals = state + .pending_partial_withdrawals()? + .iter_from(partial_withdrawals_count)? + .cloned() + .collect::>(); + *state.pending_partial_withdrawals_mut()? = List::new(new_partial_withdrawals)?; + } - // Update the next withdrawal index if this block contained withdrawals - if let Some(latest_withdrawal) = expected_withdrawals.last() { - *state.next_withdrawal_index_mut()? = latest_withdrawal.index.safe_add(1)?; + // Update the next withdrawal index if this block contained withdrawals + if let Some(latest_withdrawal) = expected_withdrawals.last() { + *state.next_withdrawal_index_mut()? = latest_withdrawal.index.safe_add(1)?; - // Update the next validator index to start the next withdrawal sweep - if expected_withdrawals.len() == E::max_withdrawals_per_payload() { - // Next sweep starts after the latest withdrawal's validator index - let next_validator_index = latest_withdrawal - .validator_index - .safe_add(1)? - .safe_rem(state.validators().len() as u64)?; - *state.next_withdrawal_validator_index_mut()? = next_validator_index; - } - } - - // Advance sweep by the max length of the sweep if there was not a full set of withdrawals - if expected_withdrawals.len() != E::max_withdrawals_per_payload() { - let next_validator_index = state - .next_withdrawal_validator_index()? - .safe_add(spec.max_validators_per_withdrawals_sweep)? + // Update the next validator index to start the next withdrawal sweep + if expected_withdrawals.len() == E::max_withdrawals_per_payload() { + // Next sweep starts after the latest withdrawal's validator index + let next_validator_index = latest_withdrawal + .validator_index + .safe_add(1)? .safe_rem(state.validators().len() as u64)?; *state.next_withdrawal_validator_index_mut()? = next_validator_index; } - - Ok(()) } + + // Advance sweep by the max length of the sweep if there was not a full set of withdrawals + if expected_withdrawals.len() != E::max_withdrawals_per_payload() { + let next_validator_index = state + .next_withdrawal_validator_index()? + .safe_add(spec.max_validators_per_withdrawals_sweep)? + .safe_rem(state.validators().len() as u64)?; + *state.next_withdrawal_validator_index_mut()? = next_validator_index; + } + + Ok(()) + } else { // these shouldn't even be encountered but they're here for completeness - BeaconState::Base(_) | BeaconState::Altair(_) | BeaconState::Bellatrix(_) => Ok(()), + Ok(()) } } diff --git a/consensus/state_processing/src/per_block_processing/process_operations.rs b/consensus/state_processing/src/per_block_processing/process_operations.rs index 22d8592364..4977f7c7e9 100644 --- a/consensus/state_processing/src/per_block_processing/process_operations.rs +++ b/consensus/state_processing/src/per_block_processing/process_operations.rs @@ -284,29 +284,22 @@ pub fn process_attestations>( ctxt: &mut ConsensusContext, spec: &ChainSpec, ) -> Result<(), BlockProcessingError> { - match block_body { - BeaconBlockBodyRef::Base(_) => { - base::process_attestations( - state, - block_body.attestations(), - verify_signatures, - ctxt, - spec, - )?; - } - BeaconBlockBodyRef::Altair(_) - | BeaconBlockBodyRef::Bellatrix(_) - | BeaconBlockBodyRef::Capella(_) - | BeaconBlockBodyRef::Deneb(_) - | BeaconBlockBodyRef::Electra(_) => { - altair_deneb::process_attestations( - state, - block_body.attestations(), - verify_signatures, - ctxt, - spec, - )?; - } + if state.fork_name_unchecked().altair_enabled() { + altair_deneb::process_attestations( + state, + block_body.attestations(), + verify_signatures, + ctxt, + spec, + )?; + } else { + base::process_attestations( + state, + block_body.attestations(), + verify_signatures, + ctxt, + spec, + )?; } Ok(()) } diff --git a/consensus/state_processing/src/per_block_processing/signature_sets.rs b/consensus/state_processing/src/per_block_processing/signature_sets.rs index 2e00ee0341..39f438f97f 100644 --- a/consensus/state_processing/src/per_block_processing/signature_sets.rs +++ b/consensus/state_processing/src/per_block_processing/signature_sets.rs @@ -387,22 +387,20 @@ where let exit = &signed_exit.message; let proposer_index = exit.validator_index as usize; - let domain = match state { - BeaconState::Base(_) - | BeaconState::Altair(_) - | BeaconState::Bellatrix(_) - | BeaconState::Capella(_) => spec.get_domain( + let domain = if state.fork_name_unchecked().deneb_enabled() { + // EIP-7044 + spec.compute_domain( + Domain::VoluntaryExit, + spec.capella_fork_version, + state.genesis_validators_root(), + ) + } else { + spec.get_domain( exit.epoch, Domain::VoluntaryExit, &state.fork(), state.genesis_validators_root(), - ), - // EIP-7044 - BeaconState::Deneb(_) | BeaconState::Electra(_) => spec.compute_domain( - Domain::VoluntaryExit, - spec.capella_fork_version, - state.genesis_validators_root(), - ), + ) }; let message = exit.signing_root(domain); diff --git a/consensus/state_processing/src/per_block_processing/verify_attestation.rs b/consensus/state_processing/src/per_block_processing/verify_attestation.rs index 6bfb51d475..0b399bea6c 100644 --- a/consensus/state_processing/src/per_block_processing/verify_attestation.rs +++ b/consensus/state_processing/src/per_block_processing/verify_attestation.rs @@ -32,21 +32,16 @@ pub fn verify_attestation_for_block_inclusion<'ctxt, E: EthSpec>( attestation: data.slot, } ); - match state { - BeaconState::Base(_) - | BeaconState::Altair(_) - | BeaconState::Bellatrix(_) - | BeaconState::Capella(_) => { - verify!( - state.slot() <= data.slot.safe_add(E::slots_per_epoch())?, - Invalid::IncludedTooLate { - state: state.slot(), - attestation: data.slot, - } - ); - } + if state.fork_name_unchecked().deneb_enabled() { // [Modified in Deneb:EIP7045] - BeaconState::Deneb(_) | BeaconState::Electra(_) => {} + } else { + verify!( + state.slot() <= data.slot.safe_add(E::slots_per_epoch())?, + Invalid::IncludedTooLate { + state: state.slot(), + attestation: data.slot, + } + ); } verify_attestation_for_state(state, attestation, ctxt, verify_signatures, spec) diff --git a/consensus/state_processing/src/per_epoch_processing.rs b/consensus/state_processing/src/per_epoch_processing.rs index 55e8853f3f..41c30c4931 100644 --- a/consensus/state_processing/src/per_epoch_processing.rs +++ b/consensus/state_processing/src/per_epoch_processing.rs @@ -41,13 +41,10 @@ pub fn process_epoch( .fork_name(spec) .map_err(Error::InconsistentStateFork)?; - match state { - BeaconState::Base(_) => base::process_epoch(state, spec), - BeaconState::Altair(_) - | BeaconState::Bellatrix(_) - | BeaconState::Capella(_) - | BeaconState::Deneb(_) - | BeaconState::Electra(_) => altair::process_epoch(state, spec), + if state.fork_name_unchecked().altair_enabled() { + altair::process_epoch(state, spec) + } else { + base::process_epoch(state, spec) } } diff --git a/consensus/state_processing/src/per_slot_processing.rs b/consensus/state_processing/src/per_slot_processing.rs index 6554423199..af1cce602c 100644 --- a/consensus/state_processing/src/per_slot_processing.rs +++ b/consensus/state_processing/src/per_slot_processing.rs @@ -1,6 +1,6 @@ use crate::upgrade::{ upgrade_to_altair, upgrade_to_bellatrix, upgrade_to_capella, upgrade_to_deneb, - upgrade_to_electra, + upgrade_to_electra, upgrade_to_fulu, }; use crate::{per_epoch_processing::EpochProcessingSummary, *}; use safe_arith::{ArithError, SafeArith}; @@ -71,6 +71,11 @@ pub fn per_slot_processing( upgrade_to_electra(state, spec)?; } + // Fulu. + if spec.fulu_fork_epoch == Some(state.current_epoch()) { + upgrade_to_fulu(state, spec)?; + } + // Additionally build all caches so that all valid states that are advanced always have // committee caches built, and we don't have to worry about initialising them at higher // layers. diff --git a/consensus/state_processing/src/upgrade.rs b/consensus/state_processing/src/upgrade.rs index 93cafa73d0..88bc87849f 100644 --- a/consensus/state_processing/src/upgrade.rs +++ b/consensus/state_processing/src/upgrade.rs @@ -3,9 +3,11 @@ pub mod bellatrix; pub mod capella; pub mod deneb; pub mod electra; +pub mod fulu; pub use altair::upgrade_to_altair; pub use bellatrix::upgrade_to_bellatrix; pub use capella::upgrade_to_capella; pub use deneb::upgrade_to_deneb; pub use electra::upgrade_to_electra; +pub use fulu::upgrade_to_fulu; diff --git a/consensus/state_processing/src/upgrade/fulu.rs b/consensus/state_processing/src/upgrade/fulu.rs new file mode 100644 index 0000000000..6e0cd3fa9d --- /dev/null +++ b/consensus/state_processing/src/upgrade/fulu.rs @@ -0,0 +1,94 @@ +use std::mem; +use types::{BeaconState, BeaconStateError as Error, BeaconStateFulu, ChainSpec, EthSpec, Fork}; + +/// Transform a `Electra` state into an `Fulu` state. +pub fn upgrade_to_fulu( + pre_state: &mut BeaconState, + spec: &ChainSpec, +) -> Result<(), Error> { + let _epoch = pre_state.current_epoch(); + + let post = upgrade_state_to_fulu(pre_state, spec)?; + + *pre_state = post; + + Ok(()) +} + +pub fn upgrade_state_to_fulu( + pre_state: &mut BeaconState, + spec: &ChainSpec, +) -> Result, Error> { + let epoch = pre_state.current_epoch(); + let pre = pre_state.as_electra_mut()?; + // Where possible, use something like `mem::take` to move fields from behind the &mut + // reference. For other fields that don't have a good default value, use `clone`. + // + // Fixed size vectors get cloned because replacing them would require the same size + // allocation as cloning. + let post = BeaconState::Fulu(BeaconStateFulu { + // Versioning + genesis_time: pre.genesis_time, + genesis_validators_root: pre.genesis_validators_root, + slot: pre.slot, + fork: Fork { + previous_version: pre.fork.current_version, + current_version: spec.fulu_fork_version, + epoch, + }, + // History + latest_block_header: pre.latest_block_header.clone(), + block_roots: pre.block_roots.clone(), + state_roots: pre.state_roots.clone(), + historical_roots: mem::take(&mut pre.historical_roots), + // Eth1 + eth1_data: pre.eth1_data.clone(), + eth1_data_votes: mem::take(&mut pre.eth1_data_votes), + eth1_deposit_index: pre.eth1_deposit_index, + // Registry + validators: mem::take(&mut pre.validators), + balances: mem::take(&mut pre.balances), + // Randomness + randao_mixes: pre.randao_mixes.clone(), + // Slashings + slashings: pre.slashings.clone(), + // `Participation + previous_epoch_participation: mem::take(&mut pre.previous_epoch_participation), + current_epoch_participation: mem::take(&mut pre.current_epoch_participation), + // Finality + justification_bits: pre.justification_bits.clone(), + previous_justified_checkpoint: pre.previous_justified_checkpoint, + current_justified_checkpoint: pre.current_justified_checkpoint, + finalized_checkpoint: pre.finalized_checkpoint, + // Inactivity + inactivity_scores: mem::take(&mut pre.inactivity_scores), + // Sync committees + current_sync_committee: pre.current_sync_committee.clone(), + next_sync_committee: pre.next_sync_committee.clone(), + // Execution + latest_execution_payload_header: pre.latest_execution_payload_header.upgrade_to_fulu(), + // Capella + next_withdrawal_index: pre.next_withdrawal_index, + next_withdrawal_validator_index: pre.next_withdrawal_validator_index, + historical_summaries: pre.historical_summaries.clone(), + // Electra + deposit_requests_start_index: pre.deposit_requests_start_index, + deposit_balance_to_consume: pre.deposit_balance_to_consume, + exit_balance_to_consume: pre.exit_balance_to_consume, + earliest_exit_epoch: pre.earliest_exit_epoch, + consolidation_balance_to_consume: pre.consolidation_balance_to_consume, + earliest_consolidation_epoch: pre.earliest_consolidation_epoch, + pending_deposits: pre.pending_deposits.clone(), + pending_partial_withdrawals: pre.pending_partial_withdrawals.clone(), + pending_consolidations: pre.pending_consolidations.clone(), + // Caches + total_active_balance: pre.total_active_balance, + progressive_balances_cache: mem::take(&mut pre.progressive_balances_cache), + committee_caches: mem::take(&mut pre.committee_caches), + pubkey_cache: mem::take(&mut pre.pubkey_cache), + exit_cache: mem::take(&mut pre.exit_cache), + slashings_cache: mem::take(&mut pre.slashings_cache), + epoch_cache: mem::take(&mut pre.epoch_cache), + }); + Ok(post) +} diff --git a/consensus/types/presets/gnosis/fulu.yaml b/consensus/types/presets/gnosis/fulu.yaml new file mode 100644 index 0000000000..35a7c98fbf --- /dev/null +++ b/consensus/types/presets/gnosis/fulu.yaml @@ -0,0 +1,3 @@ +# Gnosis preset - Fulu + +FULU_PLACEHOLDER: 0 diff --git a/consensus/types/presets/mainnet/fulu.yaml b/consensus/types/presets/mainnet/fulu.yaml new file mode 100644 index 0000000000..8aa9ccdcc3 --- /dev/null +++ b/consensus/types/presets/mainnet/fulu.yaml @@ -0,0 +1,3 @@ +# Mainnet preset - Fulu + +FULU_PLACEHOLDER: 0 diff --git a/consensus/types/presets/minimal/fulu.yaml b/consensus/types/presets/minimal/fulu.yaml new file mode 100644 index 0000000000..121c9858f4 --- /dev/null +++ b/consensus/types/presets/minimal/fulu.yaml @@ -0,0 +1,3 @@ +# Minimal preset - Fulu + +FULU_PLACEHOLDER: 0 diff --git a/consensus/types/src/beacon_block.rs b/consensus/types/src/beacon_block.rs index 801b7dd1c7..d72550aa12 100644 --- a/consensus/types/src/beacon_block.rs +++ b/consensus/types/src/beacon_block.rs @@ -16,7 +16,7 @@ use self::indexed_attestation::{IndexedAttestationBase, IndexedAttestationElectr /// A block of the `BeaconChain`. #[superstruct( - variants(Base, Altair, Bellatrix, Capella, Deneb, Electra), + variants(Base, Altair, Bellatrix, Capella, Deneb, Electra, Fulu), variant_attributes( derive( Debug, @@ -75,6 +75,8 @@ pub struct BeaconBlock = FullPayload pub body: BeaconBlockBodyDeneb, #[superstruct(only(Electra), partial_getter(rename = "body_electra"))] pub body: BeaconBlockBodyElectra, + #[superstruct(only(Fulu), partial_getter(rename = "body_fulu"))] + pub body: BeaconBlockBodyFulu, } pub type BlindedBeaconBlock = BeaconBlock>; @@ -127,8 +129,9 @@ impl> BeaconBlock { /// Usually it's better to prefer `from_ssz_bytes` which will decode the correct variant based /// on the fork slot. pub fn any_from_ssz_bytes(bytes: &[u8]) -> Result { - BeaconBlockElectra::from_ssz_bytes(bytes) - .map(BeaconBlock::Electra) + BeaconBlockFulu::from_ssz_bytes(bytes) + .map(BeaconBlock::Fulu) + .or_else(|_| BeaconBlockElectra::from_ssz_bytes(bytes).map(BeaconBlock::Electra)) .or_else(|_| BeaconBlockDeneb::from_ssz_bytes(bytes).map(BeaconBlock::Deneb)) .or_else(|_| BeaconBlockCapella::from_ssz_bytes(bytes).map(BeaconBlock::Capella)) .or_else(|_| BeaconBlockBellatrix::from_ssz_bytes(bytes).map(BeaconBlock::Bellatrix)) @@ -226,6 +229,7 @@ impl<'a, E: EthSpec, Payload: AbstractExecPayload> BeaconBlockRef<'a, E, Payl BeaconBlockRef::Capella { .. } => ForkName::Capella, BeaconBlockRef::Deneb { .. } => ForkName::Deneb, BeaconBlockRef::Electra { .. } => ForkName::Electra, + BeaconBlockRef::Fulu { .. } => ForkName::Fulu, } } @@ -704,6 +708,110 @@ impl> EmptyBlock for BeaconBlockElec } } +impl> BeaconBlockFulu { + /// Return a Fulu block where the block has maximum size. + pub fn full(spec: &ChainSpec) -> Self { + let base_block: BeaconBlockBase<_, Payload> = BeaconBlockBase::full(spec); + let indexed_attestation: IndexedAttestationElectra = IndexedAttestationElectra { + attesting_indices: VariableList::new(vec![0_u64; E::MaxValidatorsPerSlot::to_usize()]) + .unwrap(), + data: AttestationData::default(), + signature: AggregateSignature::empty(), + }; + let attester_slashings = vec![ + AttesterSlashingElectra { + attestation_1: indexed_attestation.clone(), + attestation_2: indexed_attestation, + }; + E::max_attester_slashings_electra() + ] + .into(); + let attestation = AttestationElectra { + aggregation_bits: BitList::with_capacity(E::MaxValidatorsPerSlot::to_usize()).unwrap(), + data: AttestationData::default(), + signature: AggregateSignature::empty(), + committee_bits: BitVector::new(), + }; + let mut attestations_electra = vec![]; + for _ in 0..E::MaxAttestationsElectra::to_usize() { + attestations_electra.push(attestation.clone()); + } + + let bls_to_execution_changes = vec![ + SignedBlsToExecutionChange { + message: BlsToExecutionChange { + validator_index: 0, + from_bls_pubkey: PublicKeyBytes::empty(), + to_execution_address: Address::ZERO, + }, + signature: Signature::empty() + }; + E::max_bls_to_execution_changes() + ] + .into(); + let sync_aggregate = SyncAggregate { + sync_committee_signature: AggregateSignature::empty(), + sync_committee_bits: BitVector::default(), + }; + BeaconBlockFulu { + slot: spec.genesis_slot, + proposer_index: 0, + parent_root: Hash256::zero(), + state_root: Hash256::zero(), + body: BeaconBlockBodyFulu { + proposer_slashings: base_block.body.proposer_slashings, + attester_slashings, + attestations: attestations_electra.into(), + deposits: base_block.body.deposits, + voluntary_exits: base_block.body.voluntary_exits, + bls_to_execution_changes, + sync_aggregate, + randao_reveal: Signature::empty(), + eth1_data: Eth1Data { + deposit_root: Hash256::zero(), + block_hash: Hash256::zero(), + deposit_count: 0, + }, + graffiti: Graffiti::default(), + execution_payload: Payload::Fulu::default(), + blob_kzg_commitments: VariableList::empty(), + execution_requests: ExecutionRequests::default(), + }, + } + } +} + +impl> EmptyBlock for BeaconBlockFulu { + /// Returns an empty Fulu block to be used during genesis. + fn empty(spec: &ChainSpec) -> Self { + BeaconBlockFulu { + slot: spec.genesis_slot, + proposer_index: 0, + parent_root: Hash256::zero(), + state_root: Hash256::zero(), + body: BeaconBlockBodyFulu { + randao_reveal: Signature::empty(), + eth1_data: Eth1Data { + deposit_root: Hash256::zero(), + block_hash: Hash256::zero(), + deposit_count: 0, + }, + graffiti: Graffiti::default(), + proposer_slashings: VariableList::empty(), + attester_slashings: VariableList::empty(), + attestations: VariableList::empty(), + deposits: VariableList::empty(), + voluntary_exits: VariableList::empty(), + sync_aggregate: SyncAggregate::empty(), + execution_payload: Payload::Fulu::default(), + bls_to_execution_changes: VariableList::empty(), + blob_kzg_commitments: VariableList::empty(), + execution_requests: ExecutionRequests::default(), + }, + } + } +} + // We can convert pre-Bellatrix blocks without payloads into blocks "with" payloads. impl From>> for BeaconBlockBase> @@ -785,6 +893,7 @@ impl_from!(BeaconBlockBellatrix, >, >, |b impl_from!(BeaconBlockCapella, >, >, |body: BeaconBlockBodyCapella<_, _>| body.into()); impl_from!(BeaconBlockDeneb, >, >, |body: BeaconBlockBodyDeneb<_, _>| body.into()); impl_from!(BeaconBlockElectra, >, >, |body: BeaconBlockBodyElectra<_, _>| body.into()); +impl_from!(BeaconBlockFulu, >, >, |body: BeaconBlockBodyFulu<_, _>| body.into()); // We can clone blocks with payloads to blocks without payloads, without cloning the payload. macro_rules! impl_clone_as_blinded { @@ -818,6 +927,7 @@ impl_clone_as_blinded!(BeaconBlockBellatrix, >, >, >); impl_clone_as_blinded!(BeaconBlockDeneb, >, >); impl_clone_as_blinded!(BeaconBlockElectra, >, >); +impl_clone_as_blinded!(BeaconBlockFulu, >, >); // A reference to a full beacon block can be cloned into a blinded beacon block, without cloning the // execution payload. @@ -988,6 +1098,26 @@ mod tests { }); } + #[test] + fn roundtrip_fulu_block() { + let rng = &mut XorShiftRng::from_seed([42; 16]); + let spec = &ForkName::Fulu.make_genesis_spec(MainnetEthSpec::default_spec()); + + let inner_block = BeaconBlockFulu { + slot: Slot::random_for_test(rng), + proposer_index: u64::random_for_test(rng), + parent_root: Hash256::random_for_test(rng), + state_root: Hash256::random_for_test(rng), + body: BeaconBlockBodyFulu::random_for_test(rng), + }; + + let block = BeaconBlock::Fulu(inner_block.clone()); + + test_ssz_tree_hash_pair_with(&block, &inner_block, |bytes| { + BeaconBlock::from_ssz_bytes(bytes, spec) + }); + } + #[test] fn decode_base_and_altair() { type E = MainnetEthSpec; @@ -1007,11 +1137,14 @@ mod tests { let deneb_slot = deneb_epoch.start_slot(E::slots_per_epoch()); let electra_epoch = deneb_epoch + 1; let electra_slot = electra_epoch.start_slot(E::slots_per_epoch()); + let fulu_epoch = electra_epoch + 1; + let fulu_slot = fulu_epoch.start_slot(E::slots_per_epoch()); spec.altair_fork_epoch = Some(altair_epoch); spec.capella_fork_epoch = Some(capella_epoch); spec.deneb_fork_epoch = Some(deneb_epoch); spec.electra_fork_epoch = Some(electra_epoch); + spec.fulu_fork_epoch = Some(fulu_epoch); // BeaconBlockBase { @@ -1122,5 +1255,29 @@ mod tests { BeaconBlock::from_ssz_bytes(&bad_block.as_ssz_bytes(), &spec) .expect_err("bad electra block cannot be decoded"); } + + // BeaconBlockFulu + { + let good_block = BeaconBlock::Fulu(BeaconBlockFulu { + slot: fulu_slot, + ..<_>::random_for_test(rng) + }); + // It's invalid to have a Fulu block with a epoch lower than the fork epoch. + let _bad_block = { + let mut bad = good_block.clone(); + *bad.slot_mut() = electra_slot; + bad + }; + + assert_eq!( + BeaconBlock::from_ssz_bytes(&good_block.as_ssz_bytes(), &spec) + .expect("good fulu block can be decoded"), + good_block + ); + // TODO(fulu): Uncomment once Fulu has features since without features + // and with an Electra slot it decodes successfully to Electra. + //BeaconBlock::from_ssz_bytes(&bad_block.as_ssz_bytes(), &spec) + // .expect_err("bad fulu block cannot be decoded"); + } } } diff --git a/consensus/types/src/beacon_block_body.rs b/consensus/types/src/beacon_block_body.rs index f7a701fed6..a198cdf28f 100644 --- a/consensus/types/src/beacon_block_body.rs +++ b/consensus/types/src/beacon_block_body.rs @@ -30,7 +30,7 @@ pub const BLOB_KZG_COMMITMENTS_INDEX: usize = 11; /// /// This *superstruct* abstracts over the hard-fork. #[superstruct( - variants(Base, Altair, Bellatrix, Capella, Deneb, Electra), + variants(Base, Altair, Bellatrix, Capella, Deneb, Electra, Fulu), variant_attributes( derive( Debug, @@ -58,6 +58,7 @@ pub const BLOB_KZG_COMMITMENTS_INDEX: usize = 11; Capella(metastruct(mappings(beacon_block_body_capella_fields(groups(fields))))), Deneb(metastruct(mappings(beacon_block_body_deneb_fields(groups(fields))))), Electra(metastruct(mappings(beacon_block_body_electra_fields(groups(fields))))), + Fulu(metastruct(mappings(beacon_block_body_fulu_fields(groups(fields))))) ), cast_error(ty = "Error", expr = "Error::IncorrectStateVariant"), partial_getter_error(ty = "Error", expr = "Error::IncorrectStateVariant") @@ -77,7 +78,10 @@ pub struct BeaconBlockBody = FullPay partial_getter(rename = "attester_slashings_base") )] pub attester_slashings: VariableList, E::MaxAttesterSlashings>, - #[superstruct(only(Electra), partial_getter(rename = "attester_slashings_electra"))] + #[superstruct( + only(Electra, Fulu), + partial_getter(rename = "attester_slashings_electra") + )] pub attester_slashings: VariableList, E::MaxAttesterSlashingsElectra>, #[superstruct( @@ -85,11 +89,11 @@ pub struct BeaconBlockBody = FullPay partial_getter(rename = "attestations_base") )] pub attestations: VariableList, E::MaxAttestations>, - #[superstruct(only(Electra), partial_getter(rename = "attestations_electra"))] + #[superstruct(only(Electra, Fulu), partial_getter(rename = "attestations_electra"))] pub attestations: VariableList, E::MaxAttestationsElectra>, pub deposits: VariableList, pub voluntary_exits: VariableList, - #[superstruct(only(Altair, Bellatrix, Capella, Deneb, Electra))] + #[superstruct(only(Altair, Bellatrix, Capella, Deneb, Electra, Fulu))] pub sync_aggregate: SyncAggregate, // We flatten the execution payload so that serde can use the name of the inner type, // either `execution_payload` for full payloads, or `execution_payload_header` for blinded @@ -109,12 +113,15 @@ pub struct BeaconBlockBody = FullPay #[superstruct(only(Electra), partial_getter(rename = "execution_payload_electra"))] #[serde(flatten)] pub execution_payload: Payload::Electra, - #[superstruct(only(Capella, Deneb, Electra))] + #[superstruct(only(Fulu), partial_getter(rename = "execution_payload_fulu"))] + #[serde(flatten)] + pub execution_payload: Payload::Fulu, + #[superstruct(only(Capella, Deneb, Electra, Fulu))] pub bls_to_execution_changes: VariableList, - #[superstruct(only(Deneb, Electra))] + #[superstruct(only(Deneb, Electra, Fulu))] pub blob_kzg_commitments: KzgCommitments, - #[superstruct(only(Electra))] + #[superstruct(only(Electra, Fulu))] pub execution_requests: ExecutionRequests, #[superstruct(only(Base, Altair))] #[metastruct(exclude_from(fields))] @@ -144,6 +151,7 @@ impl<'a, E: EthSpec, Payload: AbstractExecPayload> BeaconBlockBodyRef<'a, E, Self::Capella(body) => Ok(Payload::Ref::from(&body.execution_payload)), Self::Deneb(body) => Ok(Payload::Ref::from(&body.execution_payload)), Self::Electra(body) => Ok(Payload::Ref::from(&body.execution_payload)), + Self::Fulu(body) => Ok(Payload::Ref::from(&body.execution_payload)), } } @@ -174,6 +182,10 @@ impl<'a, E: EthSpec, Payload: AbstractExecPayload> BeaconBlockBodyRef<'a, E, beacon_block_body_electra_fields!(body, |_, field| leaves .push(field.tree_hash_root())); } + Self::Fulu(body) => { + beacon_block_body_fulu_fields!(body, |_, field| leaves + .push(field.tree_hash_root())); + } } leaves } @@ -202,7 +214,7 @@ impl<'a, E: EthSpec, Payload: AbstractExecPayload> BeaconBlockBodyRef<'a, E, Self::Base(_) | Self::Altair(_) | Self::Bellatrix(_) | Self::Capella(_) => { Err(Error::IncorrectStateVariant) } - Self::Deneb(_) | Self::Electra(_) => { + Self::Deneb(_) | Self::Electra(_) | Self::Fulu(_) => { // We compute the branches by generating 2 merkle trees: // 1. Merkle tree for the `blob_kzg_commitments` List object // 2. Merkle tree for the `BeaconBlockBody` container @@ -294,6 +306,7 @@ impl<'a, E: EthSpec, Payload: AbstractExecPayload> BeaconBlockBodyRef<'a, E, Self::Capella(body) => body.attestations.len(), Self::Deneb(body) => body.attestations.len(), Self::Electra(body) => body.attestations.len(), + Self::Fulu(body) => body.attestations.len(), } } @@ -305,6 +318,7 @@ impl<'a, E: EthSpec, Payload: AbstractExecPayload> BeaconBlockBodyRef<'a, E, Self::Capella(body) => body.attester_slashings.len(), Self::Deneb(body) => body.attester_slashings.len(), Self::Electra(body) => body.attester_slashings.len(), + Self::Fulu(body) => body.attester_slashings.len(), } } @@ -316,6 +330,7 @@ impl<'a, E: EthSpec, Payload: AbstractExecPayload> BeaconBlockBodyRef<'a, E, Self::Capella(body) => Box::new(body.attestations.iter().map(AttestationRef::Base)), Self::Deneb(body) => Box::new(body.attestations.iter().map(AttestationRef::Base)), Self::Electra(body) => Box::new(body.attestations.iter().map(AttestationRef::Electra)), + Self::Fulu(body) => Box::new(body.attestations.iter().map(AttestationRef::Electra)), } } @@ -351,6 +366,11 @@ impl<'a, E: EthSpec, Payload: AbstractExecPayload> BeaconBlockBodyRef<'a, E, .iter() .map(AttesterSlashingRef::Electra), ), + Self::Fulu(body) => Box::new( + body.attester_slashings + .iter() + .map(AttesterSlashingRef::Electra), + ), } } } @@ -376,6 +396,9 @@ impl<'a, E: EthSpec, Payload: AbstractExecPayload> BeaconBlockBodyRefMut<'a, Self::Electra(body) => { Box::new(body.attestations.iter_mut().map(AttestationRefMut::Electra)) } + Self::Fulu(body) => { + Box::new(body.attestations.iter_mut().map(AttestationRefMut::Electra)) + } } } } @@ -390,6 +413,7 @@ impl> BeaconBlockBodyRef<'_, E, Payl BeaconBlockBodyRef::Capella { .. } => ForkName::Capella, BeaconBlockBodyRef::Deneb { .. } => ForkName::Deneb, BeaconBlockBodyRef::Electra { .. } => ForkName::Electra, + BeaconBlockBodyRef::Fulu { .. } => ForkName::Fulu, } } } @@ -704,6 +728,52 @@ impl From>> } } +impl From>> + for ( + BeaconBlockBodyFulu>, + Option>, + ) +{ + fn from(body: BeaconBlockBodyFulu>) -> Self { + let BeaconBlockBodyFulu { + randao_reveal, + eth1_data, + graffiti, + proposer_slashings, + attester_slashings, + attestations, + deposits, + voluntary_exits, + sync_aggregate, + execution_payload: FullPayloadFulu { execution_payload }, + bls_to_execution_changes, + blob_kzg_commitments, + execution_requests, + } = body; + + ( + BeaconBlockBodyFulu { + randao_reveal, + eth1_data, + graffiti, + proposer_slashings, + attester_slashings, + attestations, + deposits, + voluntary_exits, + sync_aggregate, + execution_payload: BlindedPayloadFulu { + execution_payload_header: From::from(&execution_payload), + }, + bls_to_execution_changes, + blob_kzg_commitments: blob_kzg_commitments.clone(), + execution_requests, + }, + Some(execution_payload), + ) + } +} + // We can clone a full block into a blinded block, without cloning the payload. impl BeaconBlockBodyBase> { pub fn clone_as_blinded(&self) -> BeaconBlockBodyBase> { @@ -859,6 +929,44 @@ impl BeaconBlockBodyElectra> { } } +impl BeaconBlockBodyFulu> { + pub fn clone_as_blinded(&self) -> BeaconBlockBodyFulu> { + let BeaconBlockBodyFulu { + randao_reveal, + eth1_data, + graffiti, + proposer_slashings, + attester_slashings, + attestations, + deposits, + voluntary_exits, + sync_aggregate, + execution_payload: FullPayloadFulu { execution_payload }, + bls_to_execution_changes, + blob_kzg_commitments, + execution_requests, + } = self; + + BeaconBlockBodyFulu { + randao_reveal: randao_reveal.clone(), + eth1_data: eth1_data.clone(), + graffiti: *graffiti, + proposer_slashings: proposer_slashings.clone(), + attester_slashings: attester_slashings.clone(), + attestations: attestations.clone(), + deposits: deposits.clone(), + voluntary_exits: voluntary_exits.clone(), + sync_aggregate: sync_aggregate.clone(), + execution_payload: BlindedPayloadFulu { + execution_payload_header: execution_payload.into(), + }, + bls_to_execution_changes: bls_to_execution_changes.clone(), + blob_kzg_commitments: blob_kzg_commitments.clone(), + execution_requests: execution_requests.clone(), + } + } +} + impl From>> for ( BeaconBlockBody>, diff --git a/consensus/types/src/beacon_state.rs b/consensus/types/src/beacon_state.rs index 05f28744fa..de6077bf94 100644 --- a/consensus/types/src/beacon_state.rs +++ b/consensus/types/src/beacon_state.rs @@ -223,7 +223,7 @@ impl From for Hash256 { /// /// https://github.com/sigp/milhouse/issues/43 #[superstruct( - variants(Base, Altair, Bellatrix, Capella, Deneb, Electra), + variants(Base, Altair, Bellatrix, Capella, Deneb, Electra, Fulu), variant_attributes( derive( Derivative, @@ -326,6 +326,20 @@ impl From for Hash256 { groups(tree_lists) )), num_fields(all()), + )), + Fulu(metastruct( + mappings( + map_beacon_state_fulu_fields(), + map_beacon_state_fulu_tree_list_fields(mutable, fallible, groups(tree_lists)), + map_beacon_state_fulu_tree_list_fields_immutable(groups(tree_lists)), + ), + bimappings(bimap_beacon_state_fulu_tree_list_fields( + other_type = "BeaconStateFulu", + self_mutable, + fallible, + groups(tree_lists) + )), + num_fields(all()), )) ), cast_error(ty = "Error", expr = "Error::IncorrectStateVariant"), @@ -408,11 +422,11 @@ where // Participation (Altair and later) #[compare_fields(as_iter)] - #[superstruct(only(Altair, Bellatrix, Capella, Deneb, Electra))] + #[superstruct(only(Altair, Bellatrix, Capella, Deneb, Electra, Fulu))] #[test_random(default)] #[compare_fields(as_iter)] pub previous_epoch_participation: List, - #[superstruct(only(Altair, Bellatrix, Capella, Deneb, Electra))] + #[superstruct(only(Altair, Bellatrix, Capella, Deneb, Electra, Fulu))] #[test_random(default)] pub current_epoch_participation: List, @@ -432,15 +446,15 @@ where // Inactivity #[serde(with = "ssz_types::serde_utils::quoted_u64_var_list")] - #[superstruct(only(Altair, Bellatrix, Capella, Deneb, Electra))] + #[superstruct(only(Altair, Bellatrix, Capella, Deneb, Electra, Fulu))] #[test_random(default)] pub inactivity_scores: List, // Light-client sync committees - #[superstruct(only(Altair, Bellatrix, Capella, Deneb, Electra))] + #[superstruct(only(Altair, Bellatrix, Capella, Deneb, Electra, Fulu))] #[metastruct(exclude_from(tree_lists))] pub current_sync_committee: Arc>, - #[superstruct(only(Altair, Bellatrix, Capella, Deneb, Electra))] + #[superstruct(only(Altair, Bellatrix, Capella, Deneb, Electra, Fulu))] #[metastruct(exclude_from(tree_lists))] pub next_sync_committee: Arc>, @@ -469,56 +483,62 @@ where )] #[metastruct(exclude_from(tree_lists))] pub latest_execution_payload_header: ExecutionPayloadHeaderElectra, + #[superstruct( + only(Fulu), + partial_getter(rename = "latest_execution_payload_header_fulu") + )] + #[metastruct(exclude_from(tree_lists))] + pub latest_execution_payload_header: ExecutionPayloadHeaderFulu, // Capella - #[superstruct(only(Capella, Deneb, Electra), partial_getter(copy))] + #[superstruct(only(Capella, Deneb, Electra, Fulu), partial_getter(copy))] #[serde(with = "serde_utils::quoted_u64")] #[metastruct(exclude_from(tree_lists))] pub next_withdrawal_index: u64, - #[superstruct(only(Capella, Deneb, Electra), partial_getter(copy))] + #[superstruct(only(Capella, Deneb, Electra, Fulu), partial_getter(copy))] #[serde(with = "serde_utils::quoted_u64")] #[metastruct(exclude_from(tree_lists))] pub next_withdrawal_validator_index: u64, // Deep history valid from Capella onwards. - #[superstruct(only(Capella, Deneb, Electra))] + #[superstruct(only(Capella, Deneb, Electra, Fulu))] #[test_random(default)] pub historical_summaries: List, // Electra - #[superstruct(only(Electra), partial_getter(copy))] + #[superstruct(only(Electra, Fulu), partial_getter(copy))] #[metastruct(exclude_from(tree_lists))] #[serde(with = "serde_utils::quoted_u64")] pub deposit_requests_start_index: u64, - #[superstruct(only(Electra), partial_getter(copy))] + #[superstruct(only(Electra, Fulu), partial_getter(copy))] #[metastruct(exclude_from(tree_lists))] #[serde(with = "serde_utils::quoted_u64")] pub deposit_balance_to_consume: u64, - #[superstruct(only(Electra), partial_getter(copy))] + #[superstruct(only(Electra, Fulu), partial_getter(copy))] #[metastruct(exclude_from(tree_lists))] #[serde(with = "serde_utils::quoted_u64")] pub exit_balance_to_consume: u64, - #[superstruct(only(Electra), partial_getter(copy))] + #[superstruct(only(Electra, Fulu), partial_getter(copy))] #[metastruct(exclude_from(tree_lists))] pub earliest_exit_epoch: Epoch, - #[superstruct(only(Electra), partial_getter(copy))] + #[superstruct(only(Electra, Fulu), partial_getter(copy))] #[metastruct(exclude_from(tree_lists))] #[serde(with = "serde_utils::quoted_u64")] pub consolidation_balance_to_consume: u64, - #[superstruct(only(Electra), partial_getter(copy))] + #[superstruct(only(Electra, Fulu), partial_getter(copy))] #[metastruct(exclude_from(tree_lists))] pub earliest_consolidation_epoch: Epoch, #[compare_fields(as_iter)] #[test_random(default)] - #[superstruct(only(Electra))] + #[superstruct(only(Electra, Fulu))] pub pending_deposits: List, #[compare_fields(as_iter)] #[test_random(default)] - #[superstruct(only(Electra))] + #[superstruct(only(Electra, Fulu))] pub pending_partial_withdrawals: List, #[compare_fields(as_iter)] #[test_random(default)] - #[superstruct(only(Electra))] + #[superstruct(only(Electra, Fulu))] pub pending_consolidations: List, // Caching (not in the spec) @@ -659,6 +679,7 @@ impl BeaconState { BeaconState::Capella { .. } => ForkName::Capella, BeaconState::Deneb { .. } => ForkName::Deneb, BeaconState::Electra { .. } => ForkName::Electra, + BeaconState::Fulu { .. } => ForkName::Fulu, } } @@ -948,6 +969,9 @@ impl BeaconState { BeaconState::Electra(state) => Ok(ExecutionPayloadHeaderRef::Electra( &state.latest_execution_payload_header, )), + BeaconState::Fulu(state) => Ok(ExecutionPayloadHeaderRef::Fulu( + &state.latest_execution_payload_header, + )), } } @@ -968,6 +992,9 @@ impl BeaconState { BeaconState::Electra(state) => Ok(ExecutionPayloadHeaderRefMut::Electra( &mut state.latest_execution_payload_header, )), + BeaconState::Fulu(state) => Ok(ExecutionPayloadHeaderRefMut::Fulu( + &mut state.latest_execution_payload_header, + )), } } @@ -1481,6 +1508,16 @@ impl BeaconState { &mut state.exit_cache, &mut state.epoch_cache, )), + BeaconState::Fulu(state) => Ok(( + &mut state.validators, + &mut state.balances, + &state.previous_epoch_participation, + &state.current_epoch_participation, + &mut state.inactivity_scores, + &mut state.progressive_balances_cache, + &mut state.exit_cache, + &mut state.epoch_cache, + )), } } @@ -1662,10 +1699,12 @@ impl BeaconState { | BeaconState::Altair(_) | BeaconState::Bellatrix(_) | BeaconState::Capella(_) => self.get_validator_churn_limit(spec)?, - BeaconState::Deneb(_) | BeaconState::Electra(_) => std::cmp::min( - spec.max_per_epoch_activation_churn_limit, - self.get_validator_churn_limit(spec)?, - ), + BeaconState::Deneb(_) | BeaconState::Electra(_) | BeaconState::Fulu(_) => { + std::cmp::min( + spec.max_per_epoch_activation_churn_limit, + self.get_validator_churn_limit(spec)?, + ) + } }) } @@ -1783,6 +1822,7 @@ impl BeaconState { BeaconState::Capella(state) => Ok(&mut state.current_epoch_participation), BeaconState::Deneb(state) => Ok(&mut state.current_epoch_participation), BeaconState::Electra(state) => Ok(&mut state.current_epoch_participation), + BeaconState::Fulu(state) => Ok(&mut state.current_epoch_participation), } } else if epoch == previous_epoch { match self { @@ -1792,6 +1832,7 @@ impl BeaconState { BeaconState::Capella(state) => Ok(&mut state.previous_epoch_participation), BeaconState::Deneb(state) => Ok(&mut state.previous_epoch_participation), BeaconState::Electra(state) => Ok(&mut state.previous_epoch_participation), + BeaconState::Fulu(state) => Ok(&mut state.previous_epoch_participation), } } else { Err(BeaconStateError::EpochOutOfBounds) @@ -2045,6 +2086,11 @@ impl BeaconState { } ); } + Self::Fulu(self_inner) => { + map_beacon_state_fulu_tree_list_fields_immutable!(self_inner, |_, self_field| { + any_pending_mutations |= self_field.has_pending_updates(); + }); + } }; any_pending_mutations } @@ -2238,12 +2284,29 @@ impl BeaconState { exit_balance_to_consume .safe_add_assign(additional_epochs.safe_mul(per_epoch_churn)?)?; } - let state = self.as_electra_mut()?; - // Consume the balance and update state variables - state.exit_balance_to_consume = exit_balance_to_consume.safe_sub(exit_balance)?; - state.earliest_exit_epoch = earliest_exit_epoch; + match self { + BeaconState::Base(_) + | BeaconState::Altair(_) + | BeaconState::Bellatrix(_) + | BeaconState::Capella(_) + | BeaconState::Deneb(_) => Err(Error::IncorrectStateVariant), + BeaconState::Electra(_) => { + let state = self.as_electra_mut()?; - Ok(state.earliest_exit_epoch) + // Consume the balance and update state variables + state.exit_balance_to_consume = exit_balance_to_consume.safe_sub(exit_balance)?; + state.earliest_exit_epoch = earliest_exit_epoch; + Ok(state.earliest_exit_epoch) + } + BeaconState::Fulu(_) => { + let state = self.as_fulu_mut()?; + + // Consume the balance and update state variables + state.exit_balance_to_consume = exit_balance_to_consume.safe_sub(exit_balance)?; + state.earliest_exit_epoch = earliest_exit_epoch; + Ok(state.earliest_exit_epoch) + } + } } pub fn compute_consolidation_epoch_and_update_churn( @@ -2277,13 +2340,31 @@ impl BeaconState { consolidation_balance_to_consume .safe_add_assign(additional_epochs.safe_mul(per_epoch_consolidation_churn)?)?; } - // Consume the balance and update state variables - let state = self.as_electra_mut()?; - state.consolidation_balance_to_consume = - consolidation_balance_to_consume.safe_sub(consolidation_balance)?; - state.earliest_consolidation_epoch = earliest_consolidation_epoch; + match self { + BeaconState::Base(_) + | BeaconState::Altair(_) + | BeaconState::Bellatrix(_) + | BeaconState::Capella(_) + | BeaconState::Deneb(_) => Err(Error::IncorrectStateVariant), + BeaconState::Electra(_) => { + let state = self.as_electra_mut()?; - Ok(state.earliest_consolidation_epoch) + // Consume the balance and update state variables. + state.consolidation_balance_to_consume = + consolidation_balance_to_consume.safe_sub(consolidation_balance)?; + state.earliest_consolidation_epoch = earliest_consolidation_epoch; + Ok(state.earliest_consolidation_epoch) + } + BeaconState::Fulu(_) => { + let state = self.as_fulu_mut()?; + + // Consume the balance and update state variables. + state.consolidation_balance_to_consume = + consolidation_balance_to_consume.safe_sub(consolidation_balance)?; + state.earliest_consolidation_epoch = earliest_consolidation_epoch; + Ok(state.earliest_consolidation_epoch) + } + } } #[allow(clippy::arithmetic_side_effects)] @@ -2339,6 +2420,14 @@ impl BeaconState { ); } (Self::Electra(_), _) => (), + (Self::Fulu(self_inner), Self::Fulu(base_inner)) => { + bimap_beacon_state_fulu_tree_list_fields!( + self_inner, + base_inner, + |_, self_field, base_field| { self_field.rebase_on(base_field) } + ); + } + (Self::Fulu(_), _) => (), } // Use sync committees from `base` if they are equal. @@ -2411,6 +2500,7 @@ impl BeaconState { ForkName::Capella => BeaconStateCapella::::NUM_FIELDS.next_power_of_two(), ForkName::Deneb => BeaconStateDeneb::::NUM_FIELDS.next_power_of_two(), ForkName::Electra => BeaconStateElectra::::NUM_FIELDS.next_power_of_two(), + ForkName::Fulu => BeaconStateFulu::::NUM_FIELDS.next_power_of_two(), } } @@ -2459,6 +2549,9 @@ impl BeaconState { Self::Electra(inner) => { map_beacon_state_electra_tree_list_fields!(inner, |_, x| { x.apply_updates() }) } + Self::Fulu(inner) => { + map_beacon_state_fulu_tree_list_fields!(inner, |_, x| { x.apply_updates() }) + } } Ok(()) } @@ -2554,6 +2647,11 @@ impl BeaconState { leaves.push(field.tree_hash_root()); }); } + BeaconState::Fulu(state) => { + map_beacon_state_fulu_fields!(state, |_, field| { + leaves.push(field.tree_hash_root()); + }); + } }; leaves @@ -2611,6 +2709,7 @@ impl CompareFields for BeaconState { (BeaconState::Capella(x), BeaconState::Capella(y)) => x.compare_fields(y), (BeaconState::Deneb(x), BeaconState::Deneb(y)) => x.compare_fields(y), (BeaconState::Electra(x), BeaconState::Electra(y)) => x.compare_fields(y), + (BeaconState::Fulu(x), BeaconState::Fulu(y)) => x.compare_fields(y), _ => panic!("compare_fields: mismatched state variants",), } } diff --git a/consensus/types/src/beacon_state/progressive_balances_cache.rs b/consensus/types/src/beacon_state/progressive_balances_cache.rs index bc258ef68d..8e8a1a6aa9 100644 --- a/consensus/types/src/beacon_state/progressive_balances_cache.rs +++ b/consensus/types/src/beacon_state/progressive_balances_cache.rs @@ -285,12 +285,5 @@ impl ProgressiveBalancesCache { /// `ProgressiveBalancesCache` is only enabled from `Altair` as it uses Altair-specific logic. pub fn is_progressive_balances_enabled(state: &BeaconState) -> bool { - match state { - BeaconState::Base(_) => false, - BeaconState::Altair(_) - | BeaconState::Bellatrix(_) - | BeaconState::Capella(_) - | BeaconState::Deneb(_) - | BeaconState::Electra(_) => true, - } + state.fork_name_unchecked().altair_enabled() } diff --git a/consensus/types/src/builder_bid.rs b/consensus/types/src/builder_bid.rs index 9885f78474..2ce46ca704 100644 --- a/consensus/types/src/builder_bid.rs +++ b/consensus/types/src/builder_bid.rs @@ -1,8 +1,9 @@ use crate::beacon_block_body::KzgCommitments; use crate::{ ChainSpec, EthSpec, ExecutionPayloadHeaderBellatrix, ExecutionPayloadHeaderCapella, - ExecutionPayloadHeaderDeneb, ExecutionPayloadHeaderElectra, ExecutionPayloadHeaderRef, - ExecutionPayloadHeaderRefMut, ForkName, ForkVersionDeserialize, SignedRoot, Uint256, + ExecutionPayloadHeaderDeneb, ExecutionPayloadHeaderElectra, ExecutionPayloadHeaderFulu, + ExecutionPayloadHeaderRef, ExecutionPayloadHeaderRefMut, ForkName, ForkVersionDeserialize, + SignedRoot, Uint256, }; use bls::PublicKeyBytes; use bls::Signature; @@ -11,7 +12,7 @@ use superstruct::superstruct; use tree_hash_derive::TreeHash; #[superstruct( - variants(Bellatrix, Capella, Deneb, Electra), + variants(Bellatrix, Capella, Deneb, Electra, Fulu), variant_attributes( derive(PartialEq, Debug, Serialize, Deserialize, TreeHash, Clone), serde(bound = "E: EthSpec", deny_unknown_fields) @@ -31,7 +32,9 @@ pub struct BuilderBid { pub header: ExecutionPayloadHeaderDeneb, #[superstruct(only(Electra), partial_getter(rename = "header_electra"))] pub header: ExecutionPayloadHeaderElectra, - #[superstruct(only(Deneb, Electra))] + #[superstruct(only(Fulu), partial_getter(rename = "header_fulu"))] + pub header: ExecutionPayloadHeaderFulu, + #[superstruct(only(Deneb, Electra, Fulu))] pub blob_kzg_commitments: KzgCommitments, #[serde(with = "serde_utils::quoted_u256")] pub value: Uint256, @@ -85,6 +88,7 @@ impl ForkVersionDeserialize for BuilderBid { ForkName::Capella => Self::Capella(serde_json::from_value(value).map_err(convert_err)?), ForkName::Deneb => Self::Deneb(serde_json::from_value(value).map_err(convert_err)?), ForkName::Electra => Self::Electra(serde_json::from_value(value).map_err(convert_err)?), + ForkName::Fulu => Self::Fulu(serde_json::from_value(value).map_err(convert_err)?), ForkName::Base | ForkName::Altair => { return Err(serde::de::Error::custom(format!( "BuilderBid failed to deserialize: unsupported fork '{}'", diff --git a/consensus/types/src/chain_spec.rs b/consensus/types/src/chain_spec.rs index 9d3308cf23..f0bfeba680 100644 --- a/consensus/types/src/chain_spec.rs +++ b/consensus/types/src/chain_spec.rs @@ -192,6 +192,14 @@ pub struct ChainSpec { pub min_per_epoch_churn_limit_electra: u64, pub max_per_epoch_activation_exit_churn_limit: u64, + /* + * Fulu hard fork params + */ + pub fulu_fork_version: [u8; 4], + /// The Fulu fork epoch is optional, with `None` representing "Fulu never happens". + pub fulu_fork_epoch: Option, + pub fulu_placeholder: u64, + /* * DAS params */ @@ -313,17 +321,20 @@ impl ChainSpec { /// Returns the name of the fork which is active at `epoch`. pub fn fork_name_at_epoch(&self, epoch: Epoch) -> ForkName { - match self.electra_fork_epoch { - Some(fork_epoch) if epoch >= fork_epoch => ForkName::Electra, - _ => match self.deneb_fork_epoch { - Some(fork_epoch) if epoch >= fork_epoch => ForkName::Deneb, - _ => match self.capella_fork_epoch { - Some(fork_epoch) if epoch >= fork_epoch => ForkName::Capella, - _ => match self.bellatrix_fork_epoch { - Some(fork_epoch) if epoch >= fork_epoch => ForkName::Bellatrix, - _ => match self.altair_fork_epoch { - Some(fork_epoch) if epoch >= fork_epoch => ForkName::Altair, - _ => ForkName::Base, + match self.fulu_fork_epoch { + Some(fork_epoch) if epoch >= fork_epoch => ForkName::Fulu, + _ => match self.electra_fork_epoch { + Some(fork_epoch) if epoch >= fork_epoch => ForkName::Electra, + _ => match self.deneb_fork_epoch { + Some(fork_epoch) if epoch >= fork_epoch => ForkName::Deneb, + _ => match self.capella_fork_epoch { + Some(fork_epoch) if epoch >= fork_epoch => ForkName::Capella, + _ => match self.bellatrix_fork_epoch { + Some(fork_epoch) if epoch >= fork_epoch => ForkName::Bellatrix, + _ => match self.altair_fork_epoch { + Some(fork_epoch) if epoch >= fork_epoch => ForkName::Altair, + _ => ForkName::Base, + }, }, }, }, @@ -340,6 +351,7 @@ impl ChainSpec { ForkName::Capella => self.capella_fork_version, ForkName::Deneb => self.deneb_fork_version, ForkName::Electra => self.electra_fork_version, + ForkName::Fulu => self.fulu_fork_version, } } @@ -352,6 +364,7 @@ impl ChainSpec { ForkName::Capella => self.capella_fork_epoch, ForkName::Deneb => self.deneb_fork_epoch, ForkName::Electra => self.electra_fork_epoch, + ForkName::Fulu => self.fulu_fork_epoch, } } @@ -802,6 +815,13 @@ impl ChainSpec { }) .expect("calculation does not overflow"), + /* + * Fulu hard fork params + */ + fulu_fork_version: [0x06, 0x00, 0x00, 0x00], + fulu_fork_epoch: None, + fulu_placeholder: 0, + /* * DAS params */ @@ -917,6 +937,9 @@ impl ChainSpec { u64::checked_pow(2, 7)?.checked_mul(u64::checked_pow(10, 9)?) }) .expect("calculation does not overflow"), + // Fulu + fulu_fork_version: [0x06, 0x00, 0x00, 0x01], + fulu_fork_epoch: None, // PeerDAS eip7594_fork_epoch: None, // Other @@ -1121,6 +1144,13 @@ impl ChainSpec { }) .expect("calculation does not overflow"), + /* + * Fulu hard fork params + */ + fulu_fork_version: [0x06, 0x00, 0x00, 0x64], + fulu_fork_epoch: None, + fulu_placeholder: 0, + /* * DAS params */ @@ -1255,6 +1285,14 @@ pub struct Config { #[serde(deserialize_with = "deserialize_fork_epoch")] pub electra_fork_epoch: Option>, + #[serde(default = "default_fulu_fork_version")] + #[serde(with = "serde_utils::bytes_4_hex")] + fulu_fork_version: [u8; 4], + #[serde(default)] + #[serde(serialize_with = "serialize_fork_epoch")] + #[serde(deserialize_with = "deserialize_fork_epoch")] + pub fulu_fork_epoch: Option>, + #[serde(default)] #[serde(serialize_with = "serialize_fork_epoch")] #[serde(deserialize_with = "deserialize_fork_epoch")] @@ -1392,6 +1430,11 @@ fn default_electra_fork_version() -> [u8; 4] { [0xff, 0xff, 0xff, 0xff] } +fn default_fulu_fork_version() -> [u8; 4] { + // This value shouldn't be used. + [0xff, 0xff, 0xff, 0xff] +} + /// Placeholder value: 2^256-2^10 (115792089237316195423570985008687907853269984665640564039457584007913129638912). /// /// Taken from https://github.com/ethereum/consensus-specs/blob/d5e4828aecafaf1c57ef67a5f23c4ae7b08c5137/configs/mainnet.yaml#L15-L16 @@ -1655,6 +1698,11 @@ impl Config { .electra_fork_epoch .map(|epoch| MaybeQuoted { value: epoch }), + fulu_fork_version: spec.fulu_fork_version, + fulu_fork_epoch: spec + .fulu_fork_epoch + .map(|epoch| MaybeQuoted { value: epoch }), + eip7594_fork_epoch: spec .eip7594_fork_epoch .map(|epoch| MaybeQuoted { value: epoch }), @@ -1738,6 +1786,8 @@ impl Config { deneb_fork_version, electra_fork_epoch, electra_fork_version, + fulu_fork_epoch, + fulu_fork_version, eip7594_fork_epoch, seconds_per_slot, seconds_per_eth1_block, @@ -1801,6 +1851,8 @@ impl Config { deneb_fork_version, electra_fork_epoch: electra_fork_epoch.map(|q| q.value), electra_fork_version, + fulu_fork_epoch: fulu_fork_epoch.map(|q| q.value), + fulu_fork_version, eip7594_fork_epoch: eip7594_fork_epoch.map(|q| q.value), seconds_per_slot, seconds_per_eth1_block, diff --git a/consensus/types/src/config_and_preset.rs b/consensus/types/src/config_and_preset.rs index c80d678b2a..235bf20238 100644 --- a/consensus/types/src/config_and_preset.rs +++ b/consensus/types/src/config_and_preset.rs @@ -1,6 +1,6 @@ use crate::{ consts::altair, consts::deneb, AltairPreset, BasePreset, BellatrixPreset, CapellaPreset, - ChainSpec, Config, DenebPreset, ElectraPreset, EthSpec, ForkName, + ChainSpec, Config, DenebPreset, ElectraPreset, EthSpec, ForkName, FuluPreset, }; use maplit::hashmap; use serde::{Deserialize, Serialize}; @@ -12,7 +12,7 @@ use superstruct::superstruct; /// /// Mostly useful for the API. #[superstruct( - variants(Capella, Deneb, Electra), + variants(Deneb, Electra, Fulu), variant_attributes(derive(Serialize, Deserialize, Debug, PartialEq, Clone)) )] #[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] @@ -29,12 +29,14 @@ pub struct ConfigAndPreset { pub bellatrix_preset: BellatrixPreset, #[serde(flatten)] pub capella_preset: CapellaPreset, - #[superstruct(only(Deneb, Electra))] #[serde(flatten)] pub deneb_preset: DenebPreset, - #[superstruct(only(Electra))] + #[superstruct(only(Electra, Fulu))] #[serde(flatten)] pub electra_preset: ElectraPreset, + #[superstruct(only(Fulu))] + #[serde(flatten)] + pub fulu_preset: FuluPreset, /// The `extra_fields` map allows us to gracefully decode fields intended for future hard forks. #[serde(flatten)] pub extra_fields: HashMap, @@ -48,13 +50,31 @@ impl ConfigAndPreset { let altair_preset = AltairPreset::from_chain_spec::(spec); let bellatrix_preset = BellatrixPreset::from_chain_spec::(spec); let capella_preset = CapellaPreset::from_chain_spec::(spec); + let deneb_preset = DenebPreset::from_chain_spec::(spec); let extra_fields = get_extra_fields(spec); - if spec.electra_fork_epoch.is_some() + if spec.fulu_fork_epoch.is_some() + || fork_name.is_none() + || fork_name == Some(ForkName::Fulu) + { + let electra_preset = ElectraPreset::from_chain_spec::(spec); + let fulu_preset = FuluPreset::from_chain_spec::(spec); + + ConfigAndPreset::Fulu(ConfigAndPresetFulu { + config, + base_preset, + altair_preset, + bellatrix_preset, + capella_preset, + deneb_preset, + electra_preset, + fulu_preset, + extra_fields, + }) + } else if spec.electra_fork_epoch.is_some() || fork_name.is_none() || fork_name == Some(ForkName::Electra) { - let deneb_preset = DenebPreset::from_chain_spec::(spec); let electra_preset = ElectraPreset::from_chain_spec::(spec); ConfigAndPreset::Electra(ConfigAndPresetElectra { @@ -67,11 +87,7 @@ impl ConfigAndPreset { electra_preset, extra_fields, }) - } else if spec.deneb_fork_epoch.is_some() - || fork_name.is_none() - || fork_name == Some(ForkName::Deneb) - { - let deneb_preset = DenebPreset::from_chain_spec::(spec); + } else { ConfigAndPreset::Deneb(ConfigAndPresetDeneb { config, base_preset, @@ -81,15 +97,6 @@ impl ConfigAndPreset { deneb_preset, extra_fields, }) - } else { - ConfigAndPreset::Capella(ConfigAndPresetCapella { - config, - base_preset, - altair_preset, - bellatrix_preset, - capella_preset, - extra_fields, - }) } } } @@ -164,8 +171,8 @@ mod test { .write(false) .open(tmp_file.as_ref()) .expect("error while opening the file"); - let from: ConfigAndPresetElectra = + let from: ConfigAndPresetFulu = serde_yaml::from_reader(reader).expect("error while deserializing"); - assert_eq!(ConfigAndPreset::Electra(from), yamlconfig); + assert_eq!(ConfigAndPreset::Fulu(from), yamlconfig); } } diff --git a/consensus/types/src/execution_payload.rs b/consensus/types/src/execution_payload.rs index 9f16b676a6..c619d61487 100644 --- a/consensus/types/src/execution_payload.rs +++ b/consensus/types/src/execution_payload.rs @@ -15,7 +15,7 @@ pub type Transactions = VariableList< pub type Withdrawals = VariableList::MaxWithdrawalsPerPayload>; #[superstruct( - variants(Bellatrix, Capella, Deneb, Electra), + variants(Bellatrix, Capella, Deneb, Electra, Fulu), variant_attributes( derive( Default, @@ -82,12 +82,12 @@ pub struct ExecutionPayload { pub block_hash: ExecutionBlockHash, #[serde(with = "ssz_types::serde_utils::list_of_hex_var_list")] pub transactions: Transactions, - #[superstruct(only(Capella, Deneb, Electra))] + #[superstruct(only(Capella, Deneb, Electra, Fulu))] pub withdrawals: Withdrawals, - #[superstruct(only(Deneb, Electra), partial_getter(copy))] + #[superstruct(only(Deneb, Electra, Fulu), partial_getter(copy))] #[serde(with = "serde_utils::quoted_u64")] pub blob_gas_used: u64, - #[superstruct(only(Deneb, Electra), partial_getter(copy))] + #[superstruct(only(Deneb, Electra, Fulu), partial_getter(copy))] #[serde(with = "serde_utils::quoted_u64")] pub excess_blob_gas: u64, } @@ -114,6 +114,7 @@ impl ExecutionPayload { ForkName::Capella => ExecutionPayloadCapella::from_ssz_bytes(bytes).map(Self::Capella), ForkName::Deneb => ExecutionPayloadDeneb::from_ssz_bytes(bytes).map(Self::Deneb), ForkName::Electra => ExecutionPayloadElectra::from_ssz_bytes(bytes).map(Self::Electra), + ForkName::Fulu => ExecutionPayloadFulu::from_ssz_bytes(bytes).map(Self::Fulu), } } @@ -166,6 +167,19 @@ impl ExecutionPayload { // Max size of variable length `withdrawals` field + (E::max_withdrawals_per_payload() * ::ssz_fixed_len()) } + + #[allow(clippy::arithmetic_side_effects)] + /// Returns the maximum size of an execution payload. + pub fn max_execution_payload_fulu_size() -> usize { + // Fixed part + ExecutionPayloadFulu::::default().as_ssz_bytes().len() + // Max size of variable length `extra_data` field + + (E::max_extra_data_bytes() * ::ssz_fixed_len()) + // Max size of variable length `transactions` field + + (E::max_transactions_per_payload() * (ssz::BYTES_PER_LENGTH_OFFSET + E::max_bytes_per_transaction())) + // Max size of variable length `withdrawals` field + + (E::max_withdrawals_per_payload() * ::ssz_fixed_len()) + } } impl ForkVersionDeserialize for ExecutionPayload { @@ -184,6 +198,7 @@ impl ForkVersionDeserialize for ExecutionPayload { ForkName::Capella => Self::Capella(serde_json::from_value(value).map_err(convert_err)?), ForkName::Deneb => Self::Deneb(serde_json::from_value(value).map_err(convert_err)?), ForkName::Electra => Self::Electra(serde_json::from_value(value).map_err(convert_err)?), + ForkName::Fulu => Self::Fulu(serde_json::from_value(value).map_err(convert_err)?), ForkName::Base | ForkName::Altair => { return Err(serde::de::Error::custom(format!( "ExecutionPayload failed to deserialize: unsupported fork '{}'", @@ -201,6 +216,7 @@ impl ExecutionPayload { ExecutionPayload::Capella(_) => ForkName::Capella, ExecutionPayload::Deneb(_) => ForkName::Deneb, ExecutionPayload::Electra(_) => ForkName::Electra, + ExecutionPayload::Fulu(_) => ForkName::Fulu, } } } diff --git a/consensus/types/src/execution_payload_header.rs b/consensus/types/src/execution_payload_header.rs index 4bfbfee9bf..3012041b8b 100644 --- a/consensus/types/src/execution_payload_header.rs +++ b/consensus/types/src/execution_payload_header.rs @@ -8,7 +8,7 @@ use tree_hash::TreeHash; use tree_hash_derive::TreeHash; #[superstruct( - variants(Bellatrix, Capella, Deneb, Electra), + variants(Bellatrix, Capella, Deneb, Electra, Fulu), variant_attributes( derive( Default, @@ -78,12 +78,12 @@ pub struct ExecutionPayloadHeader { pub block_hash: ExecutionBlockHash, #[superstruct(getter(copy))] pub transactions_root: Hash256, - #[superstruct(only(Capella, Deneb, Electra), partial_getter(copy))] + #[superstruct(only(Capella, Deneb, Electra, Fulu), partial_getter(copy))] pub withdrawals_root: Hash256, - #[superstruct(only(Deneb, Electra), partial_getter(copy))] + #[superstruct(only(Deneb, Electra, Fulu), partial_getter(copy))] #[serde(with = "serde_utils::quoted_u64")] pub blob_gas_used: u64, - #[superstruct(only(Deneb, Electra), partial_getter(copy))] + #[superstruct(only(Deneb, Electra, Fulu), partial_getter(copy))] #[serde(with = "serde_utils::quoted_u64")] pub excess_blob_gas: u64, } @@ -108,18 +108,18 @@ impl ExecutionPayloadHeader { ForkName::Electra => { ExecutionPayloadHeaderElectra::from_ssz_bytes(bytes).map(Self::Electra) } + ForkName::Fulu => ExecutionPayloadHeaderFulu::from_ssz_bytes(bytes).map(Self::Fulu), } } #[allow(clippy::arithmetic_side_effects)] pub fn ssz_max_var_len_for_fork(fork_name: ForkName) -> usize { - // Matching here in case variable fields are added in future forks. - match fork_name { - ForkName::Base | ForkName::Altair => 0, - ForkName::Bellatrix | ForkName::Capella | ForkName::Deneb | ForkName::Electra => { - // Max size of variable length `extra_data` field - E::max_extra_data_bytes() * ::ssz_fixed_len() - } + // TODO(newfork): Add a new case here if there are new variable fields + if fork_name.bellatrix_enabled() { + // Max size of variable length `extra_data` field + E::max_extra_data_bytes() * ::ssz_fixed_len() + } else { + 0 } } @@ -129,6 +129,7 @@ impl ExecutionPayloadHeader { ExecutionPayloadHeader::Capella(_) => ForkName::Capella, ExecutionPayloadHeader::Deneb(_) => ForkName::Deneb, ExecutionPayloadHeader::Electra(_) => ForkName::Electra, + ExecutionPayloadHeader::Fulu(_) => ForkName::Fulu, } } } @@ -212,6 +213,30 @@ impl ExecutionPayloadHeaderDeneb { } } +impl ExecutionPayloadHeaderElectra { + pub fn upgrade_to_fulu(&self) -> ExecutionPayloadHeaderFulu { + ExecutionPayloadHeaderFulu { + parent_hash: self.parent_hash, + fee_recipient: self.fee_recipient, + state_root: self.state_root, + receipts_root: self.receipts_root, + logs_bloom: self.logs_bloom.clone(), + prev_randao: self.prev_randao, + block_number: self.block_number, + gas_limit: self.gas_limit, + gas_used: self.gas_used, + timestamp: self.timestamp, + extra_data: self.extra_data.clone(), + base_fee_per_gas: self.base_fee_per_gas, + block_hash: self.block_hash, + transactions_root: self.transactions_root, + withdrawals_root: self.withdrawals_root, + blob_gas_used: self.blob_gas_used, + excess_blob_gas: self.excess_blob_gas, + } + } +} + impl<'a, E: EthSpec> From<&'a ExecutionPayloadBellatrix> for ExecutionPayloadHeaderBellatrix { fn from(payload: &'a ExecutionPayloadBellatrix) -> Self { Self { @@ -303,6 +328,30 @@ impl<'a, E: EthSpec> From<&'a ExecutionPayloadElectra> for ExecutionPayloadHe } } +impl<'a, E: EthSpec> From<&'a ExecutionPayloadFulu> for ExecutionPayloadHeaderFulu { + fn from(payload: &'a ExecutionPayloadFulu) -> Self { + Self { + parent_hash: payload.parent_hash, + fee_recipient: payload.fee_recipient, + state_root: payload.state_root, + receipts_root: payload.receipts_root, + logs_bloom: payload.logs_bloom.clone(), + prev_randao: payload.prev_randao, + block_number: payload.block_number, + gas_limit: payload.gas_limit, + gas_used: payload.gas_used, + timestamp: payload.timestamp, + extra_data: payload.extra_data.clone(), + base_fee_per_gas: payload.base_fee_per_gas, + block_hash: payload.block_hash, + transactions_root: payload.transactions.tree_hash_root(), + withdrawals_root: payload.withdrawals.tree_hash_root(), + blob_gas_used: payload.blob_gas_used, + excess_blob_gas: payload.excess_blob_gas, + } + } +} + // These impls are required to work around an inelegance in `to_execution_payload_header`. // They only clone headers so they should be relatively cheap. impl<'a, E: EthSpec> From<&'a Self> for ExecutionPayloadHeaderBellatrix { @@ -329,6 +378,12 @@ impl<'a, E: EthSpec> From<&'a Self> for ExecutionPayloadHeaderElectra { } } +impl<'a, E: EthSpec> From<&'a Self> for ExecutionPayloadHeaderFulu { + fn from(payload: &'a Self) -> Self { + payload.clone() + } +} + impl<'a, E: EthSpec> From> for ExecutionPayloadHeader { fn from(payload: ExecutionPayloadRef<'a, E>) -> Self { map_execution_payload_ref_into_execution_payload_header!( @@ -387,6 +442,9 @@ impl ExecutionPayloadHeaderRefMut<'_, E> { ExecutionPayloadHeaderRefMut::Electra(mut_ref) => { *mut_ref = header.try_into()?; } + ExecutionPayloadHeaderRefMut::Fulu(mut_ref) => { + *mut_ref = header.try_into()?; + } } Ok(()) } @@ -404,6 +462,16 @@ impl TryFrom> for ExecutionPayloadHeaderEl } } +impl TryFrom> for ExecutionPayloadHeaderFulu { + type Error = BeaconStateError; + fn try_from(header: ExecutionPayloadHeader) -> Result { + match header { + ExecutionPayloadHeader::Fulu(execution_payload_header) => Ok(execution_payload_header), + _ => Err(BeaconStateError::IncorrectStateVariant), + } + } +} + impl ForkVersionDeserialize for ExecutionPayloadHeader { fn deserialize_by_fork<'de, D: serde::Deserializer<'de>>( value: serde_json::value::Value, @@ -423,6 +491,7 @@ impl ForkVersionDeserialize for ExecutionPayloadHeader { ForkName::Capella => Self::Capella(serde_json::from_value(value).map_err(convert_err)?), ForkName::Deneb => Self::Deneb(serde_json::from_value(value).map_err(convert_err)?), ForkName::Electra => Self::Electra(serde_json::from_value(value).map_err(convert_err)?), + ForkName::Fulu => Self::Fulu(serde_json::from_value(value).map_err(convert_err)?), ForkName::Base | ForkName::Altair => { return Err(serde::de::Error::custom(format!( "ExecutionPayloadHeader failed to deserialize: unsupported fork '{}'", diff --git a/consensus/types/src/fork_context.rs b/consensus/types/src/fork_context.rs index 0f7f0eb769..33f1c51d44 100644 --- a/consensus/types/src/fork_context.rs +++ b/consensus/types/src/fork_context.rs @@ -69,6 +69,13 @@ impl ForkContext { )); } + if spec.fulu_fork_epoch.is_some() { + fork_to_digest.push(( + ForkName::Fulu, + ChainSpec::compute_fork_digest(spec.fulu_fork_version, genesis_validators_root), + )); + } + let fork_to_digest: HashMap = fork_to_digest.into_iter().collect(); let digest_to_fork = fork_to_digest diff --git a/consensus/types/src/fork_name.rs b/consensus/types/src/fork_name.rs index 51a5b3813b..b61e0a4d4a 100644 --- a/consensus/types/src/fork_name.rs +++ b/consensus/types/src/fork_name.rs @@ -17,6 +17,7 @@ pub enum ForkName { Capella, Deneb, Electra, + Fulu, } impl ForkName { @@ -28,6 +29,7 @@ impl ForkName { ForkName::Capella, ForkName::Deneb, ForkName::Electra, + ForkName::Fulu, ] } @@ -38,6 +40,7 @@ impl ForkName { (ForkName::Capella, spec.capella_fork_epoch), (ForkName::Deneb, spec.deneb_fork_epoch), (ForkName::Electra, spec.electra_fork_epoch), + (ForkName::Fulu, spec.fulu_fork_epoch), ] } @@ -57,6 +60,7 @@ impl ForkName { spec.capella_fork_epoch = None; spec.deneb_fork_epoch = None; spec.electra_fork_epoch = None; + spec.fulu_fork_epoch = None; spec } ForkName::Altair => { @@ -65,6 +69,7 @@ impl ForkName { spec.capella_fork_epoch = None; spec.deneb_fork_epoch = None; spec.electra_fork_epoch = None; + spec.fulu_fork_epoch = None; spec } ForkName::Bellatrix => { @@ -73,6 +78,7 @@ impl ForkName { spec.capella_fork_epoch = None; spec.deneb_fork_epoch = None; spec.electra_fork_epoch = None; + spec.fulu_fork_epoch = None; spec } ForkName::Capella => { @@ -81,6 +87,7 @@ impl ForkName { spec.capella_fork_epoch = Some(Epoch::new(0)); spec.deneb_fork_epoch = None; spec.electra_fork_epoch = None; + spec.fulu_fork_epoch = None; spec } ForkName::Deneb => { @@ -89,6 +96,7 @@ impl ForkName { spec.capella_fork_epoch = Some(Epoch::new(0)); spec.deneb_fork_epoch = Some(Epoch::new(0)); spec.electra_fork_epoch = None; + spec.fulu_fork_epoch = None; spec } ForkName::Electra => { @@ -97,6 +105,16 @@ impl ForkName { spec.capella_fork_epoch = Some(Epoch::new(0)); spec.deneb_fork_epoch = Some(Epoch::new(0)); spec.electra_fork_epoch = Some(Epoch::new(0)); + spec.fulu_fork_epoch = None; + spec + } + ForkName::Fulu => { + spec.altair_fork_epoch = Some(Epoch::new(0)); + spec.bellatrix_fork_epoch = Some(Epoch::new(0)); + spec.capella_fork_epoch = Some(Epoch::new(0)); + spec.deneb_fork_epoch = Some(Epoch::new(0)); + spec.electra_fork_epoch = Some(Epoch::new(0)); + spec.fulu_fork_epoch = Some(Epoch::new(0)); spec } } @@ -113,6 +131,7 @@ impl ForkName { ForkName::Capella => Some(ForkName::Bellatrix), ForkName::Deneb => Some(ForkName::Capella), ForkName::Electra => Some(ForkName::Deneb), + ForkName::Fulu => Some(ForkName::Electra), } } @@ -126,7 +145,8 @@ impl ForkName { ForkName::Bellatrix => Some(ForkName::Capella), ForkName::Capella => Some(ForkName::Deneb), ForkName::Deneb => Some(ForkName::Electra), - ForkName::Electra => None, + ForkName::Electra => Some(ForkName::Fulu), + ForkName::Fulu => None, } } @@ -149,6 +169,10 @@ impl ForkName { pub fn electra_enabled(self) -> bool { self >= ForkName::Electra } + + pub fn fulu_enabled(self) -> bool { + self >= ForkName::Fulu + } } /// Map a fork name into a fork-versioned superstruct type like `BeaconBlock`. @@ -200,6 +224,10 @@ macro_rules! map_fork_name_with { let (value, extra_data) = $body; ($t::Electra(value), extra_data) } + ForkName::Fulu => { + let (value, extra_data) = $body; + ($t::Fulu(value), extra_data) + } } }; } @@ -215,6 +243,7 @@ impl FromStr for ForkName { "capella" => ForkName::Capella, "deneb" => ForkName::Deneb, "electra" => ForkName::Electra, + "fulu" => ForkName::Fulu, _ => return Err(format!("unknown fork name: {}", fork_name)), }) } @@ -229,6 +258,7 @@ impl Display for ForkName { ForkName::Capella => "capella".fmt(f), ForkName::Deneb => "deneb".fmt(f), ForkName::Electra => "electra".fmt(f), + ForkName::Fulu => "fulu".fmt(f), } } } diff --git a/consensus/types/src/lib.rs b/consensus/types/src/lib.rs index dd304c6296..282f27a517 100644 --- a/consensus/types/src/lib.rs +++ b/consensus/types/src/lib.rs @@ -126,13 +126,13 @@ pub use crate::attester_slashing::{ }; pub use crate::beacon_block::{ BeaconBlock, BeaconBlockAltair, BeaconBlockBase, BeaconBlockBellatrix, BeaconBlockCapella, - BeaconBlockDeneb, BeaconBlockElectra, BeaconBlockRef, BeaconBlockRefMut, BlindedBeaconBlock, - BlockImportSource, EmptyBlock, + BeaconBlockDeneb, BeaconBlockElectra, BeaconBlockFulu, BeaconBlockRef, BeaconBlockRefMut, + BlindedBeaconBlock, BlockImportSource, EmptyBlock, }; pub use crate::beacon_block_body::{ BeaconBlockBody, BeaconBlockBodyAltair, BeaconBlockBodyBase, BeaconBlockBodyBellatrix, - BeaconBlockBodyCapella, BeaconBlockBodyDeneb, BeaconBlockBodyElectra, BeaconBlockBodyRef, - BeaconBlockBodyRefMut, + BeaconBlockBodyCapella, BeaconBlockBodyDeneb, BeaconBlockBodyElectra, BeaconBlockBodyFulu, + BeaconBlockBodyRef, BeaconBlockBodyRefMut, }; pub use crate::beacon_block_header::BeaconBlockHeader; pub use crate::beacon_committee::{BeaconCommittee, OwnedBeaconCommittee}; @@ -142,7 +142,7 @@ pub use crate::bls_to_execution_change::BlsToExecutionChange; pub use crate::chain_spec::{ChainSpec, Config, Domain}; pub use crate::checkpoint::Checkpoint; pub use crate::config_and_preset::{ - ConfigAndPreset, ConfigAndPresetCapella, ConfigAndPresetDeneb, ConfigAndPresetElectra, + ConfigAndPreset, ConfigAndPresetDeneb, ConfigAndPresetElectra, ConfigAndPresetFulu, }; pub use crate::consolidation_request::ConsolidationRequest; pub use crate::contribution_and_proof::ContributionAndProof; @@ -163,12 +163,13 @@ pub use crate::execution_block_hash::ExecutionBlockHash; pub use crate::execution_block_header::{EncodableExecutionBlockHeader, ExecutionBlockHeader}; pub use crate::execution_payload::{ ExecutionPayload, ExecutionPayloadBellatrix, ExecutionPayloadCapella, ExecutionPayloadDeneb, - ExecutionPayloadElectra, ExecutionPayloadRef, Transaction, Transactions, Withdrawals, + ExecutionPayloadElectra, ExecutionPayloadFulu, ExecutionPayloadRef, Transaction, Transactions, + Withdrawals, }; pub use crate::execution_payload_header::{ ExecutionPayloadHeader, ExecutionPayloadHeaderBellatrix, ExecutionPayloadHeaderCapella, - ExecutionPayloadHeaderDeneb, ExecutionPayloadHeaderElectra, ExecutionPayloadHeaderRef, - ExecutionPayloadHeaderRefMut, + ExecutionPayloadHeaderDeneb, ExecutionPayloadHeaderElectra, ExecutionPayloadHeaderFulu, + ExecutionPayloadHeaderRef, ExecutionPayloadHeaderRefMut, }; pub use crate::execution_requests::{ExecutionRequests, RequestPrefix}; pub use crate::fork::Fork; @@ -183,31 +184,33 @@ pub use crate::indexed_attestation::{ }; pub use crate::light_client_bootstrap::{ LightClientBootstrap, LightClientBootstrapAltair, LightClientBootstrapCapella, - LightClientBootstrapDeneb, LightClientBootstrapElectra, + LightClientBootstrapDeneb, LightClientBootstrapElectra, LightClientBootstrapFulu, }; pub use crate::light_client_finality_update::{ LightClientFinalityUpdate, LightClientFinalityUpdateAltair, LightClientFinalityUpdateCapella, LightClientFinalityUpdateDeneb, LightClientFinalityUpdateElectra, + LightClientFinalityUpdateFulu, }; pub use crate::light_client_header::{ LightClientHeader, LightClientHeaderAltair, LightClientHeaderCapella, LightClientHeaderDeneb, - LightClientHeaderElectra, + LightClientHeaderElectra, LightClientHeaderFulu, }; pub use crate::light_client_optimistic_update::{ LightClientOptimisticUpdate, LightClientOptimisticUpdateAltair, LightClientOptimisticUpdateCapella, LightClientOptimisticUpdateDeneb, - LightClientOptimisticUpdateElectra, + LightClientOptimisticUpdateElectra, LightClientOptimisticUpdateFulu, }; pub use crate::light_client_update::{ Error as LightClientUpdateError, LightClientUpdate, LightClientUpdateAltair, - LightClientUpdateCapella, LightClientUpdateDeneb, LightClientUpdateElectra, MerkleProof, + LightClientUpdateCapella, LightClientUpdateDeneb, LightClientUpdateElectra, + LightClientUpdateFulu, MerkleProof, }; pub use crate::participation_flags::ParticipationFlags; pub use crate::payload::{ AbstractExecPayload, BlindedPayload, BlindedPayloadBellatrix, BlindedPayloadCapella, - BlindedPayloadDeneb, BlindedPayloadElectra, BlindedPayloadRef, BlockType, ExecPayload, - FullPayload, FullPayloadBellatrix, FullPayloadCapella, FullPayloadDeneb, FullPayloadElectra, - FullPayloadRef, OwnedExecPayload, + BlindedPayloadDeneb, BlindedPayloadElectra, BlindedPayloadFulu, BlindedPayloadRef, BlockType, + ExecPayload, FullPayload, FullPayloadBellatrix, FullPayloadCapella, FullPayloadDeneb, + FullPayloadElectra, FullPayloadFulu, FullPayloadRef, OwnedExecPayload, }; pub use crate::pending_attestation::PendingAttestation; pub use crate::pending_consolidation::PendingConsolidation; @@ -215,6 +218,7 @@ pub use crate::pending_deposit::PendingDeposit; pub use crate::pending_partial_withdrawal::PendingPartialWithdrawal; pub use crate::preset::{ AltairPreset, BasePreset, BellatrixPreset, CapellaPreset, DenebPreset, ElectraPreset, + FuluPreset, }; pub use crate::proposer_preparation_data::ProposerPreparationData; pub use crate::proposer_slashing::ProposerSlashing; @@ -229,7 +233,7 @@ pub use crate::signed_beacon_block::{ ssz_tagged_signed_beacon_block, ssz_tagged_signed_beacon_block_arc, SignedBeaconBlock, SignedBeaconBlockAltair, SignedBeaconBlockBase, SignedBeaconBlockBellatrix, SignedBeaconBlockCapella, SignedBeaconBlockDeneb, SignedBeaconBlockElectra, - SignedBeaconBlockHash, SignedBlindedBeaconBlock, + SignedBeaconBlockFulu, SignedBeaconBlockHash, SignedBlindedBeaconBlock, }; pub use crate::signed_beacon_block_header::SignedBeaconBlockHeader; pub use crate::signed_bls_to_execution_change::SignedBlsToExecutionChange; diff --git a/consensus/types/src/light_client_bootstrap.rs b/consensus/types/src/light_client_bootstrap.rs index 21a7e5416f..aa0d8836d1 100644 --- a/consensus/types/src/light_client_bootstrap.rs +++ b/consensus/types/src/light_client_bootstrap.rs @@ -2,7 +2,7 @@ use crate::{ light_client_update::*, test_utils::TestRandom, BeaconState, ChainSpec, EthSpec, FixedVector, ForkName, ForkVersionDeserialize, Hash256, LightClientHeader, LightClientHeaderAltair, LightClientHeaderCapella, LightClientHeaderDeneb, LightClientHeaderElectra, - SignedBlindedBeaconBlock, Slot, SyncCommittee, + LightClientHeaderFulu, SignedBlindedBeaconBlock, Slot, SyncCommittee, }; use derivative::Derivative; use serde::{Deserialize, Deserializer, Serialize}; @@ -17,7 +17,7 @@ use tree_hash_derive::TreeHash; /// A LightClientBootstrap is the initializer we send over to light_client nodes /// that are trying to generate their basic storage when booting up. #[superstruct( - variants(Altair, Capella, Deneb, Electra), + variants(Altair, Capella, Deneb, Electra, Fulu), variant_attributes( derive( Debug, @@ -54,6 +54,8 @@ pub struct LightClientBootstrap { pub header: LightClientHeaderDeneb, #[superstruct(only(Electra), partial_getter(rename = "header_electra"))] pub header: LightClientHeaderElectra, + #[superstruct(only(Fulu), partial_getter(rename = "header_fulu"))] + pub header: LightClientHeaderFulu, /// The `SyncCommittee` used in the requested period. pub current_sync_committee: Arc>, /// Merkle proof for sync committee @@ -63,7 +65,7 @@ pub struct LightClientBootstrap { )] pub current_sync_committee_branch: FixedVector, #[superstruct( - only(Electra), + only(Electra, Fulu), partial_getter(rename = "current_sync_committee_branch_electra") )] pub current_sync_committee_branch: FixedVector, @@ -79,6 +81,7 @@ impl LightClientBootstrap { Self::Capella(_) => func(ForkName::Capella), Self::Deneb(_) => func(ForkName::Deneb), Self::Electra(_) => func(ForkName::Electra), + Self::Fulu(_) => func(ForkName::Fulu), } } @@ -97,6 +100,7 @@ impl LightClientBootstrap { ForkName::Capella => Self::Capella(LightClientBootstrapCapella::from_ssz_bytes(bytes)?), ForkName::Deneb => Self::Deneb(LightClientBootstrapDeneb::from_ssz_bytes(bytes)?), ForkName::Electra => Self::Electra(LightClientBootstrapElectra::from_ssz_bytes(bytes)?), + ForkName::Fulu => Self::Fulu(LightClientBootstrapFulu::from_ssz_bytes(bytes)?), ForkName::Base => { return Err(ssz::DecodeError::BytesInvalid(format!( "LightClientBootstrap decoding for {fork_name} not implemented" @@ -117,6 +121,7 @@ impl LightClientBootstrap { ForkName::Capella => as Encode>::ssz_fixed_len(), ForkName::Deneb => as Encode>::ssz_fixed_len(), ForkName::Electra => as Encode>::ssz_fixed_len(), + ForkName::Fulu => as Encode>::ssz_fixed_len(), }; fixed_len + LightClientHeader::::ssz_max_var_len_for_fork(fork_name) } @@ -152,6 +157,11 @@ impl LightClientBootstrap { current_sync_committee, current_sync_committee_branch: current_sync_committee_branch.into(), }), + ForkName::Fulu => Self::Fulu(LightClientBootstrapFulu { + header: LightClientHeaderFulu::block_to_light_client_header(block)?, + current_sync_committee, + current_sync_committee_branch: current_sync_committee_branch.into(), + }), }; Ok(light_client_bootstrap) @@ -192,6 +202,11 @@ impl LightClientBootstrap { current_sync_committee, current_sync_committee_branch: current_sync_committee_branch.into(), }), + ForkName::Fulu => Self::Fulu(LightClientBootstrapFulu { + header: LightClientHeaderFulu::block_to_light_client_header(block)?, + current_sync_committee, + current_sync_committee_branch: current_sync_committee_branch.into(), + }), }; Ok(light_client_bootstrap) @@ -241,4 +256,10 @@ mod tests { use crate::{LightClientBootstrapElectra, MainnetEthSpec}; ssz_tests!(LightClientBootstrapElectra); } + + #[cfg(test)] + mod fulu { + use crate::{LightClientBootstrapFulu, MainnetEthSpec}; + ssz_tests!(LightClientBootstrapFulu); + } } diff --git a/consensus/types/src/light_client_finality_update.rs b/consensus/types/src/light_client_finality_update.rs index ba2f2083cd..ee3b53c853 100644 --- a/consensus/types/src/light_client_finality_update.rs +++ b/consensus/types/src/light_client_finality_update.rs @@ -3,7 +3,7 @@ use crate::ChainSpec; use crate::{ light_client_update::*, test_utils::TestRandom, ForkName, ForkVersionDeserialize, LightClientHeaderAltair, LightClientHeaderCapella, LightClientHeaderDeneb, - LightClientHeaderElectra, SignedBlindedBeaconBlock, + LightClientHeaderElectra, LightClientHeaderFulu, SignedBlindedBeaconBlock, }; use derivative::Derivative; use serde::{Deserialize, Deserializer, Serialize}; @@ -16,7 +16,7 @@ use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; #[superstruct( - variants(Altair, Capella, Deneb, Electra), + variants(Altair, Capella, Deneb, Electra, Fulu), variant_attributes( derive( Debug, @@ -53,6 +53,8 @@ pub struct LightClientFinalityUpdate { pub attested_header: LightClientHeaderDeneb, #[superstruct(only(Electra), partial_getter(rename = "attested_header_electra"))] pub attested_header: LightClientHeaderElectra, + #[superstruct(only(Fulu), partial_getter(rename = "attested_header_fulu"))] + pub attested_header: LightClientHeaderFulu, /// The last `BeaconBlockHeader` from the last attested finalized block (end of epoch). #[superstruct(only(Altair), partial_getter(rename = "finalized_header_altair"))] pub finalized_header: LightClientHeaderAltair, @@ -62,13 +64,18 @@ pub struct LightClientFinalityUpdate { pub finalized_header: LightClientHeaderDeneb, #[superstruct(only(Electra), partial_getter(rename = "finalized_header_electra"))] pub finalized_header: LightClientHeaderElectra, + #[superstruct(only(Fulu), partial_getter(rename = "finalized_header_fulu"))] + pub finalized_header: LightClientHeaderFulu, /// Merkle proof attesting finalized header. #[superstruct( only(Altair, Capella, Deneb), partial_getter(rename = "finality_branch_altair") )] pub finality_branch: FixedVector, - #[superstruct(only(Electra), partial_getter(rename = "finality_branch_electra"))] + #[superstruct( + only(Electra, Fulu), + partial_getter(rename = "finality_branch_electra") + )] pub finality_branch: FixedVector, /// current sync aggregate pub sync_aggregate: SyncAggregate, @@ -135,6 +142,17 @@ impl LightClientFinalityUpdate { sync_aggregate, signature_slot, }), + ForkName::Fulu => Self::Fulu(LightClientFinalityUpdateFulu { + attested_header: LightClientHeaderFulu::block_to_light_client_header( + attested_block, + )?, + finalized_header: LightClientHeaderFulu::block_to_light_client_header( + finalized_block, + )?, + finality_branch: finality_branch.into(), + sync_aggregate, + signature_slot, + }), ForkName::Base => return Err(Error::AltairForkNotActive), }; @@ -151,6 +169,7 @@ impl LightClientFinalityUpdate { Self::Capella(_) => func(ForkName::Capella), Self::Deneb(_) => func(ForkName::Deneb), Self::Electra(_) => func(ForkName::Electra), + Self::Fulu(_) => func(ForkName::Fulu), } } @@ -173,6 +192,7 @@ impl LightClientFinalityUpdate { ForkName::Electra => { Self::Electra(LightClientFinalityUpdateElectra::from_ssz_bytes(bytes)?) } + ForkName::Fulu => Self::Fulu(LightClientFinalityUpdateFulu::from_ssz_bytes(bytes)?), ForkName::Base => { return Err(ssz::DecodeError::BytesInvalid(format!( "LightClientFinalityUpdate decoding for {fork_name} not implemented" @@ -193,6 +213,7 @@ impl LightClientFinalityUpdate { ForkName::Capella => as Encode>::ssz_fixed_len(), ForkName::Deneb => as Encode>::ssz_fixed_len(), ForkName::Electra => as Encode>::ssz_fixed_len(), + ForkName::Fulu => as Encode>::ssz_fixed_len(), }; // `2 *` because there are two headers in the update fixed_size + 2 * LightClientHeader::::ssz_max_var_len_for_fork(fork_name) @@ -255,4 +276,10 @@ mod tests { use crate::{LightClientFinalityUpdateElectra, MainnetEthSpec}; ssz_tests!(LightClientFinalityUpdateElectra); } + + #[cfg(test)] + mod fulu { + use crate::{LightClientFinalityUpdateFulu, MainnetEthSpec}; + ssz_tests!(LightClientFinalityUpdateFulu); + } } diff --git a/consensus/types/src/light_client_header.rs b/consensus/types/src/light_client_header.rs index 6655e0a093..0be26a7036 100644 --- a/consensus/types/src/light_client_header.rs +++ b/consensus/types/src/light_client_header.rs @@ -4,7 +4,8 @@ use crate::ForkVersionDeserialize; use crate::{light_client_update::*, BeaconBlockBody}; use crate::{ test_utils::TestRandom, EthSpec, ExecutionPayloadHeaderCapella, ExecutionPayloadHeaderDeneb, - ExecutionPayloadHeaderElectra, FixedVector, Hash256, SignedBlindedBeaconBlock, + ExecutionPayloadHeaderElectra, ExecutionPayloadHeaderFulu, FixedVector, Hash256, + SignedBlindedBeaconBlock, }; use crate::{BeaconBlockHeader, ExecutionPayloadHeader}; use derivative::Derivative; @@ -17,7 +18,7 @@ use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; #[superstruct( - variants(Altair, Capella, Deneb, Electra), + variants(Altair, Capella, Deneb, Electra, Fulu), variant_attributes( derive( Debug, @@ -59,8 +60,10 @@ pub struct LightClientHeader { partial_getter(rename = "execution_payload_header_electra") )] pub execution: ExecutionPayloadHeaderElectra, + #[superstruct(only(Fulu), partial_getter(rename = "execution_payload_header_fulu"))] + pub execution: ExecutionPayloadHeaderFulu, - #[superstruct(only(Capella, Deneb, Electra))] + #[superstruct(only(Capella, Deneb, Electra, Fulu))] pub execution_branch: FixedVector, #[ssz(skip_serializing, skip_deserializing)] @@ -92,6 +95,9 @@ impl LightClientHeader { ForkName::Electra => LightClientHeader::Electra( LightClientHeaderElectra::block_to_light_client_header(block)?, ), + ForkName::Fulu => { + LightClientHeader::Fulu(LightClientHeaderFulu::block_to_light_client_header(block)?) + } }; Ok(header) } @@ -110,6 +116,9 @@ impl LightClientHeader { ForkName::Electra => { LightClientHeader::Electra(LightClientHeaderElectra::from_ssz_bytes(bytes)?) } + ForkName::Fulu => { + LightClientHeader::Fulu(LightClientHeaderFulu::from_ssz_bytes(bytes)?) + } ForkName::Base => { return Err(ssz::DecodeError::BytesInvalid(format!( "LightClientHeader decoding for {fork_name} not implemented" @@ -283,6 +292,48 @@ impl Default for LightClientHeaderElectra { } } +impl LightClientHeaderFulu { + pub fn block_to_light_client_header( + block: &SignedBlindedBeaconBlock, + ) -> Result { + let payload = block + .message() + .execution_payload()? + .execution_payload_fulu()?; + + let header = ExecutionPayloadHeaderFulu::from(payload); + let beacon_block_body = BeaconBlockBody::from( + block + .message() + .body_fulu() + .map_err(|_| Error::BeaconBlockBodyError)? + .to_owned(), + ); + + let execution_branch = beacon_block_body + .to_ref() + .block_body_merkle_proof(EXECUTION_PAYLOAD_INDEX)?; + + Ok(LightClientHeaderFulu { + beacon: block.message().block_header(), + execution: header, + execution_branch: FixedVector::new(execution_branch)?, + _phantom_data: PhantomData, + }) + } +} + +impl Default for LightClientHeaderFulu { + fn default() -> Self { + Self { + beacon: BeaconBlockHeader::empty(), + execution: ExecutionPayloadHeaderFulu::default(), + execution_branch: FixedVector::default(), + _phantom_data: PhantomData, + } + } +} + impl ForkVersionDeserialize for LightClientHeader { fn deserialize_by_fork<'de, D: serde::Deserializer<'de>>( value: serde_json::value::Value, @@ -301,6 +352,9 @@ impl ForkVersionDeserialize for LightClientHeader { ForkName::Electra => serde_json::from_value(value) .map(|light_client_header| Self::Electra(light_client_header)) .map_err(serde::de::Error::custom), + ForkName::Fulu => serde_json::from_value(value) + .map(|light_client_header| Self::Fulu(light_client_header)) + .map_err(serde::de::Error::custom), ForkName::Base => Err(serde::de::Error::custom(format!( "LightClientHeader deserialization for {fork_name} not implemented" ))), diff --git a/consensus/types/src/light_client_optimistic_update.rs b/consensus/types/src/light_client_optimistic_update.rs index 209388af87..fcf357757b 100644 --- a/consensus/types/src/light_client_optimistic_update.rs +++ b/consensus/types/src/light_client_optimistic_update.rs @@ -2,7 +2,8 @@ use super::{EthSpec, ForkName, ForkVersionDeserialize, LightClientHeader, Slot, use crate::test_utils::TestRandom; use crate::{ light_client_update::*, ChainSpec, LightClientHeaderAltair, LightClientHeaderCapella, - LightClientHeaderDeneb, LightClientHeaderElectra, SignedBlindedBeaconBlock, + LightClientHeaderDeneb, LightClientHeaderElectra, LightClientHeaderFulu, + SignedBlindedBeaconBlock, }; use derivative::Derivative; use serde::{Deserialize, Deserializer, Serialize}; @@ -18,7 +19,7 @@ use tree_hash_derive::TreeHash; /// A LightClientOptimisticUpdate is the update we send on each slot, /// it is based off the current unfinalized epoch is verified only against BLS signature. #[superstruct( - variants(Altair, Capella, Deneb, Electra), + variants(Altair, Capella, Deneb, Electra, Fulu), variant_attributes( derive( Debug, @@ -55,6 +56,8 @@ pub struct LightClientOptimisticUpdate { pub attested_header: LightClientHeaderDeneb, #[superstruct(only(Electra), partial_getter(rename = "attested_header_electra"))] pub attested_header: LightClientHeaderElectra, + #[superstruct(only(Fulu), partial_getter(rename = "attested_header_fulu"))] + pub attested_header: LightClientHeaderFulu, /// current sync aggregate pub sync_aggregate: SyncAggregate, /// Slot of the sync aggregated signature @@ -102,6 +105,13 @@ impl LightClientOptimisticUpdate { sync_aggregate, signature_slot, }), + ForkName::Fulu => Self::Fulu(LightClientOptimisticUpdateFulu { + attested_header: LightClientHeaderFulu::block_to_light_client_header( + attested_block, + )?, + sync_aggregate, + signature_slot, + }), ForkName::Base => return Err(Error::AltairForkNotActive), }; @@ -117,6 +127,7 @@ impl LightClientOptimisticUpdate { Self::Capella(_) => func(ForkName::Capella), Self::Deneb(_) => func(ForkName::Deneb), Self::Electra(_) => func(ForkName::Electra), + Self::Fulu(_) => func(ForkName::Fulu), } } @@ -155,6 +166,7 @@ impl LightClientOptimisticUpdate { ForkName::Electra => { Self::Electra(LightClientOptimisticUpdateElectra::from_ssz_bytes(bytes)?) } + ForkName::Fulu => Self::Fulu(LightClientOptimisticUpdateFulu::from_ssz_bytes(bytes)?), ForkName::Base => { return Err(ssz::DecodeError::BytesInvalid(format!( "LightClientOptimisticUpdate decoding for {fork_name} not implemented" @@ -175,6 +187,7 @@ impl LightClientOptimisticUpdate { ForkName::Capella => as Encode>::ssz_fixed_len(), ForkName::Deneb => as Encode>::ssz_fixed_len(), ForkName::Electra => as Encode>::ssz_fixed_len(), + ForkName::Fulu => as Encode>::ssz_fixed_len(), }; fixed_len + LightClientHeader::::ssz_max_var_len_for_fork(fork_name) } @@ -238,4 +251,10 @@ mod tests { use crate::{LightClientOptimisticUpdateElectra, MainnetEthSpec}; ssz_tests!(LightClientOptimisticUpdateElectra); } + + #[cfg(test)] + mod fulu { + use crate::{LightClientOptimisticUpdateFulu, MainnetEthSpec}; + ssz_tests!(LightClientOptimisticUpdateFulu); + } } diff --git a/consensus/types/src/light_client_update.rs b/consensus/types/src/light_client_update.rs index c3a50e71c1..0dd91edc3c 100644 --- a/consensus/types/src/light_client_update.rs +++ b/consensus/types/src/light_client_update.rs @@ -4,7 +4,7 @@ use crate::LightClientHeader; use crate::{ beacon_state, test_utils::TestRandom, ChainSpec, Epoch, ForkName, ForkVersionDeserialize, LightClientHeaderAltair, LightClientHeaderCapella, LightClientHeaderDeneb, - SignedBlindedBeaconBlock, + LightClientHeaderFulu, SignedBlindedBeaconBlock, }; use derivative::Derivative; use safe_arith::ArithError; @@ -100,7 +100,7 @@ impl From for Error { /// or to sync up to the last committee period, we need to have one ready for each ALTAIR period /// we go over, note: there is no need to keep all of the updates from [ALTAIR_PERIOD, CURRENT_PERIOD]. #[superstruct( - variants(Altair, Capella, Deneb, Electra), + variants(Altair, Capella, Deneb, Electra, Fulu), variant_attributes( derive( Debug, @@ -137,6 +137,8 @@ pub struct LightClientUpdate { pub attested_header: LightClientHeaderDeneb, #[superstruct(only(Electra), partial_getter(rename = "attested_header_electra"))] pub attested_header: LightClientHeaderElectra, + #[superstruct(only(Fulu), partial_getter(rename = "attested_header_fulu"))] + pub attested_header: LightClientHeaderFulu, /// The `SyncCommittee` used in the next period. pub next_sync_committee: Arc>, // Merkle proof for next sync committee @@ -146,7 +148,7 @@ pub struct LightClientUpdate { )] pub next_sync_committee_branch: NextSyncCommitteeBranch, #[superstruct( - only(Electra), + only(Electra, Fulu), partial_getter(rename = "next_sync_committee_branch_electra") )] pub next_sync_committee_branch: NextSyncCommitteeBranchElectra, @@ -159,13 +161,18 @@ pub struct LightClientUpdate { pub finalized_header: LightClientHeaderDeneb, #[superstruct(only(Electra), partial_getter(rename = "finalized_header_electra"))] pub finalized_header: LightClientHeaderElectra, + #[superstruct(only(Fulu), partial_getter(rename = "finalized_header_fulu"))] + pub finalized_header: LightClientHeaderFulu, /// Merkle proof attesting finalized header. #[superstruct( only(Altair, Capella, Deneb), partial_getter(rename = "finality_branch_altair") )] pub finality_branch: FinalityBranch, - #[superstruct(only(Electra), partial_getter(rename = "finality_branch_electra"))] + #[superstruct( + only(Electra, Fulu), + partial_getter(rename = "finality_branch_electra") + )] pub finality_branch: FinalityBranchElectra, /// current sync aggreggate pub sync_aggregate: SyncAggregate, @@ -285,6 +292,26 @@ impl LightClientUpdate { sync_aggregate: sync_aggregate.clone(), signature_slot: block_slot, }) + } + ForkName::Fulu => { + let attested_header = + LightClientHeaderFulu::block_to_light_client_header(attested_block)?; + + let finalized_header = if let Some(finalized_block) = finalized_block { + LightClientHeaderFulu::block_to_light_client_header(finalized_block)? + } else { + LightClientHeaderFulu::default() + }; + + Self::Fulu(LightClientUpdateFulu { + attested_header, + next_sync_committee, + next_sync_committee_branch: next_sync_committee_branch.into(), + finalized_header, + finality_branch: finality_branch.into(), + sync_aggregate: sync_aggregate.clone(), + signature_slot: block_slot, + }) } // To add a new fork, just append the new fork variant on the latest fork. Forks that // have a distinct execution header will need a new LightClientUpdate variant only // if you need to test or support lightclient usages @@ -301,6 +328,7 @@ impl LightClientUpdate { ForkName::Capella => Self::Capella(LightClientUpdateCapella::from_ssz_bytes(bytes)?), ForkName::Deneb => Self::Deneb(LightClientUpdateDeneb::from_ssz_bytes(bytes)?), ForkName::Electra => Self::Electra(LightClientUpdateElectra::from_ssz_bytes(bytes)?), + ForkName::Fulu => Self::Fulu(LightClientUpdateFulu::from_ssz_bytes(bytes)?), ForkName::Base => { return Err(ssz::DecodeError::BytesInvalid(format!( "LightClientUpdate decoding for {fork_name} not implemented" @@ -317,6 +345,7 @@ impl LightClientUpdate { LightClientUpdate::Capella(update) => update.attested_header.beacon.slot, LightClientUpdate::Deneb(update) => update.attested_header.beacon.slot, LightClientUpdate::Electra(update) => update.attested_header.beacon.slot, + LightClientUpdate::Fulu(update) => update.attested_header.beacon.slot, } } @@ -326,6 +355,7 @@ impl LightClientUpdate { LightClientUpdate::Capella(update) => update.finalized_header.beacon.slot, LightClientUpdate::Deneb(update) => update.finalized_header.beacon.slot, LightClientUpdate::Electra(update) => update.finalized_header.beacon.slot, + LightClientUpdate::Fulu(update) => update.finalized_header.beacon.slot, } } @@ -445,6 +475,7 @@ impl LightClientUpdate { ForkName::Capella => as Encode>::ssz_fixed_len(), ForkName::Deneb => as Encode>::ssz_fixed_len(), ForkName::Electra => as Encode>::ssz_fixed_len(), + ForkName::Fulu => as Encode>::ssz_fixed_len(), }; fixed_len + 2 * LightClientHeader::::ssz_max_var_len_for_fork(fork_name) } @@ -458,6 +489,7 @@ impl LightClientUpdate { Self::Capella(_) => func(ForkName::Capella), Self::Deneb(_) => func(ForkName::Deneb), Self::Electra(_) => func(ForkName::Electra), + Self::Fulu(_) => func(ForkName::Fulu), } } } @@ -513,6 +545,13 @@ mod tests { ssz_tests!(LightClientUpdateElectra); } + #[cfg(test)] + mod fulu { + use super::*; + use crate::MainnetEthSpec; + ssz_tests!(LightClientUpdateFulu); + } + #[test] fn finalized_root_params() { assert!(2usize.pow(FINALIZED_ROOT_PROOF_LEN as u32) <= FINALIZED_ROOT_INDEX); diff --git a/consensus/types/src/payload.rs b/consensus/types/src/payload.rs index e68801840a..abc9afd34c 100644 --- a/consensus/types/src/payload.rs +++ b/consensus/types/src/payload.rs @@ -84,13 +84,15 @@ pub trait AbstractExecPayload: + TryInto + TryInto + TryInto + + TryInto { type Ref<'a>: ExecPayload + Copy + From<&'a Self::Bellatrix> + From<&'a Self::Capella> + From<&'a Self::Deneb> - + From<&'a Self::Electra>; + + From<&'a Self::Electra> + + From<&'a Self::Fulu>; type Bellatrix: OwnedExecPayload + Into @@ -108,10 +110,14 @@ pub trait AbstractExecPayload: + Into + for<'a> From>> + TryFrom>; + type Fulu: OwnedExecPayload + + Into + + for<'a> From>> + + TryFrom>; } #[superstruct( - variants(Bellatrix, Capella, Deneb, Electra), + variants(Bellatrix, Capella, Deneb, Electra, Fulu), variant_attributes( derive( Debug, @@ -157,6 +163,8 @@ pub struct FullPayload { pub execution_payload: ExecutionPayloadDeneb, #[superstruct(only(Electra), partial_getter(rename = "execution_payload_electra"))] pub execution_payload: ExecutionPayloadElectra, + #[superstruct(only(Fulu), partial_getter(rename = "execution_payload_fulu"))] + pub execution_payload: ExecutionPayloadFulu, } impl From> for ExecutionPayload { @@ -273,6 +281,9 @@ impl ExecPayload for FullPayload { FullPayload::Electra(ref inner) => { Ok(inner.execution_payload.withdrawals.tree_hash_root()) } + FullPayload::Fulu(ref inner) => { + Ok(inner.execution_payload.withdrawals.tree_hash_root()) + } } } @@ -283,6 +294,7 @@ impl ExecPayload for FullPayload { } FullPayload::Deneb(ref inner) => Ok(inner.execution_payload.blob_gas_used), FullPayload::Electra(ref inner) => Ok(inner.execution_payload.blob_gas_used), + FullPayload::Fulu(ref inner) => Ok(inner.execution_payload.blob_gas_used), } } @@ -313,6 +325,7 @@ impl FullPayload { ForkName::Capella => Ok(FullPayloadCapella::default().into()), ForkName::Deneb => Ok(FullPayloadDeneb::default().into()), ForkName::Electra => Ok(FullPayloadElectra::default().into()), + ForkName::Fulu => Ok(FullPayloadFulu::default().into()), } } } @@ -412,6 +425,7 @@ impl ExecPayload for FullPayloadRef<'_, E> { FullPayloadRef::Electra(inner) => { Ok(inner.execution_payload.withdrawals.tree_hash_root()) } + FullPayloadRef::Fulu(inner) => Ok(inner.execution_payload.withdrawals.tree_hash_root()), } } @@ -422,6 +436,7 @@ impl ExecPayload for FullPayloadRef<'_, E> { } FullPayloadRef::Deneb(inner) => Ok(inner.execution_payload.blob_gas_used), FullPayloadRef::Electra(inner) => Ok(inner.execution_payload.blob_gas_used), + FullPayloadRef::Fulu(inner) => Ok(inner.execution_payload.blob_gas_used), } } @@ -444,6 +459,7 @@ impl AbstractExecPayload for FullPayload { type Capella = FullPayloadCapella; type Deneb = FullPayloadDeneb; type Electra = FullPayloadElectra; + type Fulu = FullPayloadFulu; } impl From> for FullPayload { @@ -462,7 +478,7 @@ impl TryFrom> for FullPayload { } #[superstruct( - variants(Bellatrix, Capella, Deneb, Electra), + variants(Bellatrix, Capella, Deneb, Electra, Fulu), variant_attributes( derive( Debug, @@ -507,6 +523,8 @@ pub struct BlindedPayload { pub execution_payload_header: ExecutionPayloadHeaderDeneb, #[superstruct(only(Electra), partial_getter(rename = "execution_payload_electra"))] pub execution_payload_header: ExecutionPayloadHeaderElectra, + #[superstruct(only(Fulu), partial_getter(rename = "execution_payload_fulu"))] + pub execution_payload_header: ExecutionPayloadHeaderFulu, } impl<'a, E: EthSpec> From> for BlindedPayload { @@ -599,6 +617,7 @@ impl ExecPayload for BlindedPayload { BlindedPayload::Electra(ref inner) => { Ok(inner.execution_payload_header.withdrawals_root) } + BlindedPayload::Fulu(ref inner) => Ok(inner.execution_payload_header.withdrawals_root), } } @@ -609,6 +628,7 @@ impl ExecPayload for BlindedPayload { } BlindedPayload::Deneb(ref inner) => Ok(inner.execution_payload_header.blob_gas_used), BlindedPayload::Electra(ref inner) => Ok(inner.execution_payload_header.blob_gas_used), + BlindedPayload::Fulu(ref inner) => Ok(inner.execution_payload_header.blob_gas_used), } } @@ -707,6 +727,7 @@ impl<'b, E: EthSpec> ExecPayload for BlindedPayloadRef<'b, E> { BlindedPayloadRef::Electra(inner) => { Ok(inner.execution_payload_header.withdrawals_root) } + BlindedPayloadRef::Fulu(inner) => Ok(inner.execution_payload_header.withdrawals_root), } } @@ -717,6 +738,7 @@ impl<'b, E: EthSpec> ExecPayload for BlindedPayloadRef<'b, E> { } BlindedPayloadRef::Deneb(inner) => Ok(inner.execution_payload_header.blob_gas_used), BlindedPayloadRef::Electra(inner) => Ok(inner.execution_payload_header.blob_gas_used), + BlindedPayloadRef::Fulu(inner) => Ok(inner.execution_payload_header.blob_gas_used), } } @@ -1020,6 +1042,13 @@ impl_exec_payload_for_fork!( ExecutionPayloadElectra, Electra ); +impl_exec_payload_for_fork!( + BlindedPayloadFulu, + FullPayloadFulu, + ExecutionPayloadHeaderFulu, + ExecutionPayloadFulu, + Fulu +); impl AbstractExecPayload for BlindedPayload { type Ref<'a> = BlindedPayloadRef<'a, E>; @@ -1027,6 +1056,7 @@ impl AbstractExecPayload for BlindedPayload { type Capella = BlindedPayloadCapella; type Deneb = BlindedPayloadDeneb; type Electra = BlindedPayloadElectra; + type Fulu = BlindedPayloadFulu; } impl From> for BlindedPayload { @@ -1063,6 +1093,11 @@ impl From> for BlindedPayload { execution_payload_header, }) } + ExecutionPayloadHeader::Fulu(execution_payload_header) => { + Self::Fulu(BlindedPayloadFulu { + execution_payload_header, + }) + } } } } @@ -1082,6 +1117,9 @@ impl From> for ExecutionPayloadHeader { BlindedPayload::Electra(blinded_payload) => { ExecutionPayloadHeader::Electra(blinded_payload.execution_payload_header) } + BlindedPayload::Fulu(blinded_payload) => { + ExecutionPayloadHeader::Fulu(blinded_payload.execution_payload_header) + } } } } diff --git a/consensus/types/src/preset.rs b/consensus/types/src/preset.rs index b469b7b777..f8b3665409 100644 --- a/consensus/types/src/preset.rs +++ b/consensus/types/src/preset.rs @@ -276,6 +276,21 @@ impl ElectraPreset { } } +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[serde(rename_all = "UPPERCASE")] +pub struct FuluPreset { + #[serde(with = "serde_utils::quoted_u64")] + pub fulu_placeholder: u64, +} + +impl FuluPreset { + pub fn from_chain_spec(spec: &ChainSpec) -> Self { + Self { + fulu_placeholder: spec.fulu_placeholder, + } + } +} + #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] #[serde(rename_all = "UPPERCASE")] pub struct Eip7594Preset { @@ -343,6 +358,9 @@ mod test { let electra: ElectraPreset = preset_from_file(&preset_name, "electra.yaml"); assert_eq!(electra, ElectraPreset::from_chain_spec::(&spec)); + let fulu: FuluPreset = preset_from_file(&preset_name, "fulu.yaml"); + assert_eq!(fulu, FuluPreset::from_chain_spec::(&spec)); + let eip7594: Eip7594Preset = preset_from_file(&preset_name, "eip7594.yaml"); assert_eq!(eip7594, Eip7594Preset::from_chain_spec::(&spec)); } diff --git a/consensus/types/src/signed_beacon_block.rs b/consensus/types/src/signed_beacon_block.rs index bb5e1ea34b..d9bf9bf55d 100644 --- a/consensus/types/src/signed_beacon_block.rs +++ b/consensus/types/src/signed_beacon_block.rs @@ -38,7 +38,7 @@ impl From for Hash256 { /// A `BeaconBlock` and a signature from its proposer. #[superstruct( - variants(Base, Altair, Bellatrix, Capella, Deneb, Electra), + variants(Base, Altair, Bellatrix, Capella, Deneb, Electra, Fulu), variant_attributes( derive( Debug, @@ -81,6 +81,8 @@ pub struct SignedBeaconBlock = FullP pub message: BeaconBlockDeneb, #[superstruct(only(Electra), partial_getter(rename = "message_electra"))] pub message: BeaconBlockElectra, + #[superstruct(only(Fulu), partial_getter(rename = "message_fulu"))] + pub message: BeaconBlockFulu, pub signature: Signature, } @@ -163,6 +165,9 @@ impl> SignedBeaconBlock BeaconBlock::Electra(message) => { SignedBeaconBlock::Electra(SignedBeaconBlockElectra { message, signature }) } + BeaconBlock::Fulu(message) => { + SignedBeaconBlock::Fulu(SignedBeaconBlockFulu { message, signature }) + } } } @@ -570,6 +575,64 @@ impl SignedBeaconBlockElectra> { } } +impl SignedBeaconBlockFulu> { + pub fn into_full_block( + self, + execution_payload: ExecutionPayloadFulu, + ) -> SignedBeaconBlockFulu> { + let SignedBeaconBlockFulu { + message: + BeaconBlockFulu { + slot, + proposer_index, + parent_root, + state_root, + body: + BeaconBlockBodyFulu { + randao_reveal, + eth1_data, + graffiti, + proposer_slashings, + attester_slashings, + attestations, + deposits, + voluntary_exits, + sync_aggregate, + execution_payload: BlindedPayloadFulu { .. }, + bls_to_execution_changes, + blob_kzg_commitments, + execution_requests, + }, + }, + signature, + } = self; + SignedBeaconBlockFulu { + message: BeaconBlockFulu { + slot, + proposer_index, + parent_root, + state_root, + body: BeaconBlockBodyFulu { + randao_reveal, + eth1_data, + graffiti, + proposer_slashings, + attester_slashings, + attestations, + deposits, + voluntary_exits, + sync_aggregate, + execution_payload: FullPayloadFulu { execution_payload }, + bls_to_execution_changes, + blob_kzg_commitments, + execution_requests, + }, + }, + signature, + } + } +} + impl SignedBeaconBlock> { pub fn try_into_full_block( self, @@ -590,12 +653,16 @@ impl SignedBeaconBlock> { (SignedBeaconBlock::Electra(block), Some(ExecutionPayload::Electra(payload))) => { SignedBeaconBlock::Electra(block.into_full_block(payload)) } + (SignedBeaconBlock::Fulu(block), Some(ExecutionPayload::Fulu(payload))) => { + SignedBeaconBlock::Fulu(block.into_full_block(payload)) + } // avoid wildcard matching forks so that compiler will // direct us here when a new fork has been added (SignedBeaconBlock::Bellatrix(_), _) => return None, (SignedBeaconBlock::Capella(_), _) => return None, (SignedBeaconBlock::Deneb(_), _) => return None, (SignedBeaconBlock::Electra(_), _) => return None, + (SignedBeaconBlock::Fulu(_), _) => return None, }; Some(full_block) } @@ -741,6 +808,9 @@ pub mod ssz_tagged_signed_beacon_block { ForkName::Electra => Ok(SignedBeaconBlock::Electra( SignedBeaconBlockElectra::from_ssz_bytes(body)?, )), + ForkName::Fulu => Ok(SignedBeaconBlock::Fulu( + SignedBeaconBlockFulu::from_ssz_bytes(body)?, + )), } } } @@ -841,8 +911,9 @@ mod test { ), SignedBeaconBlock::from_block( BeaconBlock::Electra(BeaconBlockElectra::empty(spec)), - sig, + sig.clone(), ), + SignedBeaconBlock::from_block(BeaconBlock::Fulu(BeaconBlockFulu::empty(spec)), sig), ]; for block in blocks { diff --git a/lcli/src/mock_el.rs b/lcli/src/mock_el.rs index 8d3220b1df..7719f02aa3 100644 --- a/lcli/src/mock_el.rs +++ b/lcli/src/mock_el.rs @@ -19,6 +19,7 @@ pub fn run(mut env: Environment, matches: &ArgMatches) -> Result< let shanghai_time = parse_required(matches, "shanghai-time")?; let cancun_time = parse_optional(matches, "cancun-time")?; let prague_time = parse_optional(matches, "prague-time")?; + let osaka_time = parse_optional(matches, "osaka-time")?; let handle = env.core_context().executor.handle().unwrap(); let spec = &E::default_spec(); @@ -37,6 +38,7 @@ pub fn run(mut env: Environment, matches: &ArgMatches) -> Result< shanghai_time: Some(shanghai_time), cancun_time, prague_time, + osaka_time, }; let kzg = None; let server: MockServer = MockServer::new_with_config(&handle, config, kzg); diff --git a/testing/ef_tests/src/cases/common.rs b/testing/ef_tests/src/cases/common.rs index e16f5b257f..62f834820f 100644 --- a/testing/ef_tests/src/cases/common.rs +++ b/testing/ef_tests/src/cases/common.rs @@ -66,6 +66,7 @@ pub fn previous_fork(fork_name: ForkName) -> ForkName { ForkName::Capella => ForkName::Bellatrix, ForkName::Deneb => ForkName::Capella, ForkName::Electra => ForkName::Deneb, + ForkName::Fulu => ForkName::Electra, } } diff --git a/testing/ef_tests/src/cases/epoch_processing.rs b/testing/ef_tests/src/cases/epoch_processing.rs index c1adf10770..e05225c171 100644 --- a/testing/ef_tests/src/cases/epoch_processing.rs +++ b/testing/ef_tests/src/cases/epoch_processing.rs @@ -100,47 +100,35 @@ type_name!(ParticipationFlagUpdates, "participation_flag_updates"); impl EpochTransition for JustificationAndFinalization { fn run(state: &mut BeaconState, spec: &ChainSpec) -> Result<(), EpochProcessingError> { - match state { - BeaconState::Base(_) => { - let mut validator_statuses = base::ValidatorStatuses::new(state, spec)?; - validator_statuses.process_attestations(state)?; - let justification_and_finalization_state = - base::process_justification_and_finalization( - state, - &validator_statuses.total_balances, - spec, - )?; - justification_and_finalization_state.apply_changes_to_state(state); - Ok(()) - } - BeaconState::Altair(_) - | BeaconState::Bellatrix(_) - | BeaconState::Capella(_) - | BeaconState::Deneb(_) - | BeaconState::Electra(_) => { - initialize_progressive_balances_cache(state, spec)?; - let justification_and_finalization_state = - altair::process_justification_and_finalization(state)?; - justification_and_finalization_state.apply_changes_to_state(state); - Ok(()) - } + if state.fork_name_unchecked().altair_enabled() { + initialize_progressive_balances_cache(state, spec)?; + let justification_and_finalization_state = + altair::process_justification_and_finalization(state)?; + justification_and_finalization_state.apply_changes_to_state(state); + Ok(()) + } else { + let mut validator_statuses = base::ValidatorStatuses::new(state, spec)?; + validator_statuses.process_attestations(state)?; + let justification_and_finalization_state = + base::process_justification_and_finalization( + state, + &validator_statuses.total_balances, + spec, + )?; + justification_and_finalization_state.apply_changes_to_state(state); + Ok(()) } } } impl EpochTransition for RewardsAndPenalties { fn run(state: &mut BeaconState, spec: &ChainSpec) -> Result<(), EpochProcessingError> { - match state { - BeaconState::Base(_) => { - let mut validator_statuses = base::ValidatorStatuses::new(state, spec)?; - validator_statuses.process_attestations(state)?; - base::process_rewards_and_penalties(state, &validator_statuses, spec) - } - BeaconState::Altair(_) - | BeaconState::Bellatrix(_) - | BeaconState::Capella(_) - | BeaconState::Deneb(_) - | BeaconState::Electra(_) => altair::process_rewards_and_penalties_slow(state, spec), + if state.fork_name_unchecked().altair_enabled() { + altair::process_rewards_and_penalties_slow(state, spec) + } else { + let mut validator_statuses = base::ValidatorStatuses::new(state, spec)?; + validator_statuses.process_attestations(state)?; + base::process_rewards_and_penalties(state, &validator_statuses, spec) } } } @@ -159,24 +147,17 @@ impl EpochTransition for RegistryUpdates { impl EpochTransition for Slashings { fn run(state: &mut BeaconState, spec: &ChainSpec) -> Result<(), EpochProcessingError> { - match state { - BeaconState::Base(_) => { - let mut validator_statuses = base::ValidatorStatuses::new(state, spec)?; - validator_statuses.process_attestations(state)?; - process_slashings( - state, - validator_statuses.total_balances.current_epoch(), - spec, - )?; - } - BeaconState::Altair(_) - | BeaconState::Bellatrix(_) - | BeaconState::Capella(_) - | BeaconState::Deneb(_) - | BeaconState::Electra(_) => { - process_slashings_slow(state, spec)?; - } - }; + if state.fork_name_unchecked().altair_enabled() { + process_slashings_slow(state, spec)?; + } else { + let mut validator_statuses = base::ValidatorStatuses::new(state, spec)?; + validator_statuses.process_attestations(state)?; + process_slashings( + state, + validator_statuses.total_balances.current_epoch(), + spec, + )?; + } Ok(()) } } @@ -251,11 +232,10 @@ impl EpochTransition for HistoricalRootsUpdate { impl EpochTransition for HistoricalSummariesUpdate { fn run(state: &mut BeaconState, _spec: &ChainSpec) -> Result<(), EpochProcessingError> { - match state { - BeaconState::Capella(_) | BeaconState::Deneb(_) | BeaconState::Electra(_) => { - process_historical_summaries_update(state) - } - _ => Ok(()), + if state.fork_name_unchecked().capella_enabled() { + process_historical_summaries_update(state) + } else { + Ok(()) } } } @@ -272,39 +252,30 @@ impl EpochTransition for ParticipationRecordUpdates { impl EpochTransition for SyncCommitteeUpdates { fn run(state: &mut BeaconState, spec: &ChainSpec) -> Result<(), EpochProcessingError> { - match state { - BeaconState::Base(_) => Ok(()), - BeaconState::Altair(_) - | BeaconState::Bellatrix(_) - | BeaconState::Capella(_) - | BeaconState::Deneb(_) - | BeaconState::Electra(_) => altair::process_sync_committee_updates(state, spec), + if state.fork_name_unchecked().altair_enabled() { + altair::process_sync_committee_updates(state, spec) + } else { + Ok(()) } } } impl EpochTransition for InactivityUpdates { fn run(state: &mut BeaconState, spec: &ChainSpec) -> Result<(), EpochProcessingError> { - match state { - BeaconState::Base(_) => Ok(()), - BeaconState::Altair(_) - | BeaconState::Bellatrix(_) - | BeaconState::Capella(_) - | BeaconState::Deneb(_) - | BeaconState::Electra(_) => altair::process_inactivity_updates_slow(state, spec), + if state.fork_name_unchecked().altair_enabled() { + altair::process_inactivity_updates_slow(state, spec) + } else { + Ok(()) } } } impl EpochTransition for ParticipationFlagUpdates { fn run(state: &mut BeaconState, _: &ChainSpec) -> Result<(), EpochProcessingError> { - match state { - BeaconState::Base(_) => Ok(()), - BeaconState::Altair(_) - | BeaconState::Bellatrix(_) - | BeaconState::Capella(_) - | BeaconState::Deneb(_) - | BeaconState::Electra(_) => altair::process_participation_flag_updates(state), + if state.fork_name_unchecked().altair_enabled() { + altair::process_participation_flag_updates(state) + } else { + Ok(()) } } } diff --git a/testing/ef_tests/src/cases/fork.rs b/testing/ef_tests/src/cases/fork.rs index 132cfb4c0a..85301e22f6 100644 --- a/testing/ef_tests/src/cases/fork.rs +++ b/testing/ef_tests/src/cases/fork.rs @@ -5,7 +5,7 @@ use crate::decode::{ssz_decode_state, yaml_decode_file}; use serde::Deserialize; use state_processing::upgrade::{ upgrade_to_altair, upgrade_to_bellatrix, upgrade_to_capella, upgrade_to_deneb, - upgrade_to_electra, + upgrade_to_electra, upgrade_to_fulu, }; use types::BeaconState; @@ -69,6 +69,7 @@ impl Case for ForkTest { ForkName::Capella => upgrade_to_capella(&mut result_state, spec).map(|_| result_state), ForkName::Deneb => upgrade_to_deneb(&mut result_state, spec).map(|_| result_state), ForkName::Electra => upgrade_to_electra(&mut result_state, spec).map(|_| result_state), + ForkName::Fulu => upgrade_to_fulu(&mut result_state, spec).map(|_| result_state), }; compare_beacon_state_results_without_caches(&mut result, &mut expected) diff --git a/testing/ef_tests/src/cases/merkle_proof_validity.rs b/testing/ef_tests/src/cases/merkle_proof_validity.rs index 49c0719784..109d2cc796 100644 --- a/testing/ef_tests/src/cases/merkle_proof_validity.rs +++ b/testing/ef_tests/src/cases/merkle_proof_validity.rs @@ -4,7 +4,7 @@ use serde::Deserialize; use tree_hash::Hash256; use types::{ light_client_update, BeaconBlockBody, BeaconBlockBodyCapella, BeaconBlockBodyDeneb, - BeaconBlockBodyElectra, BeaconState, FixedVector, FullPayload, Unsigned, + BeaconBlockBodyElectra, BeaconBlockBodyFulu, BeaconState, FixedVector, FullPayload, Unsigned, }; #[derive(Debug, Clone, Deserialize)] @@ -131,6 +131,9 @@ impl LoadCase for KzgInclusionMerkleProofValidity { ssz_decode_file::>(&path.join("object.ssz_snappy"))? .into() } + ForkName::Fulu => { + ssz_decode_file::>(&path.join("object.ssz_snappy"))?.into() + } }; let merkle_proof = yaml_decode_file(&path.join("proof.yaml"))?; // Metadata does not exist in these tests but it is left like this just in case. @@ -246,6 +249,9 @@ impl LoadCase for BeaconBlockBodyMerkleProofValidity { ssz_decode_file::>(&path.join("object.ssz_snappy"))? .into() } + ForkName::Fulu => { + ssz_decode_file::>(&path.join("object.ssz_snappy"))?.into() + } }; let merkle_proof = yaml_decode_file(&path.join("proof.yaml"))?; // Metadata does not exist in these tests but it is left like this just in case. diff --git a/testing/ef_tests/src/cases/operations.rs b/testing/ef_tests/src/cases/operations.rs index d8cade296b..adb5bee768 100644 --- a/testing/ef_tests/src/cases/operations.rs +++ b/testing/ef_tests/src/cases/operations.rs @@ -98,29 +98,24 @@ impl Operation for Attestation { ) -> Result<(), BlockProcessingError> { initialize_epoch_cache(state, spec)?; let mut ctxt = ConsensusContext::new(state.slot()); - match state { - BeaconState::Base(_) => base::process_attestations( + if state.fork_name_unchecked().altair_enabled() { + initialize_progressive_balances_cache(state, spec)?; + altair_deneb::process_attestation( + state, + self.to_ref(), + 0, + &mut ctxt, + VerifySignatures::True, + spec, + ) + } else { + base::process_attestations( state, [self.clone().to_ref()].into_iter(), VerifySignatures::True, &mut ctxt, spec, - ), - BeaconState::Altair(_) - | BeaconState::Bellatrix(_) - | BeaconState::Capella(_) - | BeaconState::Deneb(_) - | BeaconState::Electra(_) => { - initialize_progressive_balances_cache(state, spec)?; - altair_deneb::process_attestation( - state, - self.to_ref(), - 0, - &mut ctxt, - VerifySignatures::True, - spec, - ) - } + ) } } } @@ -131,14 +126,11 @@ impl Operation for AttesterSlashing { } fn decode(path: &Path, fork_name: ForkName, _spec: &ChainSpec) -> Result { - Ok(match fork_name { - ForkName::Base - | ForkName::Altair - | ForkName::Bellatrix - | ForkName::Capella - | ForkName::Deneb => Self::Base(ssz_decode_file(path)?), - ForkName::Electra => Self::Electra(ssz_decode_file(path)?), - }) + if fork_name.electra_enabled() { + Ok(Self::Electra(ssz_decode_file(path)?)) + } else { + Ok(Self::Base(ssz_decode_file(path)?)) + } } fn apply_to( @@ -308,6 +300,7 @@ impl Operation for BeaconBlockBody> { ForkName::Capella => BeaconBlockBody::Capella(<_>::from_ssz_bytes(bytes)?), ForkName::Deneb => BeaconBlockBody::Deneb(<_>::from_ssz_bytes(bytes)?), ForkName::Electra => BeaconBlockBody::Electra(<_>::from_ssz_bytes(bytes)?), + ForkName::Fulu => BeaconBlockBody::Fulu(<_>::from_ssz_bytes(bytes)?), _ => panic!(), }) }) @@ -363,6 +356,10 @@ impl Operation for BeaconBlockBody> { let inner = >>::from_ssz_bytes(bytes)?; BeaconBlockBody::Electra(inner.clone_as_blinded()) } + ForkName::Fulu => { + let inner = >>::from_ssz_bytes(bytes)?; + BeaconBlockBody::Electra(inner.clone_as_blinded()) + } _ => panic!(), }) }) diff --git a/testing/ef_tests/src/cases/transition.rs b/testing/ef_tests/src/cases/transition.rs index dc5029d53e..6d037dae87 100644 --- a/testing/ef_tests/src/cases/transition.rs +++ b/testing/ef_tests/src/cases/transition.rs @@ -60,6 +60,14 @@ impl LoadCase for TransitionTest { spec.deneb_fork_epoch = Some(Epoch::new(0)); spec.electra_fork_epoch = Some(metadata.fork_epoch); } + ForkName::Fulu => { + spec.altair_fork_epoch = Some(Epoch::new(0)); + spec.bellatrix_fork_epoch = Some(Epoch::new(0)); + spec.capella_fork_epoch = Some(Epoch::new(0)); + spec.deneb_fork_epoch = Some(Epoch::new(0)); + spec.electra_fork_epoch = Some(Epoch::new(0)); + spec.fulu_fork_epoch = Some(metadata.fork_epoch); + } } // Load blocks diff --git a/testing/ef_tests/src/handler.rs b/testing/ef_tests/src/handler.rs index f4a09de32c..e7c148645c 100644 --- a/testing/ef_tests/src/handler.rs +++ b/testing/ef_tests/src/handler.rs @@ -24,7 +24,7 @@ pub trait Handler { // Add forks here to exclude them from EF spec testing. Helpful for adding future or // unspecified forks. fn disabled_forks(&self) -> Vec { - vec![] + vec![ForkName::Fulu] } fn is_enabled_for_fork(&self, fork_name: ForkName) -> bool { @@ -287,6 +287,10 @@ impl SszStaticHandler { Self::for_forks(vec![ForkName::Electra]) } + pub fn fulu_only() -> Self { + Self::for_forks(vec![ForkName::Fulu]) + } + pub fn altair_and_later() -> Self { Self::for_forks(ForkName::list_all()[1..].to_vec()) } @@ -307,6 +311,10 @@ impl SszStaticHandler { Self::for_forks(ForkName::list_all()[5..].to_vec()) } + pub fn fulu_and_later() -> Self { + Self::for_forks(ForkName::list_all()[6..].to_vec()) + } + pub fn pre_electra() -> Self { Self::for_forks(ForkName::list_all()[0..5].to_vec()) } diff --git a/testing/simulator/src/basic_sim.rs b/testing/simulator/src/basic_sim.rs index 8f659a893f..82a7028582 100644 --- a/testing/simulator/src/basic_sim.rs +++ b/testing/simulator/src/basic_sim.rs @@ -22,7 +22,8 @@ const ALTAIR_FORK_EPOCH: u64 = 0; const BELLATRIX_FORK_EPOCH: u64 = 0; const CAPELLA_FORK_EPOCH: u64 = 1; const DENEB_FORK_EPOCH: u64 = 2; -//const ELECTRA_FORK_EPOCH: u64 = 3; +// const ELECTRA_FORK_EPOCH: u64 = 3; +// const FULU_FORK_EPOCH: u64 = 4; const SUGGESTED_FEE_RECIPIENT: [u8; 20] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]; @@ -118,6 +119,7 @@ pub fn run_basic_sim(matches: &ArgMatches) -> Result<(), String> { spec.capella_fork_epoch = Some(Epoch::new(CAPELLA_FORK_EPOCH)); spec.deneb_fork_epoch = Some(Epoch::new(DENEB_FORK_EPOCH)); //spec.electra_fork_epoch = Some(Epoch::new(ELECTRA_FORK_EPOCH)); + //spec.fulu_fork_epoch = Some(Epoch::new(FULU_FORK_EPOCH)); let spec = Arc::new(spec); env.eth2_config.spec = spec.clone(); diff --git a/testing/simulator/src/fallback_sim.rs b/testing/simulator/src/fallback_sim.rs index b3b9a46001..7d4bdfa264 100644 --- a/testing/simulator/src/fallback_sim.rs +++ b/testing/simulator/src/fallback_sim.rs @@ -21,7 +21,8 @@ const ALTAIR_FORK_EPOCH: u64 = 0; const BELLATRIX_FORK_EPOCH: u64 = 0; const CAPELLA_FORK_EPOCH: u64 = 1; const DENEB_FORK_EPOCH: u64 = 2; -//const ELECTRA_FORK_EPOCH: u64 = 3; +// const ELECTRA_FORK_EPOCH: u64 = 3; +// const FULU_FORK_EPOCH: u64 = 4; // Since simulator tests are non-deterministic and there is a non-zero chance of missed // attestations, define an acceptable network-wide attestation performance. @@ -123,6 +124,7 @@ pub fn run_fallback_sim(matches: &ArgMatches) -> Result<(), String> { spec.capella_fork_epoch = Some(Epoch::new(CAPELLA_FORK_EPOCH)); spec.deneb_fork_epoch = Some(Epoch::new(DENEB_FORK_EPOCH)); //spec.electra_fork_epoch = Some(Epoch::new(ELECTRA_FORK_EPOCH)); + //spec.fulu_fork_epoch = Some(Epoch::new(FULU_FORK_EPOCH)); let spec = Arc::new(spec); env.eth2_config.spec = spec.clone(); diff --git a/testing/simulator/src/local_network.rs b/testing/simulator/src/local_network.rs index 59efc09baa..a95c15c231 100644 --- a/testing/simulator/src/local_network.rs +++ b/testing/simulator/src/local_network.rs @@ -88,6 +88,11 @@ fn default_mock_execution_config( + spec.seconds_per_slot * E::slots_per_epoch() * electra_fork_epoch.as_u64(), ) } + if let Some(fulu_fork_epoch) = spec.fulu_fork_epoch { + mock_execution_config.osaka_time = Some( + genesis_time + spec.seconds_per_slot * E::slots_per_epoch() * fulu_fork_epoch.as_u64(), + ) + } mock_execution_config } diff --git a/validator_client/beacon_node_fallback/src/lib.rs b/validator_client/beacon_node_fallback/src/lib.rs index 95a221f189..beae176193 100644 --- a/validator_client/beacon_node_fallback/src/lib.rs +++ b/validator_client/beacon_node_fallback/src/lib.rs @@ -361,6 +361,14 @@ impl CandidateBeaconNode { "endpoint_electra_fork_epoch" => ?beacon_node_spec.electra_fork_epoch, "hint" => UPDATE_REQUIRED_LOG_HINT, ); + } else if beacon_node_spec.fulu_fork_epoch != spec.fulu_fork_epoch { + warn!( + log, + "Beacon node has mismatched Fulu fork epoch"; + "endpoint" => %self.beacon_node, + "endpoint_fulu_fork_epoch" => ?beacon_node_spec.fulu_fork_epoch, + "hint" => UPDATE_REQUIRED_LOG_HINT, + ); } Ok(()) diff --git a/validator_client/http_api/src/test_utils.rs b/validator_client/http_api/src/test_utils.rs index 390095eec7..0531626846 100644 --- a/validator_client/http_api/src/test_utils.rs +++ b/validator_client/http_api/src/test_utils.rs @@ -251,9 +251,9 @@ impl ApiTester { pub async fn test_get_lighthouse_spec(self) -> Self { let result = self .client - .get_lighthouse_spec::() + .get_lighthouse_spec::() .await - .map(|res| ConfigAndPreset::Electra(res.data)) + .map(|res| ConfigAndPreset::Fulu(res.data)) .unwrap(); let expected = ConfigAndPreset::from_chain_spec::(&E::default_spec(), None); diff --git a/validator_client/http_api/src/tests.rs b/validator_client/http_api/src/tests.rs index 7ea3d7ebaa..4e9acc4237 100644 --- a/validator_client/http_api/src/tests.rs +++ b/validator_client/http_api/src/tests.rs @@ -214,9 +214,9 @@ impl ApiTester { pub async fn test_get_lighthouse_spec(self) -> Self { let result = self .client - .get_lighthouse_spec::() + .get_lighthouse_spec::() .await - .map(|res| ConfigAndPreset::Electra(res.data)) + .map(|res| ConfigAndPreset::Fulu(res.data)) .unwrap(); let expected = ConfigAndPreset::from_chain_spec::(&E::default_spec(), None); diff --git a/validator_client/signing_method/src/web3signer.rs b/validator_client/signing_method/src/web3signer.rs index 86e7015ad3..d286449d20 100644 --- a/validator_client/signing_method/src/web3signer.rs +++ b/validator_client/signing_method/src/web3signer.rs @@ -29,6 +29,7 @@ pub enum ForkName { Capella, Deneb, Electra, + Fulu, } #[derive(Debug, PartialEq, Serialize)] @@ -107,6 +108,11 @@ impl<'a, E: EthSpec, Payload: AbstractExecPayload> Web3SignerObject<'a, E, Pa block: None, block_header: Some(block.block_header()), }), + BeaconBlock::Fulu(_) => Ok(Web3SignerObject::BeaconBlock { + version: ForkName::Fulu, + block: None, + block_header: Some(block.block_header()), + }), } } From 05727290fbef27df72e854de0ca9280ee6347101 Mon Sep 17 00:00:00 2001 From: Pawan Dhananjay Date: Fri, 10 Jan 2025 12:04:58 +0530 Subject: [PATCH 069/254] Make max_blobs_per_block a config parameter (#6329) * First pass * Add restrictions to RuntimeVariableList api * Use empty_uninitialized and fix warnings * Fix some todos * Merge branch 'unstable' into max-blobs-preset * Fix take impl on RuntimeFixedList * cleanup * Fix test compilations * Fix some more tests * Fix test from unstable * Merge branch 'unstable' into max-blobs-preset * Merge remote-tracking branch 'origin/unstable' into max-blobs-preset * Remove footgun function * Minor simplifications * Move from preset to config * Fix typo * Revert "Remove footgun function" This reverts commit de01f923c7452355c87f50c0e8031ca94fa00d36. * Try fixing tests * Thread through ChainSpec * Fix release tests * Move RuntimeFixedVector into module and rename * Add test * Remove empty RuntimeVarList awefullness * Fix tests * Simplify BlobSidecarListFromRoot * Merge remote-tracking branch 'origin/unstable' into max-blobs-preset * Bump quota to account for new target (6) * Remove clone * Fix issue from review * Try to remove ugliness * Merge branch 'unstable' into max-blobs-preset * Fix max value * Fix doctest * Fix formatting * Fix max check * Delete hardcoded max_blobs_per_block in RPC limits * Merge remote-tracking branch 'origin/unstable' into max-blobs-preset --- .cargo/config.toml | 1 + beacon_node/beacon_chain/src/beacon_chain.rs | 16 +- .../beacon_chain/src/blob_verification.rs | 2 +- .../src/block_verification_types.rs | 20 +-- .../src/data_availability_checker.rs | 14 +- .../overflow_lru_cache.rs | 147 ++++++++++-------- beacon_node/beacon_chain/src/fetch_blobs.rs | 10 +- beacon_node/beacon_chain/src/kzg_utils.rs | 17 +- .../src/observed_data_sidecars.rs | 20 +-- beacon_node/beacon_chain/src/test_utils.rs | 31 ++-- .../tests/attestation_production.rs | 9 +- .../beacon_chain/tests/block_verification.rs | 18 ++- beacon_node/beacon_chain/tests/events.rs | 2 +- beacon_node/beacon_chain/tests/store_tests.rs | 29 ++-- beacon_node/client/src/builder.rs | 4 +- .../execution_layer/src/engine_api/http.rs | 3 +- .../test_utils/execution_block_generator.rs | 11 +- .../src/test_utils/mock_execution_layer.rs | 9 +- .../execution_layer/src/test_utils/mod.rs | 9 +- beacon_node/http_api/src/block_id.rs | 25 +-- .../tests/broadcast_validation_tests.rs | 3 +- .../lighthouse_network/src/rpc/codec.rs | 22 ++- .../lighthouse_network/src/rpc/config.rs | 4 +- .../lighthouse_network/src/rpc/handler.rs | 6 +- .../lighthouse_network/src/rpc/methods.rs | 6 +- beacon_node/lighthouse_network/src/rpc/mod.rs | 5 +- .../lighthouse_network/src/rpc/protocol.rs | 36 ++--- .../src/rpc/rate_limiter.rs | 24 ++- .../src/rpc/self_limiter.rs | 20 ++- .../network_beacon_processor/rpc_methods.rs | 8 - .../src/network_beacon_processor/tests.rs | 11 +- .../src/sync/block_sidecar_coupling.rs | 72 ++++++--- beacon_node/network/src/sync/manager.rs | 2 + .../network/src/sync/network_context.rs | 38 ++++- .../src/sync/network_context/requests.rs | 1 + beacon_node/network/src/sync/tests/lookups.rs | 16 +- beacon_node/network/src/sync/tests/mod.rs | 3 +- .../store/src/blob_sidecar_list_from_root.rs | 42 +++++ beacon_node/store/src/hot_cold_store.rs | 38 +++-- .../store/src/impls/execution_payload.rs | 3 +- beacon_node/store/src/lib.rs | 2 + .../chiado/config.yaml | 2 + .../gnosis/config.yaml | 2 + .../holesky/config.yaml | 2 + .../mainnet/config.yaml | 2 + .../sepolia/config.yaml | 2 + .../src/per_block_processing.rs | 6 +- consensus/types/presets/gnosis/deneb.yaml | 2 - consensus/types/presets/mainnet/deneb.yaml | 2 - consensus/types/presets/minimal/deneb.yaml | 2 - consensus/types/src/beacon_block_body.rs | 2 - consensus/types/src/blob_sidecar.rs | 33 ++-- consensus/types/src/chain_spec.rs | 26 ++++ consensus/types/src/data_column_sidecar.rs | 17 +- consensus/types/src/eth_spec.rs | 12 +- consensus/types/src/lib.rs | 2 + consensus/types/src/preset.rs | 3 - consensus/types/src/runtime_fixed_vector.rs | 81 ++++++++++ consensus/types/src/runtime_var_list.rs | 19 ++- lcli/src/mock_el.rs | 5 +- testing/node_test_rig/src/lib.rs | 9 +- 61 files changed, 655 insertions(+), 335 deletions(-) create mode 100644 beacon_node/store/src/blob_sidecar_list_from_root.rs create mode 100644 consensus/types/src/runtime_fixed_vector.rs diff --git a/.cargo/config.toml b/.cargo/config.toml index a408305c4d..dac0163003 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,3 +1,4 @@ [env] # Set the number of arenas to 16 when using jemalloc. JEMALLOC_SYS_WITH_MALLOC_CONF = "abort_conf:true,narenas:16" + diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index d84cd9615a..81783267ba 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -117,7 +117,8 @@ use std::sync::Arc; use std::time::Duration; use store::iter::{BlockRootsIterator, ParentRootBlockIterator, StateRootsIterator}; use store::{ - DatabaseBlock, Error as DBError, HotColdDB, KeyValueStore, KeyValueStoreOp, StoreItem, StoreOp, + BlobSidecarListFromRoot, DatabaseBlock, Error as DBError, HotColdDB, KeyValueStore, + KeyValueStoreOp, StoreItem, StoreOp, }; use task_executor::{ShutdownReason, TaskExecutor}; use tokio::sync::mpsc::Receiver; @@ -1147,9 +1148,10 @@ impl BeaconChain { pub fn get_blobs_checking_early_attester_cache( &self, block_root: &Hash256, - ) -> Result, Error> { + ) -> Result, Error> { self.early_attester_cache .get_blobs(*block_root) + .map(Into::into) .map_or_else(|| self.get_blobs(block_root), Ok) } @@ -1240,11 +1242,11 @@ impl BeaconChain { /// /// ## Errors /// May return a database error. - pub fn get_blobs(&self, block_root: &Hash256) -> Result, Error> { - match self.store.get_blobs(block_root)? { - Some(blobs) => Ok(blobs), - None => Ok(BlobSidecarList::default()), - } + pub fn get_blobs( + &self, + block_root: &Hash256, + ) -> Result, Error> { + self.store.get_blobs(block_root).map_err(Error::from) } /// Returns the data columns at the given root, if any. diff --git a/beacon_node/beacon_chain/src/blob_verification.rs b/beacon_node/beacon_chain/src/blob_verification.rs index 6c87deb826..786b627bb7 100644 --- a/beacon_node/beacon_chain/src/blob_verification.rs +++ b/beacon_node/beacon_chain/src/blob_verification.rs @@ -400,7 +400,7 @@ pub fn validate_blob_sidecar_for_gossip= T::EthSpec::max_blobs_per_block() as u64 { + if blob_index >= chain.spec.max_blobs_per_block(blob_epoch) { return Err(GossipBlobError::InvalidSubnet { expected: subnet, received: blob_index, diff --git a/beacon_node/beacon_chain/src/block_verification_types.rs b/beacon_node/beacon_chain/src/block_verification_types.rs index 420c83081c..0bf3007e9b 100644 --- a/beacon_node/beacon_chain/src/block_verification_types.rs +++ b/beacon_node/beacon_chain/src/block_verification_types.rs @@ -4,11 +4,10 @@ use crate::data_column_verification::{CustodyDataColumn, CustodyDataColumnList}; use crate::eth1_finalization_cache::Eth1FinalizationData; use crate::{get_block_root, PayloadVerificationOutcome}; use derivative::Derivative; -use ssz_types::VariableList; use state_processing::ConsensusContext; use std::fmt::{Debug, Formatter}; use std::sync::Arc; -use types::blob_sidecar::{BlobIdentifier, FixedBlobSidecarList}; +use types::blob_sidecar::BlobIdentifier; use types::{ BeaconBlockRef, BeaconState, BlindedPayload, BlobSidecarList, ChainSpec, Epoch, EthSpec, Hash256, RuntimeVariableList, SignedBeaconBlock, SignedBeaconBlockHeader, Slot, @@ -176,23 +175,6 @@ impl RpcBlock { }) } - pub fn new_from_fixed( - block_root: Hash256, - block: Arc>, - blobs: FixedBlobSidecarList, - ) -> Result { - let filtered = blobs - .into_iter() - .filter_map(|b| b.clone()) - .collect::>(); - let blobs = if filtered.is_empty() { - None - } else { - Some(VariableList::from(filtered)) - }; - Self::new(Some(block_root), block, blobs) - } - #[allow(clippy::type_complexity)] pub fn deconstruct( self, diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index f6002ea0ac..4c5152239c 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -215,9 +215,12 @@ impl DataAvailabilityChecker { // Note: currently not reporting which specific blob is invalid because we fetch all blobs // from the same peer for both lookup and range sync. - let verified_blobs = - KzgVerifiedBlobList::new(blobs.iter().flatten().cloned(), &self.kzg, seen_timestamp) - .map_err(AvailabilityCheckError::InvalidBlobs)?; + let verified_blobs = KzgVerifiedBlobList::new( + blobs.into_vec().into_iter().flatten(), + &self.kzg, + seen_timestamp, + ) + .map_err(AvailabilityCheckError::InvalidBlobs)?; self.availability_cache .put_kzg_verified_blobs(block_root, verified_blobs, &self.log) @@ -400,14 +403,13 @@ impl DataAvailabilityChecker { blocks: Vec>, ) -> Result>, AvailabilityCheckError> { let mut results = Vec::with_capacity(blocks.len()); - let all_blobs: BlobSidecarList = blocks + let all_blobs = blocks .iter() .filter(|block| self.blobs_required_for_block(block.as_block())) // this clone is cheap as it's cloning an Arc .filter_map(|block| block.blobs().cloned()) .flatten() - .collect::>() - .into(); + .collect::>(); // verify kzg for all blobs at once if !all_blobs.is_empty() { 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 5ce023038d..44148922f4 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 @@ -10,13 +10,12 @@ use crate::BeaconChainTypes; use lru::LruCache; use parking_lot::RwLock; use slog::{debug, Logger}; -use ssz_types::FixedVector; use std::num::NonZeroUsize; use std::sync::Arc; use types::blob_sidecar::BlobIdentifier; use types::{ BlobSidecar, ChainSpec, ColumnIndex, DataColumnIdentifier, DataColumnSidecar, Epoch, EthSpec, - Hash256, SignedBeaconBlock, + Hash256, RuntimeFixedVector, RuntimeVariableList, SignedBeaconBlock, }; /// This represents the components of a partially available block @@ -28,7 +27,7 @@ use types::{ #[derive(Clone)] pub struct PendingComponents { pub block_root: Hash256, - pub verified_blobs: FixedVector>, E::MaxBlobsPerBlock>, + pub verified_blobs: RuntimeFixedVector>>, pub verified_data_columns: Vec>, pub executed_block: Option>, pub reconstruction_started: bool, @@ -41,9 +40,7 @@ impl PendingComponents { } /// Returns an immutable reference to the fixed vector of cached blobs. - pub fn get_cached_blobs( - &self, - ) -> &FixedVector>, E::MaxBlobsPerBlock> { + pub fn get_cached_blobs(&self) -> &RuntimeFixedVector>> { &self.verified_blobs } @@ -64,9 +61,7 @@ impl PendingComponents { } /// Returns a mutable reference to the fixed vector of cached blobs. - pub fn get_cached_blobs_mut( - &mut self, - ) -> &mut FixedVector>, E::MaxBlobsPerBlock> { + pub fn get_cached_blobs_mut(&mut self) -> &mut RuntimeFixedVector>> { &mut self.verified_blobs } @@ -138,10 +133,7 @@ impl PendingComponents { /// Blobs are only inserted if: /// 1. The blob entry at the index is empty and no block exists. /// 2. The block exists and its commitment matches the blob's commitment. - pub fn merge_blobs( - &mut self, - blobs: FixedVector>, E::MaxBlobsPerBlock>, - ) { + pub fn merge_blobs(&mut self, blobs: RuntimeFixedVector>>) { for (index, blob) in blobs.iter().cloned().enumerate() { let Some(blob) = blob else { continue }; self.merge_single_blob(index, blob); @@ -185,7 +177,7 @@ impl PendingComponents { /// Blobs that don't match the new block's commitments are evicted. pub fn merge_block(&mut self, block: DietAvailabilityPendingExecutedBlock) { self.insert_block(block); - let reinsert = std::mem::take(self.get_cached_blobs_mut()); + let reinsert = self.get_cached_blobs_mut().take(); self.merge_blobs(reinsert); } @@ -237,10 +229,10 @@ 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, max_len: usize) -> Self { Self { block_root, - verified_blobs: FixedVector::default(), + verified_blobs: RuntimeFixedVector::new(vec![None; max_len]), verified_data_columns: vec![], executed_block: None, reconstruction_started: false, @@ -299,7 +291,11 @@ impl PendingComponents { else { return Err(AvailabilityCheckError::Unexpected); }; - (Some(verified_blobs), None) + let max_len = spec.max_blobs_per_block(diet_executed_block.as_block().epoch()) as usize; + ( + Some(RuntimeVariableList::new(verified_blobs, max_len)?), + None, + ) }; let executed_block = recover(diet_executed_block)?; @@ -341,10 +337,7 @@ impl PendingComponents { } 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()); + let epoch = kzg_verified_data_column.as_data_column().epoch(); return Some(epoch); } @@ -457,7 +450,18 @@ impl DataAvailabilityCheckerInner { kzg_verified_blobs: I, log: &Logger, ) -> Result, AvailabilityCheckError> { - let mut fixed_blobs = FixedVector::default(); + let mut kzg_verified_blobs = kzg_verified_blobs.into_iter().peekable(); + + let Some(epoch) = kzg_verified_blobs + .peek() + .map(|verified_blob| verified_blob.as_blob().epoch()) + else { + // Verified blobs list should be non-empty. + return Err(AvailabilityCheckError::Unexpected); + }; + + let mut fixed_blobs = + RuntimeFixedVector::new(vec![None; self.spec.max_blobs_per_block(epoch) as usize]); for blob in kzg_verified_blobs { if let Some(blob_opt) = fixed_blobs.get_mut(blob.blob_index() as usize) { @@ -471,7 +475,9 @@ impl DataAvailabilityCheckerInner { let mut pending_components = write_lock .pop_entry(&block_root) .map(|(_, v)| v) - .unwrap_or_else(|| PendingComponents::empty(block_root)); + .unwrap_or_else(|| { + PendingComponents::empty(block_root, self.spec.max_blobs_per_block(epoch) as usize) + }); // Merge in the blobs. pending_components.merge_blobs(fixed_blobs); @@ -498,13 +504,24 @@ impl DataAvailabilityCheckerInner { kzg_verified_data_columns: I, log: &Logger, ) -> Result, AvailabilityCheckError> { + let mut kzg_verified_data_columns = kzg_verified_data_columns.into_iter().peekable(); + let Some(epoch) = kzg_verified_data_columns + .peek() + .map(|verified_blob| verified_blob.as_data_column().epoch()) + else { + // Verified data_columns list should be non-empty. + return Err(AvailabilityCheckError::Unexpected); + }; + let mut write_lock = self.critical.write(); // Grab existing entry or create a new entry. let mut pending_components = write_lock .pop_entry(&block_root) .map(|(_, v)| v) - .unwrap_or_else(|| PendingComponents::empty(block_root)); + .unwrap_or_else(|| { + PendingComponents::empty(block_root, self.spec.max_blobs_per_block(epoch) as usize) + }); // Merge in the data columns. pending_components.merge_data_columns(kzg_verified_data_columns)?; @@ -581,6 +598,7 @@ impl DataAvailabilityCheckerInner { log: &Logger, ) -> Result, AvailabilityCheckError> { let mut write_lock = self.critical.write(); + let epoch = executed_block.as_block().epoch(); let block_root = executed_block.import_data.block_root; // register the block to get the diet block @@ -592,7 +610,9 @@ impl DataAvailabilityCheckerInner { let mut pending_components = write_lock .pop_entry(&block_root) .map(|(_, v)| v) - .unwrap_or_else(|| PendingComponents::empty(block_root)); + .unwrap_or_else(|| { + PendingComponents::empty(block_root, self.spec.max_blobs_per_block(epoch) as usize) + }); // Merge in the block. pending_components.merge_block(diet_executed_block); @@ -812,7 +832,8 @@ mod test { info!(log, "done printing kzg commitments"); let gossip_verified_blobs = if let Some((kzg_proofs, blobs)) = maybe_blobs { - let sidecars = BlobSidecar::build_sidecars(blobs, &block, kzg_proofs).unwrap(); + let sidecars = + BlobSidecar::build_sidecars(blobs, &block, kzg_proofs, &chain.spec).unwrap(); Vec::from(sidecars) .into_iter() .map(|sidecar| { @@ -945,6 +966,8 @@ mod test { assert_eq!(cache.critical.read().len(), 1); } } + // remove the blob to simulate successful import + cache.remove_pending_components(root); assert!( cache.critical.read().is_empty(), "cache should be empty now that all components available" @@ -1125,7 +1148,7 @@ mod pending_components_tests { use super::*; use crate::block_verification_types::BlockImportData; use crate::eth1_finalization_cache::Eth1FinalizationData; - use crate::test_utils::{generate_rand_block_and_blobs, NumBlobs}; + use crate::test_utils::{generate_rand_block_and_blobs, test_spec, NumBlobs}; use crate::PayloadVerificationOutcome; use fork_choice::PayloadVerificationStatus; use kzg::KzgCommitment; @@ -1141,15 +1164,19 @@ mod pending_components_tests { type Setup = ( SignedBeaconBlock, - FixedVector>>, ::MaxBlobsPerBlock>, - FixedVector>>, ::MaxBlobsPerBlock>, + RuntimeFixedVector>>>, + RuntimeFixedVector>>>, + usize, ); pub fn pre_setup() -> Setup { let mut rng = StdRng::seed_from_u64(0xDEADBEEF0BAD5EEDu64); + let spec = test_spec::(); let (block, blobs_vec) = - generate_rand_block_and_blobs::(ForkName::Deneb, NumBlobs::Random, &mut rng); - let mut blobs: FixedVector<_, ::MaxBlobsPerBlock> = FixedVector::default(); + generate_rand_block_and_blobs::(ForkName::Deneb, NumBlobs::Random, &mut rng, &spec); + let max_len = spec.max_blobs_per_block(block.epoch()) as usize; + let mut blobs: RuntimeFixedVector>>> = + RuntimeFixedVector::default(max_len); for blob in blobs_vec { if let Some(b) = blobs.get_mut(blob.index as usize) { @@ -1157,10 +1184,8 @@ mod pending_components_tests { } } - let mut invalid_blobs: FixedVector< - Option>>, - ::MaxBlobsPerBlock, - > = FixedVector::default(); + let mut invalid_blobs: RuntimeFixedVector>>> = + RuntimeFixedVector::default(max_len); for (index, blob) in blobs.iter().enumerate() { if let Some(invalid_blob) = blob { let mut blob_copy = invalid_blob.as_ref().clone(); @@ -1169,21 +1194,21 @@ mod pending_components_tests { } } - (block, blobs, invalid_blobs) + (block, blobs, invalid_blobs, max_len) } type PendingComponentsSetup = ( DietAvailabilityPendingExecutedBlock, - FixedVector>, ::MaxBlobsPerBlock>, - FixedVector>, ::MaxBlobsPerBlock>, + RuntimeFixedVector>>, + RuntimeFixedVector>>, ); pub fn setup_pending_components( block: SignedBeaconBlock, - valid_blobs: FixedVector>>, ::MaxBlobsPerBlock>, - invalid_blobs: FixedVector>>, ::MaxBlobsPerBlock>, + valid_blobs: RuntimeFixedVector>>>, + invalid_blobs: RuntimeFixedVector>>>, ) -> PendingComponentsSetup { - let blobs = FixedVector::from( + let blobs = RuntimeFixedVector::new( valid_blobs .iter() .map(|blob_opt| { @@ -1193,7 +1218,7 @@ mod pending_components_tests { }) .collect::>(), ); - let invalid_blobs = FixedVector::from( + let invalid_blobs = RuntimeFixedVector::new( invalid_blobs .iter() .map(|blob_opt| { @@ -1225,10 +1250,10 @@ mod pending_components_tests { (block.into(), blobs, invalid_blobs) } - pub fn assert_cache_consistent(cache: PendingComponents) { + pub fn assert_cache_consistent(cache: PendingComponents, max_len: usize) { if let Some(cached_block) = cache.get_cached_block() { let cached_block_commitments = cached_block.get_commitments(); - for index in 0..E::max_blobs_per_block() { + for index in 0..max_len { let block_commitment = cached_block_commitments.get(index).copied(); let blob_commitment_opt = cache.get_cached_blobs().get(index).unwrap(); let blob_commitment = blob_commitment_opt.as_ref().map(|b| *b.get_commitment()); @@ -1247,40 +1272,40 @@ mod pending_components_tests { #[test] fn valid_block_invalid_blobs_valid_blobs() { - let (block_commitments, blobs, random_blobs) = pre_setup(); + let (block_commitments, blobs, random_blobs, max_len) = pre_setup(); let (block_commitments, blobs, random_blobs) = setup_pending_components(block_commitments, blobs, random_blobs); let block_root = Hash256::zero(); - let mut cache = >::empty(block_root); + let mut cache = >::empty(block_root, max_len); cache.merge_block(block_commitments); cache.merge_blobs(random_blobs); cache.merge_blobs(blobs); - assert_cache_consistent(cache); + assert_cache_consistent(cache, max_len); } #[test] fn invalid_blobs_block_valid_blobs() { - let (block_commitments, blobs, random_blobs) = pre_setup(); + let (block_commitments, blobs, random_blobs, max_len) = pre_setup(); let (block_commitments, blobs, random_blobs) = setup_pending_components(block_commitments, blobs, random_blobs); let block_root = Hash256::zero(); - let mut cache = >::empty(block_root); + let mut cache = >::empty(block_root, max_len); cache.merge_blobs(random_blobs); cache.merge_block(block_commitments); cache.merge_blobs(blobs); - assert_cache_consistent(cache); + assert_cache_consistent(cache, max_len); } #[test] fn invalid_blobs_valid_blobs_block() { - let (block_commitments, blobs, random_blobs) = pre_setup(); + let (block_commitments, blobs, random_blobs, max_len) = pre_setup(); let (block_commitments, blobs, random_blobs) = setup_pending_components(block_commitments, blobs, random_blobs); let block_root = Hash256::zero(); - let mut cache = >::empty(block_root); + let mut cache = >::empty(block_root, max_len); cache.merge_blobs(random_blobs); cache.merge_blobs(blobs); cache.merge_block(block_commitments); @@ -1290,46 +1315,46 @@ mod pending_components_tests { #[test] fn block_valid_blobs_invalid_blobs() { - let (block_commitments, blobs, random_blobs) = pre_setup(); + let (block_commitments, blobs, random_blobs, max_len) = pre_setup(); let (block_commitments, blobs, random_blobs) = setup_pending_components(block_commitments, blobs, random_blobs); let block_root = Hash256::zero(); - let mut cache = >::empty(block_root); + let mut cache = >::empty(block_root, max_len); cache.merge_block(block_commitments); cache.merge_blobs(blobs); cache.merge_blobs(random_blobs); - assert_cache_consistent(cache); + assert_cache_consistent(cache, max_len); } #[test] fn valid_blobs_block_invalid_blobs() { - let (block_commitments, blobs, random_blobs) = pre_setup(); + let (block_commitments, blobs, random_blobs, max_len) = pre_setup(); let (block_commitments, blobs, random_blobs) = setup_pending_components(block_commitments, blobs, random_blobs); let block_root = Hash256::zero(); - let mut cache = >::empty(block_root); + let mut cache = >::empty(block_root, max_len); cache.merge_blobs(blobs); cache.merge_block(block_commitments); cache.merge_blobs(random_blobs); - assert_cache_consistent(cache); + assert_cache_consistent(cache, max_len); } #[test] fn valid_blobs_invalid_blobs_block() { - let (block_commitments, blobs, random_blobs) = pre_setup(); + let (block_commitments, blobs, random_blobs, max_len) = pre_setup(); let (block_commitments, blobs, random_blobs) = setup_pending_components(block_commitments, blobs, random_blobs); let block_root = Hash256::zero(); - let mut cache = >::empty(block_root); + let mut cache = >::empty(block_root, max_len); cache.merge_blobs(blobs); cache.merge_blobs(random_blobs); cache.merge_block(block_commitments); - assert_cache_consistent(cache); + assert_cache_consistent(cache, max_len); } } diff --git a/beacon_node/beacon_chain/src/fetch_blobs.rs b/beacon_node/beacon_chain/src/fetch_blobs.rs index f740b693fb..f1646072c9 100644 --- a/beacon_node/beacon_chain/src/fetch_blobs.rs +++ b/beacon_node/beacon_chain/src/fetch_blobs.rs @@ -21,8 +21,8 @@ use std::sync::Arc; use tokio::sync::mpsc::Receiver; use types::blob_sidecar::{BlobSidecarError, FixedBlobSidecarList}; use types::{ - BeaconStateError, BlobSidecar, DataColumnSidecar, DataColumnSidecarList, EthSpec, FullPayload, - Hash256, SignedBeaconBlock, SignedBeaconBlockHeader, + BeaconStateError, BlobSidecar, ChainSpec, DataColumnSidecar, DataColumnSidecarList, EthSpec, + FullPayload, Hash256, SignedBeaconBlock, SignedBeaconBlockHeader, }; pub enum BlobsOrDataColumns { @@ -112,6 +112,7 @@ pub async fn fetch_and_process_engine_blobs( response, signed_block_header, &kzg_commitments_proof, + &chain.spec, )?; let num_fetched_blobs = fixed_blob_sidecar_list @@ -275,8 +276,11 @@ fn build_blob_sidecars( response: Vec>>, signed_block_header: SignedBeaconBlockHeader, kzg_commitments_inclusion_proof: &FixedVector, + spec: &ChainSpec, ) -> Result, FetchEngineBlobError> { - let mut fixed_blob_sidecar_list = FixedBlobSidecarList::default(); + let epoch = block.epoch(); + let mut fixed_blob_sidecar_list = + FixedBlobSidecarList::default(spec.max_blobs_per_block(epoch) as usize); for (index, blob_and_proof) in response .into_iter() .enumerate() diff --git a/beacon_node/beacon_chain/src/kzg_utils.rs b/beacon_node/beacon_chain/src/kzg_utils.rs index bd47e82215..e32ee9c24b 100644 --- a/beacon_node/beacon_chain/src/kzg_utils.rs +++ b/beacon_node/beacon_chain/src/kzg_utils.rs @@ -194,9 +194,11 @@ fn build_data_column_sidecars( spec: &ChainSpec, ) -> Result, String> { 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()); number_of_columns]; + let max_blobs_per_block = spec + .max_blobs_per_block(signed_block_header.message.slot.epoch(E::slots_per_epoch())) + as usize; + let mut columns = vec![Vec::with_capacity(max_blobs_per_block); number_of_columns]; + let mut column_kzg_proofs = vec![Vec::with_capacity(max_blobs_per_block); number_of_columns]; 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", @@ -253,6 +255,7 @@ pub fn reconstruct_blobs( data_columns: &[Arc>], blob_indices_opt: Option>, signed_block: &SignedBlindedBeaconBlock, + spec: &ChainSpec, ) -> Result, String> { // The data columns are from the database, so we assume their correctness. let first_data_column = data_columns @@ -315,10 +318,11 @@ pub fn reconstruct_blobs( .map(Arc::new) .map_err(|e| format!("{e:?}")) }) - .collect::, _>>()? - .into(); + .collect::, _>>()?; - Ok(blob_sidecars) + let max_blobs = spec.max_blobs_per_block(signed_block.epoch()) as usize; + + BlobSidecarList::new(blob_sidecars, max_blobs).map_err(|e| format!("{e:?}")) } /// Reconstruct all data columns from a subset of data column sidecars (requires at least 50%). @@ -478,6 +482,7 @@ mod test { &column_sidecars.iter().as_slice()[0..column_sidecars.len() / 2], Some(blob_indices.clone()), &signed_blinded_block, + spec, ) .unwrap(); diff --git a/beacon_node/beacon_chain/src/observed_data_sidecars.rs b/beacon_node/beacon_chain/src/observed_data_sidecars.rs index a9f4664064..48989e07d3 100644 --- a/beacon_node/beacon_chain/src/observed_data_sidecars.rs +++ b/beacon_node/beacon_chain/src/observed_data_sidecars.rs @@ -24,7 +24,7 @@ pub trait ObservableDataSidecar { fn slot(&self) -> Slot; fn block_proposer_index(&self) -> u64; fn index(&self) -> u64; - fn max_num_of_items(spec: &ChainSpec) -> usize; + fn max_num_of_items(spec: &ChainSpec, slot: Slot) -> usize; } impl ObservableDataSidecar for BlobSidecar { @@ -40,8 +40,8 @@ impl ObservableDataSidecar for BlobSidecar { self.index } - fn max_num_of_items(_spec: &ChainSpec) -> usize { - E::max_blobs_per_block() + fn max_num_of_items(spec: &ChainSpec, slot: Slot) -> usize { + spec.max_blobs_per_block(slot.epoch(E::slots_per_epoch())) as usize } } @@ -58,7 +58,7 @@ impl ObservableDataSidecar for DataColumnSidecar { self.index } - fn max_num_of_items(spec: &ChainSpec) -> usize { + fn max_num_of_items(spec: &ChainSpec, _slot: Slot) -> usize { spec.number_of_columns } } @@ -103,7 +103,9 @@ impl ObservedDataSidecars { slot: data_sidecar.slot(), proposer: data_sidecar.block_proposer_index(), }) - .or_insert_with(|| HashSet::with_capacity(T::max_num_of_items(&self.spec))); + .or_insert_with(|| { + HashSet::with_capacity(T::max_num_of_items(&self.spec, data_sidecar.slot())) + }); let did_not_exist = data_indices.insert(data_sidecar.index()); Ok(!did_not_exist) @@ -123,7 +125,7 @@ impl ObservedDataSidecars { } fn sanitize_data_sidecar(&self, data_sidecar: &T) -> Result<(), Error> { - if data_sidecar.index() >= T::max_num_of_items(&self.spec) as u64 { + if data_sidecar.index() >= T::max_num_of_items(&self.spec, data_sidecar.slot()) as u64 { return Err(Error::InvalidDataIndex(data_sidecar.index())); } let finalized_slot = self.finalized_slot; @@ -179,7 +181,7 @@ mod tests { use crate::test_utils::test_spec; use bls::Hash256; use std::sync::Arc; - use types::MainnetEthSpec; + use types::{Epoch, MainnetEthSpec}; type E = MainnetEthSpec; @@ -333,7 +335,7 @@ mod tests { #[test] fn simple_observations() { let spec = Arc::new(test_spec::()); - let mut cache = ObservedDataSidecars::>::new(spec); + let mut cache = ObservedDataSidecars::>::new(spec.clone()); // Slot 0, index 0 let proposer_index_a = 420; @@ -489,7 +491,7 @@ mod tests { ); // Try adding an out of bounds index - let invalid_index = E::max_blobs_per_block() as u64; + let invalid_index = spec.max_blobs_per_block(Epoch::new(0)); let sidecar_d = get_blob_sidecar(0, proposer_index_a, invalid_index); assert_eq!( cache.observe_sidecar(&sidecar_d), diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index d37398e4e0..fd3cc49626 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -514,7 +514,7 @@ where pub fn mock_execution_layer_with_config(mut self) -> Self { let mock = mock_execution_layer_from_parts::( - self.spec.as_ref().expect("cannot build without spec"), + self.spec.clone().expect("cannot build without spec"), self.runtime.task_executor.clone(), ); self.execution_layer = Some(mock.el.clone()); @@ -614,7 +614,7 @@ where } pub fn mock_execution_layer_from_parts( - spec: &ChainSpec, + spec: Arc, task_executor: TaskExecutor, ) -> MockExecutionLayer { let shanghai_time = spec.capella_fork_epoch.map(|epoch| { @@ -630,7 +630,7 @@ pub fn mock_execution_layer_from_parts( HARNESS_GENESIS_TIME + spec.seconds_per_slot * E::slots_per_epoch() * epoch.as_u64() }); - let kzg = get_kzg(spec); + let kzg = get_kzg(&spec); MockExecutionLayer::new( task_executor, @@ -640,7 +640,7 @@ pub fn mock_execution_layer_from_parts( prague_time, osaka_time, Some(JwtKey::from_slice(&DEFAULT_JWT_SECRET).unwrap()), - spec.clone(), + spec, Some(kzg), ) } @@ -749,15 +749,15 @@ where pub fn get_head_block(&self) -> RpcBlock { let block = self.chain.head_beacon_block(); let block_root = block.canonical_root(); - let blobs = self.chain.get_blobs(&block_root).unwrap(); - RpcBlock::new(Some(block_root), block, Some(blobs)).unwrap() + let blobs = self.chain.get_blobs(&block_root).unwrap().blobs(); + RpcBlock::new(Some(block_root), block, blobs).unwrap() } pub fn get_full_block(&self, block_root: &Hash256) -> RpcBlock { let block = self.chain.get_blinded_block(block_root).unwrap().unwrap(); let full_block = self.chain.store.make_full_block(block_root, block).unwrap(); - let blobs = self.chain.get_blobs(block_root).unwrap(); - RpcBlock::new(Some(*block_root), Arc::new(full_block), Some(blobs)).unwrap() + let blobs = self.chain.get_blobs(block_root).unwrap().blobs(); + RpcBlock::new(Some(*block_root), Arc::new(full_block), blobs).unwrap() } pub fn get_all_validators(&self) -> Vec { @@ -2020,7 +2020,7 @@ where let (block, blob_items) = block_contents; let sidecars = blob_items - .map(|(proofs, blobs)| BlobSidecar::build_sidecars(blobs, &block, proofs)) + .map(|(proofs, blobs)| BlobSidecar::build_sidecars(blobs, &block, proofs, &self.spec)) .transpose() .unwrap(); let block_hash: SignedBeaconBlockHash = self @@ -2046,7 +2046,7 @@ where let (block, blob_items) = block_contents; let sidecars = blob_items - .map(|(proofs, blobs)| BlobSidecar::build_sidecars(blobs, &block, proofs)) + .map(|(proofs, blobs)| BlobSidecar::build_sidecars(blobs, &block, proofs, &self.spec)) .transpose() .unwrap(); let block_root = block.canonical_root(); @@ -2817,11 +2817,12 @@ pub fn generate_rand_block_and_blobs( fork_name: ForkName, num_blobs: NumBlobs, rng: &mut impl Rng, + spec: &ChainSpec, ) -> (SignedBeaconBlock>, Vec>) { let inner = map_fork_name!(fork_name, BeaconBlock, <_>::random_for_test(rng)); let mut block = SignedBeaconBlock::from_block(inner, types::Signature::random_for_test(rng)); - + let max_blobs = spec.max_blobs_per_block(block.epoch()) as usize; let mut blob_sidecars = vec![]; let bundle = match block { @@ -2831,7 +2832,7 @@ pub fn generate_rand_block_and_blobs( // Get either zero blobs or a random number of blobs between 1 and Max Blobs. let payload: &mut FullPayloadDeneb = &mut message.body.execution_payload; let num_blobs = match num_blobs { - NumBlobs::Random => rng.gen_range(1..=E::max_blobs_per_block()), + NumBlobs::Random => rng.gen_range(1..=max_blobs), NumBlobs::Number(n) => n, NumBlobs::None => 0, }; @@ -2851,7 +2852,7 @@ pub fn generate_rand_block_and_blobs( // Get either zero blobs or a random number of blobs between 1 and Max Blobs. let payload: &mut FullPayloadElectra = &mut message.body.execution_payload; let num_blobs = match num_blobs { - NumBlobs::Random => rng.gen_range(1..=E::max_blobs_per_block()), + NumBlobs::Random => rng.gen_range(1..=max_blobs), NumBlobs::Number(n) => n, NumBlobs::None => 0, }; @@ -2870,7 +2871,7 @@ pub fn generate_rand_block_and_blobs( // Get either zero blobs or a random number of blobs between 1 and Max Blobs. let payload: &mut FullPayloadFulu = &mut message.body.execution_payload; let num_blobs = match num_blobs { - NumBlobs::Random => rng.gen_range(1..=E::max_blobs_per_block()), + NumBlobs::Random => rng.gen_range(1..=max_blobs), NumBlobs::Number(n) => n, NumBlobs::None => 0, }; @@ -2924,7 +2925,7 @@ pub fn generate_rand_block_and_data_columns( DataColumnSidecarList, ) { let kzg = get_kzg(spec); - let (block, blobs) = generate_rand_block_and_blobs(fork_name, num_blobs, rng); + let (block, blobs) = generate_rand_block_and_blobs(fork_name, num_blobs, rng, spec); let blob_refs = blobs.iter().map(|b| &b.blob).collect::>(); let data_columns = blobs_to_data_column_sidecars(&blob_refs, &block, &kzg, spec).unwrap(); diff --git a/beacon_node/beacon_chain/tests/attestation_production.rs b/beacon_node/beacon_chain/tests/attestation_production.rs index 87fefe7114..6000115993 100644 --- a/beacon_node/beacon_chain/tests/attestation_production.rs +++ b/beacon_node/beacon_chain/tests/attestation_production.rs @@ -155,7 +155,7 @@ async fn produces_attestations() { .store .make_full_block(&block_root, blinded_block) .unwrap(); - let blobs = chain.get_blobs(&block_root).unwrap(); + let blobs = chain.get_blobs(&block_root).unwrap().blobs(); let epoch_boundary_slot = state .current_epoch() @@ -223,7 +223,7 @@ async fn produces_attestations() { assert_eq!(data.target.root, target_root, "bad target root"); let rpc_block = - RpcBlock::::new(None, Arc::new(block.clone()), Some(blobs.clone())) + RpcBlock::::new(None, Arc::new(block.clone()), blobs.clone()) .unwrap(); let beacon_chain::data_availability_checker::MaybeAvailableBlock::Available( available_block, @@ -299,10 +299,11 @@ async fn early_attester_cache_old_request() { let head_blobs = harness .chain .get_blobs(&head.beacon_block_root) - .expect("should get blobs"); + .expect("should get blobs") + .blobs(); let rpc_block = - RpcBlock::::new(None, head.beacon_block.clone(), Some(head_blobs)).unwrap(); + RpcBlock::::new(None, head.beacon_block.clone(), head_blobs).unwrap(); let beacon_chain::data_availability_checker::MaybeAvailableBlock::Available(available_block) = harness .chain diff --git a/beacon_node/beacon_chain/tests/block_verification.rs b/beacon_node/beacon_chain/tests/block_verification.rs index 103734b224..b61f758cac 100644 --- a/beacon_node/beacon_chain/tests/block_verification.rs +++ b/beacon_node/beacon_chain/tests/block_verification.rs @@ -65,12 +65,13 @@ async fn get_chain_segment() -> (Vec>, Vec( signed_block: &SignedBeaconBlock, blobs: &mut BlobSidecarList, ) { - for old_blob_sidecar in blobs.iter_mut() { + for old_blob_sidecar in blobs.as_mut_slice() { let new_blob = Arc::new(BlobSidecar:: { index: old_blob_sidecar.index, blob: old_blob_sidecar.blob.clone(), @@ -1223,7 +1225,7 @@ async fn verify_block_for_gossip_slashing_detection() { let slasher = Arc::new( Slasher::open( SlasherConfig::new(slasher_dir.path().into()), - spec, + spec.clone(), test_logger(), ) .unwrap(), @@ -1247,7 +1249,7 @@ async fn verify_block_for_gossip_slashing_detection() { if let Some((kzg_proofs, blobs)) = blobs1 { let sidecars = - BlobSidecar::build_sidecars(blobs, verified_block.block(), kzg_proofs).unwrap(); + BlobSidecar::build_sidecars(blobs, verified_block.block(), kzg_proofs, &spec).unwrap(); for sidecar in sidecars { let blob_index = sidecar.index; let verified_blob = harness diff --git a/beacon_node/beacon_chain/tests/events.rs b/beacon_node/beacon_chain/tests/events.rs index ab784d3be4..c9bd55e062 100644 --- a/beacon_node/beacon_chain/tests/events.rs +++ b/beacon_node/beacon_chain/tests/events.rs @@ -73,7 +73,7 @@ async fn blob_sidecar_event_on_process_rpc_blobs() { let blob_1 = Arc::new(blob_1); let blob_2 = Arc::new(blob_2); - let blobs = FixedBlobSidecarList::from(vec![Some(blob_1.clone()), Some(blob_2.clone())]); + let blobs = FixedBlobSidecarList::new(vec![Some(blob_1.clone()), Some(blob_2.clone())]); let expected_sse_blobs = vec![ SseBlobSidecar::from_blob_sidecar(blob_1.as_ref()), SseBlobSidecar::from_blob_sidecar(blob_2.as_ref()), diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index ed97b8d634..60d46e8269 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -2317,7 +2317,12 @@ async fn weak_subjectivity_sync_test(slots: Vec, checkpoint_slot: Slot) { .get_full_block(&wss_block_root) .unwrap() .unwrap(); - let wss_blobs_opt = harness.chain.store.get_blobs(&wss_block_root).unwrap(); + let wss_blobs_opt = harness + .chain + .store + .get_blobs(&wss_block_root) + .unwrap() + .blobs(); let wss_state = full_store .get_state(&wss_state_root, Some(checkpoint_slot)) .unwrap() @@ -2342,8 +2347,10 @@ async fn weak_subjectivity_sync_test(slots: Vec, checkpoint_slot: Slot) { let kzg = get_kzg(&spec); - let mock = - mock_execution_layer_from_parts(&harness.spec, harness.runtime.task_executor.clone()); + let mock = mock_execution_layer_from_parts( + harness.spec.clone(), + harness.runtime.task_executor.clone(), + ); // Initialise a new beacon chain from the finalized checkpoint. // The slot clock must be set to a time ahead of the checkpoint state. @@ -2388,7 +2395,11 @@ async fn weak_subjectivity_sync_test(slots: Vec, checkpoint_slot: Slot) { .await .unwrap() .unwrap(); - let store_wss_blobs_opt = beacon_chain.store.get_blobs(&wss_block_root).unwrap(); + let store_wss_blobs_opt = beacon_chain + .store + .get_blobs(&wss_block_root) + .unwrap() + .blobs(); assert_eq!(store_wss_block, wss_block); assert_eq!(store_wss_blobs_opt, wss_blobs_opt); @@ -2407,7 +2418,7 @@ async fn weak_subjectivity_sync_test(slots: Vec, checkpoint_slot: Slot) { .await .unwrap() .unwrap(); - let blobs = harness.chain.get_blobs(&block_root).expect("blobs"); + let blobs = harness.chain.get_blobs(&block_root).expect("blobs").blobs(); let slot = full_block.slot(); let state_root = full_block.state_root(); @@ -2415,7 +2426,7 @@ async fn weak_subjectivity_sync_test(slots: Vec, checkpoint_slot: Slot) { beacon_chain .process_block( full_block.canonical_root(), - RpcBlock::new(Some(block_root), Arc::new(full_block), Some(blobs)).unwrap(), + RpcBlock::new(Some(block_root), Arc::new(full_block), blobs).unwrap(), NotifyExecutionLayer::Yes, BlockImportSource::Lookup, || Ok(()), @@ -2469,13 +2480,13 @@ async fn weak_subjectivity_sync_test(slots: Vec, checkpoint_slot: Slot) { .await .expect("should get block") .expect("should get block"); - let blobs = harness.chain.get_blobs(&block_root).expect("blobs"); + let blobs = harness.chain.get_blobs(&block_root).expect("blobs").blobs(); if let MaybeAvailableBlock::Available(block) = harness .chain .data_availability_checker .verify_kzg_for_rpc_block( - RpcBlock::new(Some(block_root), Arc::new(full_block), Some(blobs)).unwrap(), + RpcBlock::new(Some(block_root), Arc::new(full_block), blobs).unwrap(), ) .expect("should verify kzg") { @@ -3351,7 +3362,7 @@ fn check_blob_existence( .unwrap() .map(Result::unwrap) { - if let Some(blobs) = harness.chain.store.get_blobs(&block_root).unwrap() { + if let Some(blobs) = harness.chain.store.get_blobs(&block_root).unwrap().blobs() { assert!(should_exist, "blobs at slot {slot} exist but should not"); blobs_seen += blobs.len(); } else { diff --git a/beacon_node/client/src/builder.rs b/beacon_node/client/src/builder.rs index 24c6615822..1cd9e89b96 100644 --- a/beacon_node/client/src/builder.rs +++ b/beacon_node/client/src/builder.rs @@ -36,7 +36,6 @@ use network::{NetworkConfig, NetworkSenders, NetworkService}; use slasher::Slasher; use slasher_service::SlasherService; use slog::{debug, info, warn, Logger}; -use ssz::Decode; use std::net::TcpListener; use std::path::{Path, PathBuf}; use std::sync::Arc; @@ -361,10 +360,11 @@ where let anchor_block = SignedBeaconBlock::from_ssz_bytes(&anchor_block_bytes, &spec) .map_err(|e| format!("Unable to parse weak subj block SSZ: {:?}", e))?; let anchor_blobs = if anchor_block.message().body().has_blobs() { + let max_blobs_len = spec.max_blobs_per_block(anchor_block.epoch()) as usize; let anchor_blobs_bytes = anchor_blobs_bytes .ok_or("Blobs for checkpoint must be provided using --checkpoint-blobs")?; Some( - BlobSidecarList::from_ssz_bytes(&anchor_blobs_bytes) + BlobSidecarList::from_ssz_bytes(&anchor_blobs_bytes, max_blobs_len) .map_err(|e| format!("Unable to parse weak subj blobs SSZ: {e:?}"))?, ) } else { diff --git a/beacon_node/execution_layer/src/engine_api/http.rs b/beacon_node/execution_layer/src/engine_api/http.rs index 1fd9f81d46..daf2bf6ed4 100644 --- a/beacon_node/execution_layer/src/engine_api/http.rs +++ b/beacon_node/execution_layer/src/engine_api/http.rs @@ -1383,7 +1383,8 @@ mod test { impl Tester { pub fn new(with_auth: bool) -> Self { - let server = MockServer::unit_testing(); + let spec = Arc::new(MainnetEthSpec::default_spec()); + let server = MockServer::unit_testing(spec); let rpc_url = SensitiveUrl::parse(&server.url()).unwrap(); let echo_url = SensitiveUrl::parse(&format!("{}/echo", server.url())).unwrap(); diff --git a/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs b/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs index 2a39796707..9fa375b375 100644 --- a/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs +++ b/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs @@ -154,6 +154,7 @@ pub struct ExecutionBlockGenerator { pub blobs_bundles: HashMap>, pub kzg: Option>, rng: Arc>, + spec: Arc, } fn make_rng() -> Arc> { @@ -172,6 +173,7 @@ impl ExecutionBlockGenerator { cancun_time: Option, prague_time: Option, osaka_time: Option, + spec: Arc, kzg: Option>, ) -> Self { let mut gen = Self { @@ -192,6 +194,7 @@ impl ExecutionBlockGenerator { blobs_bundles: <_>::default(), kzg, rng: make_rng(), + spec, }; gen.insert_pow_block(0).unwrap(); @@ -697,7 +700,11 @@ impl ExecutionBlockGenerator { if execution_payload.fork_name().deneb_enabled() { // get random number between 0 and Max Blobs let mut rng = self.rng.lock(); - let num_blobs = rng.gen::() % (E::max_blobs_per_block() + 1); + let max_blobs = self + .spec + .max_blobs_per_block_by_fork(execution_payload.fork_name()) + as usize; + let num_blobs = rng.gen::() % (max_blobs + 1); let (bundle, transactions) = generate_blobs(num_blobs)?; for tx in Vec::from(transactions) { execution_payload @@ -906,6 +913,7 @@ mod test { const TERMINAL_DIFFICULTY: u64 = 10; const TERMINAL_BLOCK: u64 = 10; const DIFFICULTY_INCREMENT: u64 = 1; + let spec = Arc::new(MainnetEthSpec::default_spec()); let mut generator: ExecutionBlockGenerator = ExecutionBlockGenerator::new( Uint256::from(TERMINAL_DIFFICULTY), @@ -915,6 +923,7 @@ mod test { None, None, None, + spec, None, ); diff --git a/beacon_node/execution_layer/src/test_utils/mock_execution_layer.rs b/beacon_node/execution_layer/src/test_utils/mock_execution_layer.rs index 9df8d9cc5c..f45bfda9ff 100644 --- a/beacon_node/execution_layer/src/test_utils/mock_execution_layer.rs +++ b/beacon_node/execution_layer/src/test_utils/mock_execution_layer.rs @@ -13,7 +13,7 @@ pub struct MockExecutionLayer { pub server: MockServer, pub el: ExecutionLayer, pub executor: TaskExecutor, - pub spec: ChainSpec, + pub spec: Arc, } impl MockExecutionLayer { @@ -30,7 +30,7 @@ impl MockExecutionLayer { None, None, Some(JwtKey::from_slice(&DEFAULT_JWT_SECRET).unwrap()), - spec, + Arc::new(spec), None, ) } @@ -44,7 +44,7 @@ impl MockExecutionLayer { prague_time: Option, osaka_time: Option, jwt_key: Option, - spec: ChainSpec, + spec: Arc, kzg: Option>, ) -> Self { let handle = executor.handle().unwrap(); @@ -60,6 +60,7 @@ impl MockExecutionLayer { cancun_time, prague_time, osaka_time, + spec.clone(), kzg, ); @@ -323,7 +324,7 @@ impl MockExecutionLayer { pub async fn with_terminal_block(self, func: U) -> Self where - U: Fn(ChainSpec, ExecutionLayer, Option) -> V, + U: Fn(Arc, ExecutionLayer, Option) -> V, V: Future, { let terminal_block_number = self diff --git a/beacon_node/execution_layer/src/test_utils/mod.rs b/beacon_node/execution_layer/src/test_utils/mod.rs index 5934c069a2..75ff435886 100644 --- a/beacon_node/execution_layer/src/test_utils/mod.rs +++ b/beacon_node/execution_layer/src/test_utils/mod.rs @@ -21,7 +21,7 @@ use std::marker::PhantomData; use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; use std::sync::{Arc, LazyLock}; use tokio::{runtime, sync::oneshot}; -use types::{EthSpec, ExecutionBlockHash, Uint256}; +use types::{ChainSpec, EthSpec, ExecutionBlockHash, Uint256}; use warp::{http::StatusCode, Filter, Rejection}; use crate::EngineCapabilities; @@ -111,7 +111,7 @@ pub struct MockServer { } impl MockServer { - pub fn unit_testing() -> Self { + pub fn unit_testing(chain_spec: Arc) -> Self { Self::new( &runtime::Handle::current(), JwtKey::from_slice(&DEFAULT_JWT_SECRET).unwrap(), @@ -122,6 +122,7 @@ impl MockServer { None, // FIXME(deneb): should this be the default? None, // FIXME(electra): should this be the default? None, // FIXME(fulu): should this be the default? + chain_spec, None, ) } @@ -129,6 +130,7 @@ impl MockServer { pub fn new_with_config( handle: &runtime::Handle, config: MockExecutionConfig, + spec: Arc, kzg: Option>, ) -> Self { let MockExecutionConfig { @@ -152,6 +154,7 @@ impl MockServer { cancun_time, prague_time, osaka_time, + spec, kzg, ); @@ -216,6 +219,7 @@ impl MockServer { cancun_time: Option, prague_time: Option, osaka_time: Option, + spec: Arc, kzg: Option>, ) -> Self { Self::new_with_config( @@ -231,6 +235,7 @@ impl MockServer { prague_time, osaka_time, }, + spec, kzg, ) } diff --git a/beacon_node/http_api/src/block_id.rs b/beacon_node/http_api/src/block_id.rs index b9e4883318..0b00958f26 100644 --- a/beacon_node/http_api/src/block_id.rs +++ b/beacon_node/http_api/src/block_id.rs @@ -287,14 +287,16 @@ impl BlockId { })?; // Return the `BlobSidecarList` identified by `self`. + let max_blobs_per_block = chain.spec.max_blobs_per_block(block.epoch()) as usize; let blob_sidecar_list = if !blob_kzg_commitments.is_empty() { if chain.spec.is_peer_das_enabled_for_epoch(block.epoch()) { Self::get_blobs_from_data_columns(chain, root, query.indices, &block)? } else { - Self::get_blobs(chain, root, query.indices)? + Self::get_blobs(chain, root, query.indices, max_blobs_per_block)? } } else { - BlobSidecarList::default() + BlobSidecarList::new(vec![], max_blobs_per_block) + .map_err(|e| warp_utils::reject::custom_server_error(format!("{:?}", e)))? }; Ok((block, blob_sidecar_list, execution_optimistic, finalized)) @@ -304,22 +306,25 @@ impl BlockId { chain: &BeaconChain, root: Hash256, indices: Option>, + max_blobs_per_block: usize, ) -> Result, Rejection> { let blob_sidecar_list = chain .store .get_blobs(&root) .map_err(|e| warp_utils::reject::beacon_chain_error(e.into()))? + .blobs() .ok_or_else(|| { warp_utils::reject::custom_not_found(format!("no blobs stored for block {root}")) })?; let blob_sidecar_list_filtered = match indices { Some(vec) => { - let list = blob_sidecar_list + let list: Vec<_> = blob_sidecar_list .into_iter() .filter(|blob_sidecar| vec.contains(&blob_sidecar.index)) .collect(); - BlobSidecarList::new(list) + + BlobSidecarList::new(list, max_blobs_per_block) .map_err(|e| warp_utils::reject::custom_server_error(format!("{:?}", e)))? } None => blob_sidecar_list, @@ -356,11 +361,13 @@ impl BlockId { ) .collect::, _>>()?; - reconstruct_blobs(&chain.kzg, &data_columns, blob_indices, block).map_err(|e| { - warp_utils::reject::custom_server_error(format!( - "Error reconstructing data columns: {e:?}" - )) - }) + reconstruct_blobs(&chain.kzg, &data_columns, blob_indices, block, &chain.spec).map_err( + |e| { + warp_utils::reject::custom_server_error(format!( + "Error reconstructing data columns: {e:?}" + )) + }, + ) } else { Err(warp_utils::reject::custom_server_error( format!("Insufficient data columns to reconstruct blobs: required {num_required_columns}, but only {num_found_column_keys} were found.") diff --git a/beacon_node/http_api/tests/broadcast_validation_tests.rs b/beacon_node/http_api/tests/broadcast_validation_tests.rs index 8e0a51a32a..db4ef00257 100644 --- a/beacon_node/http_api/tests/broadcast_validation_tests.rs +++ b/beacon_node/http_api/tests/broadcast_validation_tests.rs @@ -1460,7 +1460,8 @@ pub async fn block_seen_on_gossip_with_some_blobs() { let blobs = blobs.expect("should have some blobs"); assert!( blobs.0.len() >= 2, - "need at least 2 blobs for partial reveal" + "need at least 2 blobs for partial reveal, got: {}", + blobs.0.len() ); let partial_kzg_proofs = vec![*blobs.0.first().unwrap()]; diff --git a/beacon_node/lighthouse_network/src/rpc/codec.rs b/beacon_node/lighthouse_network/src/rpc/codec.rs index c3d20bbfb1..61b2699ac5 100644 --- a/beacon_node/lighthouse_network/src/rpc/codec.rs +++ b/beacon_node/lighthouse_network/src/rpc/codec.rs @@ -186,6 +186,7 @@ impl Decoder for SSZSnappyInboundCodec { handle_rpc_request( self.protocol.versioned_protocol, &decoded_buffer, + self.fork_context.current_fork(), &self.fork_context.spec, ) } @@ -555,6 +556,7 @@ fn handle_length( fn handle_rpc_request( versioned_protocol: SupportedProtocol, decoded_buffer: &[u8], + current_fork: ForkName, spec: &ChainSpec, ) -> Result>, RPCError> { match versioned_protocol { @@ -586,9 +588,23 @@ fn handle_rpc_request( )?, }), ))), - SupportedProtocol::BlobsByRangeV1 => Ok(Some(RequestType::BlobsByRange( - BlobsByRangeRequest::from_ssz_bytes(decoded_buffer)?, - ))), + SupportedProtocol::BlobsByRangeV1 => { + let req = BlobsByRangeRequest::from_ssz_bytes(decoded_buffer)?; + let max_requested_blobs = req + .count + .saturating_mul(spec.max_blobs_per_block_by_fork(current_fork)); + // TODO(pawan): change this to max_blobs_per_rpc_request in the alpha10 PR + if max_requested_blobs > spec.max_request_blob_sidecars { + return Err(RPCError::ErrorResponse( + RpcErrorResponse::InvalidRequest, + format!( + "requested exceeded limit. allowed: {}, requested: {}", + spec.max_request_blob_sidecars, max_requested_blobs + ), + )); + } + Ok(Some(RequestType::BlobsByRange(req))) + } SupportedProtocol::BlobsByRootV1 => { Ok(Some(RequestType::BlobsByRoot(BlobsByRootRequest { blob_ids: RuntimeVariableList::from_ssz_bytes( diff --git a/beacon_node/lighthouse_network/src/rpc/config.rs b/beacon_node/lighthouse_network/src/rpc/config.rs index 7b3a59eac7..75d49e9cb5 100644 --- a/beacon_node/lighthouse_network/src/rpc/config.rs +++ b/beacon_node/lighthouse_network/src/rpc/config.rs @@ -110,8 +110,8 @@ impl RateLimiterConfig { pub const DEFAULT_BLOCKS_BY_RANGE_QUOTA: Quota = Quota::n_every(128, 10); pub const DEFAULT_BLOCKS_BY_ROOT_QUOTA: Quota = Quota::n_every(128, 10); // `DEFAULT_BLOCKS_BY_RANGE_QUOTA` * (target + 1) to account for high usage - pub const DEFAULT_BLOBS_BY_RANGE_QUOTA: Quota = Quota::n_every(512, 10); - pub const DEFAULT_BLOBS_BY_ROOT_QUOTA: Quota = Quota::n_every(512, 10); + pub const DEFAULT_BLOBS_BY_RANGE_QUOTA: Quota = Quota::n_every(896, 10); + pub const DEFAULT_BLOBS_BY_ROOT_QUOTA: Quota = Quota::n_every(896, 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); diff --git a/beacon_node/lighthouse_network/src/rpc/handler.rs b/beacon_node/lighthouse_network/src/rpc/handler.rs index 0a0a6ca754..3a008df023 100644 --- a/beacon_node/lighthouse_network/src/rpc/handler.rs +++ b/beacon_node/lighthouse_network/src/rpc/handler.rs @@ -855,7 +855,8 @@ where } let (req, substream) = substream; - let max_responses = req.max_responses(); + let max_responses = + req.max_responses(self.fork_context.current_fork(), &self.fork_context.spec); // store requests that expect responses if max_responses > 0 { @@ -924,7 +925,8 @@ where } // add the stream to substreams if we expect a response, otherwise drop the stream. - let max_responses = request.max_responses(); + let max_responses = + request.max_responses(self.fork_context.current_fork(), &self.fork_context.spec); if max_responses > 0 { let max_remaining_chunks = if request.expect_exactly_one_response() { // Currently enforced only for multiple responses diff --git a/beacon_node/lighthouse_network/src/rpc/methods.rs b/beacon_node/lighthouse_network/src/rpc/methods.rs index bb8bfb0e20..500188beef 100644 --- a/beacon_node/lighthouse_network/src/rpc/methods.rs +++ b/beacon_node/lighthouse_network/src/rpc/methods.rs @@ -15,6 +15,7 @@ use strum::IntoStaticStr; use superstruct::superstruct; use types::blob_sidecar::BlobIdentifier; use types::light_client_update::MAX_REQUEST_LIGHT_CLIENT_UPDATES; +use types::ForkName; use types::{ blob_sidecar::BlobSidecar, ChainSpec, ColumnIndex, DataColumnIdentifier, DataColumnSidecar, Epoch, EthSpec, Hash256, LightClientBootstrap, LightClientFinalityUpdate, @@ -327,8 +328,9 @@ pub struct BlobsByRangeRequest { } impl BlobsByRangeRequest { - pub fn max_blobs_requested(&self) -> u64 { - self.count.saturating_mul(E::max_blobs_per_block() as u64) + pub fn max_blobs_requested(&self, current_fork: ForkName, spec: &ChainSpec) -> u64 { + let max_blobs_per_block = spec.max_blobs_per_block_by_fork(current_fork); + self.count.saturating_mul(max_blobs_per_block) } } diff --git a/beacon_node/lighthouse_network/src/rpc/mod.rs b/beacon_node/lighthouse_network/src/rpc/mod.rs index 7d091da766..03f1395b8b 100644 --- a/beacon_node/lighthouse_network/src/rpc/mod.rs +++ b/beacon_node/lighthouse_network/src/rpc/mod.rs @@ -181,12 +181,13 @@ impl RPC { let inbound_limiter = inbound_rate_limiter_config.map(|config| { debug!(log, "Using inbound rate limiting params"; "config" => ?config); - RateLimiter::new_with_config(config.0) + RateLimiter::new_with_config(config.0, fork_context.clone()) .expect("Inbound limiter configuration parameters are valid") }); let self_limiter = outbound_rate_limiter_config.map(|config| { - SelfRateLimiter::new(config, log.clone()).expect("Configuration parameters are valid") + SelfRateLimiter::new(config, fork_context.clone(), log.clone()) + .expect("Configuration parameters are valid") }); RPC { diff --git a/beacon_node/lighthouse_network/src/rpc/protocol.rs b/beacon_node/lighthouse_network/src/rpc/protocol.rs index 87bde58292..681b739d59 100644 --- a/beacon_node/lighthouse_network/src/rpc/protocol.rs +++ b/beacon_node/lighthouse_network/src/rpc/protocol.rs @@ -86,6 +86,10 @@ pub static SIGNED_BEACON_BLOCK_FULU_MAX_WITHOUT_PAYLOAD: LazyLock = LazyL /// We calculate the value from its fields instead of constructing the block and checking the length. /// Note: This is only the theoretical upper bound. We further bound the max size we receive over the network /// with `max_chunk_size`. +/// +/// FIXME: Given that these limits are useless we should probably delete them. See: +/// +/// https://github.com/sigp/lighthouse/issues/6790 pub static SIGNED_BEACON_BLOCK_BELLATRIX_MAX: LazyLock = LazyLock::new(|| // Size of a full altair block *SIGNED_BEACON_BLOCK_ALTAIR_MAX @@ -102,7 +106,6 @@ pub static SIGNED_BEACON_BLOCK_DENEB_MAX: LazyLock = LazyLock::new(|| { *SIGNED_BEACON_BLOCK_CAPELLA_MAX_WITHOUT_PAYLOAD + types::ExecutionPayload::::max_execution_payload_deneb_size() // adding max size of execution payload (~16gb) + ssz::BYTES_PER_LENGTH_OFFSET // Adding the additional offsets for the `ExecutionPayload` - + (::ssz_fixed_len() * ::max_blobs_per_block()) + ssz::BYTES_PER_LENGTH_OFFSET }); // Length offset for the blob commitments field. // @@ -110,7 +113,6 @@ pub static SIGNED_BEACON_BLOCK_ELECTRA_MAX: LazyLock = LazyLock::new(|| { *SIGNED_BEACON_BLOCK_ELECTRA_MAX_WITHOUT_PAYLOAD + types::ExecutionPayload::::max_execution_payload_electra_size() // adding max size of execution payload (~16gb) + ssz::BYTES_PER_LENGTH_OFFSET // Adding the additional ssz offset for the `ExecutionPayload` field - + (::ssz_fixed_len() * ::max_blobs_per_block()) + ssz::BYTES_PER_LENGTH_OFFSET }); // Length offset for the blob commitments field. @@ -118,8 +120,6 @@ pub static SIGNED_BEACON_BLOCK_FULU_MAX: LazyLock = LazyLock::new(|| { *SIGNED_BEACON_BLOCK_FULU_MAX_WITHOUT_PAYLOAD + types::ExecutionPayload::::max_execution_payload_fulu_size() + ssz::BYTES_PER_LENGTH_OFFSET - + (::ssz_fixed_len() - * ::max_blobs_per_block()) + ssz::BYTES_PER_LENGTH_OFFSET }); @@ -129,14 +129,6 @@ pub static BLOB_SIDECAR_SIZE: LazyLock = pub static BLOB_SIDECAR_SIZE_MINIMAL: LazyLock = LazyLock::new(BlobSidecar::::max_size); -pub static DATA_COLUMNS_SIDECAR_MIN: LazyLock = LazyLock::new(|| { - DataColumnSidecar::::empty() - .as_ssz_bytes() - .len() -}); -pub static DATA_COLUMNS_SIDECAR_MAX: LazyLock = - LazyLock::new(DataColumnSidecar::::max_size); - pub static ERROR_TYPE_MIN: LazyLock = LazyLock::new(|| { VariableList::::from(Vec::::new()) .as_ssz_bytes() @@ -635,8 +627,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 => rpc_data_column_limits(), - Protocol::DataColumnsByRange => rpc_data_column_limits(), + Protocol::DataColumnsByRoot => rpc_data_column_limits::(fork_context.current_fork()), + Protocol::DataColumnsByRange => { + rpc_data_column_limits::(fork_context.current_fork()) + } Protocol::Ping => RpcLimits::new( ::ssz_fixed_len(), ::ssz_fixed_len(), @@ -716,8 +710,14 @@ pub fn rpc_blob_limits() -> RpcLimits { } } -pub fn rpc_data_column_limits() -> RpcLimits { - RpcLimits::new(*DATA_COLUMNS_SIDECAR_MIN, *DATA_COLUMNS_SIDECAR_MAX) +// TODO(peerdas): fix hardcoded max here +pub fn rpc_data_column_limits(fork_name: ForkName) -> RpcLimits { + RpcLimits::new( + DataColumnSidecar::::empty().as_ssz_bytes().len(), + DataColumnSidecar::::max_size( + E::default_spec().max_blobs_per_block_by_fork(fork_name) as usize + ), + ) } /* Inbound upgrade */ @@ -815,13 +815,13 @@ impl RequestType { /* These functions are used in the handler for stream management */ /// Maximum number of responses expected for this request. - pub fn max_responses(&self) -> u64 { + pub fn max_responses(&self, current_fork: ForkName, spec: &ChainSpec) -> u64 { match self { RequestType::Status(_) => 1, RequestType::Goodbye(_) => 0, RequestType::BlocksByRange(req) => *req.count(), RequestType::BlocksByRoot(req) => req.block_roots().len() as u64, - RequestType::BlobsByRange(req) => req.max_blobs_requested::(), + RequestType::BlobsByRange(req) => req.max_blobs_requested(current_fork, spec), RequestType::BlobsByRoot(req) => req.blob_ids.len() as u64, RequestType::DataColumnsByRoot(req) => req.data_column_ids.len() as u64, RequestType::DataColumnsByRange(req) => req.max_requested::(), diff --git a/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs b/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs index ecbacc8c11..b9e82a5f1e 100644 --- a/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs +++ b/beacon_node/lighthouse_network/src/rpc/rate_limiter.rs @@ -6,10 +6,11 @@ use serde::{Deserialize, Serialize}; use std::future::Future; use std::hash::Hash; use std::pin::Pin; +use std::sync::Arc; use std::task::{Context, Poll}; use std::time::{Duration, Instant}; use tokio::time::Interval; -use types::EthSpec; +use types::{ChainSpec, EthSpec, ForkContext, ForkName}; /// Nanoseconds since a given time. // Maintained as u64 to reduce footprint @@ -109,6 +110,7 @@ pub struct RPCRateLimiter { lc_finality_update_rl: Limiter, /// LightClientUpdatesByRange rate limiter. lc_updates_by_range_rl: Limiter, + fork_context: Arc, } /// Error type for non conformant requests @@ -176,7 +178,7 @@ impl RPCRateLimiterBuilder { self } - pub fn build(self) -> Result { + pub fn build(self, fork_context: Arc) -> Result { // get our quotas let ping_quota = self.ping_quota.ok_or("Ping quota not specified")?; let metadata_quota = self.metadata_quota.ok_or("MetaData quota not specified")?; @@ -253,13 +255,14 @@ impl RPCRateLimiterBuilder { lc_finality_update_rl, lc_updates_by_range_rl, init_time: Instant::now(), + fork_context, }) } } pub trait RateLimiterItem { fn protocol(&self) -> Protocol; - fn max_responses(&self) -> u64; + fn max_responses(&self, current_fork: ForkName, spec: &ChainSpec) -> u64; } impl RateLimiterItem for super::RequestType { @@ -267,13 +270,16 @@ impl RateLimiterItem for super::RequestType { self.versioned_protocol().protocol() } - fn max_responses(&self) -> u64 { - self.max_responses() + fn max_responses(&self, current_fork: ForkName, spec: &ChainSpec) -> u64 { + self.max_responses(current_fork, spec) } } impl RPCRateLimiter { - pub fn new_with_config(config: RateLimiterConfig) -> Result { + pub fn new_with_config( + config: RateLimiterConfig, + fork_context: Arc, + ) -> Result { // Destructure to make sure every configuration value is used. let RateLimiterConfig { ping_quota, @@ -316,7 +322,7 @@ impl RPCRateLimiter { Protocol::LightClientUpdatesByRange, light_client_updates_by_range_quota, ) - .build() + .build(fork_context) } /// Get a builder instance. @@ -330,7 +336,9 @@ impl RPCRateLimiter { request: &Item, ) -> Result<(), RateLimitedErr> { let time_since_start = self.init_time.elapsed(); - let tokens = request.max_responses().max(1); + let tokens = request + .max_responses(self.fork_context.current_fork(), &self.fork_context.spec) + .max(1); let check = |limiter: &mut Limiter| limiter.allows(time_since_start, peer_id, tokens); diff --git a/beacon_node/lighthouse_network/src/rpc/self_limiter.rs b/beacon_node/lighthouse_network/src/rpc/self_limiter.rs index e968ad11e3..e0c8593f29 100644 --- a/beacon_node/lighthouse_network/src/rpc/self_limiter.rs +++ b/beacon_node/lighthouse_network/src/rpc/self_limiter.rs @@ -1,5 +1,6 @@ use std::{ collections::{hash_map::Entry, HashMap, VecDeque}, + sync::Arc, task::{Context, Poll}, time::Duration, }; @@ -9,7 +10,7 @@ use libp2p::{swarm::NotifyHandler, PeerId}; use slog::{crit, debug, Logger}; use smallvec::SmallVec; use tokio_util::time::DelayQueue; -use types::EthSpec; +use types::{EthSpec, ForkContext}; use super::{ config::OutboundRateLimiterConfig, @@ -50,9 +51,13 @@ pub enum Error { impl SelfRateLimiter { /// Creates a new [`SelfRateLimiter`] based on configration values. - pub fn new(config: OutboundRateLimiterConfig, log: Logger) -> Result { + pub fn new( + config: OutboundRateLimiterConfig, + fork_context: Arc, + log: Logger, + ) -> Result { debug!(log, "Using self rate limiting params"; "config" => ?config); - let limiter = RateLimiter::new_with_config(config.0)?; + let limiter = RateLimiter::new_with_config(config.0, fork_context)?; Ok(SelfRateLimiter { delayed_requests: Default::default(), @@ -215,7 +220,7 @@ mod tests { use crate::service::api_types::{AppRequestId, RequestId, SyncRequestId}; use libp2p::PeerId; use std::time::Duration; - use types::MainnetEthSpec; + use types::{EthSpec, ForkContext, Hash256, MainnetEthSpec, Slot}; /// Test that `next_peer_request_ready` correctly maintains the queue. #[tokio::test] @@ -225,8 +230,13 @@ mod tests { ping_quota: Quota::n_every(1, 2), ..Default::default() }); + let fork_context = std::sync::Arc::new(ForkContext::new::( + Slot::new(0), + Hash256::ZERO, + &MainnetEthSpec::default_spec(), + )); let mut limiter: SelfRateLimiter = - SelfRateLimiter::new(config, log).unwrap(); + SelfRateLimiter::new(config, fork_context, log).unwrap(); let peer_id = PeerId::random(); for i in 1..=5u32 { 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 c4944078fe..b4f19f668d 100644 --- a/beacon_node/network/src/network_beacon_processor/rpc_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/rpc_methods.rs @@ -890,14 +890,6 @@ impl NetworkBeaconProcessor { "start_slot" => req.start_slot, ); - // Should not send more than max request blocks - if req.max_blobs_requested::() > self.chain.spec.max_request_blob_sidecars { - return Err(( - RpcErrorResponse::InvalidRequest, - "Request exceeded `MAX_REQUEST_BLOBS_SIDECARS`", - )); - } - let request_start_slot = Slot::from(req.start_slot); let data_availability_boundary_slot = match self.chain.data_availability_boundary() { diff --git a/beacon_node/network/src/network_beacon_processor/tests.rs b/beacon_node/network/src/network_beacon_processor/tests.rs index 7e27a91bd6..8238fa146d 100644 --- a/beacon_node/network/src/network_beacon_processor/tests.rs +++ b/beacon_node/network/src/network_beacon_processor/tests.rs @@ -259,7 +259,7 @@ impl TestRig { assert!(beacon_processor.is_ok()); let block = next_block_tuple.0; let blob_sidecars = if let Some((kzg_proofs, blobs)) = next_block_tuple.1 { - Some(BlobSidecar::build_sidecars(blobs, &block, kzg_proofs).unwrap()) + Some(BlobSidecar::build_sidecars(blobs, &block, kzg_proofs, &chain.spec).unwrap()) } else { None }; @@ -344,7 +344,7 @@ impl TestRig { } pub fn enqueue_single_lookup_rpc_blobs(&self) { if let Some(blobs) = self.next_blobs.clone() { - let blobs = FixedBlobSidecarList::from(blobs.into_iter().map(Some).collect::>()); + let blobs = FixedBlobSidecarList::new(blobs.into_iter().map(Some).collect::>()); self.network_beacon_processor .send_rpc_blobs( self.next_block.canonical_root(), @@ -1130,7 +1130,12 @@ async fn test_blobs_by_range() { .block_root_at_slot(Slot::new(slot), WhenSlotSkipped::None) .unwrap(); blob_count += root - .map(|root| rig.chain.get_blobs(&root).unwrap_or_default().len()) + .map(|root| { + rig.chain + .get_blobs(&root) + .map(|list| list.len()) + .unwrap_or(0) + }) .unwrap_or(0); } let mut actual_count = 0; diff --git a/beacon_node/network/src/sync/block_sidecar_coupling.rs b/beacon_node/network/src/sync/block_sidecar_coupling.rs index 966ce55fab..7a234eaef0 100644 --- a/beacon_node/network/src/sync/block_sidecar_coupling.rs +++ b/beacon_node/network/src/sync/block_sidecar_coupling.rs @@ -2,13 +2,13 @@ use beacon_chain::{ block_verification_types::RpcBlock, data_column_verification::CustodyDataColumn, get_block_root, }; use lighthouse_network::PeerId; -use ssz_types::VariableList; use std::{ collections::{HashMap, VecDeque}, sync::Arc, }; use types::{ - BlobSidecar, ChainSpec, ColumnIndex, DataColumnSidecar, EthSpec, Hash256, SignedBeaconBlock, + BlobSidecar, ChainSpec, ColumnIndex, DataColumnSidecar, EthSpec, Hash256, RuntimeVariableList, + SignedBeaconBlock, }; #[derive(Debug)] @@ -31,6 +31,7 @@ pub struct RangeBlockComponentsRequest { num_custody_column_requests: Option, /// The peers the request was made to. pub(crate) peer_ids: Vec, + max_blobs_per_block: usize, } impl RangeBlockComponentsRequest { @@ -39,6 +40,7 @@ impl RangeBlockComponentsRequest { expects_custody_columns: Option>, num_custody_column_requests: Option, peer_ids: Vec, + max_blobs_per_block: usize, ) -> Self { Self { blocks: <_>::default(), @@ -51,6 +53,7 @@ impl RangeBlockComponentsRequest { expects_custody_columns, num_custody_column_requests, peer_ids, + max_blobs_per_block, } } @@ -100,7 +103,7 @@ impl RangeBlockComponentsRequest { 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()); + let mut blob_list = Vec::with_capacity(self.max_blobs_per_block); while { let pair_next_blob = blob_iter .peek() @@ -111,7 +114,7 @@ impl RangeBlockComponentsRequest { blob_list.push(blob_iter.next().ok_or("Missing next blob".to_string())?); } - let mut blobs_buffer = vec![None; E::max_blobs_per_block()]; + let mut blobs_buffer = vec![None; self.max_blobs_per_block]; for blob in blob_list { let blob_index = blob.index as usize; let Some(blob_opt) = blobs_buffer.get_mut(blob_index) else { @@ -123,7 +126,11 @@ impl RangeBlockComponentsRequest { *blob_opt = Some(blob); } } - let blobs = VariableList::from(blobs_buffer.into_iter().flatten().collect::>()); + let blobs = RuntimeVariableList::new( + blobs_buffer.into_iter().flatten().collect::>(), + self.max_blobs_per_block, + ) + .map_err(|_| "Blobs returned exceeds max length".to_string())?; responses.push(RpcBlock::new(None, block, Some(blobs)).map_err(|e| format!("{e:?}"))?) } @@ -245,12 +252,18 @@ mod tests { #[test] fn no_blobs_into_responses() { + let spec = test_spec::(); let peer_id = PeerId::random(); - 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) + .map(|_| { + generate_rand_block_and_blobs::(ForkName::Base, NumBlobs::None, &mut rng, &spec) + .0 + }) .collect::>(); + let max_len = spec.max_blobs_per_block(blocks.first().unwrap().epoch()) as usize; + let mut info = + RangeBlockComponentsRequest::::new(false, None, None, vec![peer_id], max_len); // Send blocks and complete terminate response for block in blocks { @@ -265,15 +278,24 @@ mod tests { #[test] fn empty_blobs_into_responses() { + let spec = test_spec::(); let peer_id = PeerId::random(); - let mut info = RangeBlockComponentsRequest::::new(true, None, None, vec![peer_id]); let mut rng = XorShiftRng::from_seed([42; 16]); let blocks = (0..4) .map(|_| { // Always generate some blobs. - generate_rand_block_and_blobs::(ForkName::Deneb, NumBlobs::Number(3), &mut rng).0 + generate_rand_block_and_blobs::( + ForkName::Deneb, + NumBlobs::Number(3), + &mut rng, + &spec, + ) + .0 }) .collect::>(); + let max_len = spec.max_blobs_per_block(blocks.first().unwrap().epoch()) as usize; + let mut info = + RangeBlockComponentsRequest::::new(true, None, None, vec![peer_id], max_len); // Send blocks and complete terminate response for block in blocks { @@ -294,12 +316,7 @@ mod tests { 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()), - Some(expects_custody_columns.len()), - vec![PeerId::random()], - ); + let mut rng = XorShiftRng::from_seed([42; 16]); let blocks = (0..4) .map(|_| { @@ -311,7 +328,14 @@ mod tests { ) }) .collect::>(); - + let max_len = spec.max_blobs_per_block(blocks.first().unwrap().0.epoch()) as usize; + let mut info = RangeBlockComponentsRequest::::new( + false, + Some(expects_custody_columns.clone()), + Some(expects_custody_columns.len()), + vec![PeerId::random()], + max_len, + ); // Send blocks and complete terminate response for block in &blocks { info.add_block_response(Some(block.0.clone().into())); @@ -355,12 +379,7 @@ mod tests { 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(|_| { @@ -372,7 +391,14 @@ mod tests { ) }) .collect::>(); - + let max_len = spec.max_blobs_per_block(blocks.first().unwrap().0.epoch()) as usize; + let mut info = RangeBlockComponentsRequest::::new( + false, + Some(expects_custody_columns.clone()), + Some(num_of_data_column_requests), + vec![PeerId::random()], + max_len, + ); // Send blocks and complete terminate response for block in &blocks { info.add_block_response(Some(block.0.clone().into())); diff --git a/beacon_node/network/src/sync/manager.rs b/beacon_node/network/src/sync/manager.rs index 5d02be2b4c..2df8b5f94c 100644 --- a/beacon_node/network/src/sync/manager.rs +++ b/beacon_node/network/src/sync/manager.rs @@ -1234,6 +1234,7 @@ impl SyncManager { .network .range_block_and_blob_response(id, block_or_blob) { + let epoch = resp.sender_id.batch_id(); match resp.responses { Ok(blocks) => { match resp.sender_id { @@ -1277,6 +1278,7 @@ impl SyncManager { resp.expects_custody_columns, None, vec![], + self.chain.spec.max_blobs_per_block(epoch) as usize, ), ); // inform range that the request needs to be treated as failed diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index b6b7b315f3..e1b2b974ec 100644 --- a/beacon_node/network/src/sync/network_context.rs +++ b/beacon_node/network/src/sync/network_context.rs @@ -67,6 +67,15 @@ pub enum RangeRequestId { }, } +impl RangeRequestId { + pub fn batch_id(&self) -> BatchId { + match self { + RangeRequestId::RangeSync { batch_id, .. } => *batch_id, + RangeRequestId::BackfillSync { batch_id, .. } => *batch_id, + } + } +} + #[derive(Debug)] pub enum RpcEvent { StreamTermination, @@ -445,11 +454,14 @@ impl SyncNetworkContext { (None, None) }; + // TODO(pawan): this would break if a batch contains multiple epochs + let max_blobs_len = self.chain.spec.max_blobs_per_block(epoch); let info = RangeBlockComponentsRequest::new( expected_blobs, expects_columns, num_of_column_req, requested_peers, + max_blobs_len as usize, ); self.range_block_components_requests .insert(id, (sender_id, info)); @@ -950,12 +962,23 @@ impl SyncNetworkContext { ) -> Option>> { let response = self.blobs_by_root_requests.on_response(id, rpc_event); let response = response.map(|res| { - res.and_then( - |(blobs, seen_timestamp)| match to_fixed_blob_sidecar_list(blobs) { - Ok(blobs) => Ok((blobs, seen_timestamp)), - Err(e) => Err(e.into()), - }, - ) + res.and_then(|(blobs, seen_timestamp)| { + if let Some(max_len) = blobs + .first() + .map(|blob| self.chain.spec.max_blobs_per_block(blob.epoch()) as usize) + { + match to_fixed_blob_sidecar_list(blobs, max_len) { + Ok(blobs) => Ok((blobs, seen_timestamp)), + Err(e) => Err(e.into()), + } + } else { + Err(RpcResponseError::VerifyError( + LookupVerifyError::InternalError( + "Requested blobs for a block that has no blobs".to_string(), + ), + )) + } + }) }); if let Some(Err(RpcResponseError::VerifyError(e))) = &response { self.report_peer(peer_id, PeerAction::LowToleranceError, e.into()); @@ -1150,8 +1173,9 @@ impl SyncNetworkContext { fn to_fixed_blob_sidecar_list( blobs: Vec>>, + max_len: usize, ) -> Result, LookupVerifyError> { - let mut fixed_list = FixedBlobSidecarList::default(); + let mut fixed_list = FixedBlobSidecarList::new(vec![None; max_len]); for blob in blobs.into_iter() { let index = blob.index as usize; *fixed_list diff --git a/beacon_node/network/src/sync/network_context/requests.rs b/beacon_node/network/src/sync/network_context/requests.rs index b9214bafcd..4a5a16459d 100644 --- a/beacon_node/network/src/sync/network_context/requests.rs +++ b/beacon_node/network/src/sync/network_context/requests.rs @@ -28,6 +28,7 @@ pub enum LookupVerifyError { UnrequestedIndex(u64), InvalidInclusionProof, DuplicateData, + InternalError(String), } /// Collection of active requests of a single ReqResp method, i.e. `blocks_by_root` diff --git a/beacon_node/network/src/sync/tests/lookups.rs b/beacon_node/network/src/sync/tests/lookups.rs index a43b3bd022..b9e38237c5 100644 --- a/beacon_node/network/src/sync/tests/lookups.rs +++ b/beacon_node/network/src/sync/tests/lookups.rs @@ -119,6 +119,8 @@ impl TestRig { .network_globals .set_sync_state(SyncState::Synced); + let spec = chain.spec.clone(); + let rng = XorShiftRng::from_seed([42; 16]); TestRig { beacon_processor_rx, @@ -142,6 +144,7 @@ impl TestRig { harness, fork_name, log, + spec, } } @@ -213,7 +216,7 @@ impl TestRig { ) -> (SignedBeaconBlock, Vec>) { let fork_name = self.fork_name; let rng = &mut self.rng; - generate_rand_block_and_blobs::(fork_name, num_blobs, rng) + generate_rand_block_and_blobs::(fork_name, num_blobs, rng, &self.spec) } fn rand_block_and_data_columns( @@ -1328,8 +1331,10 @@ impl TestRig { #[test] fn stable_rng() { + let spec = types::MainnetEthSpec::default_spec(); let mut rng = XorShiftRng::from_seed([42; 16]); - let (block, _) = generate_rand_block_and_blobs::(ForkName::Base, NumBlobs::None, &mut rng); + let (block, _) = + generate_rand_block_and_blobs::(ForkName::Base, NumBlobs::None, &mut rng, &spec); assert_eq!( block.canonical_root(), Hash256::from_slice( @@ -2187,8 +2192,8 @@ mod deneb_only { block_verification_types::{AsBlock, RpcBlock}, data_availability_checker::AvailabilityCheckError, }; - use ssz_types::VariableList; use std::collections::VecDeque; + use types::RuntimeVariableList; struct DenebTester { rig: TestRig, @@ -2546,12 +2551,15 @@ mod deneb_only { fn parent_block_unknown_parent(mut self) -> Self { self.rig.log("parent_block_unknown_parent"); let block = self.unknown_parent_block.take().unwrap(); + let max_len = self.rig.spec.max_blobs_per_block(block.epoch()) as usize; // Now this block is the one we expect requests from self.block = block.clone(); let block = RpcBlock::new( Some(block.canonical_root()), block, - self.unknown_parent_blobs.take().map(VariableList::from), + self.unknown_parent_blobs + .take() + .map(|vec| RuntimeVariableList::from_vec(vec, max_len)), ) .unwrap(); self.rig.parent_block_processed( diff --git a/beacon_node/network/src/sync/tests/mod.rs b/beacon_node/network/src/sync/tests/mod.rs index 47666b413c..6ed5c7f8fa 100644 --- a/beacon_node/network/src/sync/tests/mod.rs +++ b/beacon_node/network/src/sync/tests/mod.rs @@ -12,7 +12,7 @@ use slot_clock::ManualSlotClock; use std::sync::Arc; use store::MemoryStore; use tokio::sync::mpsc; -use types::{test_utils::XorShiftRng, ForkName, MinimalEthSpec as E}; +use types::{test_utils::XorShiftRng, ChainSpec, ForkName, MinimalEthSpec as E}; mod lookups; mod range; @@ -64,4 +64,5 @@ struct TestRig { rng: XorShiftRng, fork_name: ForkName, log: Logger, + spec: Arc, } diff --git a/beacon_node/store/src/blob_sidecar_list_from_root.rs b/beacon_node/store/src/blob_sidecar_list_from_root.rs new file mode 100644 index 0000000000..de63eaa76c --- /dev/null +++ b/beacon_node/store/src/blob_sidecar_list_from_root.rs @@ -0,0 +1,42 @@ +use std::sync::Arc; +use types::{BlobSidecar, BlobSidecarList, EthSpec}; + +#[derive(Debug, Clone)] +pub enum BlobSidecarListFromRoot { + /// Valid root that exists in the DB, but has no blobs associated with it. + NoBlobs, + /// Contains > 1 blob for the requested root. + Blobs(BlobSidecarList), + /// No root exists in the db or cache for the requested root. + NoRoot, +} + +impl From> for BlobSidecarListFromRoot { + fn from(value: BlobSidecarList) -> Self { + Self::Blobs(value) + } +} + +impl BlobSidecarListFromRoot { + pub fn blobs(self) -> Option> { + match self { + Self::NoBlobs | Self::NoRoot => None, + Self::Blobs(blobs) => Some(blobs), + } + } + + #[allow(clippy::len_without_is_empty)] + pub fn len(&self) -> usize { + match self { + Self::NoBlobs | Self::NoRoot => 0, + Self::Blobs(blobs) => blobs.len(), + } + } + + pub fn iter(&self) -> impl Iterator>> { + match self { + Self::NoBlobs | Self::NoRoot => [].iter(), + Self::Blobs(list) => list.iter(), + } + } +} diff --git a/beacon_node/store/src/hot_cold_store.rs b/beacon_node/store/src/hot_cold_store.rs index c6148e5314..c29305f983 100644 --- a/beacon_node/store/src/hot_cold_store.rs +++ b/beacon_node/store/src/hot_cold_store.rs @@ -14,8 +14,8 @@ use crate::metadata::{ }; use crate::state_cache::{PutStateOutcome, StateCache}; use crate::{ - get_data_column_key, get_key_for_col, DBColumn, DatabaseBlock, Error, ItemStore, - KeyValueStoreOp, StoreItem, StoreOp, + get_data_column_key, get_key_for_col, BlobSidecarListFromRoot, DBColumn, DatabaseBlock, Error, + ItemStore, KeyValueStoreOp, StoreItem, StoreOp, }; use crate::{metrics, parse_data_column_key}; use itertools::{process_results, Itertools}; @@ -1280,9 +1280,10 @@ impl, Cold: ItemStore> HotColdDB StoreOp::PutBlobs(_, _) | StoreOp::PutDataColumns(_, _) => true, StoreOp::DeleteBlobs(block_root) => { match self.get_blobs(block_root) { - Ok(Some(blob_sidecar_list)) => { + Ok(BlobSidecarListFromRoot::Blobs(blob_sidecar_list)) => { blobs_to_delete.push((*block_root, blob_sidecar_list)); } + Ok(BlobSidecarListFromRoot::NoBlobs | BlobSidecarListFromRoot::NoRoot) => {} Err(e) => { error!( self.log, "Error getting blobs"; @@ -1290,7 +1291,6 @@ impl, Cold: ItemStore> HotColdDB "error" => ?e ); } - _ => (), } true } @@ -2045,11 +2045,11 @@ impl, Cold: ItemStore> HotColdDB } /// Fetch blobs for a given block from the store. - pub fn get_blobs(&self, block_root: &Hash256) -> Result>, Error> { + pub fn get_blobs(&self, block_root: &Hash256) -> Result, Error> { // Check the cache. if let Some(blobs) = self.block_cache.lock().get_blobs(block_root) { metrics::inc_counter(&metrics::BEACON_BLOBS_CACHE_HIT_COUNT); - return Ok(Some(blobs.clone())); + return Ok(blobs.clone().into()); } match self @@ -2057,13 +2057,27 @@ impl, Cold: ItemStore> HotColdDB .get_bytes(DBColumn::BeaconBlob.into(), block_root.as_slice())? { Some(ref blobs_bytes) => { - let blobs = BlobSidecarList::from_ssz_bytes(blobs_bytes)?; - self.block_cache - .lock() - .put_blobs(*block_root, blobs.clone()); - Ok(Some(blobs)) + // We insert a VariableList of BlobSidecars into the db, but retrieve + // a plain vec since we don't know the length limit of the list without + // knowing the slot. + // The encoding of a VariableList is the same as a regular vec. + let blobs: Vec>> = Vec::<_>::from_ssz_bytes(blobs_bytes)?; + if let Some(max_blobs_per_block) = blobs + .first() + .map(|blob| self.spec.max_blobs_per_block(blob.epoch())) + { + let blobs = BlobSidecarList::from_vec(blobs, max_blobs_per_block as usize); + self.block_cache + .lock() + .put_blobs(*block_root, blobs.clone()); + + Ok(BlobSidecarListFromRoot::Blobs(blobs)) + } else { + // This always implies that there were no blobs for this block_root + Ok(BlobSidecarListFromRoot::NoBlobs) + } } - None => Ok(None), + None => Ok(BlobSidecarListFromRoot::NoRoot), } } diff --git a/beacon_node/store/src/impls/execution_payload.rs b/beacon_node/store/src/impls/execution_payload.rs index 5c60aa8d7e..097b069a66 100644 --- a/beacon_node/store/src/impls/execution_payload.rs +++ b/beacon_node/store/src/impls/execution_payload.rs @@ -1,7 +1,7 @@ use crate::{DBColumn, Error, StoreItem}; use ssz::{Decode, Encode}; use types::{ - BlobSidecarList, EthSpec, ExecutionPayload, ExecutionPayloadBellatrix, ExecutionPayloadCapella, + EthSpec, ExecutionPayload, ExecutionPayloadBellatrix, ExecutionPayloadCapella, ExecutionPayloadDeneb, ExecutionPayloadElectra, ExecutionPayloadFulu, }; @@ -27,7 +27,6 @@ impl_store_item!(ExecutionPayloadCapella); impl_store_item!(ExecutionPayloadDeneb); impl_store_item!(ExecutionPayloadElectra); impl_store_item!(ExecutionPayloadFulu); -impl_store_item!(BlobSidecarList); /// This fork-agnostic implementation should be only used for writing. /// diff --git a/beacon_node/store/src/lib.rs b/beacon_node/store/src/lib.rs index 09ae9a32dd..1458fa846c 100644 --- a/beacon_node/store/src/lib.rs +++ b/beacon_node/store/src/lib.rs @@ -7,6 +7,7 @@ //! //! Provides a simple API for storing/retrieving all types that sometimes needs type-hints. See //! tests for implementation examples. +pub mod blob_sidecar_list_from_root; pub mod chunked_iter; pub mod chunked_vector; pub mod config; @@ -28,6 +29,7 @@ pub mod state_cache; pub mod iter; +pub use self::blob_sidecar_list_from_root::BlobSidecarListFromRoot; pub use self::config::StoreConfig; pub use self::consensus_context::OnDiskConsensusContext; pub use self::hot_cold_store::{HotColdDB, HotStateSummary, Split}; 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 a107f6147a..a303bea268 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 @@ -135,6 +135,8 @@ MAX_REQUEST_BLOB_SIDECARS: 768 MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS: 16384 # `6` BLOB_SIDECAR_SUBNET_COUNT: 6 +# `uint64(6)` +MAX_BLOBS_PER_BLOCK: 6 # DAS CUSTODY_REQUIREMENT: 4 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 f71984059a..68d2b0eafe 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 @@ -118,6 +118,8 @@ MAX_REQUEST_BLOB_SIDECARS: 768 MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS: 16384 # `6` BLOB_SIDECAR_SUBNET_COUNT: 6 +# `uint64(6)` +MAX_BLOBS_PER_BLOCK: 6 # DAS CUSTODY_REQUIREMENT: 4 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 6d344b5b52..930ce0a1bc 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 @@ -124,6 +124,8 @@ MAX_REQUEST_BLOB_SIDECARS: 768 MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS: 4096 # `6` BLOB_SIDECAR_SUBNET_COUNT: 6 +# `uint64(6)` +MAX_BLOBS_PER_BLOCK: 6 # DAS CUSTODY_REQUIREMENT: 4 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 244ddd564d..638f6fe42f 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 @@ -141,6 +141,8 @@ MAX_REQUEST_BLOB_SIDECARS: 768 MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS: 4096 # `6` BLOB_SIDECAR_SUBNET_COUNT: 6 +# `uint64(6)` +MAX_BLOBS_PER_BLOCK: 6 # DAS CUSTODY_REQUIREMENT: 4 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 88f8359bd1..3818518897 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,6 +119,8 @@ MAX_REQUEST_BLOB_SIDECARS: 768 MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS: 4096 # `6` BLOB_SIDECAR_SUBNET_COUNT: 6 +# `uint64(6)` +MAX_BLOBS_PER_BLOCK: 6 # DAS CUSTODY_REQUIREMENT: 4 diff --git a/consensus/state_processing/src/per_block_processing.rs b/consensus/state_processing/src/per_block_processing.rs index 22e0a5eab3..782dbe2a54 100644 --- a/consensus/state_processing/src/per_block_processing.rs +++ b/consensus/state_processing/src/per_block_processing.rs @@ -391,10 +391,12 @@ pub fn partially_verify_execution_payload = VariableList::MaxBlobCommitmentsPerBlock>; -pub type KzgCommitmentOpts = - FixedVector, ::MaxBlobsPerBlock>; /// The number of leaves (including padding) on the `BeaconBlockBody` Merkle tree. /// diff --git a/consensus/types/src/blob_sidecar.rs b/consensus/types/src/blob_sidecar.rs index 302aa2a4c1..ff4555747c 100644 --- a/consensus/types/src/blob_sidecar.rs +++ b/consensus/types/src/blob_sidecar.rs @@ -1,10 +1,10 @@ use crate::test_utils::TestRandom; use crate::{ - beacon_block_body::BLOB_KZG_COMMITMENTS_INDEX, BeaconBlockHeader, BeaconStateError, Blob, - Epoch, EthSpec, FixedVector, Hash256, SignedBeaconBlockHeader, Slot, VariableList, + beacon_block_body::BLOB_KZG_COMMITMENTS_INDEX, AbstractExecPayload, BeaconBlockHeader, + BeaconStateError, Blob, ChainSpec, Epoch, EthSpec, FixedVector, ForkName, + ForkVersionDeserialize, Hash256, KzgProofs, RuntimeFixedVector, RuntimeVariableList, + SignedBeaconBlock, SignedBeaconBlockHeader, Slot, VariableList, }; -use crate::{AbstractExecPayload, ForkName}; -use crate::{ForkVersionDeserialize, KzgProofs, SignedBeaconBlock}; use bls::Signature; use derivative::Derivative; use kzg::{Blob as KzgBlob, Kzg, KzgCommitment, KzgProof, BYTES_PER_BLOB, BYTES_PER_FIELD_ELEMENT}; @@ -30,19 +30,6 @@ pub struct BlobIdentifier { pub index: u64, } -impl BlobIdentifier { - pub fn get_all_blob_ids(block_root: Hash256) -> Vec { - let mut blob_ids = Vec::with_capacity(E::max_blobs_per_block()); - for i in 0..E::max_blobs_per_block() { - blob_ids.push(BlobIdentifier { - block_root, - index: i as u64, - }); - } - blob_ids - } -} - impl PartialOrd for BlobIdentifier { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) @@ -291,19 +278,23 @@ impl BlobSidecar { blobs: BlobsList, block: &SignedBeaconBlock, kzg_proofs: KzgProofs, + spec: &ChainSpec, ) -> Result, BlobSidecarError> { let mut blob_sidecars = vec![]; for (i, (kzg_proof, blob)) in kzg_proofs.iter().zip(blobs).enumerate() { let blob_sidecar = BlobSidecar::new(i, blob, block, *kzg_proof)?; blob_sidecars.push(Arc::new(blob_sidecar)); } - Ok(VariableList::from(blob_sidecars)) + Ok(RuntimeVariableList::from_vec( + blob_sidecars, + spec.max_blobs_per_block(block.epoch()) as usize, + )) } } -pub type BlobSidecarList = VariableList>, ::MaxBlobsPerBlock>; -pub type FixedBlobSidecarList = - FixedVector>>, ::MaxBlobsPerBlock>; +pub type BlobSidecarList = RuntimeVariableList>>; +/// Alias for a non length-constrained list of `BlobSidecar`s. +pub type FixedBlobSidecarList = RuntimeFixedVector>>>; pub type BlobsList = VariableList, ::MaxBlobCommitmentsPerBlock>; impl ForkVersionDeserialize for BlobSidecarList { diff --git a/consensus/types/src/chain_spec.rs b/consensus/types/src/chain_spec.rs index f0bfeba680..65f4c37aa1 100644 --- a/consensus/types/src/chain_spec.rs +++ b/consensus/types/src/chain_spec.rs @@ -237,6 +237,7 @@ pub struct ChainSpec { pub max_request_data_column_sidecars: u64, pub min_epochs_for_blob_sidecars_requests: u64, pub blob_sidecar_subnet_count: u64, + max_blobs_per_block: u64, /* * Networking Derived @@ -616,6 +617,17 @@ impl ChainSpec { } } + /// Return the value of `MAX_BLOBS_PER_BLOCK` appropriate for the fork at `epoch`. + pub fn max_blobs_per_block(&self, epoch: Epoch) -> u64 { + self.max_blobs_per_block_by_fork(self.fork_name_at_epoch(epoch)) + } + + /// Return the value of `MAX_BLOBS_PER_BLOCK` appropriate for `fork`. + pub fn max_blobs_per_block_by_fork(&self, _fork_name: ForkName) -> u64 { + // TODO(electra): add Electra blobs per block change here + self.max_blobs_per_block + } + pub fn data_columns_per_subnet(&self) -> usize { self.number_of_columns .safe_div(self.data_column_sidecar_subnet_count as usize) @@ -859,6 +871,7 @@ 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(), + max_blobs_per_block: default_max_blobs_per_block(), /* * Derived Deneb Specific @@ -1187,6 +1200,7 @@ 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(), + max_blobs_per_block: default_max_blobs_per_block(), /* * Derived Deneb Specific @@ -1388,6 +1402,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_max_blobs_per_block")] + #[serde(with = "serde_utils::quoted_u64")] + max_blobs_per_block: u64, #[serde(default = "default_min_per_epoch_churn_limit_electra")] #[serde(with = "serde_utils::quoted_u64")] @@ -1523,6 +1540,12 @@ const fn default_blob_sidecar_subnet_count() -> u64 { 6 } +/// Its important to keep this consistent with the deneb preset value for +/// `MAX_BLOBS_PER_BLOCK` else we might run into consensus issues. +const fn default_max_blobs_per_block() -> u64 { + 6 +} + const fn default_min_per_epoch_churn_limit_electra() -> u64 { 128_000_000_000 } @@ -1745,6 +1768,7 @@ 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, + max_blobs_per_block: spec.max_blobs_per_block, min_per_epoch_churn_limit_electra: spec.min_per_epoch_churn_limit_electra, max_per_epoch_activation_exit_churn_limit: spec @@ -1822,6 +1846,7 @@ impl Config { max_request_data_column_sidecars, min_epochs_for_blob_sidecars_requests, blob_sidecar_subnet_count, + max_blobs_per_block, min_per_epoch_churn_limit_electra, max_per_epoch_activation_exit_churn_limit, @@ -1890,6 +1915,7 @@ impl Config { max_request_data_column_sidecars, min_epochs_for_blob_sidecars_requests, blob_sidecar_subnet_count, + max_blobs_per_block, min_per_epoch_churn_limit_electra, max_per_epoch_activation_exit_churn_limit, diff --git a/consensus/types/src/data_column_sidecar.rs b/consensus/types/src/data_column_sidecar.rs index 57251e319a..b2a050e9d5 100644 --- a/consensus/types/src/data_column_sidecar.rs +++ b/consensus/types/src/data_column_sidecar.rs @@ -1,7 +1,7 @@ use crate::beacon_block_body::{KzgCommitments, BLOB_KZG_COMMITMENTS_INDEX}; use crate::test_utils::TestRandom; use crate::BeaconStateError; -use crate::{BeaconBlockHeader, EthSpec, Hash256, KzgProofs, SignedBeaconBlockHeader, Slot}; +use crate::{BeaconBlockHeader, Epoch, EthSpec, Hash256, KzgProofs, SignedBeaconBlockHeader, Slot}; use bls::Signature; use derivative::Derivative; use kzg::Error as KzgError; @@ -11,7 +11,6 @@ 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::hash::Hash; @@ -68,6 +67,10 @@ impl DataColumnSidecar { self.signed_block_header.message.slot } + pub fn epoch(&self) -> Epoch { + self.slot().epoch(E::slots_per_epoch()) + } + pub fn block_root(&self) -> Hash256 { self.signed_block_header.message.tree_hash_root() } @@ -110,18 +113,16 @@ impl DataColumnSidecar { .len() } - pub fn max_size() -> usize { + pub fn max_size(max_blobs_per_block: usize) -> usize { Self { index: 0, - column: VariableList::new(vec![Cell::::default(); E::MaxBlobsPerBlock::to_usize()]) - .unwrap(), + column: VariableList::new(vec![Cell::::default(); max_blobs_per_block]).unwrap(), kzg_commitments: VariableList::new(vec![ KzgCommitment::empty_for_testing(); - E::MaxBlobsPerBlock::to_usize() + max_blobs_per_block ]) .unwrap(), - kzg_proofs: VariableList::new(vec![KzgProof::empty(); E::MaxBlobsPerBlock::to_usize()]) - .unwrap(), + kzg_proofs: VariableList::new(vec![KzgProof::empty(); max_blobs_per_block]).unwrap(), signed_block_header: SignedBeaconBlockHeader { message: BeaconBlockHeader::empty(), signature: Signature::empty(), diff --git a/consensus/types/src/eth_spec.rs b/consensus/types/src/eth_spec.rs index 23e8276209..976766dfa9 100644 --- a/consensus/types/src/eth_spec.rs +++ b/consensus/types/src/eth_spec.rs @@ -4,8 +4,7 @@ use safe_arith::SafeArith; use serde::{Deserialize, Serialize}; use ssz_types::typenum::{ bit::B0, UInt, U0, U1, U1024, U1048576, U1073741824, U1099511627776, U128, U131072, U134217728, - U16, U16777216, U2, U2048, U256, U262144, U32, U4, U4096, U512, U6, U625, U64, U65536, U8, - U8192, + U16, U16777216, U2, U2048, U256, U262144, U32, U4, U4096, U512, U625, U64, U65536, U8, U8192, }; use ssz_types::typenum::{U17, U9}; use std::fmt::{self, Debug}; @@ -109,7 +108,6 @@ pub trait EthSpec: /* * New in Deneb */ - 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 BytesPerFieldElement: Unsigned + Clone + Sync + Send + Debug + PartialEq; @@ -281,11 +279,6 @@ pub trait EthSpec: Self::MaxWithdrawalsPerPayload::to_usize() } - /// Returns the `MAX_BLOBS_PER_BLOCK` constant for this specification. - fn max_blobs_per_block() -> usize { - Self::MaxBlobsPerBlock::to_usize() - } - /// Returns the `MAX_BLOB_COMMITMENTS_PER_BLOCK` constant for this specification. fn max_blob_commitments_per_block() -> usize { Self::MaxBlobCommitmentsPerBlock::to_usize() @@ -421,7 +414,6 @@ impl EthSpec for MainnetEthSpec { type GasLimitDenominator = U1024; type MinGasLimit = U5000; type MaxExtraDataBytes = U32; - type MaxBlobsPerBlock = U6; type MaxBlobCommitmentsPerBlock = U4096; type BytesPerFieldElement = U32; type FieldElementsPerBlob = U4096; @@ -505,7 +497,6 @@ impl EthSpec for MinimalEthSpec { MinGasLimit, MaxExtraDataBytes, MaxBlsToExecutionChanges, - MaxBlobsPerBlock, BytesPerFieldElement, PendingDepositsLimit, MaxPendingDepositsPerEpoch, @@ -559,7 +550,6 @@ impl EthSpec for GnosisEthSpec { type SlotsPerEth1VotingPeriod = U1024; // 64 epochs * 16 slots per epoch type MaxBlsToExecutionChanges = U16; type MaxWithdrawalsPerPayload = U8; - type MaxBlobsPerBlock = U6; type MaxBlobCommitmentsPerBlock = U4096; type FieldElementsPerBlob = U4096; type BytesPerFieldElement = U32; diff --git a/consensus/types/src/lib.rs b/consensus/types/src/lib.rs index 282f27a517..54d8bf51b6 100644 --- a/consensus/types/src/lib.rs +++ b/consensus/types/src/lib.rs @@ -108,6 +108,7 @@ pub mod data_column_sidecar; pub mod data_column_subnet_id; pub mod light_client_header; pub mod non_zero_usize; +pub mod runtime_fixed_vector; pub mod runtime_var_list; pub use crate::activation_queue::ActivationQueue; @@ -223,6 +224,7 @@ pub use crate::preset::{ pub use crate::proposer_preparation_data::ProposerPreparationData; pub use crate::proposer_slashing::ProposerSlashing; pub use crate::relative_epoch::{Error as RelativeEpochError, RelativeEpoch}; +pub use crate::runtime_fixed_vector::RuntimeFixedVector; pub use crate::runtime_var_list::RuntimeVariableList; pub use crate::selection_proof::SelectionProof; pub use crate::shuffling_id::AttestationShufflingId; diff --git a/consensus/types/src/preset.rs b/consensus/types/src/preset.rs index f8b3665409..f64b7051e5 100644 --- a/consensus/types/src/preset.rs +++ b/consensus/types/src/preset.rs @@ -205,8 +205,6 @@ impl CapellaPreset { #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] #[serde(rename_all = "UPPERCASE")] pub struct DenebPreset { - #[serde(with = "serde_utils::quoted_u64")] - pub max_blobs_per_block: u64, #[serde(with = "serde_utils::quoted_u64")] pub max_blob_commitments_per_block: u64, #[serde(with = "serde_utils::quoted_u64")] @@ -216,7 +214,6 @@ pub struct DenebPreset { impl DenebPreset { pub fn from_chain_spec(_spec: &ChainSpec) -> Self { Self { - 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, } diff --git a/consensus/types/src/runtime_fixed_vector.rs b/consensus/types/src/runtime_fixed_vector.rs new file mode 100644 index 0000000000..2b08b7bf70 --- /dev/null +++ b/consensus/types/src/runtime_fixed_vector.rs @@ -0,0 +1,81 @@ +//! Emulates a fixed size array but with the length set at runtime. +//! +//! The length of the list cannot be changed once it is set. + +#[derive(Clone, Debug)] +pub struct RuntimeFixedVector { + vec: Vec, + len: usize, +} + +impl RuntimeFixedVector { + pub fn new(vec: Vec) -> Self { + let len = vec.len(); + Self { vec, len } + } + + pub fn to_vec(&self) -> Vec { + self.vec.clone() + } + + pub fn as_slice(&self) -> &[T] { + self.vec.as_slice() + } + + #[allow(clippy::len_without_is_empty)] + pub fn len(&self) -> usize { + self.len + } + + pub fn into_vec(self) -> Vec { + self.vec + } + + pub fn default(max_len: usize) -> Self { + Self { + vec: vec![T::default(); max_len], + len: max_len, + } + } + + pub fn take(&mut self) -> Self { + let new = std::mem::take(&mut self.vec); + *self = Self::new(vec![T::default(); self.len]); + Self { + vec: new, + len: self.len, + } + } +} + +impl std::ops::Deref for RuntimeFixedVector { + type Target = [T]; + + fn deref(&self) -> &[T] { + &self.vec[..] + } +} + +impl std::ops::DerefMut for RuntimeFixedVector { + fn deref_mut(&mut self) -> &mut [T] { + &mut self.vec[..] + } +} + +impl IntoIterator for RuntimeFixedVector { + type Item = T; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.vec.into_iter() + } +} + +impl<'a, T> IntoIterator for &'a RuntimeFixedVector { + type Item = &'a T; + type IntoIter = std::slice::Iter<'a, T>; + + fn into_iter(self) -> Self::IntoIter { + self.vec.iter() + } +} diff --git a/consensus/types/src/runtime_var_list.rs b/consensus/types/src/runtime_var_list.rs index 8290876fa1..857073b3b8 100644 --- a/consensus/types/src/runtime_var_list.rs +++ b/consensus/types/src/runtime_var_list.rs @@ -2,7 +2,7 @@ use derivative::Derivative; use serde::{Deserialize, Serialize}; use ssz::Decode; use ssz_types::Error; -use std::ops::{Deref, DerefMut, Index, IndexMut}; +use std::ops::{Deref, Index, IndexMut}; use std::slice::SliceIndex; /// Emulates a SSZ `List`. @@ -10,6 +10,8 @@ use std::slice::SliceIndex; /// An ordered, heap-allocated, variable-length, homogeneous collection of `T`, with no more than /// `max_len` values. /// +/// To ensure there are no inconsistent states, we do not allow any mutating operation if `max_len` is not set. +/// /// ## Example /// /// ``` @@ -35,6 +37,7 @@ use std::slice::SliceIndex; /// /// // 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"))] @@ -65,7 +68,7 @@ impl RuntimeVariableList { Self { vec, max_len } } - /// Create an empty list. + /// Create an empty list with the given `max_len`. pub fn empty(max_len: usize) -> Self { Self { vec: vec![], @@ -77,6 +80,10 @@ impl RuntimeVariableList { self.vec.as_slice() } + pub fn as_mut_slice(&mut self) -> &mut [T] { + self.vec.as_mut_slice() + } + /// Returns the number of values presently in `self`. pub fn len(&self) -> usize { self.vec.len() @@ -88,6 +95,8 @@ impl RuntimeVariableList { } /// Returns the type-level maximum length. + /// + /// Returns `None` if self is uninitialized with a max_len. pub fn max_len(&self) -> usize { self.max_len } @@ -169,12 +178,6 @@ impl Deref for RuntimeVariableList { } } -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>; diff --git a/lcli/src/mock_el.rs b/lcli/src/mock_el.rs index 7719f02aa3..2e2c27a2db 100644 --- a/lcli/src/mock_el.rs +++ b/lcli/src/mock_el.rs @@ -9,6 +9,7 @@ use execution_layer::{ }; use std::net::Ipv4Addr; use std::path::PathBuf; +use std::sync::Arc; use types::*; pub fn run(mut env: Environment, matches: &ArgMatches) -> Result<(), String> { @@ -22,7 +23,7 @@ pub fn run(mut env: Environment, matches: &ArgMatches) -> Result< let osaka_time = parse_optional(matches, "osaka-time")?; let handle = env.core_context().executor.handle().unwrap(); - let spec = &E::default_spec(); + let spec = Arc::new(E::default_spec()); let jwt_key = JwtKey::from_slice(&DEFAULT_JWT_SECRET).unwrap(); std::fs::write(jwt_path, hex::encode(DEFAULT_JWT_SECRET)).unwrap(); @@ -41,7 +42,7 @@ pub fn run(mut env: Environment, matches: &ArgMatches) -> Result< osaka_time, }; let kzg = None; - let server: MockServer = MockServer::new_with_config(&handle, config, kzg); + let server: MockServer = MockServer::new_with_config(&handle, config, spec, kzg); if all_payloads_valid { eprintln!( diff --git a/testing/node_test_rig/src/lib.rs b/testing/node_test_rig/src/lib.rs index ac01c84b9d..6e632ccf54 100644 --- a/testing/node_test_rig/src/lib.rs +++ b/testing/node_test_rig/src/lib.rs @@ -7,6 +7,7 @@ use environment::RuntimeContext; use eth2::{reqwest::ClientBuilder, BeaconNodeHttpClient, Timeouts}; use sensitive_url::SensitiveUrl; use std::path::PathBuf; +use std::sync::Arc; use std::time::Duration; use std::time::{SystemTime, UNIX_EPOCH}; use tempfile::{Builder as TempBuilder, TempDir}; @@ -248,8 +249,14 @@ impl LocalExecutionNode { if let Err(e) = std::fs::write(jwt_file_path, config.jwt_key.hex_string()) { panic!("Failed to write jwt file {}", e); } + let spec = Arc::new(E::default_spec()); Self { - server: MockServer::new_with_config(&context.executor.handle().unwrap(), config, None), + server: MockServer::new_with_config( + &context.executor.handle().unwrap(), + config, + spec, + None, + ), datadir, } } From 348fbdb83816ba213945e5c7fcd8c5c5342aa0ed Mon Sep 17 00:00:00 2001 From: Mac L Date: Fri, 10 Jan 2025 11:35:05 +0400 Subject: [PATCH 070/254] Add missing crates to cargo workspace (#6774) * Add the remaining crates to cargo workspace * Merge branch 'unstable' into add-remaining-crates-workspace --- Cargo.toml | 7 ++- beacon_node/genesis/Cargo.toml | 18 +++--- beacon_node/operation_pool/Cargo.toml | 14 ++--- common/filesystem/Cargo.toml | 1 - consensus/merkle_proof/Cargo.toml | 2 +- consensus/types/Cargo.toml | 80 +++++++++++++-------------- 6 files changed, 63 insertions(+), 59 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 23e52a306b..233e5fa775 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,11 +9,13 @@ members = [ "beacon_node/client", "beacon_node/eth1", "beacon_node/execution_layer", + "beacon_node/genesis", "beacon_node/http_api", "beacon_node/http_metrics", "beacon_node/lighthouse_network", "beacon_node/lighthouse_network/gossipsub", "beacon_node/network", + "beacon_node/operation_pool", "beacon_node/store", "beacon_node/timer", @@ -30,6 +32,7 @@ members = [ "common/eth2_interop_keypairs", "common/eth2_network_config", "common/eth2_wallet_manager", + "common/filesystem", "common/lighthouse_version", "common/lockfile", "common/logging", @@ -48,14 +51,16 @@ members = [ "common/unused_port", "common/validator_dir", "common/warp_utils", + "consensus/fixed_bytes", "consensus/fork_choice", - "consensus/int_to_bytes", + "consensus/merkle_proof", "consensus/proto_array", "consensus/safe_arith", "consensus/state_processing", "consensus/swap_or_not_shuffle", + "consensus/types", "crypto/bls", "crypto/eth2_key_derivation", diff --git a/beacon_node/genesis/Cargo.toml b/beacon_node/genesis/Cargo.toml index b01e6a6aea..eeca393947 100644 --- a/beacon_node/genesis/Cargo.toml +++ b/beacon_node/genesis/Cargo.toml @@ -9,16 +9,16 @@ eth1_test_rig = { workspace = true } sensitive_url = { workspace = true } [dependencies] -futures = { workspace = true } -types = { workspace = true } environment = { workspace = true } eth1 = { workspace = true } -rayon = { workspace = true } -state_processing = { workspace = true } -merkle_proof = { workspace = true } -ethereum_ssz = { workspace = true } ethereum_hashing = { workspace = true } -tree_hash = { workspace = true } -tokio = { workspace = true } -slog = { workspace = true } +ethereum_ssz = { workspace = true } +futures = { workspace = true } int_to_bytes = { workspace = true } +merkle_proof = { workspace = true } +rayon = { workspace = true } +slog = { workspace = true } +state_processing = { workspace = true } +tokio = { workspace = true } +tree_hash = { workspace = true } +types = { workspace = true } diff --git a/beacon_node/operation_pool/Cargo.toml b/beacon_node/operation_pool/Cargo.toml index 5b48e3f0d8..570b74226c 100644 --- a/beacon_node/operation_pool/Cargo.toml +++ b/beacon_node/operation_pool/Cargo.toml @@ -5,24 +5,24 @@ authors = ["Michael Sproul "] edition = { workspace = true } [dependencies] +bitvec = { workspace = true } derivative = { workspace = true } +ethereum_ssz = { workspace = true } +ethereum_ssz_derive = { workspace = true } itertools = { workspace = true } metrics = { workspace = true } parking_lot = { workspace = true } -types = { workspace = true } -state_processing = { workspace = true } -ethereum_ssz = { workspace = true } -ethereum_ssz_derive = { workspace = true } +rand = { workspace = true } rayon = { workspace = true } serde = { workspace = true } +state_processing = { workspace = true } store = { workspace = true } -bitvec = { workspace = true } -rand = { workspace = true } +types = { workspace = true } [dev-dependencies] beacon_chain = { workspace = true } -tokio = { workspace = true } maplit = { workspace = true } +tokio = { workspace = true } [features] portable = ["beacon_chain/portable"] diff --git a/common/filesystem/Cargo.toml b/common/filesystem/Cargo.toml index fd026bd517..1b5abf03f4 100644 --- a/common/filesystem/Cargo.toml +++ b/common/filesystem/Cargo.toml @@ -3,7 +3,6 @@ name = "filesystem" version = "0.1.0" authors = ["Mark Mackey "] edition = { workspace = true } - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] diff --git a/consensus/merkle_proof/Cargo.toml b/consensus/merkle_proof/Cargo.toml index c2c6bf270a..2f721d917b 100644 --- a/consensus/merkle_proof/Cargo.toml +++ b/consensus/merkle_proof/Cargo.toml @@ -7,8 +7,8 @@ edition = { workspace = true } [dependencies] alloy-primitives = { workspace = true } ethereum_hashing = { workspace = true } -safe_arith = { workspace = true } fixed_bytes = { workspace = true } +safe_arith = { workspace = true } [dev-dependencies] quickcheck = { workspace = true } diff --git a/consensus/types/Cargo.toml b/consensus/types/Cargo.toml index 21a15fc517..79beb81282 100644 --- a/consensus/types/Cargo.toml +++ b/consensus/types/Cargo.toml @@ -10,56 +10,56 @@ harness = false [dependencies] alloy-primitives = { workspace = true } -merkle_proof = { workspace = true } -bls = { workspace = true, features = ["arbitrary"] } -kzg = { workspace = true } -compare_fields = { workspace = true } -compare_fields_derive = { workspace = true } -eth2_interop_keypairs = { path = "../../common/eth2_interop_keypairs" } -ethereum_hashing = { workspace = true } -hex = { workspace = true } -int_to_bytes = { workspace = true } -log = { workspace = true } -rayon = { workspace = true } -rand = { workspace = true } -safe_arith = { workspace = true } -serde = { workspace = true, features = ["rc"] } -slog = { workspace = true } -ethereum_ssz = { workspace = true, features = ["arbitrary"] } -ethereum_ssz_derive = { workspace = true } -ssz_types = { workspace = true, features = ["arbitrary"] } -swap_or_not_shuffle = { workspace = true, features = ["arbitrary"] } -test_random_derive = { path = "../../common/test_random_derive" } -tree_hash = { workspace = true } -tree_hash_derive = { workspace = true } -rand_xorshift = "0.3.0" -serde_yaml = { workspace = true } -tempfile = { workspace = true } -derivative = { workspace = true } -rusqlite = { workspace = true } +alloy-rlp = { version = "0.3.4", features = ["derive"] } # The arbitrary dependency is enabled by default since Capella to avoid complexity introduced by # `AbstractExecPayload` arbitrary = { workspace = true, features = ["derive"] } +bls = { workspace = true, features = ["arbitrary"] } +compare_fields = { workspace = true } +compare_fields_derive = { workspace = true } +derivative = { workspace = true } +eth2_interop_keypairs = { path = "../../common/eth2_interop_keypairs" } +ethereum_hashing = { workspace = true } ethereum_serde_utils = { workspace = true } -regex = { workspace = true } -parking_lot = { workspace = true } -itertools = { workspace = true } -superstruct = { workspace = true } -metastruct = "0.1.0" -serde_json = { workspace = true } -smallvec = { workspace = true } -maplit = { workspace = true } -alloy-rlp = { version = "0.3.4", features = ["derive"] } -milhouse = { workspace = true } -rpds = { workspace = true } +ethereum_ssz = { workspace = true, features = ["arbitrary"] } +ethereum_ssz_derive = { workspace = true } fixed_bytes = { workspace = true } +hex = { workspace = true } +int_to_bytes = { workspace = true } +itertools = { workspace = true } +kzg = { workspace = true } +log = { workspace = true } +maplit = { workspace = true } +merkle_proof = { workspace = true } +metastruct = "0.1.0" +milhouse = { workspace = true } +parking_lot = { workspace = true } +rand = { workspace = true } +rand_xorshift = "0.3.0" +rayon = { workspace = true } +regex = { workspace = true } +rpds = { workspace = true } +rusqlite = { workspace = true } +safe_arith = { workspace = true } +serde = { workspace = true, features = ["rc"] } +serde_json = { workspace = true } +serde_yaml = { workspace = true } +slog = { workspace = true } +smallvec = { workspace = true } +ssz_types = { workspace = true, features = ["arbitrary"] } +superstruct = { workspace = true } +swap_or_not_shuffle = { workspace = true, features = ["arbitrary"] } +tempfile = { workspace = true } +test_random_derive = { path = "../../common/test_random_derive" } +tree_hash = { workspace = true } +tree_hash_derive = { workspace = true } [dev-dependencies] -criterion = { workspace = true } beacon_chain = { workspace = true } +criterion = { workspace = true } +paste = { workspace = true } state_processing = { workspace = true } tokio = { workspace = true } -paste = { workspace = true } [features] default = ["sqlite", "legacy-arith"] From c9747fb77f3ff1d8da765003b4988c20ce2d7cd8 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Mon, 13 Jan 2025 11:08:51 +1100 Subject: [PATCH 071/254] Refactor feature testing for spec tests (#6737) * Refactor spec testing for features and simplify usage. * Fix `SszStatic` tests for PeerDAS: exclude eip7594 test vectors when testing Electra types. * Merge branch 'unstable' into refactor-ef-tests-features --- testing/ef_tests/src/cases.rs | 36 +++++++++++++-- .../ef_tests/src/cases/get_custody_columns.rs | 9 ++++ .../src/cases/kzg_blob_to_kzg_commitment.rs | 4 -- .../src/cases/kzg_compute_blob_kzg_proof.rs | 4 -- .../cases/kzg_compute_cells_and_kzg_proofs.rs | 8 +++- .../src/cases/kzg_compute_kzg_proof.rs | 4 -- .../cases/kzg_recover_cells_and_kzg_proofs.rs | 8 +++- .../src/cases/kzg_verify_blob_kzg_proof.rs | 4 -- .../cases/kzg_verify_blob_kzg_proof_batch.rs | 4 -- .../cases/kzg_verify_cell_kzg_proof_batch.rs | 8 +++- .../src/cases/kzg_verify_kzg_proof.rs | 4 -- testing/ef_tests/src/handler.rs | 46 ++++++++++++++----- testing/ef_tests/tests/tests.rs | 20 ++++---- 13 files changed, 103 insertions(+), 56 deletions(-) diff --git a/testing/ef_tests/src/cases.rs b/testing/ef_tests/src/cases.rs index 63274ee0c0..8f5571d64a 100644 --- a/testing/ef_tests/src/cases.rs +++ b/testing/ef_tests/src/cases.rs @@ -74,11 +74,37 @@ pub use ssz_generic::*; pub use ssz_static::*; pub use transition::TransitionTest; -#[derive(Debug, PartialEq)] +/// Used for running feature tests for future forks that have not yet been added to `ForkName`. +/// This runs tests in the directory named by the feature instead of the fork name. This has been +/// the pattern used in the `consensus-spec-tests` repository: +/// `consensus-spec-tests/tests/general/[feature_name]/[runner_name].` +/// e.g. consensus-spec-tests/tests/general/peerdas/ssz_static +/// +/// The feature tests can be run with one of the following methods: +/// 1. `handler.run_for_feature(feature_name)` for new tests that are not on existing fork, i.e. a +/// new handler. This will be temporary and the test will need to be updated to use +/// `handle.run()` once the feature is incorporated into a fork. +/// 2. `handler.run()` for tests that are already on existing forks, but with new test vectors for +/// the feature. In this case the `handler.is_enabled_for_feature` will need to be implemented +/// to return `true` for the feature in order for the feature test vector to be tested. +#[derive(Debug, PartialEq, Clone, Copy)] pub enum FeatureName { Eip7594, } +impl FeatureName { + pub fn list_all() -> Vec { + vec![FeatureName::Eip7594] + } + + /// `ForkName` to use when running the feature tests. + pub fn fork_name(&self) -> ForkName { + match self { + FeatureName::Eip7594 => ForkName::Deneb, + } + } +} + impl Display for FeatureName { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { @@ -107,11 +133,13 @@ pub trait Case: Debug + Sync { true } - /// Whether or not this test exists for the given `feature_name`. + /// Whether or not this test exists for the given `feature_name`. This is intended to be used + /// for features that have not been added to a fork yet, and there is usually a separate folder + /// for the feature in the `consensus-spec-tests` repository. /// - /// Returns `true` by default. + /// Returns `false` by default. fn is_enabled_for_feature(_feature_name: FeatureName) -> bool { - true + false } /// Execute a test and return the result. diff --git a/testing/ef_tests/src/cases/get_custody_columns.rs b/testing/ef_tests/src/cases/get_custody_columns.rs index 9665f87730..71b17aeaa3 100644 --- a/testing/ef_tests/src/cases/get_custody_columns.rs +++ b/testing/ef_tests/src/cases/get_custody_columns.rs @@ -21,6 +21,14 @@ impl LoadCase for GetCustodyColumns { } impl Case for GetCustodyColumns { + fn is_enabled_for_fork(_fork_name: ForkName) -> bool { + false + } + + 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 spec = E::default_spec(); let node_id = U256::from_str_radix(&self.node_id, 10) @@ -33,6 +41,7 @@ impl Case for GetCustodyColumns { ) .expect("should compute custody columns") .collect::>(); + let expected = &self.result; if computed == *expected { Ok(()) 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 fa16a5fcb7..feb9a4ff5c 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,10 +31,6 @@ impl Case for KZGBlobToKZGCommitment { 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 kzg = get_kzg(); let commitment = parse_blob::(&self.input.blob).and_then(|blob| { 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 694013e251..4aadc37af2 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,10 +32,6 @@ 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)?; 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 2a9f8ceeef..a7219f0629 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 @@ -26,8 +26,12 @@ impl LoadCase for KZGComputeCellsAndKZGProofs { } impl Case for KZGComputeCellsAndKZGProofs { - fn is_enabled_for_fork(fork_name: ForkName) -> bool { - fork_name == ForkName::Deneb + fn is_enabled_for_fork(_fork_name: ForkName) -> bool { + false + } + + fn is_enabled_for_feature(feature_name: FeatureName) -> bool { + feature_name == FeatureName::Eip7594 } fn result(&self, _case_index: usize, _fork_name: ForkName) -> Result<(), Error> { 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 6f53038f28..4a47fe35eb 100644 --- a/testing/ef_tests/src/cases/kzg_compute_kzg_proof.rs +++ b/testing/ef_tests/src/cases/kzg_compute_kzg_proof.rs @@ -39,10 +39,6 @@ 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)?; 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 10cc866fbe..b72b3a05cd 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 @@ -27,8 +27,12 @@ impl LoadCase for KZGRecoverCellsAndKZGProofs { } impl Case for KZGRecoverCellsAndKZGProofs { - fn is_enabled_for_fork(fork_name: ForkName) -> bool { - fork_name == ForkName::Deneb + fn is_enabled_for_fork(_fork_name: ForkName) -> bool { + false + } + + fn is_enabled_for_feature(feature_name: FeatureName) -> bool { + feature_name == FeatureName::Eip7594 } fn result(&self, _case_index: usize, _fork_name: ForkName) -> Result<(), Error> { 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 3dc955bdcc..66f50d534b 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 @@ -116,10 +116,6 @@ 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)?; 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 80cd0a2849..efd4158806 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,10 +33,6 @@ 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 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 5887d764ca..815ad7a5bc 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 @@ -29,8 +29,12 @@ impl LoadCase for KZGVerifyCellKZGProofBatch { } impl Case for KZGVerifyCellKZGProofBatch { - fn is_enabled_for_fork(fork_name: ForkName) -> bool { - fork_name == ForkName::Deneb + fn is_enabled_for_fork(_fork_name: ForkName) -> bool { + false + } + + fn is_enabled_for_feature(feature_name: FeatureName) -> bool { + feature_name == FeatureName::Eip7594 } fn result(&self, _case_index: usize, _fork_name: ForkName) -> Result<(), Error> { 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 ed7583dbd0..07df05a6ac 100644 --- a/testing/ef_tests/src/cases/kzg_verify_kzg_proof.rs +++ b/testing/ef_tests/src/cases/kzg_verify_kzg_proof.rs @@ -33,10 +33,6 @@ 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)?; diff --git a/testing/ef_tests/src/handler.rs b/testing/ef_tests/src/handler.rs index e7c148645c..d8fe061061 100644 --- a/testing/ef_tests/src/handler.rs +++ b/testing/ef_tests/src/handler.rs @@ -7,9 +7,6 @@ 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; @@ -39,13 +36,12 @@ pub trait Handler { 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); + } + } - 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); - } + for feature_name in FeatureName::list_all() { + if self.is_enabled_for_feature(feature_name) { + self.run_for_feature(feature_name); } } } @@ -96,8 +92,9 @@ pub trait Handler { crate::results::assert_tests_pass(&name, &handler_path, &results); } - fn run_for_feature(&self, fork_name: ForkName, feature_name: FeatureName) { + fn run_for_feature(&self, feature_name: FeatureName) { let feature_name_str = feature_name.to_string(); + let fork_name = feature_name.fork_name(); let handler_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("consensus-spec-tests") @@ -352,6 +349,22 @@ where fn is_enabled_for_fork(&self, fork_name: ForkName) -> bool { self.supported_forks.contains(&fork_name) } + + fn is_enabled_for_feature(&self, feature_name: FeatureName) -> bool { + // This ensures we only run the tests **once** for `Eip7594`, using the types matching the + // correct fork, e.g. `Eip7594` uses SSZ types from `Deneb` as of spec test version + // `v1.5.0-alpha.8`, therefore the `Eip7594` tests should get included when testing Deneb types. + // + // e.g. Eip7594 test vectors are executed in the first line below, but excluded in the 2nd + // line when testing the type `AttestationElectra`: + // + // ``` + // SszStaticHandler::, MainnetEthSpec>::pre_electra().run(); + // SszStaticHandler::, MainnetEthSpec>::electra_only().run(); + // ``` + feature_name == FeatureName::Eip7594 + && self.supported_forks.contains(&feature_name.fork_name()) + } } impl Handler for SszStaticTHCHandler, E> @@ -371,6 +384,10 @@ where fn handler_name(&self) -> String { BeaconState::::name().into() } + + fn is_enabled_for_feature(&self, feature_name: FeatureName) -> bool { + feature_name == FeatureName::Eip7594 + } } impl Handler for SszStaticWithSpecHandler @@ -392,6 +409,10 @@ where fn handler_name(&self) -> String { T::name().into() } + + fn is_enabled_for_feature(&self, feature_name: FeatureName) -> bool { + feature_name == FeatureName::Eip7594 + } } #[derive(Derivative)] @@ -971,9 +992,12 @@ impl Handler for KzgInclusionMerkleProofValidityHandler bool { - // Enabled in Deneb fork_name.deneb_enabled() } + + fn is_enabled_for_feature(&self, feature_name: FeatureName) -> bool { + feature_name == FeatureName::Eip7594 + } } #[derive(Derivative)] diff --git a/testing/ef_tests/tests/tests.rs b/testing/ef_tests/tests/tests.rs index 292625a371..691d27951a 100644 --- a/testing/ef_tests/tests/tests.rs +++ b/testing/ef_tests/tests/tests.rs @@ -627,17 +627,17 @@ mod ssz_static { #[test] fn data_column_sidecar() { SszStaticHandler::, MinimalEthSpec>::deneb_only() - .run_for_feature(ForkName::Deneb, FeatureName::Eip7594); + .run_for_feature(FeatureName::Eip7594); SszStaticHandler::, MainnetEthSpec>::deneb_only() - .run_for_feature(ForkName::Deneb, FeatureName::Eip7594); + .run_for_feature(FeatureName::Eip7594); } #[test] fn data_column_identifier() { SszStaticHandler::::deneb_only() - .run_for_feature(ForkName::Deneb, FeatureName::Eip7594); + .run_for_feature(FeatureName::Eip7594); SszStaticHandler::::deneb_only() - .run_for_feature(ForkName::Deneb, FeatureName::Eip7594); + .run_for_feature(FeatureName::Eip7594); } #[test] @@ -902,19 +902,19 @@ fn kzg_verify_kzg_proof() { #[test] fn kzg_compute_cells_and_proofs() { KZGComputeCellsAndKZGProofHandler::::default() - .run_for_feature(ForkName::Deneb, FeatureName::Eip7594); + .run_for_feature(FeatureName::Eip7594); } #[test] fn kzg_verify_cell_proof_batch() { KZGVerifyCellKZGProofBatchHandler::::default() - .run_for_feature(ForkName::Deneb, FeatureName::Eip7594); + .run_for_feature(FeatureName::Eip7594); } #[test] fn kzg_recover_cells_and_proofs() { KZGRecoverCellsAndKZGProofHandler::::default() - .run_for_feature(ForkName::Deneb, FeatureName::Eip7594); + .run_for_feature(FeatureName::Eip7594); } #[test] @@ -949,8 +949,6 @@ fn rewards() { #[test] fn get_custody_columns() { - GetCustodyColumnsHandler::::default() - .run_for_feature(ForkName::Deneb, FeatureName::Eip7594); - GetCustodyColumnsHandler::::default() - .run_for_feature(ForkName::Deneb, FeatureName::Eip7594); + GetCustodyColumnsHandler::::default().run_for_feature(FeatureName::Eip7594); + GetCustodyColumnsHandler::::default().run_for_feature(FeatureName::Eip7594); } From 06e4d22d4954807ddc755feaea7518ad2ce06f35 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Tue, 14 Jan 2025 10:17:00 +1100 Subject: [PATCH 072/254] Electra spec changes for `v1.5.0-beta.0` (#6731) * First pass * Add restrictions to RuntimeVariableList api * Use empty_uninitialized and fix warnings * Fix some todos * Merge branch 'unstable' into max-blobs-preset * Fix take impl on RuntimeFixedList * cleanup * Fix test compilations * Fix some more tests * Fix test from unstable * Merge branch 'unstable' into max-blobs-preset * Implement "Bugfix and more withdrawal tests" * Implement "Add missed exit checks to consolidation processing" * Implement "Update initial earliest_exit_epoch calculation" * Implement "Limit consolidating balance by validator.effective_balance" * Implement "Use 16-bit random value in validator filter" * Implement "Do not change creds type on consolidation" * Rename PendingPartialWithdraw index field to validator_index * Skip slots to get test to pass and add TODO * Implement "Synchronously check all transactions to have non-zero length" * Merge remote-tracking branch 'origin/unstable' into max-blobs-preset * Remove footgun function * Minor simplifications * Move from preset to config * Fix typo * Revert "Remove footgun function" This reverts commit de01f923c7452355c87f50c0e8031ca94fa00d36. * Try fixing tests * Implement "bump minimal preset MAX_BLOB_COMMITMENTS_PER_BLOCK and KZG_COMMITMENT_INCLUSION_PROOF_DEPTH" * Thread through ChainSpec * Fix release tests * Move RuntimeFixedVector into module and rename * Add test * Implement "Remove post-altair `initialize_beacon_state_from_eth1` from specs" * Update preset YAML * Remove empty RuntimeVarList awefullness * Make max_blobs_per_block a config parameter (#6329) Squashed commit of the following: commit 04b3743ec1e0b650269dd8e58b540c02430d1c0d Author: Michael Sproul Date: Mon Jan 6 17:36:58 2025 +1100 Add test commit 440e85419940d4daba406d910e7908dd1fe78668 Author: Michael Sproul Date: Mon Jan 6 17:24:50 2025 +1100 Move RuntimeFixedVector into module and rename commit f66e179a40c3917eee39a93534ecf75480172699 Author: Michael Sproul Date: Mon Jan 6 17:17:17 2025 +1100 Fix release tests commit e4bfe71cd1f0a2784d0bd57f85b2f5d8cf503ac1 Author: Michael Sproul Date: Mon Jan 6 17:05:30 2025 +1100 Thread through ChainSpec commit 063b79c16abd3f6df47b85efcf3858177bc933b9 Author: Michael Sproul Date: Mon Jan 6 15:32:16 2025 +1100 Try fixing tests commit 88bedf09bc647de66bd1ff944bbc8fb13e2b7590 Author: Michael Sproul Date: Mon Jan 6 15:04:37 2025 +1100 Revert "Remove footgun function" This reverts commit de01f923c7452355c87f50c0e8031ca94fa00d36. commit 32483d385b66f252d50cee5b524e2924157bdcd4 Author: Michael Sproul Date: Mon Jan 6 15:04:32 2025 +1100 Fix typo commit 2e86585b478c012f6e3483989c87e38161227674 Author: Michael Sproul Date: Mon Jan 6 15:04:15 2025 +1100 Move from preset to config commit 1095d60a40be20dd3c229b759fc3c228b51e51e3 Author: Michael Sproul Date: Mon Jan 6 14:38:40 2025 +1100 Minor simplifications commit de01f923c7452355c87f50c0e8031ca94fa00d36 Author: Michael Sproul Date: Mon Jan 6 14:06:57 2025 +1100 Remove footgun function commit 0c2c8c42245c25b8cf17885faf20acd3b81140ec Merge: 21ecb58ff f51a292f7 Author: Michael Sproul Date: Mon Jan 6 14:02:50 2025 +1100 Merge remote-tracking branch 'origin/unstable' into max-blobs-preset commit f51a292f77575a1786af34271fb44954f141c377 Author: Daniel Knopik <107140945+dknopik@users.noreply.github.com> Date: Fri Jan 3 20:27:21 2025 +0100 fully lint only explicitly to avoid unnecessary rebuilds (#6753) * fully lint only explicitly to avoid unnecessary rebuilds commit 7e0cddef321c2a069582c65b58e5f46590d60c49 Author: Akihito Nakano Date: Tue Dec 24 10:38:56 2024 +0900 Make sure we have fanout peers when publish (#6738) * Ensure that `fanout_peers` is always non-empty if it's `Some` commit 21ecb58ff88b86435ab62d9ac227394c10fdcd22 Merge: 2fcb2935e 9aefb5539 Author: Pawan Dhananjay Date: Mon Oct 21 14:46:00 2024 -0700 Merge branch 'unstable' into max-blobs-preset commit 2fcb2935ec7ef4cd18bbdd8aedb7de61fac69e61 Author: Pawan Dhananjay Date: Fri Sep 6 18:28:31 2024 -0700 Fix test from unstable commit 12c6ef118a1a6d910c48d9d4b23004f3609264c7 Author: Pawan Dhananjay Date: Wed Sep 4 16:16:36 2024 -0700 Fix some more tests commit d37733b846ce58e318e976d6503ca394b4901141 Author: Pawan Dhananjay Date: Wed Sep 4 12:47:36 2024 -0700 Fix test compilations commit 52bb581e071d5f474d519366e860a4b3a0b52f78 Author: Pawan Dhananjay Date: Tue Sep 3 18:38:19 2024 -0700 cleanup commit e71020e3e613910e0315f558ead661b490a0ff20 Author: Pawan Dhananjay Date: Tue Sep 3 17:16:10 2024 -0700 Fix take impl on RuntimeFixedList commit 13f9bba6470b2140e5c34f14aed06dab2b062c1c Merge: 60100fc6b 4e675cf5d Author: Pawan Dhananjay Date: Tue Sep 3 16:08:59 2024 -0700 Merge branch 'unstable' into max-blobs-preset commit 60100fc6be72792ff33913d7e5a53434c792aacf Author: Pawan Dhananjay Date: Fri Aug 30 16:04:11 2024 -0700 Fix some todos commit a9cb329a221a809f7dd818984753826f91c2e26b Author: Pawan Dhananjay Date: Fri Aug 30 15:54:00 2024 -0700 Use empty_uninitialized and fix warnings commit 4dc6e6515ecf75cefa4de840edc7b57e76a8fc9e Author: Pawan Dhananjay Date: Fri Aug 30 15:53:18 2024 -0700 Add restrictions to RuntimeVariableList api commit 25feedfde348b530c4fa2348cc71a06b746898ed Author: Pawan Dhananjay Date: Thu Aug 29 16:11:19 2024 -0700 First pass * Fix tests * Implement max_blobs_per_block_electra * Fix config issues * Simplify BlobSidecarListFromRoot * Disable PeerDAS tests * Merge remote-tracking branch 'origin/unstable' into max-blobs-preset * Bump quota to account for new target (6) * Remove clone * Fix issue from review * Try to remove ugliness * Merge branch 'unstable' into max-blobs-preset * Merge remote-tracking branch 'origin/unstable' into electra-alpha10 * Merge commit '04b3743ec1e0b650269dd8e58b540c02430d1c0d' into electra-alpha10 * Merge remote-tracking branch 'pawan/max-blobs-preset' into electra-alpha10 * Update tests to v1.5.0-beta.0 * Resolve merge conflicts * Linting * fmt * Fix test and add TODO * Gracefully handle slashed proposers in fork choice tests * Merge remote-tracking branch 'origin/unstable' into electra-alpha10 * Keep latest changes from max_blobs_per_block PR in codec.rs * Revert a few more regressions and add a comment * Disable more DAS tests * Improve validator monitor test a little * Make test more robust * Fix sync test that didn't understand blobs * Fill out cropped comment --- .../beacon_chain/tests/validator_monitor.rs | 82 ++++++++----------- .../src/engine_api/new_payload_request.rs | 5 ++ beacon_node/execution_layer/src/lib.rs | 1 + .../lighthouse_network/src/rpc/protocol.rs | 2 +- beacon_node/network/src/sync/tests/range.rs | 53 ++++++++---- consensus/fork_choice/tests/tests.rs | 61 +++++++------- .../src/per_block_processing.rs | 36 +++++--- .../process_operations.rs | 33 +++++--- .../src/per_epoch_processing/single_pass.rs | 4 +- .../state_processing/src/upgrade/electra.rs | 4 +- consensus/types/presets/gnosis/electra.yaml | 13 ++- consensus/types/presets/mainnet/altair.yaml | 2 + consensus/types/presets/mainnet/electra.yaml | 13 ++- consensus/types/presets/mainnet/phase0.yaml | 2 +- consensus/types/presets/minimal/altair.yaml | 2 + consensus/types/presets/minimal/deneb.yaml | 8 +- consensus/types/presets/minimal/electra.yaml | 15 ++-- consensus/types/presets/minimal/phase0.yaml | 6 +- consensus/types/src/beacon_state.rs | 53 ++++++++++-- consensus/types/src/chain_spec.rs | 24 +++++- consensus/types/src/eth_spec.rs | 14 ++-- .../types/src/pending_partial_withdrawal.rs | 2 +- consensus/types/src/preset.rs | 4 +- testing/ef_tests/Makefile | 2 +- testing/ef_tests/check_all_files_accessed.py | 6 ++ .../src/cases/genesis_initialization.rs | 3 +- .../ef_tests/src/cases/genesis_validity.rs | 4 + testing/ef_tests/src/handler.rs | 23 ++++-- testing/ef_tests/tests/tests.rs | 10 ++- 29 files changed, 309 insertions(+), 178 deletions(-) diff --git a/beacon_node/beacon_chain/tests/validator_monitor.rs b/beacon_node/beacon_chain/tests/validator_monitor.rs index 91de4fe270..180db6d76d 100644 --- a/beacon_node/beacon_chain/tests/validator_monitor.rs +++ b/beacon_node/beacon_chain/tests/validator_monitor.rs @@ -4,7 +4,7 @@ use beacon_chain::test_utils::{ use beacon_chain::validator_monitor::{ValidatorMonitorConfig, MISSED_BLOCK_LAG_SLOTS}; use logging::test_logger; use std::sync::LazyLock; -use types::{Epoch, EthSpec, ForkName, Keypair, MainnetEthSpec, PublicKeyBytes, Slot}; +use types::{Epoch, EthSpec, Keypair, MainnetEthSpec, PublicKeyBytes, Slot}; // Should ideally be divisible by 3. pub const VALIDATOR_COUNT: usize = 48; @@ -117,7 +117,7 @@ async fn missed_blocks_across_epochs() { } #[tokio::test] -async fn produces_missed_blocks() { +async fn missed_blocks_basic() { let validator_count = 16; let slots_per_epoch = E::slots_per_epoch(); @@ -127,13 +127,10 @@ async fn produces_missed_blocks() { // Generate 63 slots (2 epochs * 32 slots per epoch - 1) let initial_blocks = slots_per_epoch * nb_epoch_to_simulate.as_u64() - 1; - // The validator index of the validator that is 'supposed' to miss a block - let validator_index_to_monitor = 1; - // 1st scenario // // // Missed block happens when slot and prev_slot are in the same epoch - let harness1 = get_harness(validator_count, vec![validator_index_to_monitor]); + let harness1 = get_harness(validator_count, vec![]); harness1 .extend_chain( initial_blocks as usize, @@ -153,7 +150,7 @@ async fn produces_missed_blocks() { let mut prev_slot = Slot::new(idx - 1); let mut duplicate_block_root = *_state.block_roots().get(idx as usize).unwrap(); let mut validator_indexes = _state.get_beacon_proposer_indices(&harness1.spec).unwrap(); - let mut validator_index = validator_indexes[slot_in_epoch.as_usize()]; + let mut missed_block_proposer = validator_indexes[slot_in_epoch.as_usize()]; let mut proposer_shuffling_decision_root = _state .proposer_shuffling_decision_root(duplicate_block_root) .unwrap(); @@ -170,7 +167,7 @@ async fn produces_missed_blocks() { beacon_proposer_cache.lock().insert( epoch, proposer_shuffling_decision_root, - validator_indexes.into_iter().collect::>(), + validator_indexes, _state.fork() ), Ok(()) @@ -187,12 +184,15 @@ async fn produces_missed_blocks() { // Let's validate the state which will call the function responsible for // adding the missed blocks to the validator monitor let mut validator_monitor = harness1.chain.validator_monitor.write(); + + validator_monitor.add_validator_pubkey(KEYPAIRS[missed_block_proposer].pk.compress()); validator_monitor.process_valid_state(nb_epoch_to_simulate, _state, &harness1.chain.spec); // We should have one entry in the missed blocks map assert_eq!( - validator_monitor.get_monitored_validator_missed_block_count(validator_index as u64), - 1 + validator_monitor + .get_monitored_validator_missed_block_count(missed_block_proposer as u64), + 1, ); } @@ -201,23 +201,7 @@ async fn produces_missed_blocks() { // Missed block happens when slot and prev_slot are not in the same epoch // making sure that the cache reloads when the epoch changes // in that scenario the slot that missed a block is the first slot of the epoch - // We are adding other validators to monitor as these ones will miss a block depending on - // the fork name specified when running the test as the proposer cache differs depending on - // the fork name (cf. seed) - // - // If you are adding a new fork and seeing errors, print - // `validator_indexes[slot_in_epoch.as_usize()]` and add it below. - let validator_index_to_monitor = match harness1.spec.fork_name_at_slot::(Slot::new(0)) { - ForkName::Base => 7, - ForkName::Altair => 2, - ForkName::Bellatrix => 4, - ForkName::Capella => 11, - ForkName::Deneb => 3, - ForkName::Electra => 1, - ForkName::Fulu => 6, - }; - - let harness2 = get_harness(validator_count, vec![validator_index_to_monitor]); + let harness2 = get_harness(validator_count, vec![]); let advance_slot_by = 9; harness2 .extend_chain( @@ -238,11 +222,7 @@ async fn produces_missed_blocks() { slot_in_epoch = slot % slots_per_epoch; duplicate_block_root = *_state2.block_roots().get(idx as usize).unwrap(); validator_indexes = _state2.get_beacon_proposer_indices(&harness2.spec).unwrap(); - validator_index = validator_indexes[slot_in_epoch.as_usize()]; - // If you are adding a new fork and seeing errors, it means the fork seed has changed the - // validator_index. Uncomment this line, run the test again and add the resulting index to the - // list above. - //eprintln!("new index which needs to be added => {:?}", validator_index); + missed_block_proposer = validator_indexes[slot_in_epoch.as_usize()]; let beacon_proposer_cache = harness2 .chain @@ -256,7 +236,7 @@ async fn produces_missed_blocks() { beacon_proposer_cache.lock().insert( epoch, duplicate_block_root, - validator_indexes.into_iter().collect::>(), + validator_indexes.clone(), _state2.fork() ), Ok(()) @@ -271,10 +251,12 @@ async fn produces_missed_blocks() { // Let's validate the state which will call the function responsible for // adding the missed blocks to the validator monitor let mut validator_monitor2 = harness2.chain.validator_monitor.write(); + validator_monitor2.add_validator_pubkey(KEYPAIRS[missed_block_proposer].pk.compress()); validator_monitor2.process_valid_state(epoch, _state2, &harness2.chain.spec); // We should have one entry in the missed blocks map assert_eq!( - validator_monitor2.get_monitored_validator_missed_block_count(validator_index as u64), + validator_monitor2 + .get_monitored_validator_missed_block_count(missed_block_proposer as u64), 1 ); @@ -282,19 +264,20 @@ async fn produces_missed_blocks() { // // A missed block happens but the validator is not monitored // it should not be flagged as a missed block - idx = initial_blocks + (advance_slot_by) - 7; + while validator_indexes[(idx % slots_per_epoch) as usize] == missed_block_proposer + && idx / slots_per_epoch == epoch.as_u64() + { + idx += 1; + } slot = Slot::new(idx); prev_slot = Slot::new(idx - 1); slot_in_epoch = slot % slots_per_epoch; duplicate_block_root = *_state2.block_roots().get(idx as usize).unwrap(); - validator_indexes = _state2.get_beacon_proposer_indices(&harness2.spec).unwrap(); - let not_monitored_validator_index = validator_indexes[slot_in_epoch.as_usize()]; - // This could do with a refactor: https://github.com/sigp/lighthouse/issues/6293 - assert_ne!( - not_monitored_validator_index, - validator_index_to_monitor, - "this test has a fragile dependency on hardcoded indices. you need to tweak some settings or rewrite this" - ); + let second_missed_block_proposer = validator_indexes[slot_in_epoch.as_usize()]; + + // This test may fail if we can't find another distinct proposer in the same epoch. + // However, this should be vanishingly unlikely: P ~= (1/16)^32 = 2e-39. + assert_ne!(missed_block_proposer, second_missed_block_proposer); assert_eq!( _state2.set_block_root(prev_slot, duplicate_block_root), @@ -306,10 +289,9 @@ async fn produces_missed_blocks() { validator_monitor2.process_valid_state(epoch, _state2, &harness2.chain.spec); // We shouldn't have any entry in the missed blocks map - assert_ne!(validator_index, not_monitored_validator_index); assert_eq!( validator_monitor2 - .get_monitored_validator_missed_block_count(not_monitored_validator_index as u64), + .get_monitored_validator_missed_block_count(second_missed_block_proposer as u64), 0 ); } @@ -318,7 +300,7 @@ async fn produces_missed_blocks() { // // A missed block happens at state.slot - LOG_SLOTS_PER_EPOCH // it shouldn't be flagged as a missed block - let harness3 = get_harness(validator_count, vec![validator_index_to_monitor]); + let harness3 = get_harness(validator_count, vec![]); harness3 .extend_chain( slots_per_epoch as usize, @@ -338,7 +320,7 @@ async fn produces_missed_blocks() { prev_slot = Slot::new(idx - 1); duplicate_block_root = *_state3.block_roots().get(idx as usize).unwrap(); validator_indexes = _state3.get_beacon_proposer_indices(&harness3.spec).unwrap(); - validator_index = validator_indexes[slot_in_epoch.as_usize()]; + missed_block_proposer = validator_indexes[slot_in_epoch.as_usize()]; proposer_shuffling_decision_root = _state3 .proposer_shuffling_decision_root_at_epoch(epoch, duplicate_block_root) .unwrap(); @@ -355,7 +337,7 @@ async fn produces_missed_blocks() { beacon_proposer_cache.lock().insert( epoch, proposer_shuffling_decision_root, - validator_indexes.into_iter().collect::>(), + validator_indexes, _state3.fork() ), Ok(()) @@ -372,11 +354,13 @@ async fn produces_missed_blocks() { // Let's validate the state which will call the function responsible for // adding the missed blocks to the validator monitor let mut validator_monitor3 = harness3.chain.validator_monitor.write(); + validator_monitor3.add_validator_pubkey(KEYPAIRS[missed_block_proposer].pk.compress()); validator_monitor3.process_valid_state(epoch, _state3, &harness3.chain.spec); // We shouldn't have one entry in the missed blocks map assert_eq!( - validator_monitor3.get_monitored_validator_missed_block_count(validator_index as u64), + validator_monitor3 + .get_monitored_validator_missed_block_count(missed_block_proposer as u64), 0 ); } diff --git a/beacon_node/execution_layer/src/engine_api/new_payload_request.rs b/beacon_node/execution_layer/src/engine_api/new_payload_request.rs index a86b2fd9bb..23610c9ae4 100644 --- a/beacon_node/execution_layer/src/engine_api/new_payload_request.rs +++ b/beacon_node/execution_layer/src/engine_api/new_payload_request.rs @@ -128,6 +128,11 @@ impl<'block, E: EthSpec> NewPayloadRequest<'block, E> { let _timer = metrics::start_timer(&metrics::EXECUTION_LAYER_VERIFY_BLOCK_HASH); + // Check that no transactions in the payload are zero length + if payload.transactions().iter().any(|slice| slice.is_empty()) { + return Err(Error::ZeroLengthTransaction); + } + let (header_hash, rlp_transactions_root) = calculate_execution_block_hash( payload, parent_beacon_block_root, diff --git a/beacon_node/execution_layer/src/lib.rs b/beacon_node/execution_layer/src/lib.rs index 118d7adfca..f7abe73543 100644 --- a/beacon_node/execution_layer/src/lib.rs +++ b/beacon_node/execution_layer/src/lib.rs @@ -157,6 +157,7 @@ pub enum Error { payload: ExecutionBlockHash, transactions_root: Hash256, }, + ZeroLengthTransaction, PayloadBodiesByRangeNotSupported, InvalidJWTSecret(String), InvalidForkForPayload, diff --git a/beacon_node/lighthouse_network/src/rpc/protocol.rs b/beacon_node/lighthouse_network/src/rpc/protocol.rs index 681b739d59..780dff937d 100644 --- a/beacon_node/lighthouse_network/src/rpc/protocol.rs +++ b/beacon_node/lighthouse_network/src/rpc/protocol.rs @@ -710,7 +710,7 @@ pub fn rpc_blob_limits() -> RpcLimits { } } -// TODO(peerdas): fix hardcoded max here +// TODO(das): fix hardcoded max here pub fn rpc_data_column_limits(fork_name: ForkName) -> RpcLimits { RpcLimits::new( DataColumnSidecar::::empty().as_ssz_bytes().len(), diff --git a/beacon_node/network/src/sync/tests/range.rs b/beacon_node/network/src/sync/tests/range.rs index 6faa8b7247..05d5e4a414 100644 --- a/beacon_node/network/src/sync/tests/range.rs +++ b/beacon_node/network/src/sync/tests/range.rs @@ -4,12 +4,15 @@ use crate::sync::manager::SLOT_IMPORT_TOLERANCE; use crate::sync::range_sync::RangeSyncType; use crate::sync::SyncMessage; use beacon_chain::test_utils::{AttestationStrategy, BlockStrategy}; -use beacon_chain::EngineState; +use beacon_chain::{block_verification_types::RpcBlock, EngineState, NotifyExecutionLayer}; use lighthouse_network::rpc::{RequestType, StatusMessage}; use lighthouse_network::service::api_types::{AppRequestId, Id, SyncRequestId}; use lighthouse_network::{PeerId, SyncInfo}; use std::time::Duration; -use types::{EthSpec, Hash256, MinimalEthSpec as E, SignedBeaconBlock, Slot}; +use types::{ + BlobSidecarList, BlockImportSource, EthSpec, Hash256, MinimalEthSpec as E, SignedBeaconBlock, + SignedBeaconBlockHash, Slot, +}; const D: Duration = Duration::new(0, 0); @@ -154,7 +157,9 @@ impl TestRig { } } - async fn create_canonical_block(&mut self) -> SignedBeaconBlock { + async fn create_canonical_block( + &mut self, + ) -> (SignedBeaconBlock, Option>) { self.harness.advance_slot(); let block_root = self @@ -165,19 +170,39 @@ impl TestRig { AttestationStrategy::AllValidators, ) .await; - self.harness - .chain - .store - .get_full_block(&block_root) - .unwrap() - .unwrap() + // TODO(das): this does not handle data columns yet + let store = &self.harness.chain.store; + let block = store.get_full_block(&block_root).unwrap().unwrap(); + let blobs = if block.fork_name_unchecked().deneb_enabled() { + store.get_blobs(&block_root).unwrap().blobs() + } else { + None + }; + (block, blobs) } - async fn remember_block(&mut self, block: SignedBeaconBlock) { - self.harness - .process_block(block.slot(), block.canonical_root(), (block.into(), None)) + async fn remember_block( + &mut self, + (block, blob_sidecars): (SignedBeaconBlock, Option>), + ) { + // This code is kind of duplicated from Harness::process_block, but takes sidecars directly. + let block_root = block.canonical_root(); + self.harness.set_current_slot(block.slot()); + let _: SignedBeaconBlockHash = self + .harness + .chain + .process_block( + block_root, + RpcBlock::new(Some(block_root), block.into(), blob_sidecars).unwrap(), + NotifyExecutionLayer::Yes, + BlockImportSource::RangeSync, + || Ok(()), + ) .await + .unwrap() + .try_into() .unwrap(); + self.harness.chain.recompute_head_at_current_slot().await; } } @@ -217,9 +242,9 @@ async fn state_update_while_purging() { // Need to create blocks that can be inserted into the fork-choice and fit the "known // conditions" below. let head_peer_block = rig_2.create_canonical_block().await; - let head_peer_root = head_peer_block.canonical_root(); + let head_peer_root = head_peer_block.0.canonical_root(); let finalized_peer_block = rig_2.create_canonical_block().await; - let finalized_peer_root = finalized_peer_block.canonical_root(); + let finalized_peer_root = finalized_peer_block.0.canonical_root(); // Get a peer with an advanced head let head_peer = rig.add_head_peer_with_root(head_peer_root); diff --git a/consensus/fork_choice/tests/tests.rs b/consensus/fork_choice/tests/tests.rs index 001b80fe11..70b4b73d52 100644 --- a/consensus/fork_choice/tests/tests.rs +++ b/consensus/fork_choice/tests/tests.rs @@ -10,6 +10,7 @@ use beacon_chain::{ use fork_choice::{ ForkChoiceStore, InvalidAttestation, InvalidBlock, PayloadVerificationStatus, QueuedAttestation, }; +use state_processing::state_advance::complete_state_advance; use std::fmt; use std::sync::Mutex; use std::time::Duration; @@ -172,6 +173,20 @@ impl ForkChoiceTest { let validators = self.harness.get_all_validators(); loop { let slot = self.harness.get_current_slot(); + + // Skip slashed proposers, as we expect validators to get slashed in these tests. + // Presently `make_block` will panic if the proposer is slashed, so we just avoid + // calling it in this case. + complete_state_advance(&mut state, None, slot, &self.harness.spec).unwrap(); + state.build_caches(&self.harness.spec).unwrap(); + let proposer_index = state + .get_beacon_proposer_index(slot, &self.harness.chain.spec) + .unwrap(); + if state.validators().get(proposer_index).unwrap().slashed { + self.harness.advance_slot(); + continue; + } + let (block_contents, state_) = self.harness.make_block(state, slot).await; state = state_; if !predicate(block_contents.0.message(), &state) { @@ -196,17 +211,20 @@ impl ForkChoiceTest { } /// Apply `count` blocks to the chain (with attestations). + /// + /// Note that in the case of slashed validators, their proposals will be skipped and the chain + /// may be advanced by *more than* `count` slots. pub async fn apply_blocks(self, count: usize) -> Self { - self.harness.advance_slot(); - self.harness - .extend_chain( - count, - BlockStrategy::OnCanonicalHead, - AttestationStrategy::AllValidators, - ) - .await; - - self + // Use `Self::apply_blocks_while` which gracefully handles slashed validators. + let mut blocks_applied = 0; + self.apply_blocks_while(|_, _| { + // Blocks are applied after the predicate is called, so continue applying the block if + // less than *or equal* to the count. + blocks_applied += 1; + blocks_applied <= count + }) + .await + .unwrap() } /// Slash a validator from the previous epoch committee. @@ -244,6 +262,7 @@ impl ForkChoiceTest { /// Apply `count` blocks to the chain (without attestations). pub async fn apply_blocks_without_new_attestations(self, count: usize) -> Self { + // This function does not gracefully handle slashed proposers, but may need to in future. self.harness.advance_slot(); self.harness .extend_chain( @@ -1226,14 +1245,6 @@ async fn progressive_balances_cache_attester_slashing() { .apply_blocks_while(|_, state| state.finalized_checkpoint().epoch == 0) .await .unwrap() - // Note: This test may fail if the shuffling used changes, right now it re-runs with - // deterministic shuffling. A shuffling change my cause the slashed proposer to propose - // again in the next epoch, which results in a block processing failure - // (`HeaderInvalid::ProposerSlashed`). The harness should be re-worked to successfully skip - // the slot in this scenario rather than panic-ing. The same applies to - // `progressive_balances_cache_proposer_slashing`. - .apply_blocks(2) - .await .add_previous_epoch_attester_slashing() .await // expect fork choice to import blocks successfully after a previous epoch attester is @@ -1244,7 +1255,7 @@ async fn progressive_balances_cache_attester_slashing() { // expect fork choice to import another epoch of blocks successfully - the slashed // attester's balance should be excluded from the current epoch total balance in // `ProgressiveBalancesCache` as well. - .apply_blocks(MainnetEthSpec::slots_per_epoch() as usize) + .apply_blocks(E::slots_per_epoch() as usize) .await; } @@ -1257,15 +1268,7 @@ async fn progressive_balances_cache_proposer_slashing() { .apply_blocks_while(|_, state| state.finalized_checkpoint().epoch == 0) .await .unwrap() - // Note: This test may fail if the shuffling used changes, right now it re-runs with - // deterministic shuffling. A shuffling change may cause the slashed proposer to propose - // again in the next epoch, which results in a block processing failure - // (`HeaderInvalid::ProposerSlashed`). The harness should be re-worked to successfully skip - // the slot in this scenario rather than panic-ing. The same applies to - // `progressive_balances_cache_attester_slashing`. - .apply_blocks(1) - .await - .add_previous_epoch_proposer_slashing(MainnetEthSpec::slots_per_epoch()) + .add_previous_epoch_proposer_slashing(E::slots_per_epoch()) .await // expect fork choice to import blocks successfully after a previous epoch proposer is // slashed, i.e. the slashed proposer's balance is correctly excluded from @@ -1275,6 +1278,6 @@ async fn progressive_balances_cache_proposer_slashing() { // expect fork choice to import another epoch of blocks successfully - the slashed // proposer's balance should be excluded from the current epoch total balance in // `ProgressiveBalancesCache` as well. - .apply_blocks(MainnetEthSpec::slots_per_epoch() as usize) + .apply_blocks(E::slots_per_epoch() as usize) .await; } diff --git a/consensus/state_processing/src/per_block_processing.rs b/consensus/state_processing/src/per_block_processing.rs index 782dbe2a54..502ad25838 100644 --- a/consensus/state_processing/src/per_block_processing.rs +++ b/consensus/state_processing/src/per_block_processing.rs @@ -1,7 +1,7 @@ use crate::consensus_context::ConsensusContext; use errors::{BlockOperationError, BlockProcessingError, HeaderInvalid}; use rayon::prelude::*; -use safe_arith::{ArithError, SafeArith}; +use safe_arith::{ArithError, SafeArith, SafeArithIter}; use signature_sets::{block_proposal_signature_set, get_pubkey_from_state, randao_signature_set}; use std::borrow::Cow; use tree_hash::TreeHash; @@ -509,7 +509,7 @@ pub fn compute_timestamp_at_slot( /// Compute the next batch of withdrawals which should be included in a block. /// -/// https://github.com/ethereum/consensus-specs/blob/dev/specs/capella/beacon-chain.md#new-get_expected_withdrawals +/// https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#new-get_expected_withdrawals pub fn get_expected_withdrawals( state: &BeaconState, spec: &ChainSpec, @@ -522,9 +522,9 @@ pub fn get_expected_withdrawals( // [New in Electra:EIP7251] // Consume pending partial withdrawals - let partial_withdrawals_count = + let processed_partial_withdrawals_count = if let Ok(partial_withdrawals) = state.pending_partial_withdrawals() { - let mut partial_withdrawals_count = 0; + let mut processed_partial_withdrawals_count = 0; for withdrawal in partial_withdrawals { if withdrawal.withdrawable_epoch > epoch || withdrawals.len() == spec.max_pending_partials_per_withdrawals_sweep as usize @@ -532,8 +532,8 @@ pub fn get_expected_withdrawals( break; } - let withdrawal_balance = state.get_balance(withdrawal.index as usize)?; - let validator = state.get_validator(withdrawal.index as usize)?; + let withdrawal_balance = state.get_balance(withdrawal.validator_index as usize)?; + let validator = state.get_validator(withdrawal.validator_index as usize)?; let has_sufficient_effective_balance = validator.effective_balance >= spec.min_activation_balance; @@ -549,7 +549,7 @@ pub fn get_expected_withdrawals( ); withdrawals.push(Withdrawal { index: withdrawal_index, - validator_index: withdrawal.index, + validator_index: withdrawal.validator_index, address: validator .get_execution_withdrawal_address(spec) .ok_or(BeaconStateError::NonExecutionAddresWithdrawalCredential)?, @@ -557,9 +557,9 @@ pub fn get_expected_withdrawals( }); withdrawal_index.safe_add_assign(1)?; } - partial_withdrawals_count.safe_add_assign(1)?; + processed_partial_withdrawals_count.safe_add_assign(1)?; } - Some(partial_withdrawals_count) + Some(processed_partial_withdrawals_count) } else { None }; @@ -570,9 +570,19 @@ pub fn get_expected_withdrawals( ); for _ in 0..bound { let validator = state.get_validator(validator_index as usize)?; - let balance = *state.balances().get(validator_index as usize).ok_or( - BeaconStateError::BalancesOutOfBounds(validator_index as usize), - )?; + let partially_withdrawn_balance = withdrawals + .iter() + .filter_map(|withdrawal| { + (withdrawal.validator_index == validator_index).then_some(withdrawal.amount) + }) + .safe_sum()?; + let balance = state + .balances() + .get(validator_index as usize) + .ok_or(BeaconStateError::BalancesOutOfBounds( + validator_index as usize, + ))? + .safe_sub(partially_withdrawn_balance)?; if validator.is_fully_withdrawable_at(balance, epoch, spec, fork_name) { withdrawals.push(Withdrawal { index: withdrawal_index, @@ -604,7 +614,7 @@ pub fn get_expected_withdrawals( .safe_rem(state.validators().len() as u64)?; } - Ok((withdrawals.into(), partial_withdrawals_count)) + Ok((withdrawals.into(), processed_partial_withdrawals_count)) } /// Apply withdrawals to the state. diff --git a/consensus/state_processing/src/per_block_processing/process_operations.rs b/consensus/state_processing/src/per_block_processing/process_operations.rs index 4977f7c7e9..82dd616724 100644 --- a/consensus/state_processing/src/per_block_processing/process_operations.rs +++ b/consensus/state_processing/src/per_block_processing/process_operations.rs @@ -507,11 +507,11 @@ pub fn process_withdrawal_requests( } // Verify pubkey exists - let Some(index) = state.pubkey_cache().get(&request.validator_pubkey) else { + let Some(validator_index) = state.pubkey_cache().get(&request.validator_pubkey) else { continue; }; - let validator = state.get_validator(index)?; + let validator = state.get_validator(validator_index)?; // Verify withdrawal credentials let has_correct_credential = validator.has_execution_withdrawal_credential(spec); let is_correct_source_address = validator @@ -542,16 +542,16 @@ pub fn process_withdrawal_requests( continue; } - let pending_balance_to_withdraw = state.get_pending_balance_to_withdraw(index)?; + let pending_balance_to_withdraw = state.get_pending_balance_to_withdraw(validator_index)?; if is_full_exit_request { // Only exit validator if it has no pending withdrawals in the queue if pending_balance_to_withdraw == 0 { - initiate_validator_exit(state, index, spec)? + initiate_validator_exit(state, validator_index, spec)? } continue; } - let balance = state.get_balance(index)?; + let balance = state.get_balance(validator_index)?; let has_sufficient_effective_balance = validator.effective_balance >= spec.min_activation_balance; let has_excess_balance = balance @@ -576,7 +576,7 @@ pub fn process_withdrawal_requests( state .pending_partial_withdrawals_mut()? .push(PendingPartialWithdrawal { - index: index as u64, + validator_index: validator_index as u64, amount: to_withdraw, withdrawable_epoch, })?; @@ -739,8 +739,8 @@ pub fn process_consolidation_request( } let target_validator = state.get_validator(target_index)?; - // Verify the target has execution withdrawal credentials - if !target_validator.has_execution_withdrawal_credential(spec) { + // Verify the target has compounding withdrawal credentials + if !target_validator.has_compounding_withdrawal_credential(spec) { return Ok(()); } @@ -757,6 +757,18 @@ pub fn process_consolidation_request( { return Ok(()); } + // Verify the source has been active long enough + if current_epoch + < source_validator + .activation_epoch + .safe_add(spec.shard_committee_period)? + { + return Ok(()); + } + // Verify the source has no pending withdrawals in the queue + if state.get_pending_balance_to_withdraw(source_index)? > 0 { + return Ok(()); + } // Initiate source validator exit and append pending consolidation let source_exit_epoch = state @@ -772,10 +784,5 @@ pub fn process_consolidation_request( target_index: target_index as u64, })?; - let target_validator = state.get_validator(target_index)?; - // Churn any target excess active balance of target and raise its max - if target_validator.has_eth1_withdrawal_credential(spec) { - state.switch_to_compounding_validator(target_index, spec)?; - } Ok(()) } diff --git a/consensus/state_processing/src/per_epoch_processing/single_pass.rs b/consensus/state_processing/src/per_epoch_processing/single_pass.rs index 904e68e368..a4a81c8eef 100644 --- a/consensus/state_processing/src/per_epoch_processing/single_pass.rs +++ b/consensus/state_processing/src/per_epoch_processing/single_pass.rs @@ -1057,14 +1057,12 @@ fn process_pending_consolidations( } // Calculate the consolidated balance - let max_effective_balance = - source_validator.get_max_effective_balance(spec, state_ctxt.fork_name); let source_effective_balance = std::cmp::min( *state .balances() .get(source_index) .ok_or(BeaconStateError::UnknownValidator(source_index))?, - max_effective_balance, + source_validator.effective_balance, ); // Move active balance to target. Excess balance is withdrawable. diff --git a/consensus/state_processing/src/upgrade/electra.rs b/consensus/state_processing/src/upgrade/electra.rs index 1e64ef2897..0f32e1553d 100644 --- a/consensus/state_processing/src/upgrade/electra.rs +++ b/consensus/state_processing/src/upgrade/electra.rs @@ -14,13 +14,15 @@ pub fn upgrade_to_electra( ) -> Result<(), Error> { let epoch = pre_state.current_epoch(); + let activation_exit_epoch = spec.compute_activation_exit_epoch(epoch)?; let earliest_exit_epoch = pre_state .validators() .iter() .filter(|v| v.exit_epoch != spec.far_future_epoch) .map(|v| v.exit_epoch) .max() - .unwrap_or(epoch) + .unwrap_or(activation_exit_epoch) + .max(activation_exit_epoch) .safe_add(1)?; // The total active balance cache must be built before the consolidation churn limit diff --git a/consensus/types/presets/gnosis/electra.yaml b/consensus/types/presets/gnosis/electra.yaml index 660ed9b64c..42afbb233e 100644 --- a/consensus/types/presets/gnosis/electra.yaml +++ b/consensus/types/presets/gnosis/electra.yaml @@ -10,7 +10,7 @@ MAX_EFFECTIVE_BALANCE_ELECTRA: 2048000000000 # State list lengths # --------------------------------------------------------------- # `uint64(2**27)` (= 134,217,728) -PENDING_BALANCE_DEPOSITS_LIMIT: 134217728 +PENDING_DEPOSITS_LIMIT: 134217728 # `uint64(2**27)` (= 134,217,728) PENDING_PARTIAL_WITHDRAWALS_LIMIT: 134217728 # `uint64(2**18)` (= 262,144) @@ -29,12 +29,12 @@ WHISTLEBLOWER_REWARD_QUOTIENT_ELECTRA: 4096 MAX_ATTESTER_SLASHINGS_ELECTRA: 1 # `uint64(2**3)` (= 8) MAX_ATTESTATIONS_ELECTRA: 8 -# `uint64(2**0)` (= 1) -MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD: 1 +# `uint64(2**1)` (= 2) +MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD: 2 # Execution # --------------------------------------------------------------- -# 2**13 (= 8192) receipts +# 2**13 (= 8192) deposit requests MAX_DEPOSIT_REQUESTS_PER_PAYLOAD: 8192 # 2**4 (= 16) withdrawal requests MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD: 16 @@ -43,3 +43,8 @@ MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD: 16 # --------------------------------------------------------------- # 2**3 ( = 8) pending withdrawals MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP: 8 + +# Pending deposits processing +# --------------------------------------------------------------- +# 2**4 ( = 4) pending deposits +MAX_PENDING_DEPOSITS_PER_EPOCH: 16 diff --git a/consensus/types/presets/mainnet/altair.yaml b/consensus/types/presets/mainnet/altair.yaml index 9a17b78032..813ef72122 100644 --- a/consensus/types/presets/mainnet/altair.yaml +++ b/consensus/types/presets/mainnet/altair.yaml @@ -22,3 +22,5 @@ EPOCHS_PER_SYNC_COMMITTEE_PERIOD: 256 # --------------------------------------------------------------- # 1 MIN_SYNC_COMMITTEE_PARTICIPANTS: 1 +# SLOTS_PER_EPOCH * EPOCHS_PER_SYNC_COMMITTEE_PERIOD (= 32 * 256) +UPDATE_TIMEOUT: 8192 diff --git a/consensus/types/presets/mainnet/electra.yaml b/consensus/types/presets/mainnet/electra.yaml index 660ed9b64c..42afbb233e 100644 --- a/consensus/types/presets/mainnet/electra.yaml +++ b/consensus/types/presets/mainnet/electra.yaml @@ -10,7 +10,7 @@ MAX_EFFECTIVE_BALANCE_ELECTRA: 2048000000000 # State list lengths # --------------------------------------------------------------- # `uint64(2**27)` (= 134,217,728) -PENDING_BALANCE_DEPOSITS_LIMIT: 134217728 +PENDING_DEPOSITS_LIMIT: 134217728 # `uint64(2**27)` (= 134,217,728) PENDING_PARTIAL_WITHDRAWALS_LIMIT: 134217728 # `uint64(2**18)` (= 262,144) @@ -29,12 +29,12 @@ WHISTLEBLOWER_REWARD_QUOTIENT_ELECTRA: 4096 MAX_ATTESTER_SLASHINGS_ELECTRA: 1 # `uint64(2**3)` (= 8) MAX_ATTESTATIONS_ELECTRA: 8 -# `uint64(2**0)` (= 1) -MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD: 1 +# `uint64(2**1)` (= 2) +MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD: 2 # Execution # --------------------------------------------------------------- -# 2**13 (= 8192) receipts +# 2**13 (= 8192) deposit requests MAX_DEPOSIT_REQUESTS_PER_PAYLOAD: 8192 # 2**4 (= 16) withdrawal requests MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD: 16 @@ -43,3 +43,8 @@ MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD: 16 # --------------------------------------------------------------- # 2**3 ( = 8) pending withdrawals MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP: 8 + +# Pending deposits processing +# --------------------------------------------------------------- +# 2**4 ( = 4) pending deposits +MAX_PENDING_DEPOSITS_PER_EPOCH: 16 diff --git a/consensus/types/presets/mainnet/phase0.yaml b/consensus/types/presets/mainnet/phase0.yaml index 02bc96c8cd..00133ba369 100644 --- a/consensus/types/presets/mainnet/phase0.yaml +++ b/consensus/types/presets/mainnet/phase0.yaml @@ -85,4 +85,4 @@ MAX_ATTESTATIONS: 128 # 2**4 (= 16) MAX_DEPOSITS: 16 # 2**4 (= 16) -MAX_VOLUNTARY_EXITS: 16 +MAX_VOLUNTARY_EXITS: 16 \ No newline at end of file diff --git a/consensus/types/presets/minimal/altair.yaml b/consensus/types/presets/minimal/altair.yaml index 88d78bea36..5e472c49cf 100644 --- a/consensus/types/presets/minimal/altair.yaml +++ b/consensus/types/presets/minimal/altair.yaml @@ -22,3 +22,5 @@ EPOCHS_PER_SYNC_COMMITTEE_PERIOD: 8 # --------------------------------------------------------------- # 1 MIN_SYNC_COMMITTEE_PARTICIPANTS: 1 +# SLOTS_PER_EPOCH * EPOCHS_PER_SYNC_COMMITTEE_PERIOD (= 8 * 8) +UPDATE_TIMEOUT: 64 diff --git a/consensus/types/presets/minimal/deneb.yaml b/consensus/types/presets/minimal/deneb.yaml index b1bbc4ee54..c101de3162 100644 --- a/consensus/types/presets/minimal/deneb.yaml +++ b/consensus/types/presets/minimal/deneb.yaml @@ -2,9 +2,9 @@ # Misc # --------------------------------------------------------------- -# [customized] +# `uint64(4096)` FIELD_ELEMENTS_PER_BLOB: 4096 # [customized] -MAX_BLOB_COMMITMENTS_PER_BLOCK: 16 -# [customized] `floorlog2(BLOB_KZG_COMMITMENTS_GINDEX) + 1 + ceillog2(MAX_BLOB_COMMITMENTS_PER_BLOCK)` = 4 + 1 + 4 = 9 -KZG_COMMITMENT_INCLUSION_PROOF_DEPTH: 9 +MAX_BLOB_COMMITMENTS_PER_BLOCK: 32 +# [customized] `floorlog2(get_generalized_index(BeaconBlockBody, 'blob_kzg_commitments')) + 1 + ceillog2(MAX_BLOB_COMMITMENTS_PER_BLOCK)` = 4 + 1 + 5 = 10 +KZG_COMMITMENT_INCLUSION_PROOF_DEPTH: 10 diff --git a/consensus/types/presets/minimal/electra.yaml b/consensus/types/presets/minimal/electra.yaml index ef1ce494d8..44e4769756 100644 --- a/consensus/types/presets/minimal/electra.yaml +++ b/consensus/types/presets/minimal/electra.yaml @@ -10,7 +10,7 @@ MAX_EFFECTIVE_BALANCE_ELECTRA: 2048000000000 # State list lengths # --------------------------------------------------------------- # `uint64(2**27)` (= 134,217,728) -PENDING_BALANCE_DEPOSITS_LIMIT: 134217728 +PENDING_DEPOSITS_LIMIT: 134217728 # [customized] `uint64(2**6)` (= 64) PENDING_PARTIAL_WITHDRAWALS_LIMIT: 64 # [customized] `uint64(2**6)` (= 64) @@ -29,8 +29,8 @@ WHISTLEBLOWER_REWARD_QUOTIENT_ELECTRA: 4096 MAX_ATTESTER_SLASHINGS_ELECTRA: 1 # `uint64(2**3)` (= 8) MAX_ATTESTATIONS_ELECTRA: 8 -# `uint64(2**0)` (= 1) -MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD: 1 +# `uint64(2**1)` (= 2) +MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD: 2 # Execution # --------------------------------------------------------------- @@ -41,5 +41,10 @@ MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD: 2 # Withdrawals processing # --------------------------------------------------------------- -# 2**0 ( = 1) pending withdrawals -MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP: 1 +# 2**1 ( = 2) pending withdrawals +MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP: 2 + +# Pending deposits processing +# --------------------------------------------------------------- +# 2**4 ( = 4) pending deposits +MAX_PENDING_DEPOSITS_PER_EPOCH: 16 diff --git a/consensus/types/presets/minimal/phase0.yaml b/consensus/types/presets/minimal/phase0.yaml index 1f75603142..d9a6a2b6c0 100644 --- a/consensus/types/presets/minimal/phase0.yaml +++ b/consensus/types/presets/minimal/phase0.yaml @@ -4,11 +4,11 @@ # --------------------------------------------------------------- # [customized] Just 4 committees for slot for testing purposes MAX_COMMITTEES_PER_SLOT: 4 -# [customized] unsecure, but fast +# [customized] insecure, but fast TARGET_COMMITTEE_SIZE: 4 # 2**11 (= 2,048) MAX_VALIDATORS_PER_COMMITTEE: 2048 -# [customized] Faster, but unsecure. +# [customized] Faster, but insecure. SHUFFLE_ROUND_COUNT: 10 # 4 HYSTERESIS_QUOTIENT: 4 @@ -85,4 +85,4 @@ MAX_ATTESTATIONS: 128 # 2**4 (= 16) MAX_DEPOSITS: 16 # 2**4 (= 16) -MAX_VOLUNTARY_EXITS: 16 +MAX_VOLUNTARY_EXITS: 16 \ No newline at end of file diff --git a/consensus/types/src/beacon_state.rs b/consensus/types/src/beacon_state.rs index de6077bf94..6f44998cdf 100644 --- a/consensus/types/src/beacon_state.rs +++ b/consensus/types/src/beacon_state.rs @@ -46,6 +46,7 @@ mod tests; pub const CACHED_EPOCHS: usize = 3; const MAX_RANDOM_BYTE: u64 = (1 << 8) - 1; +const MAX_RANDOM_VALUE: u64 = (1 << 16) - 1; pub type Validators = List::ValidatorRegistryLimit>; pub type Balances = List::ValidatorRegistryLimit>; @@ -916,6 +917,11 @@ impl BeaconState { } let max_effective_balance = spec.max_effective_balance_for_fork(self.fork_name_unchecked()); + let max_random_value = if self.fork_name_unchecked().electra_enabled() { + MAX_RANDOM_VALUE + } else { + MAX_RANDOM_BYTE + }; let mut i = 0; loop { @@ -929,10 +935,10 @@ impl BeaconState { let candidate_index = *indices .get(shuffled_index) .ok_or(Error::ShuffleIndexOutOfBounds(shuffled_index))?; - let random_byte = Self::shuffling_random_byte(i, seed)?; + let random_value = self.shuffling_random_value(i, seed)?; let effective_balance = self.get_effective_balance(candidate_index)?; - if effective_balance.safe_mul(MAX_RANDOM_BYTE)? - >= max_effective_balance.safe_mul(u64::from(random_byte))? + if effective_balance.safe_mul(max_random_value)? + >= max_effective_balance.safe_mul(random_value)? { return Ok(candidate_index); } @@ -940,6 +946,19 @@ impl BeaconState { } } + /// Fork-aware abstraction for the shuffling. + /// + /// In Electra and later, the random value is a 16-bit integer stored in a `u64`. + /// + /// Prior to Electra, the random value is an 8-bit integer stored in a `u64`. + fn shuffling_random_value(&self, i: usize, seed: &[u8]) -> Result { + if self.fork_name_unchecked().electra_enabled() { + Self::shuffling_random_u16_electra(i, seed).map(u64::from) + } else { + Self::shuffling_random_byte(i, seed).map(u64::from) + } + } + /// Get a random byte from the given `seed`. /// /// Used by the proposer & sync committee selection functions. @@ -953,6 +972,21 @@ impl BeaconState { .ok_or(Error::ShuffleIndexOutOfBounds(index)) } + /// Get two random bytes from the given `seed`. + /// + /// This is used in place of `shuffling_random_byte` from Electra onwards. + fn shuffling_random_u16_electra(i: usize, seed: &[u8]) -> Result { + let mut preimage = seed.to_vec(); + preimage.append(&mut int_to_bytes8(i.safe_div(16)? as u64)); + let offset = i.safe_rem(16)?.safe_mul(2)?; + hash(&preimage) + .get(offset..offset.safe_add(2)?) + .ok_or(Error::ShuffleIndexOutOfBounds(offset))? + .try_into() + .map(u16::from_le_bytes) + .map_err(|_| Error::ShuffleIndexOutOfBounds(offset)) + } + /// Convenience accessor for the `execution_payload_header` as an `ExecutionPayloadHeaderRef`. pub fn latest_execution_payload_header(&self) -> Result, Error> { match self { @@ -1120,6 +1154,11 @@ impl BeaconState { let seed = self.get_seed(epoch, Domain::SyncCommittee, spec)?; let max_effective_balance = spec.max_effective_balance_for_fork(self.fork_name_unchecked()); + let max_random_value = if self.fork_name_unchecked().electra_enabled() { + MAX_RANDOM_VALUE + } else { + MAX_RANDOM_BYTE + }; let mut i = 0; let mut sync_committee_indices = Vec::with_capacity(E::SyncCommitteeSize::to_usize()); @@ -1134,10 +1173,10 @@ impl BeaconState { let candidate_index = *active_validator_indices .get(shuffled_index) .ok_or(Error::ShuffleIndexOutOfBounds(shuffled_index))?; - let random_byte = Self::shuffling_random_byte(i, seed.as_slice())?; + let random_value = self.shuffling_random_value(i, seed.as_slice())?; let effective_balance = self.get_validator(candidate_index)?.effective_balance; - if effective_balance.safe_mul(MAX_RANDOM_BYTE)? - >= max_effective_balance.safe_mul(u64::from(random_byte))? + if effective_balance.safe_mul(max_random_value)? + >= max_effective_balance.safe_mul(random_value)? { sync_committee_indices.push(candidate_index); } @@ -2205,7 +2244,7 @@ impl BeaconState { for withdrawal in self .pending_partial_withdrawals()? .iter() - .filter(|withdrawal| withdrawal.index as usize == validator_index) + .filter(|withdrawal| withdrawal.validator_index as usize == validator_index) { pending_balance.safe_add_assign(withdrawal.amount)?; } diff --git a/consensus/types/src/chain_spec.rs b/consensus/types/src/chain_spec.rs index 65f4c37aa1..ea4d8641f6 100644 --- a/consensus/types/src/chain_spec.rs +++ b/consensus/types/src/chain_spec.rs @@ -191,6 +191,7 @@ pub struct ChainSpec { pub max_pending_partials_per_withdrawals_sweep: u64, pub min_per_epoch_churn_limit_electra: u64, pub max_per_epoch_activation_exit_churn_limit: u64, + pub max_blobs_per_block_electra: u64, /* * Fulu hard fork params @@ -623,9 +624,12 @@ impl ChainSpec { } /// Return the value of `MAX_BLOBS_PER_BLOCK` appropriate for `fork`. - pub fn max_blobs_per_block_by_fork(&self, _fork_name: ForkName) -> u64 { - // TODO(electra): add Electra blobs per block change here - self.max_blobs_per_block + pub fn max_blobs_per_block_by_fork(&self, fork_name: ForkName) -> u64 { + if fork_name.electra_enabled() { + self.max_blobs_per_block_electra + } else { + self.max_blobs_per_block + } } pub fn data_columns_per_subnet(&self) -> usize { @@ -826,6 +830,7 @@ impl ChainSpec { u64::checked_pow(2, 8)?.checked_mul(u64::checked_pow(10, 9)?) }) .expect("calculation does not overflow"), + max_blobs_per_block_electra: default_max_blobs_per_block_electra(), /* * Fulu hard fork params @@ -940,7 +945,7 @@ impl ChainSpec { // Electra electra_fork_version: [0x05, 0x00, 0x00, 0x01], electra_fork_epoch: None, - max_pending_partials_per_withdrawals_sweep: u64::checked_pow(2, 0) + max_pending_partials_per_withdrawals_sweep: u64::checked_pow(2, 1) .expect("pow does not overflow"), min_per_epoch_churn_limit_electra: option_wrapper(|| { u64::checked_pow(2, 6)?.checked_mul(u64::checked_pow(10, 9)?) @@ -1156,6 +1161,7 @@ impl ChainSpec { u64::checked_pow(2, 8)?.checked_mul(u64::checked_pow(10, 9)?) }) .expect("calculation does not overflow"), + max_blobs_per_block_electra: default_max_blobs_per_block_electra(), /* * Fulu hard fork params @@ -1412,6 +1418,9 @@ 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(default = "default_max_blobs_per_block_electra")] + #[serde(with = "serde_utils::quoted_u64")] + max_blobs_per_block_electra: u64, #[serde(default = "default_custody_requirement")] #[serde(with = "serde_utils::quoted_u64")] @@ -1554,6 +1563,10 @@ const fn default_max_per_epoch_activation_exit_churn_limit() -> u64 { 256_000_000_000 } +const fn default_max_blobs_per_block_electra() -> u64 { + 9 +} + const fn default_attestation_propagation_slot_range() -> u64 { 32 } @@ -1773,6 +1786,7 @@ 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, + max_blobs_per_block_electra: spec.max_blobs_per_block_electra, custody_requirement: spec.custody_requirement, data_column_sidecar_subnet_count: spec.data_column_sidecar_subnet_count, @@ -1850,6 +1864,7 @@ impl Config { min_per_epoch_churn_limit_electra, max_per_epoch_activation_exit_churn_limit, + max_blobs_per_block_electra, custody_requirement, data_column_sidecar_subnet_count, number_of_columns, @@ -1919,6 +1934,7 @@ impl Config { min_per_epoch_churn_limit_electra, max_per_epoch_activation_exit_churn_limit, + max_blobs_per_block_electra, // 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/eth_spec.rs b/consensus/types/src/eth_spec.rs index 976766dfa9..0bc074072f 100644 --- a/consensus/types/src/eth_spec.rs +++ b/consensus/types/src/eth_spec.rs @@ -3,10 +3,10 @@ use crate::*; use safe_arith::SafeArith; use serde::{Deserialize, Serialize}; use ssz_types::typenum::{ - bit::B0, UInt, U0, U1, U1024, U1048576, U1073741824, U1099511627776, U128, U131072, U134217728, - U16, U16777216, U2, U2048, U256, U262144, U32, U4, U4096, U512, U625, U64, U65536, U8, U8192, + bit::B0, UInt, U0, U1, U10, U1024, U1048576, U1073741824, U1099511627776, U128, U131072, + U134217728, U16, U16777216, U17, U2, U2048, U256, U262144, U32, U4, U4096, U512, U625, U64, + U65536, U8, U8192, }; -use ssz_types::typenum::{U17, U9}; use std::fmt::{self, Debug}; use std::str::FromStr; @@ -431,7 +431,7 @@ impl EthSpec for MainnetEthSpec { type PendingDepositsLimit = U134217728; type PendingPartialWithdrawalsLimit = U134217728; type PendingConsolidationsLimit = U262144; - type MaxConsolidationRequestsPerPayload = U1; + type MaxConsolidationRequestsPerPayload = U2; type MaxDepositRequestsPerPayload = U8192; type MaxAttesterSlashingsElectra = U1; type MaxAttestationsElectra = U8; @@ -466,8 +466,8 @@ impl EthSpec for MinimalEthSpec { type MaxWithdrawalsPerPayload = U4; type FieldElementsPerBlob = U4096; type BytesPerBlob = U131072; - type MaxBlobCommitmentsPerBlock = U16; - type KzgCommitmentInclusionProofDepth = U9; + type MaxBlobCommitmentsPerBlock = U32; + type KzgCommitmentInclusionProofDepth = U10; type PendingPartialWithdrawalsLimit = U64; type PendingConsolidationsLimit = U64; type MaxDepositRequestsPerPayload = U4; @@ -558,7 +558,7 @@ impl EthSpec for GnosisEthSpec { type PendingDepositsLimit = U134217728; type PendingPartialWithdrawalsLimit = U134217728; type PendingConsolidationsLimit = U262144; - type MaxConsolidationRequestsPerPayload = U1; + type MaxConsolidationRequestsPerPayload = U2; type MaxDepositRequestsPerPayload = U8192; type MaxAttesterSlashingsElectra = U1; type MaxAttestationsElectra = U8; diff --git a/consensus/types/src/pending_partial_withdrawal.rs b/consensus/types/src/pending_partial_withdrawal.rs index e5ace7b273..846dd97360 100644 --- a/consensus/types/src/pending_partial_withdrawal.rs +++ b/consensus/types/src/pending_partial_withdrawal.rs @@ -21,7 +21,7 @@ use tree_hash_derive::TreeHash; )] pub struct PendingPartialWithdrawal { #[serde(with = "serde_utils::quoted_u64")] - pub index: u64, + pub validator_index: u64, #[serde(with = "serde_utils::quoted_u64")] pub amount: u64, pub withdrawable_epoch: Epoch, diff --git a/consensus/types/src/preset.rs b/consensus/types/src/preset.rs index f64b7051e5..9a9915e458 100644 --- a/consensus/types/src/preset.rs +++ b/consensus/types/src/preset.rs @@ -234,7 +234,7 @@ pub struct ElectraPreset { #[serde(with = "serde_utils::quoted_u64")] pub max_pending_partials_per_withdrawals_sweep: u64, #[serde(with = "serde_utils::quoted_u64")] - pub pending_balance_deposits_limit: u64, + pub pending_deposits_limit: u64, #[serde(with = "serde_utils::quoted_u64")] pub pending_partial_withdrawals_limit: u64, #[serde(with = "serde_utils::quoted_u64")] @@ -260,7 +260,7 @@ impl ElectraPreset { whistleblower_reward_quotient_electra: spec.whistleblower_reward_quotient_electra, max_pending_partials_per_withdrawals_sweep: spec .max_pending_partials_per_withdrawals_sweep, - pending_balance_deposits_limit: E::pending_deposits_limit() as u64, + pending_deposits_limit: E::pending_deposits_limit() as u64, pending_partial_withdrawals_limit: E::pending_partial_withdrawals_limit() as u64, pending_consolidations_limit: E::pending_consolidations_limit() as u64, max_consolidation_requests_per_payload: E::max_consolidation_requests_per_payload() diff --git a/testing/ef_tests/Makefile b/testing/ef_tests/Makefile index d5f4997bb7..7108e3e8f6 100644 --- a/testing/ef_tests/Makefile +++ b/testing/ef_tests/Makefile @@ -1,4 +1,4 @@ -TESTS_TAG := v1.5.0-alpha.8 +TESTS_TAG := v1.5.0-beta.0 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 dacca204c1..bf9e5d6cfa 100755 --- a/testing/ef_tests/check_all_files_accessed.py +++ b/testing/ef_tests/check_all_files_accessed.py @@ -35,6 +35,8 @@ excluded_paths = [ "tests/.*/.*/ssz_static/LightClientStore", # LightClientSnapshot "tests/.*/.*/ssz_static/LightClientSnapshot", + # LightClientDataCollection + "tests/minimal/.*/light_client/data_collection", # One of the EF researchers likes to pack the tarballs on a Mac ".*\\.DS_Store.*", # More Mac weirdness. @@ -48,6 +50,10 @@ excluded_paths = [ "tests/.*/eip6110", "tests/.*/whisk", "tests/.*/eip7594", + # Fulu tests are not yet being run + "tests/.*/fulu", + # TODO(electra): SingleAttestation tests are waiting on Eitan's PR + "tests/.*/electra/ssz_static/SingleAttestation" ] diff --git a/testing/ef_tests/src/cases/genesis_initialization.rs b/testing/ef_tests/src/cases/genesis_initialization.rs index 11402c75e6..210e18f781 100644 --- a/testing/ef_tests/src/cases/genesis_initialization.rs +++ b/testing/ef_tests/src/cases/genesis_initialization.rs @@ -66,8 +66,7 @@ impl LoadCase for GenesisInitialization { impl Case for GenesisInitialization { fn is_enabled_for_fork(fork_name: ForkName) -> bool { - // Altair genesis and later requires real crypto. - fork_name == ForkName::Base || cfg!(not(feature = "fake_crypto")) + fork_name == ForkName::Base } fn result(&self, _case_index: usize, fork_name: ForkName) -> Result<(), Error> { diff --git a/testing/ef_tests/src/cases/genesis_validity.rs b/testing/ef_tests/src/cases/genesis_validity.rs index e977fa3d63..8fb9f2fbdc 100644 --- a/testing/ef_tests/src/cases/genesis_validity.rs +++ b/testing/ef_tests/src/cases/genesis_validity.rs @@ -39,6 +39,10 @@ impl LoadCase for GenesisValidity { } impl Case for GenesisValidity { + fn is_enabled_for_fork(fork_name: ForkName) -> bool { + fork_name == ForkName::Base + } + fn result(&self, _case_index: usize, fork_name: ForkName) -> Result<(), Error> { let spec = &testing_spec::(fork_name); diff --git a/testing/ef_tests/src/handler.rs b/testing/ef_tests/src/handler.rs index d8fe061061..2e49b1301d 100644 --- a/testing/ef_tests/src/handler.rs +++ b/testing/ef_tests/src/handler.rs @@ -350,7 +350,7 @@ where self.supported_forks.contains(&fork_name) } - fn is_enabled_for_feature(&self, feature_name: FeatureName) -> bool { + fn is_enabled_for_feature(&self, _feature_name: FeatureName) -> bool { // This ensures we only run the tests **once** for `Eip7594`, using the types matching the // correct fork, e.g. `Eip7594` uses SSZ types from `Deneb` as of spec test version // `v1.5.0-alpha.8`, therefore the `Eip7594` tests should get included when testing Deneb types. @@ -362,8 +362,11 @@ where // SszStaticHandler::, MainnetEthSpec>::pre_electra().run(); // SszStaticHandler::, MainnetEthSpec>::electra_only().run(); // ``` + /* TODO(das): re-enable feature_name == FeatureName::Eip7594 && self.supported_forks.contains(&feature_name.fork_name()) + */ + false } } @@ -385,8 +388,10 @@ where BeaconState::::name().into() } - fn is_enabled_for_feature(&self, feature_name: FeatureName) -> bool { - feature_name == FeatureName::Eip7594 + fn is_enabled_for_feature(&self, _feature_name: FeatureName) -> bool { + // TODO(das): re-enable + // feature_name == FeatureName::Eip7594 + false } } @@ -410,8 +415,10 @@ where T::name().into() } - fn is_enabled_for_feature(&self, feature_name: FeatureName) -> bool { - feature_name == FeatureName::Eip7594 + fn is_enabled_for_feature(&self, _feature_name: FeatureName) -> bool { + // TODO(das): re-enable + // feature_name == FeatureName::Eip7594 + false } } @@ -995,8 +1002,10 @@ impl Handler for KzgInclusionMerkleProofValidityHandler bool { - feature_name == FeatureName::Eip7594 + fn is_enabled_for_feature(&self, _feature_name: FeatureName) -> bool { + // TODO(das): re-enable this + // feature_name == FeatureName::Eip7594 + false } } diff --git a/testing/ef_tests/tests/tests.rs b/testing/ef_tests/tests/tests.rs index 691d27951a..7c268123fa 100644 --- a/testing/ef_tests/tests/tests.rs +++ b/testing/ef_tests/tests/tests.rs @@ -237,9 +237,7 @@ macro_rules! ssz_static_test_no_run { #[cfg(feature = "fake_crypto")] mod ssz_static { - use ef_tests::{ - FeatureName, Handler, SszStaticHandler, SszStaticTHCHandler, SszStaticWithSpecHandler, - }; + use ef_tests::{Handler, SszStaticHandler, SszStaticTHCHandler, SszStaticWithSpecHandler}; use types::historical_summary::HistoricalSummary; use types::{ AttesterSlashingBase, AttesterSlashingElectra, ConsolidationRequest, DepositRequest, @@ -624,6 +622,7 @@ mod ssz_static { SszStaticHandler::::capella_and_later().run(); } + /* FIXME(das): re-enable #[test] fn data_column_sidecar() { SszStaticHandler::, MinimalEthSpec>::deneb_only() @@ -639,6 +638,7 @@ mod ssz_static { SszStaticHandler::::deneb_only() .run_for_feature(FeatureName::Eip7594); } + */ #[test] fn consolidation() { @@ -899,6 +899,7 @@ fn kzg_verify_kzg_proof() { KZGVerifyKZGProofHandler::::default().run(); } +/* FIXME(das): re-enable these tests #[test] fn kzg_compute_cells_and_proofs() { KZGComputeCellsAndKZGProofHandler::::default() @@ -916,6 +917,7 @@ fn kzg_recover_cells_and_proofs() { KZGRecoverCellsAndKZGProofHandler::::default() .run_for_feature(FeatureName::Eip7594); } +*/ #[test] fn beacon_state_merkle_proof_validity() { @@ -947,8 +949,10 @@ fn rewards() { } } +/* FIXME(das): re-enable these tests #[test] fn get_custody_columns() { GetCustodyColumnsHandler::::default().run_for_feature(FeatureName::Eip7594); GetCustodyColumnsHandler::::default().run_for_feature(FeatureName::Eip7594); } +*/ From 93f9c2c7183b6b59cbb02355e2379490fee81d94 Mon Sep 17 00:00:00 2001 From: Pawan Dhananjay Date: Tue, 14 Jan 2025 07:36:27 +0530 Subject: [PATCH 073/254] Execution requests with prefix (#6801) * Exclude empty requests and add back prefix * cleanup * fix after rebase --- .../src/engine_api/json_structures.rs | 89 +++++++++++++------ consensus/types/src/execution_requests.rs | 45 +++++++--- consensus/types/src/lib.rs | 2 +- 3 files changed, 99 insertions(+), 37 deletions(-) diff --git a/beacon_node/execution_layer/src/engine_api/json_structures.rs b/beacon_node/execution_layer/src/engine_api/json_structures.rs index 86acaaaf3b..95b4b50925 100644 --- a/beacon_node/execution_layer/src/engine_api/json_structures.rs +++ b/beacon_node/execution_layer/src/engine_api/json_structures.rs @@ -7,7 +7,7 @@ use superstruct::superstruct; use types::beacon_block_body::KzgCommitments; use types::blob_sidecar::BlobsList; use types::execution_requests::{ - ConsolidationRequests, DepositRequests, RequestPrefix, WithdrawalRequests, + ConsolidationRequests, DepositRequests, RequestType, WithdrawalRequests, }; use types::{Blob, FixedVector, KzgProof, Unsigned}; @@ -401,47 +401,80 @@ impl From> for ExecutionPayload { } } +#[derive(Debug, Clone)] +pub enum RequestsError { + InvalidHex(hex::FromHexError), + EmptyRequest(usize), + InvalidOrdering, + InvalidPrefix(u8), + DecodeError(String), +} + /// Format of `ExecutionRequests` received over the engine api. /// -/// Array of ssz-encoded requests list encoded as hex bytes. -/// The prefix of the request type is used to index into the array. -/// -/// For e.g. [0xab, 0xcd, 0xef] -/// Here, 0xab are the deposits bytes (prefix and index == 0) -/// 0xcd are the withdrawals bytes (prefix and index == 1) -/// 0xef are the consolidations bytes (prefix and index == 2) +/// Array of ssz-encoded requests list encoded as hex bytes prefixed +/// with a `RequestType` #[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] #[serde(transparent)] pub struct JsonExecutionRequests(pub Vec); impl TryFrom for ExecutionRequests { - type Error = String; + type Error = RequestsError; fn try_from(value: JsonExecutionRequests) -> Result { let mut requests = ExecutionRequests::default(); - + let mut prev_prefix: Option = None; for (i, request) in value.0.into_iter().enumerate() { // hex string let decoded_bytes = hex::decode(request.strip_prefix("0x").unwrap_or(&request)) - .map_err(|e| format!("Invalid hex {:?}", e))?; - match RequestPrefix::from_prefix(i as u8) { - Some(RequestPrefix::Deposit) => { - requests.deposits = DepositRequests::::from_ssz_bytes(&decoded_bytes) - .map_err(|e| format!("Failed to decode DepositRequest from EL: {:?}", e))?; + .map_err(RequestsError::InvalidHex)?; + + // The first byte of each element is the `request_type` and the remaining bytes are the `request_data`. + // Elements with empty `request_data` **MUST** be excluded from the list. + let Some((prefix_byte, request_bytes)) = decoded_bytes.split_first() else { + return Err(RequestsError::EmptyRequest(i)); + }; + if request_bytes.is_empty() { + return Err(RequestsError::EmptyRequest(i)); + } + // Elements of the list **MUST** be ordered by `request_type` in ascending order + let current_prefix = RequestType::from_u8(*prefix_byte) + .ok_or(RequestsError::InvalidPrefix(*prefix_byte))?; + if let Some(prev) = prev_prefix { + if prev.to_u8() >= current_prefix.to_u8() { + return Err(RequestsError::InvalidOrdering); } - Some(RequestPrefix::Withdrawal) => { - requests.withdrawals = WithdrawalRequests::::from_ssz_bytes(&decoded_bytes) + } + prev_prefix = Some(current_prefix); + + match current_prefix { + RequestType::Deposit => { + requests.deposits = DepositRequests::::from_ssz_bytes(request_bytes) .map_err(|e| { - format!("Failed to decode WithdrawalRequest from EL: {:?}", e) + RequestsError::DecodeError(format!( + "Failed to decode DepositRequest from EL: {:?}", + e + )) })?; } - Some(RequestPrefix::Consolidation) => { - requests.consolidations = - ConsolidationRequests::::from_ssz_bytes(&decoded_bytes).map_err( - |e| format!("Failed to decode ConsolidationRequest from EL: {:?}", e), - )?; + RequestType::Withdrawal => { + requests.withdrawals = WithdrawalRequests::::from_ssz_bytes(request_bytes) + .map_err(|e| { + RequestsError::DecodeError(format!( + "Failed to decode WithdrawalRequest from EL: {:?}", + e + )) + })?; + } + RequestType::Consolidation => { + requests.consolidations = + ConsolidationRequests::::from_ssz_bytes(request_bytes).map_err(|e| { + RequestsError::DecodeError(format!( + "Failed to decode ConsolidationRequest from EL: {:?}", + e + )) + })?; } - None => return Err("Empty requests string".to_string()), } } Ok(requests) @@ -510,7 +543,9 @@ impl TryFrom> for GetPayloadResponse { block_value: response.block_value, blobs_bundle: response.blobs_bundle.into(), should_override_builder: response.should_override_builder, - requests: response.execution_requests.try_into()?, + requests: response.execution_requests.try_into().map_err(|e| { + format!("Failed to convert json to execution requests : {:?}", e) + })?, })) } JsonGetPayloadResponse::V5(response) => { @@ -519,7 +554,9 @@ impl TryFrom> for GetPayloadResponse { block_value: response.block_value, blobs_bundle: response.blobs_bundle.into(), should_override_builder: response.should_override_builder, - requests: response.execution_requests.try_into()?, + requests: response.execution_requests.try_into().map_err(|e| { + format!("Failed to convert json to execution requests {:?}", e) + })?, })) } } diff --git a/consensus/types/src/execution_requests.rs b/consensus/types/src/execution_requests.rs index 96a3905420..223c6444cc 100644 --- a/consensus/types/src/execution_requests.rs +++ b/consensus/types/src/execution_requests.rs @@ -43,10 +43,29 @@ impl ExecutionRequests { /// Returns the encoding according to EIP-7685 to send /// to the execution layer over the engine api. pub fn get_execution_requests_list(&self) -> Vec { - let deposit_bytes = Bytes::from(self.deposits.as_ssz_bytes()); - let withdrawal_bytes = Bytes::from(self.withdrawals.as_ssz_bytes()); - let consolidation_bytes = Bytes::from(self.consolidations.as_ssz_bytes()); - vec![deposit_bytes, withdrawal_bytes, consolidation_bytes] + let mut requests_list = Vec::new(); + if !self.deposits.is_empty() { + requests_list.push(Bytes::from_iter( + [RequestType::Deposit.to_u8()] + .into_iter() + .chain(self.deposits.as_ssz_bytes()), + )); + } + if !self.withdrawals.is_empty() { + requests_list.push(Bytes::from_iter( + [RequestType::Withdrawal.to_u8()] + .into_iter() + .chain(self.withdrawals.as_ssz_bytes()), + )); + } + if !self.consolidations.is_empty() { + requests_list.push(Bytes::from_iter( + [RequestType::Consolidation.to_u8()] + .into_iter() + .chain(self.consolidations.as_ssz_bytes()), + )); + } + requests_list } /// Generate the execution layer `requests_hash` based on EIP-7685. @@ -55,9 +74,8 @@ impl ExecutionRequests { pub fn requests_hash(&self) -> Hash256 { let mut hasher = DynamicContext::new(); - for (i, request) in self.get_execution_requests_list().iter().enumerate() { + for request in self.get_execution_requests_list().iter() { let mut request_hasher = DynamicContext::new(); - request_hasher.update(&[i as u8]); request_hasher.update(request); let request_hash = request_hasher.finalize(); @@ -68,16 +86,16 @@ impl ExecutionRequests { } } -/// This is used to index into the `execution_requests` array. +/// The prefix types for `ExecutionRequest` objects. #[derive(Debug, Copy, Clone)] -pub enum RequestPrefix { +pub enum RequestType { Deposit, Withdrawal, Consolidation, } -impl RequestPrefix { - pub fn from_prefix(prefix: u8) -> Option { +impl RequestType { + pub fn from_u8(prefix: u8) -> Option { match prefix { 0 => Some(Self::Deposit), 1 => Some(Self::Withdrawal), @@ -85,6 +103,13 @@ impl RequestPrefix { _ => None, } } + pub fn to_u8(&self) -> u8 { + match self { + Self::Deposit => 0, + Self::Withdrawal => 1, + Self::Consolidation => 2, + } + } } #[cfg(test)] diff --git a/consensus/types/src/lib.rs b/consensus/types/src/lib.rs index 54d8bf51b6..76e414b2f1 100644 --- a/consensus/types/src/lib.rs +++ b/consensus/types/src/lib.rs @@ -172,7 +172,7 @@ pub use crate::execution_payload_header::{ ExecutionPayloadHeaderDeneb, ExecutionPayloadHeaderElectra, ExecutionPayloadHeaderFulu, ExecutionPayloadHeaderRef, ExecutionPayloadHeaderRefMut, }; -pub use crate::execution_requests::{ExecutionRequests, RequestPrefix}; +pub use crate::execution_requests::{ExecutionRequests, RequestType}; pub use crate::fork::Fork; pub use crate::fork_context::ForkContext; pub use crate::fork_data::ForkData; From 587c3e2b8c293787b1b5a7d3b38628a8e4488c49 Mon Sep 17 00:00:00 2001 From: Pawan Dhananjay Date: Tue, 14 Jan 2025 11:52:30 +0530 Subject: [PATCH 074/254] Implement changes for EIP 7691 (#6803) * Add new config options * Use electra_enabled --- .../lighthouse_network/src/service/mod.rs | 13 ++--- .../lighthouse_network/src/service/utils.rs | 15 +++--- .../lighthouse_network/src/types/mod.rs | 2 +- .../lighthouse_network/src/types/topics.rs | 31 +++++++---- consensus/types/src/chain_spec.rs | 52 +++++++++++++++++-- 5 files changed, 85 insertions(+), 28 deletions(-) diff --git a/beacon_node/lighthouse_network/src/service/mod.rs b/beacon_node/lighthouse_network/src/service/mod.rs index afcbfce173..999803b8fe 100644 --- a/beacon_node/lighthouse_network/src/service/mod.rs +++ b/beacon_node/lighthouse_network/src/service/mod.rs @@ -16,7 +16,7 @@ use crate::rpc::{ use crate::types::{ attestation_sync_committee_topics, fork_core_topics, subnet_from_topic_hash, GossipEncoding, GossipKind, GossipTopic, SnappyTransform, Subnet, SubnetDiscovery, ALTAIR_CORE_TOPICS, - BASE_CORE_TOPICS, CAPELLA_CORE_TOPICS, DENEB_CORE_TOPICS, LIGHT_CLIENT_GOSSIP_TOPICS, + BASE_CORE_TOPICS, CAPELLA_CORE_TOPICS, LIGHT_CLIENT_GOSSIP_TOPICS, }; use crate::EnrExt; use crate::Eth2Enr; @@ -285,26 +285,23 @@ 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.blob_sidecar_subnet_count_electra as usize + ctx.chain_spec.data_column_sidecar_subnet_count as usize + BASE_CORE_TOPICS.len() + ALTAIR_CORE_TOPICS.len() - + CAPELLA_CORE_TOPICS.len() - + DENEB_CORE_TOPICS.len() + + CAPELLA_CORE_TOPICS.len() // 0 core deneb and electra topics + LIGHT_CLIENT_GOSSIP_TOPICS.len(); let possible_fork_digests = ctx.fork_context.all_fork_digests(); let filter = gossipsub::MaxCountSubscriptionFilter { filter: utils::create_whitelist_filter( possible_fork_digests, - ctx.chain_spec.attestation_subnet_count, + &ctx.chain_spec, 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, - // 418 in theory = (64 attestation + 4 sync committee + 7 core topics + 6 blob topics + 128 column topics) * 2 + // 424 in theory = (64 attestation + 4 sync committee + 7 core topics + 9 blob topics + 128 column topics) * 2 max_subscriptions_per_request: max_topics * 2, }; diff --git a/beacon_node/lighthouse_network/src/service/utils.rs b/beacon_node/lighthouse_network/src/service/utils.rs index 490928c08c..a9eaa002ff 100644 --- a/beacon_node/lighthouse_network/src/service/utils.rs +++ b/beacon_node/lighthouse_network/src/service/utils.rs @@ -236,10 +236,8 @@ pub fn load_or_build_metadata( /// possible fork digests. pub(crate) fn create_whitelist_filter( possible_fork_digests: Vec<[u8; 4]>, - attestation_subnet_count: u64, + spec: &ChainSpec, sync_committee_subnet_count: u64, - blob_sidecar_subnet_count: u64, - data_column_sidecar_subnet_count: u64, ) -> gossipsub::WhitelistSubscriptionFilter { let mut possible_hashes = HashSet::new(); for fork_digest in possible_fork_digests { @@ -259,16 +257,21 @@ pub(crate) fn create_whitelist_filter( add(BlsToExecutionChange); add(LightClientFinalityUpdate); add(LightClientOptimisticUpdate); - for id in 0..attestation_subnet_count { + for id in 0..spec.attestation_subnet_count { add(Attestation(SubnetId::new(id))); } for id in 0..sync_committee_subnet_count { add(SyncCommitteeMessage(SyncSubnetId::new(id))); } - for id in 0..blob_sidecar_subnet_count { + let blob_subnet_count = if spec.electra_fork_epoch.is_some() { + spec.blob_sidecar_subnet_count_electra + } else { + spec.blob_sidecar_subnet_count + }; + for id in 0..blob_subnet_count { add(BlobSidecar(id)); } - for id in 0..data_column_sidecar_subnet_count { + for id in 0..spec.data_column_sidecar_subnet_count { add(DataColumnSidecar(DataColumnSubnetId::new(id))); } } diff --git a/beacon_node/lighthouse_network/src/types/mod.rs b/beacon_node/lighthouse_network/src/types/mod.rs index 6f266fd2ba..a1eedaef74 100644 --- a/beacon_node/lighthouse_network/src/types/mod.rs +++ b/beacon_node/lighthouse_network/src/types/mod.rs @@ -18,5 +18,5 @@ pub use sync_state::{BackFillState, SyncState}; pub use topics::{ attestation_sync_committee_topics, core_topics_to_subscribe, fork_core_topics, subnet_from_topic_hash, GossipEncoding, GossipKind, GossipTopic, ALTAIR_CORE_TOPICS, - BASE_CORE_TOPICS, CAPELLA_CORE_TOPICS, DENEB_CORE_TOPICS, LIGHT_CLIENT_GOSSIP_TOPICS, + BASE_CORE_TOPICS, CAPELLA_CORE_TOPICS, LIGHT_CLIENT_GOSSIP_TOPICS, }; diff --git a/beacon_node/lighthouse_network/src/types/topics.rs b/beacon_node/lighthouse_network/src/types/topics.rs index 8cdecc6bfa..475b459ccb 100644 --- a/beacon_node/lighthouse_network/src/types/topics.rs +++ b/beacon_node/lighthouse_network/src/types/topics.rs @@ -41,8 +41,6 @@ pub const LIGHT_CLIENT_GOSSIP_TOPICS: [GossipKind; 2] = [ GossipKind::LightClientOptimisticUpdate, ]; -pub const DENEB_CORE_TOPICS: [GossipKind; 0] = []; - /// Returns the core topics associated with each fork that are new to the previous fork pub fn fork_core_topics(fork_name: &ForkName, spec: &ChainSpec) -> Vec { match fork_name { @@ -56,11 +54,16 @@ pub fn fork_core_topics(fork_name: &ForkName, spec: &ChainSpec) -> V for i in 0..spec.blob_sidecar_subnet_count { deneb_blob_topics.push(GossipKind::BlobSidecar(i)); } - let mut deneb_topics = DENEB_CORE_TOPICS.to_vec(); - deneb_topics.append(&mut deneb_blob_topics); - deneb_topics + deneb_blob_topics + } + ForkName::Electra => { + // All of electra blob topics are core topics + let mut electra_blob_topics = Vec::new(); + for i in 0..spec.blob_sidecar_subnet_count_electra { + electra_blob_topics.push(GossipKind::BlobSidecar(i)); + } + electra_blob_topics } - ForkName::Electra => vec![], ForkName::Fulu => vec![], } } @@ -88,7 +91,12 @@ pub fn core_topics_to_subscribe( topics.extend(previous_fork_topics); current_fork = previous_fork; } + // Remove duplicates topics + .into_iter() + .collect::>() + .into_iter() + .collect() } /// A gossipsub topic which encapsulates the type of messages that should be sent and received over @@ -467,16 +475,19 @@ mod tests { type E = MainnetEthSpec; let spec = E::default_spec(); let mut all_topics = Vec::new(); + let mut electra_core_topics = fork_core_topics::(&ForkName::Electra, &spec); let mut deneb_core_topics = fork_core_topics::(&ForkName::Deneb, &spec); + all_topics.append(&mut electra_core_topics); all_topics.append(&mut deneb_core_topics); all_topics.extend(CAPELLA_CORE_TOPICS); all_topics.extend(ALTAIR_CORE_TOPICS); all_topics.extend(BASE_CORE_TOPICS); let latest_fork = *ForkName::list_all().last().unwrap(); - assert_eq!( - core_topics_to_subscribe::(latest_fork, &spec), - all_topics - ); + let core_topics = core_topics_to_subscribe::(latest_fork, &spec); + // Need to check all the topics exist in an order independent manner + for topic in all_topics { + assert!(core_topics.contains(&topic)); + } } } diff --git a/consensus/types/src/chain_spec.rs b/consensus/types/src/chain_spec.rs index ea4d8641f6..6594f3c44e 100644 --- a/consensus/types/src/chain_spec.rs +++ b/consensus/types/src/chain_spec.rs @@ -191,7 +191,6 @@ pub struct ChainSpec { pub max_pending_partials_per_withdrawals_sweep: u64, pub min_per_epoch_churn_limit_electra: u64, pub max_per_epoch_activation_exit_churn_limit: u64, - pub max_blobs_per_block_electra: u64, /* * Fulu hard fork params @@ -240,6 +239,13 @@ pub struct ChainSpec { pub blob_sidecar_subnet_count: u64, max_blobs_per_block: u64, + /* + * Networking Electra + */ + max_blobs_per_block_electra: u64, + pub blob_sidecar_subnet_count_electra: u64, + pub max_request_blob_sidecars_electra: u64, + /* * Networking Derived * @@ -618,6 +624,14 @@ impl ChainSpec { } } + pub fn max_request_blob_sidecars(&self, fork_name: ForkName) -> usize { + if fork_name.electra_enabled() { + self.max_request_blob_sidecars_electra as usize + } else { + self.max_request_blob_sidecars as usize + } + } + /// Return the value of `MAX_BLOBS_PER_BLOCK` appropriate for the fork at `epoch`. pub fn max_blobs_per_block(&self, epoch: Epoch) -> u64 { self.max_blobs_per_block_by_fork(self.fork_name_at_epoch(epoch)) @@ -830,7 +844,6 @@ impl ChainSpec { u64::checked_pow(2, 8)?.checked_mul(u64::checked_pow(10, 9)?) }) .expect("calculation does not overflow"), - max_blobs_per_block_electra: default_max_blobs_per_block_electra(), /* * Fulu hard fork params @@ -886,6 +899,13 @@ impl ChainSpec { max_blobs_by_root_request: default_max_blobs_by_root_request(), max_data_columns_by_root_request: default_data_columns_by_root_request(), + /* + * Networking Electra specific + */ + max_blobs_per_block_electra: default_max_blobs_per_block_electra(), + blob_sidecar_subnet_count_electra: default_blob_sidecar_subnet_count_electra(), + max_request_blob_sidecars_electra: default_max_request_blob_sidecars_electra(), + /* * Application specific */ @@ -1161,7 +1181,6 @@ impl ChainSpec { u64::checked_pow(2, 8)?.checked_mul(u64::checked_pow(10, 9)?) }) .expect("calculation does not overflow"), - max_blobs_per_block_electra: default_max_blobs_per_block_electra(), /* * Fulu hard fork params @@ -1216,6 +1235,13 @@ impl ChainSpec { max_blobs_by_root_request: default_max_blobs_by_root_request(), max_data_columns_by_root_request: default_data_columns_by_root_request(), + /* + * Networking Electra specific + */ + max_blobs_per_block_electra: default_max_blobs_per_block_electra(), + blob_sidecar_subnet_count_electra: default_blob_sidecar_subnet_count_electra(), + max_request_blob_sidecars_electra: default_max_request_blob_sidecars_electra(), + /* * Application specific */ @@ -1421,6 +1447,12 @@ pub struct Config { #[serde(default = "default_max_blobs_per_block_electra")] #[serde(with = "serde_utils::quoted_u64")] max_blobs_per_block_electra: u64, + #[serde(default = "default_blob_sidecar_subnet_count_electra")] + #[serde(with = "serde_utils::quoted_u64")] + pub blob_sidecar_subnet_count_electra: u64, + #[serde(default = "default_max_request_blob_sidecars_electra")] + #[serde(with = "serde_utils::quoted_u64")] + max_request_blob_sidecars_electra: u64, #[serde(default = "default_custody_requirement")] #[serde(with = "serde_utils::quoted_u64")] @@ -1555,6 +1587,14 @@ const fn default_max_blobs_per_block() -> u64 { 6 } +const fn default_blob_sidecar_subnet_count_electra() -> u64 { + 9 +} + +const fn default_max_request_blob_sidecars_electra() -> u64 { + 1152 +} + const fn default_min_per_epoch_churn_limit_electra() -> u64 { 128_000_000_000 } @@ -1787,6 +1827,8 @@ impl Config { max_per_epoch_activation_exit_churn_limit: spec .max_per_epoch_activation_exit_churn_limit, max_blobs_per_block_electra: spec.max_blobs_per_block_electra, + blob_sidecar_subnet_count_electra: spec.blob_sidecar_subnet_count_electra, + max_request_blob_sidecars_electra: spec.max_request_blob_sidecars_electra, custody_requirement: spec.custody_requirement, data_column_sidecar_subnet_count: spec.data_column_sidecar_subnet_count, @@ -1865,6 +1907,8 @@ impl Config { min_per_epoch_churn_limit_electra, max_per_epoch_activation_exit_churn_limit, max_blobs_per_block_electra, + blob_sidecar_subnet_count_electra, + max_request_blob_sidecars_electra, custody_requirement, data_column_sidecar_subnet_count, number_of_columns, @@ -1935,6 +1979,8 @@ impl Config { min_per_epoch_churn_limit_electra, max_per_epoch_activation_exit_churn_limit, max_blobs_per_block_electra, + max_request_blob_sidecars_electra, + blob_sidecar_subnet_count_electra, // 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), From 4fd8e521a4245ec7bb729895f103d3ca5083927b Mon Sep 17 00:00:00 2001 From: jking-aus <72330194+jking-aus@users.noreply.github.com> Date: Wed, 15 Jan 2025 14:53:40 +1100 Subject: [PATCH 075/254] Use existing peer count metrics loop to check for open_nat toggle (#6800) * implement update_nat_open function in network_behaviour for tracking incoming peers below a given threshold count * implement update_nat_open function in network_behaviour for tracking incoming peers below a given threshold count * tidy logic and comments * move logic to existing metrics loop * revert change to network_behaviour protocol check * clippy * clippy matches! macro * pull nat_open check outside of peercounting loop * missing close bracket * make threshold const --- .../src/peer_manager/mod.rs | 33 +++++++++++++++++-- .../src/peer_manager/peerdb/peer_info.rs | 18 ++++++++++ 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/beacon_node/lighthouse_network/src/peer_manager/mod.rs b/beacon_node/lighthouse_network/src/peer_manager/mod.rs index 4df2566dac..6502a8dbff 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/mod.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/mod.rs @@ -63,6 +63,8 @@ pub const MIN_OUTBOUND_ONLY_FACTOR: f32 = 0.2; /// limit is 55, and we are at 55 peers, the following parameter provisions a few more slots of /// dialing priority peers we need for validator duties. pub const PRIORITY_PEER_EXCESS: f32 = 0.2; +/// The numbre of inbound libp2p peers we have seen before we consider our NAT to be open. +pub const LIBP2P_NAT_OPEN_THRESHOLD: usize = 3; /// The main struct that handles peer's reputation and connection status. pub struct PeerManager { @@ -1307,7 +1309,9 @@ impl PeerManager { fn update_peer_count_metrics(&self) { let mut peers_connected = 0; let mut clients_per_peer = HashMap::new(); - let mut peers_connected_mutli: HashMap<(&str, &str), i32> = HashMap::new(); + let mut inbound_ipv4_peers_connected: usize = 0; + let mut inbound_ipv6_peers_connected: usize = 0; + let mut peers_connected_multi: HashMap<(&str, &str), i32> = HashMap::new(); let mut peers_per_custody_subnet_count: HashMap = HashMap::new(); for (_, peer_info) in self.network_globals.peers.read().connected_peers() { @@ -1336,7 +1340,7 @@ impl PeerManager { }) }) .unwrap_or("unknown"); - *peers_connected_mutli + *peers_connected_multi .entry((direction, transport)) .or_default() += 1; @@ -1345,6 +1349,29 @@ impl PeerManager { .entry(meta_data.custody_subnet_count) .or_default() += 1; } + // Check if incoming peer is ipv4 + if peer_info.is_incoming_ipv4_connection() { + inbound_ipv4_peers_connected += 1; + } + + // Check if incoming peer is ipv6 + if peer_info.is_incoming_ipv6_connection() { + inbound_ipv6_peers_connected += 1; + } + } + + // Set ipv4 nat_open metric flag if threshold of peercount is met, unset if below threshold + if inbound_ipv4_peers_connected >= LIBP2P_NAT_OPEN_THRESHOLD { + metrics::set_gauge_vec(&metrics::NAT_OPEN, &["libp2p_ipv4"], 1); + } else { + metrics::set_gauge_vec(&metrics::NAT_OPEN, &["libp2p_ipv4"], 0); + } + + // Set ipv6 nat_open metric flag if threshold of peercount is met, unset if below threshold + if inbound_ipv6_peers_connected >= LIBP2P_NAT_OPEN_THRESHOLD { + metrics::set_gauge_vec(&metrics::NAT_OPEN, &["libp2p_ipv6"], 1); + } else { + metrics::set_gauge_vec(&metrics::NAT_OPEN, &["libp2p_ipv6"], 0); } // PEERS_CONNECTED @@ -1375,7 +1402,7 @@ impl PeerManager { metrics::set_gauge_vec( &metrics::PEERS_CONNECTED_MULTI, &[direction, transport], - *peers_connected_mutli + *peers_connected_multi .get(&(direction, transport)) .unwrap_or(&0) as i64, ); 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 27c8463a55..d8b3568a28 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 @@ -122,6 +122,24 @@ impl PeerInfo { self.connection_direction.as_ref() } + /// Returns true if this is an incoming ipv4 connection. + pub fn is_incoming_ipv4_connection(&self) -> bool { + self.seen_multiaddrs.iter().any(|multiaddr| { + multiaddr + .iter() + .any(|protocol| matches!(protocol, libp2p::core::multiaddr::Protocol::Ip4(_))) + }) + } + + /// Returns true if this is an incoming ipv6 connection. + pub fn is_incoming_ipv6_connection(&self) -> bool { + self.seen_multiaddrs.iter().any(|multiaddr| { + multiaddr + .iter() + .any(|protocol| matches!(protocol, libp2p::core::multiaddr::Protocol::Ip6(_))) + }) + } + /// Returns the sync status of the peer. pub fn sync_status(&self) -> &SyncStatus { &self.sync_status From dd7591f7123dfe072631c0deb0abc1b78cc82733 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Wed, 15 Jan 2025 17:56:51 +1100 Subject: [PATCH 076/254] Fix data columns not persisting for PeerDAS due to a `getBlobs` race condition (#6756) * Fix data columns not persisting for PeerDAS due to a `getBlobs` race condition. * Refactor blobs and columns logic in `chain.import_block` for clarity. Add more docs on `data_column_recv`. * Add more code comments for clarity. * Merge remote-tracking branch 'origin/unstable' into fix-column-race # Conflicts: # beacon_node/beacon_chain/src/block_verification_types.rs # beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs * Fix lint. --- beacon_node/beacon_chain/src/beacon_chain.rs | 181 ++++++++++-------- .../beacon_chain/src/block_verification.rs | 1 + .../src/block_verification_types.rs | 15 +- .../src/data_availability_checker.rs | 14 +- .../overflow_lru_cache.rs | 71 +++++-- .../state_lru_cache.rs | 1 + beacon_node/beacon_chain/src/fetch_blobs.rs | 8 +- .../beacon_chain/tests/block_verification.rs | 2 +- 8 files changed, 188 insertions(+), 105 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 81783267ba..a6da610c0e 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -121,7 +121,7 @@ use store::{ KeyValueStoreOp, StoreItem, StoreOp, }; use task_executor::{ShutdownReason, TaskExecutor}; -use tokio::sync::mpsc::Receiver; +use tokio::sync::oneshot; use tokio_stream::Stream; use tree_hash::TreeHash; use types::blob_sidecar::FixedBlobSidecarList; @@ -3088,7 +3088,7 @@ impl BeaconChain { slot: Slot, block_root: Hash256, blobs: FixedBlobSidecarList, - data_column_recv: Option>>, + data_column_recv: Option>>, ) -> 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. @@ -3216,7 +3216,7 @@ impl BeaconChain { }; let r = self - .process_availability(slot, availability, None, || Ok(())) + .process_availability(slot, availability, || Ok(())) .await; self.remove_notified(&block_root, r) .map(|availability_processing_status| { @@ -3344,7 +3344,7 @@ impl BeaconChain { match executed_block { ExecutedBlock::Available(block) => { - self.import_available_block(Box::new(block), None).await + self.import_available_block(Box::new(block)).await } ExecutedBlock::AvailabilityPending(block) => { self.check_block_availability_and_import(block).await @@ -3476,7 +3476,7 @@ impl BeaconChain { let availability = self .data_availability_checker .put_pending_executed_block(block)?; - self.process_availability(slot, availability, None, || Ok(())) + self.process_availability(slot, availability, || Ok(())) .await } @@ -3492,7 +3492,7 @@ impl BeaconChain { } let availability = self.data_availability_checker.put_gossip_blob(blob)?; - self.process_availability(slot, availability, None, || Ok(())) + self.process_availability(slot, availability, || Ok(())) .await } @@ -3515,7 +3515,7 @@ impl BeaconChain { .data_availability_checker .put_gossip_data_columns(block_root, data_columns)?; - self.process_availability(slot, availability, None, publish_fn) + self.process_availability(slot, availability, publish_fn) .await } @@ -3559,7 +3559,7 @@ impl BeaconChain { .data_availability_checker .put_rpc_blobs(block_root, blobs)?; - self.process_availability(slot, availability, None, || Ok(())) + self.process_availability(slot, availability, || Ok(())) .await } @@ -3568,14 +3568,14 @@ impl BeaconChain { slot: Slot, block_root: Hash256, blobs: FixedBlobSidecarList, - data_column_recv: Option>>, + data_column_recv: Option>>, ) -> Result { self.check_blobs_for_slashability(block_root, &blobs)?; - let availability = self - .data_availability_checker - .put_engine_blobs(block_root, blobs)?; + let availability = + self.data_availability_checker + .put_engine_blobs(block_root, blobs, data_column_recv)?; - self.process_availability(slot, availability, data_column_recv, || Ok(())) + self.process_availability(slot, availability, || Ok(())) .await } @@ -3615,7 +3615,7 @@ impl BeaconChain { .data_availability_checker .put_rpc_custody_columns(block_root, custody_columns)?; - self.process_availability(slot, availability, None, || Ok(())) + self.process_availability(slot, availability, || Ok(())) .await } @@ -3627,14 +3627,13 @@ impl BeaconChain { self: &Arc, slot: Slot, availability: Availability, - recv: Option>>, publish_fn: impl FnOnce() -> Result<(), BlockError>, ) -> Result { match availability { Availability::Available(block) => { publish_fn()?; // Block is fully available, import into fork choice - self.import_available_block(block, recv).await + self.import_available_block(block).await } Availability::MissingComponents(block_root) => Ok( AvailabilityProcessingStatus::MissingComponents(slot, block_root), @@ -3645,7 +3644,6 @@ impl BeaconChain { pub async fn import_available_block( self: &Arc, block: Box>, - data_column_recv: Option>>, ) -> Result { let AvailableExecutedBlock { block, @@ -3660,6 +3658,7 @@ impl BeaconChain { parent_eth1_finalization_data, confirmed_state_roots, consensus_context, + data_column_recv, } = import_data; // Record the time at which this block's blobs became available. @@ -3726,7 +3725,7 @@ impl BeaconChain { parent_block: SignedBlindedBeaconBlock, parent_eth1_finalization_data: Eth1FinalizationData, mut consensus_context: ConsensusContext, - data_column_recv: Option>>, + data_column_recv: Option>>, ) -> Result { // ----------------------------- BLOCK NOT YET ATTESTABLE ---------------------------------- // Everything in this initial section is on the hot path between processing the block and @@ -3894,44 +3893,32 @@ impl BeaconChain { // end up with blocks in fork choice that are missing from disk. // See https://github.com/sigp/lighthouse/issues/2028 let (_, signed_block, blobs, data_columns) = signed_block.deconstruct(); - // TODO(das) we currently store all subnet sampled columns. Tracking issue to exclude non - // custody columns: https://github.com/sigp/lighthouse/issues/6465 - let custody_columns_count = self.data_availability_checker.get_sampling_column_count(); - // if block is made available via blobs, dropped the data columns. - let data_columns = data_columns.filter(|columns| columns.len() == custody_columns_count); - let data_columns = match (data_columns, data_column_recv) { - // If the block was made available via custody columns received from gossip / rpc, use them - // since we already have them. - (Some(columns), _) => Some(columns), - // Otherwise, it means blobs were likely available via fetching from EL, in this case we - // wait for the data columns to be computed (blocking). - (None, Some(mut data_column_recv)) => { - let _column_recv_timer = - metrics::start_timer(&metrics::BLOCK_PROCESSING_DATA_COLUMNS_WAIT); - // Unable to receive data columns from sender, sender is either dropped or - // failed to compute data columns from blobs. We restore fork choice here and - // return to avoid inconsistency in database. - if let Some(columns) = data_column_recv.blocking_recv() { - Some(columns) - } else { - let err_msg = "Did not receive data columns from sender"; - error!( - self.log, - "Failed to store data columns into the database"; - "msg" => "Restoring fork choice from disk", - "error" => err_msg, - ); - return Err(self - .handle_import_block_db_write_error(fork_choice) - .err() - .unwrap_or(BlockError::InternalError(err_msg.to_string()))); - } + match self.get_blobs_or_columns_store_op( + block_root, + signed_block.epoch(), + blobs, + data_columns, + data_column_recv, + ) { + Ok(Some(blobs_or_columns_store_op)) => { + ops.push(blobs_or_columns_store_op); } - // No data columns present and compute data columns task was not spawned. - // Could either be no blobs in the block or before PeerDAS activation. - (None, None) => None, - }; + Ok(None) => {} + Err(e) => { + error!( + self.log, + "Failed to store data columns into the database"; + "msg" => "Restoring fork choice from disk", + "error" => &e, + "block_root" => ?block_root + ); + return Err(self + .handle_import_block_db_write_error(fork_choice) + .err() + .unwrap_or(BlockError::InternalError(e))); + } + } let block = signed_block.message(); let db_write_timer = metrics::start_timer(&metrics::BLOCK_PROCESSING_DB_WRITE); @@ -3943,30 +3930,6 @@ impl BeaconChain { ops.push(StoreOp::PutBlock(block_root, signed_block.clone())); ops.push(StoreOp::PutState(block.state_root(), &state)); - if let Some(blobs) = blobs { - if !blobs.is_empty() { - debug!( - self.log, "Writing blobs to store"; - "block_root" => %block_root, - "count" => blobs.len(), - ); - ops.push(StoreOp::PutBlobs(block_root, blobs)); - } - } - - if let Some(data_columns) = data_columns { - // TODO(das): `available_block includes all sampled columns, but we only need to store - // custody columns. To be clarified in spec. - 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) { @@ -7184,6 +7147,68 @@ impl BeaconChain { reqresp_pre_import_cache_len: self.reqresp_pre_import_cache.read().len(), } } + + fn get_blobs_or_columns_store_op( + &self, + block_root: Hash256, + block_epoch: Epoch, + blobs: Option>, + data_columns: Option>, + data_column_recv: Option>>, + ) -> Result>, String> { + if self.spec.is_peer_das_enabled_for_epoch(block_epoch) { + // TODO(das) we currently store all subnet sampled columns. Tracking issue to exclude non + // custody columns: https://github.com/sigp/lighthouse/issues/6465 + let custody_columns_count = self.data_availability_checker.get_sampling_column_count(); + + let custody_columns_available = data_columns + .as_ref() + .as_ref() + .is_some_and(|columns| columns.len() == custody_columns_count); + + let data_columns_to_persist = if custody_columns_available { + // If the block was made available via custody columns received from gossip / rpc, use them + // since we already have them. + data_columns + } else if let Some(data_column_recv) = data_column_recv { + // Blobs were available from the EL, in this case we wait for the data columns to be computed (blocking). + let _column_recv_timer = + metrics::start_timer(&metrics::BLOCK_PROCESSING_DATA_COLUMNS_WAIT); + // Unable to receive data columns from sender, sender is either dropped or + // failed to compute data columns from blobs. We restore fork choice here and + // return to avoid inconsistency in database. + let computed_data_columns = data_column_recv + .blocking_recv() + .map_err(|e| format!("Did not receive data columns from sender: {e:?}"))?; + Some(computed_data_columns) + } else { + // No blobs in the block. + None + }; + + if let Some(data_columns) = data_columns_to_persist { + if !data_columns.is_empty() { + debug!( + self.log, "Writing data_columns to store"; + "block_root" => %block_root, + "count" => data_columns.len(), + ); + return Ok(Some(StoreOp::PutDataColumns(block_root, data_columns))); + } + } + } else if let Some(blobs) = blobs { + if !blobs.is_empty() { + debug!( + self.log, "Writing blobs to store"; + "block_root" => %block_root, + "count" => blobs.len(), + ); + return Ok(Some(StoreOp::PutBlobs(block_root, blobs))); + } + } + + Ok(None) + } } impl Drop for BeaconChain { diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index ddb7bb614a..315105ac2b 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -1677,6 +1677,7 @@ impl ExecutionPendingBlock { parent_eth1_finalization_data, confirmed_state_roots, consensus_context, + data_column_recv: None, }, payload_verification_handle, }) diff --git a/beacon_node/beacon_chain/src/block_verification_types.rs b/beacon_node/beacon_chain/src/block_verification_types.rs index 0bf3007e9b..b81e728257 100644 --- a/beacon_node/beacon_chain/src/block_verification_types.rs +++ b/beacon_node/beacon_chain/src/block_verification_types.rs @@ -7,10 +7,11 @@ use derivative::Derivative; use state_processing::ConsensusContext; use std::fmt::{Debug, Formatter}; use std::sync::Arc; +use tokio::sync::oneshot; use types::blob_sidecar::BlobIdentifier; use types::{ - BeaconBlockRef, BeaconState, BlindedPayload, BlobSidecarList, ChainSpec, Epoch, EthSpec, - Hash256, RuntimeVariableList, SignedBeaconBlock, SignedBeaconBlockHeader, Slot, + BeaconBlockRef, BeaconState, BlindedPayload, BlobSidecarList, ChainSpec, DataColumnSidecarList, + Epoch, EthSpec, Hash256, RuntimeVariableList, SignedBeaconBlock, SignedBeaconBlockHeader, Slot, }; /// A block that has been received over RPC. It has 2 internal variants: @@ -337,7 +338,8 @@ impl AvailabilityPendingExecutedBlock { } } -#[derive(Debug, PartialEq)] +#[derive(Debug, Derivative)] +#[derivative(PartialEq)] pub struct BlockImportData { pub block_root: Hash256, pub state: BeaconState, @@ -345,6 +347,12 @@ pub struct BlockImportData { pub parent_eth1_finalization_data: Eth1FinalizationData, pub confirmed_state_roots: Vec, pub consensus_context: ConsensusContext, + #[derivative(PartialEq = "ignore")] + /// An optional receiver for `DataColumnSidecarList`. + /// + /// This field is `Some` when data columns are being computed asynchronously. + /// The resulting `DataColumnSidecarList` will be sent through this receiver. + pub data_column_recv: Option>>, } impl BlockImportData { @@ -363,6 +371,7 @@ impl BlockImportData { }, confirmed_state_roots: vec![], consensus_context: ConsensusContext::new(Slot::new(0)), + data_column_recv: None, } } } diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index 4c5152239c..3ac1a95494 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -15,6 +15,7 @@ use std::num::NonZeroUsize; use std::sync::Arc; use std::time::Duration; use task_executor::TaskExecutor; +use tokio::sync::oneshot; use types::blob_sidecar::{BlobIdentifier, BlobSidecar, FixedBlobSidecarList}; use types::{ BlobSidecarList, ChainSpec, DataColumnIdentifier, DataColumnSidecar, DataColumnSidecarList, @@ -223,7 +224,7 @@ impl DataAvailabilityChecker { .map_err(AvailabilityCheckError::InvalidBlobs)?; self.availability_cache - .put_kzg_verified_blobs(block_root, verified_blobs, &self.log) + .put_kzg_verified_blobs(block_root, verified_blobs, None, &self.log) } /// Put a list of custody columns received via RPC into the availability cache. This performs KZG @@ -263,6 +264,7 @@ impl DataAvailabilityChecker { &self, block_root: Hash256, blobs: FixedBlobSidecarList, + data_column_recv: Option>>, ) -> Result, AvailabilityCheckError> { let seen_timestamp = self .slot_clock @@ -272,8 +274,12 @@ impl DataAvailabilityChecker { let verified_blobs = KzgVerifiedBlobList::from_verified(blobs.iter().flatten().cloned(), seen_timestamp); - self.availability_cache - .put_kzg_verified_blobs(block_root, verified_blobs, &self.log) + self.availability_cache.put_kzg_verified_blobs( + block_root, + verified_blobs, + data_column_recv, + &self.log, + ) } /// Check if we've cached other blobs for this block. If it completes a set and we also @@ -288,6 +294,7 @@ impl DataAvailabilityChecker { self.availability_cache.put_kzg_verified_blobs( gossip_blob.block_root(), vec![gossip_blob.into_inner()], + None, &self.log, ) } @@ -803,7 +810,6 @@ impl AvailableBlock { block, blobs, data_columns, - blobs_available_timestamp: _, .. } = self; (block_root, block, blobs, data_columns) 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 44148922f4..a2936206ae 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 @@ -12,28 +12,46 @@ use parking_lot::RwLock; use slog::{debug, Logger}; use std::num::NonZeroUsize; use std::sync::Arc; +use tokio::sync::oneshot; use types::blob_sidecar::BlobIdentifier; use types::{ - BlobSidecar, ChainSpec, ColumnIndex, DataColumnIdentifier, DataColumnSidecar, Epoch, EthSpec, - Hash256, RuntimeFixedVector, RuntimeVariableList, SignedBeaconBlock, + BlobSidecar, ChainSpec, ColumnIndex, DataColumnIdentifier, DataColumnSidecar, + DataColumnSidecarList, Epoch, EthSpec, Hash256, RuntimeFixedVector, RuntimeVariableList, + SignedBeaconBlock, }; /// This represents the components of a partially available block /// /// 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(Clone)] pub struct PendingComponents { pub block_root: Hash256, pub verified_blobs: RuntimeFixedVector>>, pub verified_data_columns: Vec>, pub executed_block: Option>, pub reconstruction_started: bool, + /// Receiver for data columns that are computed asynchronously; + /// + /// If `data_column_recv` is `Some`, it means data column computation or reconstruction has been + /// started. This can happen either via engine blobs fetching or data column reconstruction + /// (triggered when >= 50% columns are received via gossip). + pub data_column_recv: Option>>, } impl PendingComponents { + /// Clones the `PendingComponent` without cloning `data_column_recv`, as `Receiver` is not cloneable. + /// This should only be used when the receiver is no longer needed. + pub fn clone_without_column_recv(&self) -> Self { + PendingComponents { + block_root: self.block_root, + verified_blobs: self.verified_blobs.clone(), + verified_data_columns: self.verified_data_columns.clone(), + executed_block: self.executed_block.clone(), + reconstruction_started: self.reconstruction_started, + data_column_recv: None, + } + } + /// Returns an immutable reference to the cached block. pub fn get_cached_block(&self) -> &Option> { &self.executed_block @@ -236,6 +254,7 @@ impl PendingComponents { verified_data_columns: vec![], executed_block: None, reconstruction_started: false, + data_column_recv: None, } } @@ -260,6 +279,7 @@ impl PendingComponents { verified_blobs, verified_data_columns, executed_block, + data_column_recv, .. } = self; @@ -302,10 +322,12 @@ impl PendingComponents { let AvailabilityPendingExecutedBlock { block, - import_data, + mut import_data, payload_verification_outcome, } = executed_block; + import_data.data_column_recv = data_column_recv; + let available_block = AvailableBlock { block_root, block, @@ -444,10 +466,17 @@ impl DataAvailabilityCheckerInner { f(self.critical.read().peek(block_root)) } + /// Puts the KZG verified blobs into the availability cache as pending components. + /// + /// The `data_column_recv` parameter is an optional `Receiver` for data columns that are + /// computed asynchronously. This method remains **used** after PeerDAS activation, because + /// blocks can be made available if the EL already has the blobs and returns them via the + /// `getBlobsV1` engine method. More details in [fetch_blobs.rs](https://github.com/sigp/lighthouse/blob/44f8add41ea2252769bb967864af95b3c13af8ca/beacon_node/beacon_chain/src/fetch_blobs.rs). pub fn put_kzg_verified_blobs>>( &self, block_root: Hash256, kzg_verified_blobs: I, + data_column_recv: Option>>, log: &Logger, ) -> Result, AvailabilityCheckError> { let mut kzg_verified_blobs = kzg_verified_blobs.into_iter().peekable(); @@ -482,9 +511,17 @@ impl DataAvailabilityCheckerInner { // Merge in the blobs. pending_components.merge_blobs(fixed_blobs); + if data_column_recv.is_some() { + // If `data_column_recv` is `Some`, it means we have all the blobs from engine, and have + // started computing data columns. We store the receiver in `PendingComponents` for + // later use when importing the block. + pending_components.data_column_recv = data_column_recv; + } + if pending_components.is_available(self.sampling_column_count, log) { - write_lock.put(block_root, pending_components.clone()); - // No need to hold the write lock anymore + // We keep the pending components in the availability cache during block import (#5845). + // `data_column_recv` is returned as part of the available block and is no longer needed here. + write_lock.put(block_root, pending_components.clone_without_column_recv()); drop(write_lock); pending_components.make_available(&self.spec, |diet_block| { self.state_cache.recover_pending_executed_block(diet_block) @@ -527,8 +564,9 @@ impl DataAvailabilityCheckerInner { pending_components.merge_data_columns(kzg_verified_data_columns)?; if pending_components.is_available(self.sampling_column_count, log) { - write_lock.put(block_root, pending_components.clone()); - // No need to hold the write lock anymore + // We keep the pending components in the availability cache during block import (#5845). + // `data_column_recv` is returned as part of the available block and is no longer needed here. + write_lock.put(block_root, pending_components.clone_without_column_recv()); drop(write_lock); pending_components.make_available(&self.spec, |diet_block| { self.state_cache.recover_pending_executed_block(diet_block) @@ -577,7 +615,7 @@ impl DataAvailabilityCheckerInner { } pending_components.reconstruction_started = true; - ReconstructColumnsDecision::Yes(pending_components.clone()) + ReconstructColumnsDecision::Yes(pending_components.clone_without_column_recv()) } /// This could mean some invalid data columns made it through to the `DataAvailabilityChecker`. @@ -619,8 +657,9 @@ impl DataAvailabilityCheckerInner { // Check if we have all components and entire set is consistent. if pending_components.is_available(self.sampling_column_count, log) { - write_lock.put(block_root, pending_components.clone()); - // No need to hold the write lock anymore + // We keep the pending components in the availability cache during block import (#5845). + // `data_column_recv` is returned as part of the available block and is no longer needed here. + write_lock.put(block_root, pending_components.clone_without_column_recv()); drop(write_lock); pending_components.make_available(&self.spec, |diet_block| { self.state_cache.recover_pending_executed_block(diet_block) @@ -855,6 +894,7 @@ mod test { parent_eth1_finalization_data, confirmed_state_roots: vec![], consensus_context, + data_column_recv: None, }; let payload_verification_outcome = PayloadVerificationOutcome { @@ -957,7 +997,7 @@ mod test { for (blob_index, gossip_blob) in blobs.into_iter().enumerate() { kzg_verified_blobs.push(gossip_blob.into_inner()); let availability = cache - .put_kzg_verified_blobs(root, kzg_verified_blobs.clone(), harness.logger()) + .put_kzg_verified_blobs(root, kzg_verified_blobs.clone(), None, harness.logger()) .expect("should put blob"); if blob_index == blobs_expected - 1 { assert!(matches!(availability, Availability::Available(_))); @@ -985,7 +1025,7 @@ mod test { for gossip_blob in blobs { kzg_verified_blobs.push(gossip_blob.into_inner()); let availability = cache - .put_kzg_verified_blobs(root, kzg_verified_blobs.clone(), harness.logger()) + .put_kzg_verified_blobs(root, kzg_verified_blobs.clone(), None, harness.logger()) .expect("should put blob"); assert_eq!( availability, @@ -1241,6 +1281,7 @@ mod pending_components_tests { }, confirmed_state_roots: vec![], consensus_context: ConsensusContext::new(Slot::new(0)), + data_column_recv: None, }, payload_verification_outcome: PayloadVerificationOutcome { payload_verification_status: PayloadVerificationStatus::Verified, 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 5b9b7c7023..2a2a0431cc 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 @@ -136,6 +136,7 @@ impl StateLRUCache { consensus_context: diet_executed_block .consensus_context .into_consensus_context(), + data_column_recv: None, }, payload_verification_outcome: diet_executed_block.payload_verification_outcome, }) diff --git a/beacon_node/beacon_chain/src/fetch_blobs.rs b/beacon_node/beacon_chain/src/fetch_blobs.rs index f1646072c9..49e46a50fe 100644 --- a/beacon_node/beacon_chain/src/fetch_blobs.rs +++ b/beacon_node/beacon_chain/src/fetch_blobs.rs @@ -18,7 +18,7 @@ use slog::{debug, error, o, Logger}; use ssz_types::FixedVector; use state_processing::per_block_processing::deneb::kzg_commitment_to_versioned_hash; use std::sync::Arc; -use tokio::sync::mpsc::Receiver; +use tokio::sync::oneshot; use types::blob_sidecar::{BlobSidecarError, FixedBlobSidecarList}; use types::{ BeaconStateError, BlobSidecar, ChainSpec, DataColumnSidecar, DataColumnSidecarList, EthSpec, @@ -213,9 +213,9 @@ fn spawn_compute_and_publish_data_columns_task( blobs: FixedBlobSidecarList, publish_fn: impl Fn(BlobsOrDataColumns) + Send + 'static, log: Logger, -) -> Receiver>>> { +) -> oneshot::Receiver>>> { let chain_cloned = chain.clone(); - let (data_columns_sender, data_columns_receiver) = tokio::sync::mpsc::channel(1); + let (data_columns_sender, data_columns_receiver) = oneshot::channel(); chain.task_executor.spawn_blocking( move || { @@ -248,7 +248,7 @@ fn spawn_compute_and_publish_data_columns_task( } }; - if let Err(e) = data_columns_sender.try_send(all_data_columns.clone()) { + if let Err(e) = data_columns_sender.send(all_data_columns.clone()) { error!(log, "Failed to send computed data columns"; "error" => ?e); }; diff --git a/beacon_node/beacon_chain/tests/block_verification.rs b/beacon_node/beacon_chain/tests/block_verification.rs index b61f758cac..1a651332ad 100644 --- a/beacon_node/beacon_chain/tests/block_verification.rs +++ b/beacon_node/beacon_chain/tests/block_verification.rs @@ -1737,7 +1737,7 @@ async fn import_execution_pending_block( .unwrap() { ExecutedBlock::Available(block) => chain - .import_available_block(Box::from(block), None) + .import_available_block(Box::from(block)) .await .map_err(|e| format!("{e:?}")), ExecutedBlock::AvailabilityPending(_) => { From e98209d1186419264868a28591c2b14af3f6abff Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Wed, 15 Jan 2025 18:40:26 +1100 Subject: [PATCH 077/254] Implement PeerDAS subnet decoupling (aka custody groups) (#6736) * Implement PeerDAS subnet decoupling (aka custody groups). * Merge branch 'unstable' into decouple-subnets * Refactor feature testing for spec tests (#6737) Squashed commit of the following: commit 898d05ee17114bef77ab748dda6ff4a41a2c61d5 Merge: ffbd25e2b 7e0cddef3 Author: Jimmy Chen Date: Tue Dec 24 14:41:19 2024 +1100 Merge branch 'unstable' into refactor-ef-tests-features commit ffbd25e2be041584e823123b051cf96775dd9e6c Author: Jimmy Chen Date: Tue Dec 24 14:40:38 2024 +1100 Fix `SszStatic` tests for PeerDAS: exclude eip7594 test vectors when testing Electra types. commit aa593cf35c51da9dc1f6131a4e1699a321d2d2e0 Author: Jimmy Chen Date: Fri Dec 20 12:08:54 2024 +1100 Refactor spec testing for features and simplify usage. * Fix build. * Add input validation and improve arithmetic handling when calculating custody groups. * Address review comments re code style consistency. * Merge branch 'unstable' into decouple-subnets # Conflicts: # beacon_node/beacon_chain/src/kzg_utils.rs # beacon_node/beacon_chain/src/observed_data_sidecars.rs # beacon_node/lighthouse_network/src/discovery/subnet_predicate.rs # common/eth2_network_config/built_in_network_configs/chiado/config.yaml # common/eth2_network_config/built_in_network_configs/gnosis/config.yaml # common/eth2_network_config/built_in_network_configs/holesky/config.yaml # common/eth2_network_config/built_in_network_configs/mainnet/config.yaml # common/eth2_network_config/built_in_network_configs/sepolia/config.yaml # consensus/types/src/chain_spec.rs * Update consensus/types/src/chain_spec.rs Co-authored-by: Lion - dapplion <35266934+dapplion@users.noreply.github.com> * Merge remote-tracking branch 'origin/unstable' into decouple-subnets * Update error handling. * Address review comment. * Merge remote-tracking branch 'origin/unstable' into decouple-subnets # Conflicts: # consensus/types/src/chain_spec.rs * Update PeerDAS spec tests to `1.5.0-beta.0` and fix failing unit tests. * Merge remote-tracking branch 'origin/unstable' into decouple-subnets # Conflicts: # beacon_node/lighthouse_network/src/peer_manager/mod.rs --- .../src/block_verification_types.rs | 2 +- .../src/data_availability_checker.rs | 21 +-- .../overflow_lru_cache.rs | 4 +- .../src/data_column_verification.rs | 4 +- beacon_node/beacon_chain/src/kzg_utils.rs | 6 +- .../src/observed_data_sidecars.rs | 2 +- beacon_node/http_api/src/block_id.rs | 2 +- beacon_node/http_api/src/publish_blocks.rs | 10 +- .../lighthouse_network/src/discovery/enr.rs | 46 ++--- .../src/discovery/subnet_predicate.rs | 14 +- beacon_node/lighthouse_network/src/metrics.rs | 8 +- .../src/peer_manager/mod.rs | 104 +++++++----- .../src/peer_manager/peerdb.rs | 18 +- .../src/peer_manager/peerdb/peer_info.rs | 6 +- .../lighthouse_network/src/rpc/codec.rs | 2 +- .../lighthouse_network/src/rpc/methods.rs | 8 +- .../lighthouse_network/src/service/mod.rs | 11 +- .../lighthouse_network/src/service/utils.rs | 12 +- .../lighthouse_network/src/types/globals.rs | 80 +++++---- .../src/network_beacon_processor/mod.rs | 9 +- .../network/src/sync/network_context.rs | 6 +- beacon_node/network/src/sync/tests/lookups.rs | 2 +- .../chiado/config.yaml | 5 +- .../gnosis/config.yaml | 5 +- .../holesky/config.yaml | 5 +- .../mainnet/config.yaml | 5 +- .../sepolia/config.yaml | 5 +- consensus/types/src/chain_spec.rs | 81 ++++++--- .../types/src/data_column_custody_group.rs | 142 ++++++++++++++++ consensus/types/src/data_column_subnet_id.rs | 160 +----------------- consensus/types/src/lib.rs | 1 + testing/ef_tests/src/cases.rs | 14 +- .../compute_columns_for_custody_groups.rs | 47 +++++ ...stody_columns.rs => get_custody_groups.rs} | 26 +-- .../cases/kzg_compute_cells_and_kzg_proofs.rs | 2 +- .../cases/kzg_recover_cells_and_kzg_proofs.rs | 2 +- .../cases/kzg_verify_cell_kzg_proof_batch.rs | 2 +- testing/ef_tests/src/handler.rs | 65 ++++--- testing/ef_tests/tests/tests.rs | 38 +++-- 39 files changed, 552 insertions(+), 430 deletions(-) create mode 100644 consensus/types/src/data_column_custody_group.rs create mode 100644 testing/ef_tests/src/cases/compute_columns_for_custody_groups.rs rename testing/ef_tests/src/cases/{get_custody_columns.rs => get_custody_groups.rs} (65%) diff --git a/beacon_node/beacon_chain/src/block_verification_types.rs b/beacon_node/beacon_chain/src/block_verification_types.rs index b81e728257..38d0fc708c 100644 --- a/beacon_node/beacon_chain/src/block_verification_types.rs +++ b/beacon_node/beacon_chain/src/block_verification_types.rs @@ -165,7 +165,7 @@ impl RpcBlock { let inner = if !custody_columns.is_empty() { RpcBlockInner::BlockAndCustodyColumns( block, - RuntimeVariableList::new(custody_columns, spec.number_of_columns)?, + RuntimeVariableList::new(custody_columns, spec.number_of_columns as usize)?, ) } 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 3ac1a95494..aa4689121c 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -117,21 +117,16 @@ impl DataAvailabilityChecker { spec: Arc, log: Logger, ) -> Result { - let custody_subnet_count = if import_all_data_columns { - spec.data_column_sidecar_subnet_count as usize - } else { - spec.custody_requirement as usize - }; - - let subnet_sampling_size = - std::cmp::max(custody_subnet_count, spec.samples_per_slot as usize); - let sampling_column_count = - subnet_sampling_size.saturating_mul(spec.data_columns_per_subnet()); + let custody_group_count = spec.custody_group_count(import_all_data_columns); + // This should only panic if the chain spec contains invalid values. + let sampling_size = spec + .sampling_size(custody_group_count) + .expect("should compute node sampling size from valid chain spec"); let inner = DataAvailabilityCheckerInner::new( OVERFLOW_LRU_CAPACITY, store, - sampling_column_count, + sampling_size as usize, spec.clone(), )?; Ok(Self { @@ -148,7 +143,7 @@ impl DataAvailabilityChecker { } pub(crate) fn is_supernode(&self) -> bool { - self.get_sampling_column_count() == self.spec.number_of_columns + self.get_sampling_column_count() == self.spec.number_of_columns as usize } /// Checks if the block root is currenlty in the availability cache awaiting import because @@ -433,7 +428,7 @@ impl DataAvailabilityChecker { .map(CustodyDataColumn::into_inner) .collect::>(); let all_data_columns = - RuntimeVariableList::from_vec(all_data_columns, self.spec.number_of_columns); + RuntimeVariableList::from_vec(all_data_columns, self.spec.number_of_columns as usize); // verify kzg for all data columns at once if !all_data_columns.is_empty() { 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 a2936206ae..c8e92f7e9f 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 @@ -598,7 +598,7 @@ impl DataAvailabilityCheckerInner { // If we're sampling all columns, it means we must be custodying all columns. let custody_column_count = self.sampling_column_count(); - let total_column_count = self.spec.number_of_columns; + let total_column_count = self.spec.number_of_columns as usize; let received_column_count = pending_components.verified_data_columns.len(); if pending_components.reconstruction_started { @@ -607,7 +607,7 @@ impl DataAvailabilityCheckerInner { if custody_column_count != total_column_count { return ReconstructColumnsDecision::No("not required for full node"); } - if received_column_count == self.spec.number_of_columns { + if received_column_count >= total_column_count { return ReconstructColumnsDecision::No("all columns received"); } if received_column_count < total_column_count / 2 { diff --git a/beacon_node/beacon_chain/src/data_column_verification.rs b/beacon_node/beacon_chain/src/data_column_verification.rs index 6cfd26786a..1bd17485ab 100644 --- a/beacon_node/beacon_chain/src/data_column_verification.rs +++ b/beacon_node/beacon_chain/src/data_column_verification.rs @@ -423,7 +423,7 @@ fn verify_data_column_sidecar( data_column: &DataColumnSidecar, spec: &ChainSpec, ) -> Result<(), GossipDataColumnError> { - if data_column.index >= spec.number_of_columns as u64 { + if data_column.index >= spec.number_of_columns { return Err(GossipDataColumnError::InvalidColumnIndex(data_column.index)); } if data_column.kzg_commitments.is_empty() { @@ -611,7 +611,7 @@ fn verify_index_matches_subnet( spec: &ChainSpec, ) -> Result<(), GossipDataColumnError> { let expected_subnet: u64 = - DataColumnSubnetId::from_column_index::(data_column.index as usize, spec).into(); + DataColumnSubnetId::from_column_index(data_column.index, spec).into(); if expected_subnet != subnet { return Err(GossipDataColumnError::InvalidSubnetId { received: subnet, diff --git a/beacon_node/beacon_chain/src/kzg_utils.rs b/beacon_node/beacon_chain/src/kzg_utils.rs index e32ee9c24b..dcb3864f78 100644 --- a/beacon_node/beacon_chain/src/kzg_utils.rs +++ b/beacon_node/beacon_chain/src/kzg_utils.rs @@ -193,7 +193,7 @@ fn build_data_column_sidecars( blob_cells_and_proofs_vec: Vec, spec: &ChainSpec, ) -> Result, String> { - let number_of_columns = spec.number_of_columns; + let number_of_columns = spec.number_of_columns as usize; let max_blobs_per_block = spec .max_blobs_per_block(signed_block_header.message.slot.epoch(E::slots_per_epoch())) as usize; @@ -428,7 +428,7 @@ mod test { .kzg_commitments_merkle_proof() .unwrap(); - assert_eq!(column_sidecars.len(), spec.number_of_columns); + assert_eq!(column_sidecars.len(), spec.number_of_columns as usize); for (idx, col_sidecar) in column_sidecars.iter().enumerate() { assert_eq!(col_sidecar.index, idx as u64); @@ -461,7 +461,7 @@ mod test { ) .unwrap(); - for i in 0..spec.number_of_columns { + for i in 0..spec.number_of_columns as usize { 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 48989e07d3..1ca6c03f00 100644 --- a/beacon_node/beacon_chain/src/observed_data_sidecars.rs +++ b/beacon_node/beacon_chain/src/observed_data_sidecars.rs @@ -59,7 +59,7 @@ impl ObservableDataSidecar for DataColumnSidecar { } fn max_num_of_items(spec: &ChainSpec, _slot: Slot) -> usize { - spec.number_of_columns + spec.number_of_columns as usize } } diff --git a/beacon_node/http_api/src/block_id.rs b/beacon_node/http_api/src/block_id.rs index 0b00958f26..be70f615e3 100644 --- a/beacon_node/http_api/src/block_id.rs +++ b/beacon_node/http_api/src/block_id.rs @@ -347,7 +347,7 @@ impl BlockId { let num_found_column_keys = column_indices.len(); let num_required_columns = chain.spec.number_of_columns / 2; - let is_blob_available = num_found_column_keys >= num_required_columns; + let is_blob_available = num_found_column_keys >= num_required_columns as usize; if is_blob_available { let data_columns = column_indices diff --git a/beacon_node/http_api/src/publish_blocks.rs b/beacon_node/http_api/src/publish_blocks.rs index b5aa23acf8..60d4b2f16e 100644 --- a/beacon_node/http_api/src/publish_blocks.rs +++ b/beacon_node/http_api/src/publish_blocks.rs @@ -395,9 +395,8 @@ fn build_gossip_verified_data_columns( let gossip_verified_data_columns = data_column_sidecars .into_iter() .map(|data_column_sidecar| { - let column_index = data_column_sidecar.index as usize; - let subnet = - DataColumnSubnetId::from_column_index::(column_index, &chain.spec); + let column_index = data_column_sidecar.index; + let subnet = DataColumnSubnetId::from_column_index(column_index, &chain.spec); let gossip_verified_column = GossipVerifiedDataColumn::new(data_column_sidecar, subnet.into(), chain); @@ -520,10 +519,7 @@ fn publish_column_sidecars( let pubsub_messages = data_column_sidecars .into_iter() .map(|data_col| { - let subnet = DataColumnSubnetId::from_column_index::( - data_col.index as usize, - &chain.spec, - ); + let subnet = DataColumnSubnetId::from_column_index(data_col.index, &chain.spec); PubsubMessage::DataColumnSidecar(Box::new((subnet, data_col))) }) .collect::>(); diff --git a/beacon_node/lighthouse_network/src/discovery/enr.rs b/beacon_node/lighthouse_network/src/discovery/enr.rs index ce29480ffd..8946c7753c 100644 --- a/beacon_node/lighthouse_network/src/discovery/enr.rs +++ b/beacon_node/lighthouse_network/src/discovery/enr.rs @@ -25,8 +25,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 = "csc"; +/// The ENR field specifying the peerdas custody group count. +pub const PEERDAS_CUSTODY_GROUP_COUNT_ENR_KEY: &str = "cgc"; /// Extension trait for ENR's within Eth2. pub trait Eth2Enr { @@ -38,8 +38,8 @@ pub trait Eth2Enr { &self, ) -> Result, &'static str>; - /// The peerdas custody subnet count associated with the ENR. - fn custody_subnet_count(&self, spec: &ChainSpec) -> Result; + /// The peerdas custody group count associated with the ENR. + fn custody_group_count(&self, spec: &ChainSpec) -> Result; fn eth2(&self) -> Result; } @@ -67,16 +67,16 @@ impl Eth2Enr for Enr { .map_err(|_| "Could not decode the ENR syncnets bitfield") } - fn custody_subnet_count(&self, spec: &ChainSpec) -> Result { - let csc = self - .get_decodable::(PEERDAS_CUSTODY_SUBNET_COUNT_ENR_KEY) - .ok_or("ENR custody subnet count non-existent")? - .map_err(|_| "Could not decode the ENR custody subnet count")?; + fn custody_group_count(&self, spec: &ChainSpec) -> Result { + let cgc = self + .get_decodable::(PEERDAS_CUSTODY_GROUP_COUNT_ENR_KEY) + .ok_or("ENR custody group count non-existent")? + .map_err(|_| "Could not decode the ENR custody group count")?; - if csc >= spec.custody_requirement && csc <= spec.data_column_sidecar_subnet_count { - Ok(csc) + if (spec.custody_requirement..=spec.number_of_custody_groups).contains(&cgc) { + Ok(cgc) } else { - Err("Invalid custody subnet count in ENR") + Err("Invalid custody group count in ENR") } } @@ -253,14 +253,14 @@ pub fn build_enr( &bitfield.as_ssz_bytes().into(), ); - // only set `csc` if PeerDAS fork epoch has been scheduled + // only set `cgc` if PeerDAS fork epoch has been scheduled if spec.is_peer_das_scheduled() { - let custody_subnet_count = if config.subscribe_all_data_column_subnets { - spec.data_column_sidecar_subnet_count + let custody_group_count = if config.subscribe_all_data_column_subnets { + spec.number_of_custody_groups } else { spec.custody_requirement }; - builder.add_value(PEERDAS_CUSTODY_SUBNET_COUNT_ENR_KEY, &custody_subnet_count); + builder.add_value(PEERDAS_CUSTODY_GROUP_COUNT_ENR_KEY, &custody_group_count); } builder @@ -287,11 +287,11 @@ fn compare_enr(local_enr: &Enr, disk_enr: &Enr) -> bool { && (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 and - // PEERDAS_CUSTODY_SUBNET_COUNT_ENR_KEY key to match, otherwise we use a new ENR. This will + // PEERDAS_CUSTODY_GROUP_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_decodable::(ATTESTATION_BITFIELD_ENR_KEY) == disk_enr.get_decodable(ATTESTATION_BITFIELD_ENR_KEY) && local_enr.get_decodable::(SYNC_COMMITTEE_BITFIELD_ENR_KEY) == disk_enr.get_decodable(SYNC_COMMITTEE_BITFIELD_ENR_KEY) - && local_enr.get_decodable::(PEERDAS_CUSTODY_SUBNET_COUNT_ENR_KEY) == disk_enr.get_decodable(PEERDAS_CUSTODY_SUBNET_COUNT_ENR_KEY) + && local_enr.get_decodable::(PEERDAS_CUSTODY_GROUP_COUNT_ENR_KEY) == disk_enr.get_decodable(PEERDAS_CUSTODY_GROUP_COUNT_ENR_KEY) } /// Loads enr from the given directory @@ -348,7 +348,7 @@ mod test { } #[test] - fn custody_subnet_count_default() { + fn custody_group_count_default() { let config = NetworkConfig { subscribe_all_data_column_subnets: false, ..NetworkConfig::default() @@ -358,13 +358,13 @@ mod test { let enr = build_enr_with_config(config, &spec).0; assert_eq!( - enr.custody_subnet_count::(&spec).unwrap(), + enr.custody_group_count::(&spec).unwrap(), spec.custody_requirement, ); } #[test] - fn custody_subnet_count_all() { + fn custody_group_count_all() { let config = NetworkConfig { subscribe_all_data_column_subnets: true, ..NetworkConfig::default() @@ -373,8 +373,8 @@ mod test { let enr = build_enr_with_config(config, &spec).0; assert_eq!( - enr.custody_subnet_count::(&spec).unwrap(), - spec.data_column_sidecar_subnet_count, + enr.custody_group_count::(&spec).unwrap(), + spec.number_of_custody_groups, ); } diff --git a/beacon_node/lighthouse_network/src/discovery/subnet_predicate.rs b/beacon_node/lighthouse_network/src/discovery/subnet_predicate.rs index 751f8dbb83..400a0c2d56 100644 --- a/beacon_node/lighthouse_network/src/discovery/subnet_predicate.rs +++ b/beacon_node/lighthouse_network/src/discovery/subnet_predicate.rs @@ -1,10 +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::{ChainSpec, DataColumnSubnetId}; +use types::data_column_custody_group::compute_subnets_for_node; +use types::ChainSpec; /// Returns the predicate for a given subnet. pub fn subnet_predicate( @@ -37,13 +37,9 @@ where .as_ref() .is_ok_and(|b| b.get(*s.deref() as usize).unwrap_or(false)), Subnet::DataColumn(s) => { - if let Ok(custody_subnet_count) = enr.custody_subnet_count::(&spec) { - DataColumnSubnetId::compute_custody_subnets::( - enr.node_id().raw(), - custody_subnet_count, - &spec, - ) - .is_ok_and(|mut subnets| subnets.contains(s)) + if let Ok(custody_group_count) = enr.custody_group_count::(&spec) { + compute_subnets_for_node(enr.node_id().raw(), custody_group_count, &spec) + .is_ok_and(|subnets| subnets.contains(s)) } else { false } diff --git a/beacon_node/lighthouse_network/src/metrics.rs b/beacon_node/lighthouse_network/src/metrics.rs index cb9c007b91..b36cb8075d 100644 --- a/beacon_node/lighthouse_network/src/metrics.rs +++ b/beacon_node/lighthouse_network/src/metrics.rs @@ -93,11 +93,11 @@ pub static PEERS_PER_CLIENT: LazyLock> = LazyLock::new(|| { ) }); -pub static PEERS_PER_CUSTODY_SUBNET_COUNT: LazyLock> = LazyLock::new(|| { +pub static PEERS_PER_CUSTODY_GROUP_COUNT: LazyLock> = LazyLock::new(|| { try_create_int_gauge_vec( - "peers_per_custody_subnet_count", - "The current count of peers by custody subnet count", - &["custody_subnet_count"], + "peers_per_custody_group_count", + "The current count of peers by custody group count", + &["custody_group_count"], ) }); diff --git a/beacon_node/lighthouse_network/src/peer_manager/mod.rs b/beacon_node/lighthouse_network/src/peer_manager/mod.rs index 6502a8dbff..07c4be7959 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/mod.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/mod.rs @@ -34,6 +34,9 @@ pub use peerdb::sync_status::{SyncInfo, SyncStatus}; use std::collections::{hash_map::Entry, HashMap, HashSet}; use std::net::IpAddr; use strum::IntoEnumIterator; +use types::data_column_custody_group::{ + compute_subnets_from_custody_group, get_custody_groups, CustodyIndex, +}; pub mod config; mod network_behaviour; @@ -101,6 +104,8 @@ pub struct PeerManager { /// discovery queries for subnet peers if we disconnect from existing sync /// committee subnet peers. sync_committee_subnets: HashMap, + /// A mapping of all custody groups to column subnets to avoid re-computation. + subnets_by_custody_group: HashMap>, /// The heartbeat interval to perform routine maintenance. heartbeat: tokio::time::Interval, /// Keeps track of whether the discovery service is enabled or not. @@ -160,6 +165,21 @@ impl PeerManager { // Set up the peer manager heartbeat interval let heartbeat = tokio::time::interval(tokio::time::Duration::from_secs(HEARTBEAT_INTERVAL)); + // Compute subnets for all custody groups + let subnets_by_custody_group = if network_globals.spec.is_peer_das_scheduled() { + (0..network_globals.spec.number_of_custody_groups) + .map(|custody_index| { + let subnets = + compute_subnets_from_custody_group(custody_index, &network_globals.spec) + .expect("Should compute subnets for all custody groups") + .collect(); + (custody_index, subnets) + }) + .collect::>>() + } else { + HashMap::new() + }; + Ok(PeerManager { network_globals, events: SmallVec::new(), @@ -170,6 +190,7 @@ impl PeerManager { target_peers: target_peer_count, temporary_banned_peers: LRUTimeCache::new(PEER_RECONNECTION_TIMEOUT), sync_committee_subnets: Default::default(), + subnets_by_custody_group, heartbeat, discovery_enabled, metrics_enabled, @@ -711,22 +732,39 @@ impl PeerManager { "peer_id" => %peer_id, "new_seq_no" => meta_data.seq_number()); } - let custody_subnet_count_opt = meta_data.custody_subnet_count().copied().ok(); + let custody_group_count_opt = meta_data.custody_group_count().copied().ok(); peer_info.set_meta_data(meta_data); if self.network_globals.spec.is_peer_das_scheduled() { // Gracefully ignore metadata/v2 peers. Potentially downscore after PeerDAS to // prioritize PeerDAS peers. - if let Some(custody_subnet_count) = custody_subnet_count_opt { - match self.compute_peer_custody_subnets(peer_id, custody_subnet_count) { - Ok(custody_subnets) => { + if let Some(custody_group_count) = custody_group_count_opt { + match self.compute_peer_custody_groups(peer_id, custody_group_count) { + Ok(custody_groups) => { + let custody_subnets = custody_groups + .into_iter() + .flat_map(|custody_index| { + self.subnets_by_custody_group + .get(&custody_index) + .cloned() + .unwrap_or_else(|| { + warn!( + self.log, + "Custody group not found in subnet mapping"; + "custody_index" => custody_index, + "peer_id" => %peer_id + ); + vec![] + }) + }) + .collect(); peer_info.set_custody_subnets(custody_subnets); } Err(err) => { - debug!(self.log, "Unable to compute peer custody subnets from metadata"; + debug!(self.log, "Unable to compute peer custody groups from metadata"; "info" => "Sending goodbye to peer", "peer_id" => %peer_id, - "custody_subnet_count" => custody_subnet_count, + "custody_group_count" => custody_group_count, "error" => ?err, ); invalid_meta_data = true; @@ -1312,7 +1350,7 @@ impl PeerManager { let mut inbound_ipv4_peers_connected: usize = 0; let mut inbound_ipv6_peers_connected: usize = 0; let mut peers_connected_multi: HashMap<(&str, &str), i32> = HashMap::new(); - let mut peers_per_custody_subnet_count: HashMap = HashMap::new(); + let mut peers_per_custody_group_count: HashMap = HashMap::new(); for (_, peer_info) in self.network_globals.peers.read().connected_peers() { peers_connected += 1; @@ -1345,8 +1383,8 @@ impl PeerManager { .or_default() += 1; if let Some(MetaData::V3(meta_data)) = peer_info.meta_data() { - *peers_per_custody_subnet_count - .entry(meta_data.custody_subnet_count) + *peers_per_custody_group_count + .entry(meta_data.custody_group_count) .or_default() += 1; } // Check if incoming peer is ipv4 @@ -1377,11 +1415,11 @@ impl PeerManager { // PEERS_CONNECTED metrics::set_gauge(&metrics::PEERS_CONNECTED, peers_connected); - // CUSTODY_SUBNET_COUNT - for (custody_subnet_count, peer_count) in peers_per_custody_subnet_count.into_iter() { + // CUSTODY_GROUP_COUNT + for (custody_group_count, peer_count) in peers_per_custody_group_count.into_iter() { metrics::set_gauge_vec( - &metrics::PEERS_PER_CUSTODY_SUBNET_COUNT, - &[&custody_subnet_count.to_string()], + &metrics::PEERS_PER_CUSTODY_GROUP_COUNT, + &[&custody_group_count.to_string()], peer_count, ) } @@ -1410,43 +1448,27 @@ impl PeerManager { } } - fn compute_peer_custody_subnets( + fn compute_peer_custody_groups( &self, peer_id: &PeerId, - custody_subnet_count: u64, - ) -> Result, String> { + custody_group_count: u64, + ) -> Result, String> { // If we don't have a node id, we cannot compute the custody duties anyway let node_id = peer_id_to_node_id(peer_id)?; let spec = &self.network_globals.spec; - if !(spec.custody_requirement..=spec.data_column_sidecar_subnet_count) - .contains(&custody_subnet_count) + if !(spec.custody_requirement..=spec.number_of_custody_groups) + .contains(&custody_group_count) { - return Err("Invalid custody subnet count in metadata: out of range".to_string()); + return Err("Invalid custody group count in metadata: out of range".to_string()); } - let custody_subnets = DataColumnSubnetId::compute_custody_subnets::( - node_id.raw(), - custody_subnet_count, - spec, - ) - .map(|subnets| subnets.collect()) - .unwrap_or_else(|e| { - // This is an unreachable scenario unless there's a bug, as we've validated the csc - // just above. - error!( - self.log, - "Computing peer custody subnets failed unexpectedly"; - "info" => "Falling back to default custody requirement subnets", - "peer_id" => %peer_id, - "custody_subnet_count" => custody_subnet_count, - "error" => ?e - ); - DataColumnSubnetId::compute_custody_requirement_subnets::(node_id.raw(), spec) - .collect() - }); - - Ok(custody_subnets) + get_custody_groups(node_id.raw(), custody_group_count, spec).map_err(|e| { + format!( + "Error computing peer custody groups for node {} with cgc={}: {:?}", + node_id, custody_group_count, e + ) + }) } } diff --git a/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs b/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs index 22a3df1ae8..37cb5df6ea 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs @@ -1,4 +1,4 @@ -use crate::discovery::enr::PEERDAS_CUSTODY_SUBNET_COUNT_ENR_KEY; +use crate::discovery::enr::PEERDAS_CUSTODY_GROUP_COUNT_ENR_KEY; use crate::discovery::{peer_id_to_node_id, CombinedKey}; use crate::{metrics, multiaddr::Multiaddr, types::Subnet, Enr, EnrExt, Gossipsub, PeerId}; use itertools::Itertools; @@ -13,6 +13,7 @@ use std::{ fmt::Formatter, }; use sync_status::SyncStatus; +use types::data_column_custody_group::compute_subnets_for_node; use types::{ChainSpec, DataColumnSubnetId, EthSpec}; pub mod client; @@ -695,8 +696,8 @@ impl PeerDB { if supernode { enr.insert( - PEERDAS_CUSTODY_SUBNET_COUNT_ENR_KEY, - &spec.data_column_sidecar_subnet_count, + PEERDAS_CUSTODY_GROUP_COUNT_ENR_KEY, + &spec.number_of_custody_groups, &enr_key, ) .expect("u64 can be encoded"); @@ -714,19 +715,14 @@ impl PeerDB { if supernode { let peer_info = self.peers.get_mut(&peer_id).expect("peer exists"); let all_subnets = (0..spec.data_column_sidecar_subnet_count) - .map(|csc| csc.into()) + .map(|subnet_id| subnet_id.into()) .collect(); peer_info.set_custody_subnets(all_subnets); } else { let peer_info = self.peers.get_mut(&peer_id).expect("peer exists"); let node_id = peer_id_to_node_id(&peer_id).expect("convert peer_id to node_id"); - let subnets = DataColumnSubnetId::compute_custody_subnets::( - node_id.raw(), - spec.custody_requirement, - spec, - ) - .expect("should compute custody subnets") - .collect(); + let subnets = compute_subnets_for_node(node_id.raw(), spec.custody_requirement, spec) + .expect("should compute custody subnets"); peer_info.set_custody_subnets(subnets); } 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 d8b3568a28..2e8f462565 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 @@ -89,7 +89,7 @@ impl PeerInfo { } /// Returns if the peer is subscribed to a given `Subnet` from the metadata attnets/syncnets field. - /// Also returns true if the peer is assigned to custody a given data column `Subnet` computed from the metadata `custody_column_count` field or ENR `csc` field. + /// Also returns true if the peer is assigned to custody a given data column `Subnet` computed from the metadata `custody_group_count` field or ENR `cgc` field. pub fn on_subnet_metadata(&self, subnet: &Subnet) -> bool { if let Some(meta_data) = &self.meta_data { match subnet { @@ -101,7 +101,9 @@ impl PeerInfo { .syncnets() .is_ok_and(|s| s.get(**id as usize).unwrap_or(false)) } - Subnet::DataColumn(column) => return self.custody_subnets.contains(column), + Subnet::DataColumn(subnet_id) => { + return self.is_assigned_to_custody_subnet(subnet_id) + } } } false diff --git a/beacon_node/lighthouse_network/src/rpc/codec.rs b/beacon_node/lighthouse_network/src/rpc/codec.rs index 61b2699ac5..8981a75aed 100644 --- a/beacon_node/lighthouse_network/src/rpc/codec.rs +++ b/beacon_node/lighthouse_network/src/rpc/codec.rs @@ -1139,7 +1139,7 @@ mod tests { seq_number: 1, attnets: EnrAttestationBitfield::::default(), syncnets: EnrSyncCommitteeBitfield::::default(), - custody_subnet_count: 1, + custody_group_count: 1, }) } diff --git a/beacon_node/lighthouse_network/src/rpc/methods.rs b/beacon_node/lighthouse_network/src/rpc/methods.rs index 500188beef..958041c53f 100644 --- a/beacon_node/lighthouse_network/src/rpc/methods.rs +++ b/beacon_node/lighthouse_network/src/rpc/methods.rs @@ -138,7 +138,7 @@ pub struct MetaData { #[superstruct(only(V2, V3))] pub syncnets: EnrSyncCommitteeBitfield, #[superstruct(only(V3))] - pub custody_subnet_count: u64, + pub custody_group_count: u64, } impl MetaData { @@ -181,13 +181,13 @@ impl MetaData { seq_number: metadata.seq_number, attnets: metadata.attnets.clone(), syncnets: Default::default(), - custody_subnet_count: spec.custody_requirement, + custody_group_count: spec.custody_requirement, }), MetaData::V2(metadata) => MetaData::V3(MetaDataV3 { seq_number: metadata.seq_number, attnets: metadata.attnets.clone(), syncnets: metadata.syncnets.clone(), - custody_subnet_count: spec.custody_requirement, + custody_group_count: spec.custody_requirement, }), md @ MetaData::V3(_) => md.clone(), } @@ -364,7 +364,7 @@ impl DataColumnsByRangeRequest { DataColumnsByRangeRequest { start_slot: 0, count: 0, - columns: vec![0; spec.number_of_columns], + columns: vec![0; spec.number_of_columns as usize], } .as_ssz_bytes() .len() diff --git a/beacon_node/lighthouse_network/src/service/mod.rs b/beacon_node/lighthouse_network/src/service/mod.rs index 999803b8fe..4738c76d0c 100644 --- a/beacon_node/lighthouse_network/src/service/mod.rs +++ b/beacon_node/lighthouse_network/src/service/mod.rs @@ -198,15 +198,12 @@ impl Network { )?; // Construct the metadata - let custody_subnet_count = ctx.chain_spec.is_peer_das_scheduled().then(|| { - if config.subscribe_all_data_column_subnets { - ctx.chain_spec.data_column_sidecar_subnet_count - } else { - ctx.chain_spec.custody_requirement - } + let custody_group_count = ctx.chain_spec.is_peer_das_scheduled().then(|| { + ctx.chain_spec + .custody_group_count(config.subscribe_all_data_column_subnets) }); let meta_data = - utils::load_or_build_metadata(&config.network_dir, custody_subnet_count, &log); + utils::load_or_build_metadata(&config.network_dir, custody_group_count, &log); let seq_number = *meta_data.seq_number(); let globals = NetworkGlobals::new( enr, diff --git a/beacon_node/lighthouse_network/src/service/utils.rs b/beacon_node/lighthouse_network/src/service/utils.rs index a9eaa002ff..5746c13c58 100644 --- a/beacon_node/lighthouse_network/src/service/utils.rs +++ b/beacon_node/lighthouse_network/src/service/utils.rs @@ -164,8 +164,8 @@ pub fn strip_peer_id(addr: &mut Multiaddr) { /// Load metadata from persisted file. Return default metadata if loading fails. pub fn load_or_build_metadata( - network_dir: &std::path::Path, - custody_subnet_count: Option, + network_dir: &Path, + custody_group_count_opt: Option, log: &slog::Logger, ) -> MetaData { // We load a V2 metadata version by default (regardless of current fork) @@ -216,12 +216,12 @@ pub fn load_or_build_metadata( }; // Wrap the MetaData - let meta_data = if let Some(custody_count) = custody_subnet_count { + let meta_data = if let Some(custody_group_count) = custody_group_count_opt { MetaData::V3(MetaDataV3 { attnets: meta_data.attnets, seq_number: meta_data.seq_number, syncnets: meta_data.syncnets, - custody_subnet_count: custody_count, + custody_group_count, }) } else { MetaData::V2(meta_data) @@ -286,8 +286,8 @@ pub(crate) fn save_metadata_to_disk( ) { let _ = std::fs::create_dir_all(dir); // We always store the metadata v2 to disk because - // custody_subnet_count parameter doesn't need to be persisted across runs. - // custody_subnet_count is what the user sets it for the current run. + // custody_group_count parameter doesn't need to be persisted across runs. + // custody_group_count is what the user sets it for the current run. // This is to prevent ugly branching logic when reading the metadata from disk. let metadata_bytes = metadata.metadata_v2().as_ssz_bytes(); match File::create(dir.join(METADATA_FILENAME)).and_then(|mut f| f.write_all(&metadata_bytes)) { diff --git a/beacon_node/lighthouse_network/src/types/globals.rs b/beacon_node/lighthouse_network/src/types/globals.rs index 92583b7b5d..8cce9a0f25 100644 --- a/beacon_node/lighthouse_network/src/types/globals.rs +++ b/beacon_node/lighthouse_network/src/types/globals.rs @@ -3,10 +3,13 @@ use crate::peer_manager::peerdb::PeerDB; use crate::rpc::{MetaData, MetaDataV3}; use crate::types::{BackFillState, SyncState}; use crate::{Client, Enr, EnrExt, GossipTopic, Multiaddr, NetworkConfig, PeerId}; -use itertools::Itertools; use parking_lot::RwLock; +use slog::error; use std::collections::HashSet; use std::sync::Arc; +use types::data_column_custody_group::{ + compute_columns_for_custody_group, compute_subnets_from_custody_group, get_custody_groups, +}; use types::{ChainSpec, ColumnIndex, DataColumnSubnetId, EthSpec}; pub struct NetworkGlobals { @@ -27,8 +30,8 @@ pub struct NetworkGlobals { /// The current state of the backfill sync. pub backfill_state: RwLock, /// The computed sampling subnets and columns is stored to avoid re-computing. - pub sampling_subnets: Vec, - pub sampling_columns: Vec, + pub sampling_subnets: HashSet, + pub sampling_columns: HashSet, /// Network-related configuration. Immutable after initialization. pub config: Arc, /// Ethereum chain configuration. Immutable after initialization. @@ -48,30 +51,43 @@ impl NetworkGlobals { let (sampling_subnets, sampling_columns) = if spec.is_peer_das_scheduled() { let node_id = enr.node_id().raw(); - let custody_subnet_count = local_metadata - .custody_subnet_count() - .copied() - .expect("custody subnet count must be set if PeerDAS is scheduled"); + let custody_group_count = match local_metadata.custody_group_count() { + Ok(&cgc) if cgc <= spec.number_of_custody_groups => cgc, + _ => { + error!( + log, + "custody_group_count from metadata is either invalid or not set. This is a bug!"; + "info" => "falling back to default custody requirement" + ); + spec.custody_requirement + } + }; - let subnet_sampling_size = std::cmp::max(custody_subnet_count, spec.samples_per_slot); + // The below `expect` calls will panic on start up if the chain spec config values used + // are invalid + let sampling_size = spec + .sampling_size(custody_group_count) + .expect("should compute node sampling size from valid chain spec"); + let custody_groups = get_custody_groups(node_id, sampling_size, &spec) + .expect("should compute node custody groups"); - let sampling_subnets = DataColumnSubnetId::compute_custody_subnets::( - node_id, - subnet_sampling_size, - &spec, - ) - .expect("sampling subnet count must be valid") - .collect::>(); + let mut sampling_subnets = HashSet::new(); + for custody_index in &custody_groups { + let subnets = compute_subnets_from_custody_group(*custody_index, &spec) + .expect("should compute custody subnets for node"); + sampling_subnets.extend(subnets); + } - let sampling_columns = sampling_subnets - .iter() - .flat_map(|subnet| subnet.columns::(&spec)) - .sorted() - .collect(); + let mut sampling_columns = HashSet::new(); + for custody_index in &custody_groups { + let columns = compute_columns_for_custody_group(*custody_index, &spec) + .expect("should compute custody columns for node"); + sampling_columns.extend(columns); + } (sampling_subnets, sampling_columns) } else { - (vec![], vec![]) + (HashSet::new(), HashSet::new()) }; NetworkGlobals { @@ -159,8 +175,8 @@ impl NetworkGlobals { pub fn custody_peers_for_column(&self, column_index: ColumnIndex) -> Vec { self.peers .read() - .good_custody_subnet_peer(DataColumnSubnetId::from_column_index::( - column_index as usize, + .good_custody_subnet_peer(DataColumnSubnetId::from_column_index( + column_index, &self.spec, )) .cloned() @@ -178,7 +194,7 @@ impl NetworkGlobals { seq_number: 0, attnets: Default::default(), syncnets: Default::default(), - custody_subnet_count: spec.custody_requirement, + custody_group_count: spec.custody_requirement, }); Self::new_test_globals_with_metadata(trusted_peers, metadata, log, config, spec) } @@ -209,9 +225,9 @@ mod test { let mut spec = E::default_spec(); spec.eip7594_fork_epoch = Some(Epoch::new(0)); - let custody_subnet_count = spec.data_column_sidecar_subnet_count / 2; - let subnet_sampling_size = std::cmp::max(custody_subnet_count, spec.samples_per_slot); - let metadata = get_metadata(custody_subnet_count); + let custody_group_count = spec.number_of_custody_groups / 2; + let subnet_sampling_size = spec.sampling_size(custody_group_count).unwrap(); + let metadata = get_metadata(custody_group_count); let config = Arc::new(NetworkConfig::default()); let globals = NetworkGlobals::::new_test_globals_with_metadata( @@ -233,9 +249,9 @@ mod test { let mut spec = E::default_spec(); spec.eip7594_fork_epoch = Some(Epoch::new(0)); - let custody_subnet_count = spec.data_column_sidecar_subnet_count / 2; - let subnet_sampling_size = std::cmp::max(custody_subnet_count, spec.samples_per_slot); - let metadata = get_metadata(custody_subnet_count); + let custody_group_count = spec.number_of_custody_groups / 2; + let subnet_sampling_size = spec.sampling_size(custody_group_count).unwrap(); + let metadata = get_metadata(custody_group_count); let config = Arc::new(NetworkConfig::default()); let globals = NetworkGlobals::::new_test_globals_with_metadata( @@ -251,12 +267,12 @@ mod test { ); } - fn get_metadata(custody_subnet_count: u64) -> MetaData { + fn get_metadata(custody_group_count: u64) -> MetaData { MetaData::V3(MetaDataV3 { seq_number: 0, attnets: Default::default(), syncnets: Default::default(), - custody_subnet_count, + custody_group_count, }) } } diff --git a/beacon_node/network/src/network_beacon_processor/mod.rs b/beacon_node/network/src/network_beacon_processor/mod.rs index d81d964e7c..2d15d39c6f 100644 --- a/beacon_node/network/src/network_beacon_processor/mod.rs +++ b/beacon_node/network/src/network_beacon_processor/mod.rs @@ -1122,10 +1122,8 @@ impl NetworkBeaconProcessor { messages: columns .into_iter() .map(|d| { - let subnet = DataColumnSubnetId::from_column_index::( - d.index as usize, - &chain.spec, - ); + let subnet = + DataColumnSubnetId::from_column_index(d.index, &chain.spec); PubsubMessage::DataColumnSidecar(Box::new((subnet, d))) }) .collect(), @@ -1139,7 +1137,8 @@ impl NetworkBeaconProcessor { let blob_publication_batch_interval = chain.config.blob_publication_batch_interval; let blob_publication_batches = chain.config.blob_publication_batches; - let batch_size = chain.spec.number_of_columns / blob_publication_batches; + let number_of_columns = chain.spec.number_of_columns as usize; + let batch_size = number_of_columns / blob_publication_batches; let mut publish_count = 0usize; for batch in data_columns_to_publish.chunks(batch_size) { diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index e1b2b974ec..0a6bc8961f 100644 --- a/beacon_node/network/src/sync/network_context.rs +++ b/beacon_node/network/src/sync/network_context.rs @@ -36,7 +36,7 @@ use requests::{ }; use slog::{debug, error, warn}; use std::collections::hash_map::Entry; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::sync::Arc; use std::time::Duration; use tokio::sync::mpsc; @@ -458,7 +458,7 @@ impl SyncNetworkContext { let max_blobs_len = self.chain.spec.max_blobs_per_block(epoch); let info = RangeBlockComponentsRequest::new( expected_blobs, - expects_columns, + expects_columns.map(|c| c.into_iter().collect()), num_of_column_req, requested_peers, max_blobs_len as usize, @@ -471,7 +471,7 @@ impl SyncNetworkContext { fn make_columns_by_range_requests( &self, request: BlocksByRangeRequest, - custody_indexes: &Vec, + custody_indexes: &HashSet, ) -> Result, RpcRequestSendError> { let mut peer_id_to_request_map = HashMap::new(); diff --git a/beacon_node/network/src/sync/tests/lookups.rs b/beacon_node/network/src/sync/tests/lookups.rs index b9e38237c5..f623aa2c12 100644 --- a/beacon_node/network/src/sync/tests/lookups.rs +++ b/beacon_node/network/src/sync/tests/lookups.rs @@ -2170,7 +2170,7 @@ fn custody_lookup_happy_path() { let id = r.expect_block_lookup_request(block.canonical_root()); r.complete_valid_block_request(id, block.into(), true); // for each slot we download `samples_per_slot` columns - let sample_column_count = spec.samples_per_slot * spec.data_columns_per_subnet() as u64; + let sample_column_count = spec.samples_per_slot * spec.data_columns_per_group(); let custody_ids = r.expect_only_data_columns_by_root_requests(block_root, sample_column_count as usize); r.complete_valid_custody_request(custody_ids, data_columns, false); 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 a303bea268..35ba3af28b 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 @@ -139,7 +139,8 @@ BLOB_SIDECAR_SUBNET_COUNT: 6 MAX_BLOBS_PER_BLOCK: 6 # DAS -CUSTODY_REQUIREMENT: 4 -DATA_COLUMN_SIDECAR_SUBNET_COUNT: 128 NUMBER_OF_COLUMNS: 128 +NUMBER_OF_CUSTODY_GROUPS: 128 +DATA_COLUMN_SIDECAR_SUBNET_COUNT: 128 SAMPLES_PER_SLOT: 8 +CUSTODY_REQUIREMENT: 4 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 68d2b0eafe..9ff5a16198 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 @@ -122,7 +122,8 @@ BLOB_SIDECAR_SUBNET_COUNT: 6 MAX_BLOBS_PER_BLOCK: 6 # DAS -CUSTODY_REQUIREMENT: 4 -DATA_COLUMN_SIDECAR_SUBNET_COUNT: 128 NUMBER_OF_COLUMNS: 128 +NUMBER_OF_CUSTODY_GROUPS: 128 +DATA_COLUMN_SIDECAR_SUBNET_COUNT: 128 SAMPLES_PER_SLOT: 8 +CUSTODY_REQUIREMENT: 4 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 930ce0a1bc..d0b61422e0 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 @@ -128,7 +128,8 @@ BLOB_SIDECAR_SUBNET_COUNT: 6 MAX_BLOBS_PER_BLOCK: 6 # DAS -CUSTODY_REQUIREMENT: 4 -DATA_COLUMN_SIDECAR_SUBNET_COUNT: 128 NUMBER_OF_COLUMNS: 128 +NUMBER_OF_CUSTODY_GROUPS: 128 +DATA_COLUMN_SIDECAR_SUBNET_COUNT: 128 SAMPLES_PER_SLOT: 8 +CUSTODY_REQUIREMENT: 4 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 638f6fe42f..f92de4225d 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,7 +145,8 @@ BLOB_SIDECAR_SUBNET_COUNT: 6 MAX_BLOBS_PER_BLOCK: 6 # DAS -CUSTODY_REQUIREMENT: 4 -DATA_COLUMN_SIDECAR_SUBNET_COUNT: 128 NUMBER_OF_COLUMNS: 128 +NUMBER_OF_CUSTODY_GROUPS: 128 +DATA_COLUMN_SIDECAR_SUBNET_COUNT: 128 SAMPLES_PER_SLOT: 8 +CUSTODY_REQUIREMENT: 4 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 3818518897..7564d8f0f6 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 @@ -123,7 +123,8 @@ BLOB_SIDECAR_SUBNET_COUNT: 6 MAX_BLOBS_PER_BLOCK: 6 # DAS -CUSTODY_REQUIREMENT: 4 -DATA_COLUMN_SIDECAR_SUBNET_COUNT: 128 NUMBER_OF_COLUMNS: 128 +NUMBER_OF_CUSTODY_GROUPS: 128 +DATA_COLUMN_SIDECAR_SUBNET_COUNT: 128 SAMPLES_PER_SLOT: 8 +CUSTODY_REQUIREMENT: 4 diff --git a/consensus/types/src/chain_spec.rs b/consensus/types/src/chain_spec.rs index 6594f3c44e..9177f66b94 100644 --- a/consensus/types/src/chain_spec.rs +++ b/consensus/types/src/chain_spec.rs @@ -204,10 +204,11 @@ pub struct ChainSpec { * DAS params */ pub eip7594_fork_epoch: Option, - pub custody_requirement: u64, + pub number_of_columns: u64, + pub number_of_custody_groups: u64, pub data_column_sidecar_subnet_count: u64, - pub number_of_columns: usize, pub samples_per_slot: u64, + pub custody_requirement: u64, /* * Networking @@ -237,7 +238,7 @@ pub struct ChainSpec { pub max_request_data_column_sidecars: u64, pub min_epochs_for_blob_sidecars_requests: u64, pub blob_sidecar_subnet_count: u64, - max_blobs_per_block: u64, + pub max_blobs_per_block: u64, /* * Networking Electra @@ -646,10 +647,33 @@ impl ChainSpec { } } - pub fn data_columns_per_subnet(&self) -> usize { + /// Returns the number of data columns per custody group. + pub fn data_columns_per_group(&self) -> u64 { self.number_of_columns - .safe_div(self.data_column_sidecar_subnet_count as usize) - .expect("Subnet count must be greater than 0") + .safe_div(self.number_of_custody_groups) + .expect("Custody group count must be greater than 0") + } + + /// Returns the number of column sidecars to sample per slot. + pub fn sampling_size(&self, custody_group_count: u64) -> Result { + let columns_per_custody_group = self + .number_of_columns + .safe_div(self.number_of_custody_groups) + .map_err(|_| "number_of_custody_groups must be greater than 0")?; + + let custody_column_count = columns_per_custody_group + .safe_mul(custody_group_count) + .map_err(|_| "Computing sampling size should not overflow")?; + + Ok(std::cmp::max(custody_column_count, self.samples_per_slot)) + } + + pub fn custody_group_count(&self, is_supernode: bool) -> u64 { + if is_supernode { + self.number_of_custody_groups + } else { + self.custody_requirement + } } /// Returns a `ChainSpec` compatible with the Ethereum Foundation specification. @@ -856,10 +880,11 @@ impl ChainSpec { * DAS params */ eip7594_fork_epoch: None, - custody_requirement: 4, - data_column_sidecar_subnet_count: 128, number_of_columns: 128, + number_of_custody_groups: 128, + data_column_sidecar_subnet_count: 128, samples_per_slot: 8, + custody_requirement: 4, /* * Network specific @@ -1193,10 +1218,12 @@ impl ChainSpec { * DAS params */ eip7594_fork_epoch: None, - custody_requirement: 4, - data_column_sidecar_subnet_count: 128, number_of_columns: 128, + number_of_custody_groups: 128, + data_column_sidecar_subnet_count: 128, samples_per_slot: 8, + custody_requirement: 4, + /* * Network specific */ @@ -1454,18 +1481,21 @@ pub struct Config { #[serde(with = "serde_utils::quoted_u64")] max_request_blob_sidecars_electra: u64, - #[serde(default = "default_custody_requirement")] - #[serde(with = "serde_utils::quoted_u64")] - custody_requirement: u64, - #[serde(default = "default_data_column_sidecar_subnet_count")] - #[serde(with = "serde_utils::quoted_u64")] - data_column_sidecar_subnet_count: u64, #[serde(default = "default_number_of_columns")] #[serde(with = "serde_utils::quoted_u64")] number_of_columns: u64, + #[serde(default = "default_number_of_custody_groups")] + #[serde(with = "serde_utils::quoted_u64")] + number_of_custody_groups: u64, + #[serde(default = "default_data_column_sidecar_subnet_count")] + #[serde(with = "serde_utils::quoted_u64")] + data_column_sidecar_subnet_count: u64, #[serde(default = "default_samples_per_slot")] #[serde(with = "serde_utils::quoted_u64")] samples_per_slot: u64, + #[serde(default = "default_custody_requirement")] + #[serde(with = "serde_utils::quoted_u64")] + custody_requirement: u64, } fn default_bellatrix_fork_version() -> [u8; 4] { @@ -1627,6 +1657,10 @@ const fn default_number_of_columns() -> u64 { 128 } +const fn default_number_of_custody_groups() -> u64 { + 128 +} + const fn default_samples_per_slot() -> u64 { 8 } @@ -1830,10 +1864,11 @@ impl Config { blob_sidecar_subnet_count_electra: spec.blob_sidecar_subnet_count_electra, max_request_blob_sidecars_electra: spec.max_request_blob_sidecars_electra, - custody_requirement: spec.custody_requirement, + number_of_columns: spec.number_of_columns, + number_of_custody_groups: spec.number_of_custody_groups, data_column_sidecar_subnet_count: spec.data_column_sidecar_subnet_count, - number_of_columns: spec.number_of_columns as u64, samples_per_slot: spec.samples_per_slot, + custody_requirement: spec.custody_requirement, } } @@ -1909,10 +1944,11 @@ impl Config { max_blobs_per_block_electra, blob_sidecar_subnet_count_electra, max_request_blob_sidecars_electra, - custody_requirement, - data_column_sidecar_subnet_count, number_of_columns, + number_of_custody_groups, + data_column_sidecar_subnet_count, samples_per_slot, + custody_requirement, } = self; if preset_base != E::spec_name().to_string().as_str() { @@ -1992,10 +2028,11 @@ impl Config { max_request_data_column_sidecars, ), - custody_requirement, + number_of_columns, + number_of_custody_groups, data_column_sidecar_subnet_count, - number_of_columns: number_of_columns as usize, samples_per_slot, + custody_requirement, ..chain_spec.clone() }) diff --git a/consensus/types/src/data_column_custody_group.rs b/consensus/types/src/data_column_custody_group.rs new file mode 100644 index 0000000000..bb204c34a2 --- /dev/null +++ b/consensus/types/src/data_column_custody_group.rs @@ -0,0 +1,142 @@ +use crate::{ChainSpec, ColumnIndex, DataColumnSubnetId}; +use alloy_primitives::U256; +use itertools::Itertools; +use maplit::hashset; +use safe_arith::{ArithError, SafeArith}; +use std::collections::HashSet; + +pub type CustodyIndex = u64; + +#[derive(Debug)] +pub enum DataColumnCustodyGroupError { + InvalidCustodyGroup(CustodyIndex), + InvalidCustodyGroupCount(u64), + ArithError(ArithError), +} + +/// The `get_custody_groups` function is used to determine the custody groups that a node is +/// assigned to. +/// +/// spec: https://github.com/ethereum/consensus-specs/blob/8e0d0d48e81d6c7c5a8253ab61340f5ea5bac66a/specs/fulu/das-core.md#get_custody_groups +pub fn get_custody_groups( + raw_node_id: [u8; 32], + custody_group_count: u64, + spec: &ChainSpec, +) -> Result, DataColumnCustodyGroupError> { + if custody_group_count > spec.number_of_custody_groups { + return Err(DataColumnCustodyGroupError::InvalidCustodyGroupCount( + custody_group_count, + )); + } + + let mut custody_groups: HashSet = hashset![]; + let mut current_id = U256::from_be_slice(&raw_node_id); + while custody_groups.len() < custody_group_count as usize { + let mut node_id_bytes = [0u8; 32]; + node_id_bytes.copy_from_slice(current_id.as_le_slice()); + let hash = ethereum_hashing::hash_fixed(&node_id_bytes); + 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 custody_group = hash_prefix_u64 + .safe_rem(spec.number_of_custody_groups) + .expect("spec.number_of_custody_groups must not be zero"); + custody_groups.insert(custody_group); + + current_id = current_id.wrapping_add(U256::from(1u64)); + } + + Ok(custody_groups) +} + +/// Returns the columns that are associated with a given custody group. +/// +/// spec: https://github.com/ethereum/consensus-specs/blob/8e0d0d48e81d6c7c5a8253ab61340f5ea5bac66a/specs/fulu/das-core.md#compute_columns_for_custody_group +pub fn compute_columns_for_custody_group( + custody_group: CustodyIndex, + spec: &ChainSpec, +) -> Result, DataColumnCustodyGroupError> { + let number_of_custody_groups = spec.number_of_custody_groups; + if custody_group >= number_of_custody_groups { + return Err(DataColumnCustodyGroupError::InvalidCustodyGroup( + custody_group, + )); + } + + let mut columns = Vec::new(); + for i in 0..spec.data_columns_per_group() { + let column = number_of_custody_groups + .safe_mul(i) + .and_then(|v| v.safe_add(custody_group)) + .map_err(DataColumnCustodyGroupError::ArithError)?; + columns.push(column); + } + + Ok(columns.into_iter()) +} + +pub fn compute_subnets_for_node( + raw_node_id: [u8; 32], + custody_group_count: u64, + spec: &ChainSpec, +) -> Result, DataColumnCustodyGroupError> { + let custody_groups = get_custody_groups(raw_node_id, custody_group_count, spec)?; + let mut subnets = HashSet::new(); + + for custody_group in custody_groups { + let custody_group_subnets = compute_subnets_from_custody_group(custody_group, spec)?; + subnets.extend(custody_group_subnets); + } + + Ok(subnets) +} + +/// Returns the subnets that are associated with a given custody group. +pub fn compute_subnets_from_custody_group( + custody_group: CustodyIndex, + spec: &ChainSpec, +) -> Result + '_, DataColumnCustodyGroupError> { + let result = compute_columns_for_custody_group(custody_group, spec)? + .map(|column_index| DataColumnSubnetId::from_column_index(column_index, spec)) + .unique(); + Ok(result) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_compute_columns_for_custody_group() { + let mut spec = ChainSpec::mainnet(); + spec.number_of_custody_groups = 64; + spec.number_of_columns = 128; + let columns_per_custody_group = spec.number_of_columns / spec.number_of_custody_groups; + + for custody_group in 0..spec.number_of_custody_groups { + let columns = compute_columns_for_custody_group(custody_group, &spec) + .unwrap() + .collect::>(); + assert_eq!(columns.len(), columns_per_custody_group as usize); + } + } + + #[test] + fn test_compute_subnets_from_custody_group() { + let mut spec = ChainSpec::mainnet(); + spec.number_of_custody_groups = 64; + spec.number_of_columns = 256; + spec.data_column_sidecar_subnet_count = 128; + + let subnets_per_custody_group = + spec.data_column_sidecar_subnet_count / spec.number_of_custody_groups; + + for custody_group in 0..spec.number_of_custody_groups { + let subnets = compute_subnets_from_custody_group(custody_group, &spec) + .unwrap() + .collect::>(); + assert_eq!(subnets.len(), subnets_per_custody_group as usize); + } + } +} diff --git a/consensus/types/src/data_column_subnet_id.rs b/consensus/types/src/data_column_subnet_id.rs index df61d711c1..5b3eef24cc 100644 --- a/consensus/types/src/data_column_subnet_id.rs +++ b/consensus/types/src/data_column_subnet_id.rs @@ -1,11 +1,8 @@ //! Identifies each data column subnet by an integer identifier. use crate::data_column_sidecar::ColumnIndex; -use crate::{ChainSpec, EthSpec}; -use alloy_primitives::U256; -use itertools::Itertools; +use crate::ChainSpec; use safe_arith::{ArithError, SafeArith}; use serde::{Deserialize, Serialize}; -use std::collections::HashSet; use std::fmt::{self, Display}; use std::ops::{Deref, DerefMut}; @@ -18,76 +15,14 @@ impl DataColumnSubnetId { id.into() } - pub fn from_column_index(column_index: usize, spec: &ChainSpec) -> Self { - (column_index - .safe_rem(spec.data_column_sidecar_subnet_count as usize) + pub fn from_column_index(column_index: ColumnIndex, spec: &ChainSpec) -> Self { + column_index + .safe_rem(spec.data_column_sidecar_subnet_count) .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, spec: &ChainSpec) -> impl Iterator { - let subnet = self.0; - 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. - #[allow(clippy::arithmetic_side_effects)] - pub fn compute_custody_subnets( - raw_node_id: [u8; 32], - custody_subnet_count: u64, - spec: &ChainSpec, - ) -> Result, Error> { - if custody_subnet_count > spec.data_column_sidecar_subnet_count { - return Err(Error::InvalidCustodySubnetCount(custody_subnet_count)); - } - - let mut subnets: HashSet = HashSet::new(); - let mut current_id = U256::from_be_slice(&raw_node_id); - while (subnets.len() as u64) < custody_subnet_count { - let mut node_id_bytes = [0u8; 32]; - node_id_bytes.copy_from_slice(current_id.as_le_slice()); - let hash = ethereum_hashing::hash_fixed(&node_id_bytes); - 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; - - if !subnets.contains(&subnet) { - subnets.insert(subnet); - } - - if current_id == U256::MAX { - current_id = U256::ZERO - } - current_id += U256::from(1u64) - } - Ok(subnets.into_iter().map(DataColumnSubnetId::new)) - } - - /// Compute the custody subnets for a given node id with the default `custody_requirement`. - /// This operation should be infallable, and empty iterator is returned if it fails unexpectedly. - pub fn compute_custody_requirement_subnets( - node_id: [u8; 32], - spec: &ChainSpec, - ) -> impl Iterator { - Self::compute_custody_subnets::(node_id, spec.custody_requirement, spec) - .expect("should compute default custody subnets") - } - - pub fn compute_custody_columns( - raw_node_id: [u8; 32], - custody_subnet_count: u64, - spec: &ChainSpec, - ) -> Result, Error> { - Self::compute_custody_subnets::(raw_node_id, custody_subnet_count, spec) - .map(|subnet| subnet.flat_map(|subnet| subnet.columns::(spec)).sorted()) - } } impl Display for DataColumnSubnetId { @@ -139,88 +74,3 @@ impl From for Error { Error::ArithError(e) } } - -#[cfg(test)] -mod test { - use crate::data_column_subnet_id::DataColumnSubnetId; - use crate::MainnetEthSpec; - use crate::Uint256; - use crate::{EthSpec, GnosisEthSpec, MinimalEthSpec}; - - type E = MainnetEthSpec; - - #[test] - fn test_compute_subnets_for_data_column() { - let spec = E::default_spec(); - let node_ids = [ - "0", - "88752428858350697756262172400162263450541348766581994718383409852729519486397", - "18732750322395381632951253735273868184515463718109267674920115648614659369468", - "27726842142488109545414954493849224833670205008410190955613662332153332462900", - "39755236029158558527862903296867805548949739810920318269566095185775868999998", - "31899136003441886988955119620035330314647133604576220223892254902004850516297", - "58579998103852084482416614330746509727562027284701078483890722833654510444626", - "28248042035542126088870192155378394518950310811868093527036637864276176517397", - "60930578857433095740782970114409273483106482059893286066493409689627770333527", - "103822458477361691467064888613019442068586830412598673713899771287914656699997", - ] - .into_iter() - .map(|v| Uint256::from_str_radix(v, 10).unwrap().to_be_bytes::<32>()) - .collect::>(); - - let custody_requirement = 4; - for node_id in node_ids { - let computed_subnets = DataColumnSubnetId::compute_custody_subnets::( - node_id, - custody_requirement, - &spec, - ) - .unwrap(); - 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 = spec.data_column_sidecar_subnet_count; - for subnet in computed_subnets { - 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(), 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); - } - } - } - } - - #[test] - fn test_compute_custody_requirement_subnets_never_panics() { - let node_id = [1u8; 32]; - test_compute_custody_requirement_subnets_with_spec::(node_id); - test_compute_custody_requirement_subnets_with_spec::(node_id); - test_compute_custody_requirement_subnets_with_spec::(node_id); - } - - fn test_compute_custody_requirement_subnets_with_spec(node_id: [u8; 32]) { - let _ = DataColumnSubnetId::compute_custody_requirement_subnets::( - node_id, - &E::default_spec(), - ); - } - - #[test] - fn test_columns_subnet_conversion() { - 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::(&spec) { - assert_eq!( - subnet_id, - DataColumnSubnetId::from_column_index::(column_index as usize, &spec) - ); - } - } - } -} diff --git a/consensus/types/src/lib.rs b/consensus/types/src/lib.rs index 76e414b2f1..dcfa918146 100644 --- a/consensus/types/src/lib.rs +++ b/consensus/types/src/lib.rs @@ -104,6 +104,7 @@ pub mod slot_data; pub mod sqlite; pub mod blob_sidecar; +pub mod data_column_custody_group; pub mod data_column_sidecar; pub mod data_column_subnet_id; pub mod light_client_header; diff --git a/testing/ef_tests/src/cases.rs b/testing/ef_tests/src/cases.rs index 8f5571d64a..54a142a96b 100644 --- a/testing/ef_tests/src/cases.rs +++ b/testing/ef_tests/src/cases.rs @@ -13,12 +13,13 @@ mod bls_fast_aggregate_verify; mod bls_sign_msg; mod bls_verify_msg; mod common; +mod compute_columns_for_custody_groups; mod epoch_processing; mod fork; mod fork_choice; mod genesis_initialization; mod genesis_validity; -mod get_custody_columns; +mod get_custody_groups; mod kzg_blob_to_kzg_commitment; mod kzg_compute_blob_kzg_proof; mod kzg_compute_cells_and_kzg_proofs; @@ -49,11 +50,12 @@ pub use bls_fast_aggregate_verify::*; pub use bls_sign_msg::*; pub use bls_verify_msg::*; pub use common::SszStaticType; +pub use compute_columns_for_custody_groups::*; pub use epoch_processing::*; pub use fork::ForkTest; pub use genesis_initialization::*; pub use genesis_validity::*; -pub use get_custody_columns::*; +pub use get_custody_groups::*; pub use kzg_blob_to_kzg_commitment::*; pub use kzg_compute_blob_kzg_proof::*; pub use kzg_compute_cells_and_kzg_proofs::*; @@ -89,18 +91,18 @@ pub use transition::TransitionTest; /// to return `true` for the feature in order for the feature test vector to be tested. #[derive(Debug, PartialEq, Clone, Copy)] pub enum FeatureName { - Eip7594, + Fulu, } impl FeatureName { pub fn list_all() -> Vec { - vec![FeatureName::Eip7594] + vec![FeatureName::Fulu] } /// `ForkName` to use when running the feature tests. pub fn fork_name(&self) -> ForkName { match self { - FeatureName::Eip7594 => ForkName::Deneb, + FeatureName::Fulu => ForkName::Electra, } } } @@ -108,7 +110,7 @@ impl FeatureName { impl Display for FeatureName { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - FeatureName::Eip7594 => f.write_str("eip7594"), + FeatureName::Fulu => f.write_str("fulu"), } } } diff --git a/testing/ef_tests/src/cases/compute_columns_for_custody_groups.rs b/testing/ef_tests/src/cases/compute_columns_for_custody_groups.rs new file mode 100644 index 0000000000..1d0bf951bc --- /dev/null +++ b/testing/ef_tests/src/cases/compute_columns_for_custody_groups.rs @@ -0,0 +1,47 @@ +use super::*; +use serde::Deserialize; +use std::marker::PhantomData; +use types::data_column_custody_group::{compute_columns_for_custody_group, CustodyIndex}; + +#[derive(Debug, Clone, Deserialize)] +#[serde(bound = "E: EthSpec", deny_unknown_fields)] +pub struct ComputeColumnsForCustodyGroups { + /// The custody group index. + pub custody_group: CustodyIndex, + /// The list of resulting custody columns. + pub result: Vec, + #[serde(skip)] + _phantom: PhantomData, +} + +impl LoadCase for ComputeColumnsForCustodyGroups { + fn load_from_dir(path: &Path, _fork_name: ForkName) -> Result { + decode::yaml_decode_file(path.join("meta.yaml").as_path()) + } +} + +impl Case for ComputeColumnsForCustodyGroups { + fn is_enabled_for_fork(_fork_name: ForkName) -> bool { + false + } + + fn is_enabled_for_feature(feature_name: FeatureName) -> bool { + feature_name == FeatureName::Fulu + } + + fn result(&self, _case_index: usize, _fork_name: ForkName) -> Result<(), Error> { + let spec = E::default_spec(); + let computed_columns = compute_columns_for_custody_group(self.custody_group, &spec) + .expect("should compute custody columns from group") + .collect::>(); + + let expected = &self.result; + if computed_columns == *expected { + Ok(()) + } else { + Err(Error::NotEqual(format!( + "Got {computed_columns:?}\nExpected {expected:?}" + ))) + } + } +} diff --git a/testing/ef_tests/src/cases/get_custody_columns.rs b/testing/ef_tests/src/cases/get_custody_groups.rs similarity index 65% rename from testing/ef_tests/src/cases/get_custody_columns.rs rename to testing/ef_tests/src/cases/get_custody_groups.rs index 71b17aeaa3..f8c4370aeb 100644 --- a/testing/ef_tests/src/cases/get_custody_columns.rs +++ b/testing/ef_tests/src/cases/get_custody_groups.rs @@ -2,31 +2,34 @@ use super::*; use alloy_primitives::U256; use serde::Deserialize; use std::marker::PhantomData; -use types::DataColumnSubnetId; +use types::data_column_custody_group::get_custody_groups; #[derive(Debug, Clone, Deserialize)] #[serde(bound = "E: EthSpec", deny_unknown_fields)] -pub struct GetCustodyColumns { +pub struct GetCustodyGroups { + /// The NodeID input. pub node_id: String, - pub custody_subnet_count: u64, + /// The count of custody groups. + pub custody_group_count: u64, + /// The list of resulting custody groups. pub result: Vec, #[serde(skip)] _phantom: PhantomData, } -impl LoadCase for GetCustodyColumns { +impl LoadCase for GetCustodyGroups { fn load_from_dir(path: &Path, _fork_name: ForkName) -> Result { decode::yaml_decode_file(path.join("meta.yaml").as_path()) } } -impl Case for GetCustodyColumns { +impl Case for GetCustodyGroups { fn is_enabled_for_fork(_fork_name: ForkName) -> bool { false } fn is_enabled_for_feature(feature_name: FeatureName) -> bool { - feature_name == FeatureName::Eip7594 + feature_name == FeatureName::Fulu } fn result(&self, _case_index: usize, _fork_name: ForkName) -> Result<(), Error> { @@ -34,13 +37,10 @@ impl Case for GetCustodyColumns { let node_id = U256::from_str_radix(&self.node_id, 10) .map_err(|e| Error::FailedToParseTest(format!("{e:?}")))?; let raw_node_id = node_id.to_be_bytes::<32>(); - let computed = DataColumnSubnetId::compute_custody_columns::( - raw_node_id, - self.custody_subnet_count, - &spec, - ) - .expect("should compute custody columns") - .collect::>(); + let mut computed = get_custody_groups(raw_node_id, self.custody_group_count, &spec) + .map(|set| set.into_iter().collect::>()) + .expect("should compute custody groups"); + computed.sort(); let expected = &self.result; if computed == *expected { 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 a7219f0629..8df43bb267 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 @@ -31,7 +31,7 @@ impl Case for KZGComputeCellsAndKZGProofs { } fn is_enabled_for_feature(feature_name: FeatureName) -> bool { - feature_name == FeatureName::Eip7594 + feature_name == FeatureName::Fulu } fn result(&self, _case_index: usize, _fork_name: ForkName) -> Result<(), Error> { 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 b72b3a05cd..26ab4e96b5 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 @@ -32,7 +32,7 @@ impl Case for KZGRecoverCellsAndKZGProofs { } fn is_enabled_for_feature(feature_name: FeatureName) -> bool { - feature_name == FeatureName::Eip7594 + feature_name == FeatureName::Fulu } fn result(&self, _case_index: usize, _fork_name: ForkName) -> Result<(), Error> { 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 815ad7a5bc..fc625063b1 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 @@ -34,7 +34,7 @@ impl Case for KZGVerifyCellKZGProofBatch { } fn is_enabled_for_feature(feature_name: FeatureName) -> bool { - feature_name == FeatureName::Eip7594 + feature_name == FeatureName::Fulu } fn result(&self, _case_index: usize, _fork_name: ForkName) -> Result<(), Error> { diff --git a/testing/ef_tests/src/handler.rs b/testing/ef_tests/src/handler.rs index 2e49b1301d..6c0165efab 100644 --- a/testing/ef_tests/src/handler.rs +++ b/testing/ef_tests/src/handler.rs @@ -39,6 +39,10 @@ pub trait Handler { } } + // Run feature tests for future forks that are not yet added to `ForkName`. + // This runs tests in the directory named by the feature instead of the fork name. + // e.g. consensus-spec-tests/tests/general/[feature_name]/[runner_name] + // e.g. consensus-spec-tests/tests/general/peerdas/ssz_static for feature_name in FeatureName::list_all() { if self.is_enabled_for_feature(feature_name) { self.run_for_feature(feature_name); @@ -350,23 +354,20 @@ where self.supported_forks.contains(&fork_name) } - fn is_enabled_for_feature(&self, _feature_name: FeatureName) -> bool { - // This ensures we only run the tests **once** for `Eip7594`, using the types matching the - // correct fork, e.g. `Eip7594` uses SSZ types from `Deneb` as of spec test version - // `v1.5.0-alpha.8`, therefore the `Eip7594` tests should get included when testing Deneb types. + fn is_enabled_for_feature(&self, feature_name: FeatureName) -> bool { + // This ensures we only run the tests **once** for the feature, using the types matching the + // correct fork, e.g. `Fulu` uses SSZ types from `Electra` fork as of spec test version + // `v1.5.0-beta.0`, therefore the `Fulu` tests should get included when testing Electra types. // - // e.g. Eip7594 test vectors are executed in the first line below, but excluded in the 2nd + // e.g. Fulu test vectors are executed in the first line below, but excluded in the 2nd // line when testing the type `AttestationElectra`: // // ``` // SszStaticHandler::, MainnetEthSpec>::pre_electra().run(); // SszStaticHandler::, MainnetEthSpec>::electra_only().run(); // ``` - /* TODO(das): re-enable - feature_name == FeatureName::Eip7594 + feature_name == FeatureName::Fulu && self.supported_forks.contains(&feature_name.fork_name()) - */ - false } } @@ -388,10 +389,8 @@ where BeaconState::::name().into() } - fn is_enabled_for_feature(&self, _feature_name: FeatureName) -> bool { - // TODO(das): re-enable - // feature_name == FeatureName::Eip7594 - false + fn is_enabled_for_feature(&self, feature_name: FeatureName) -> bool { + feature_name == FeatureName::Fulu } } @@ -415,10 +414,8 @@ where T::name().into() } - fn is_enabled_for_feature(&self, _feature_name: FeatureName) -> bool { - // TODO(das): re-enable - // feature_name == FeatureName::Eip7594 - false + fn is_enabled_for_feature(&self, feature_name: FeatureName) -> bool { + feature_name == FeatureName::Fulu } } @@ -877,10 +874,10 @@ impl Handler for KZGVerifyKZGProofHandler { #[derive(Derivative)] #[derivative(Default(bound = ""))] -pub struct GetCustodyColumnsHandler(PhantomData); +pub struct GetCustodyGroupsHandler(PhantomData); -impl Handler for GetCustodyColumnsHandler { - type Case = cases::GetCustodyColumns; +impl Handler for GetCustodyGroupsHandler { + type Case = cases::GetCustodyGroups; fn config_name() -> &'static str { E::name() @@ -891,7 +888,27 @@ impl Handler for GetCustodyColumnsHandler { } fn handler_name(&self) -> String { - "get_custody_columns".into() + "get_custody_groups".into() + } +} + +#[derive(Derivative)] +#[derivative(Default(bound = ""))] +pub struct ComputeColumnsForCustodyGroupHandler(PhantomData); + +impl Handler for ComputeColumnsForCustodyGroupHandler { + type Case = cases::ComputeColumnsForCustodyGroups; + + fn config_name() -> &'static str { + E::name() + } + + fn runner_name() -> &'static str { + "networking" + } + + fn handler_name(&self) -> String { + "compute_columns_for_custody_group".into() } } @@ -1002,10 +1019,8 @@ impl Handler for KzgInclusionMerkleProofValidityHandler bool { - // TODO(das): re-enable this - // feature_name == FeatureName::Eip7594 - false + fn is_enabled_for_feature(&self, feature_name: FeatureName) -> bool { + feature_name == FeatureName::Fulu } } diff --git a/testing/ef_tests/tests/tests.rs b/testing/ef_tests/tests/tests.rs index 7c268123fa..61581128d4 100644 --- a/testing/ef_tests/tests/tests.rs +++ b/testing/ef_tests/tests/tests.rs @@ -237,7 +237,9 @@ macro_rules! ssz_static_test_no_run { #[cfg(feature = "fake_crypto")] mod ssz_static { - use ef_tests::{Handler, SszStaticHandler, SszStaticTHCHandler, SszStaticWithSpecHandler}; + use ef_tests::{ + FeatureName, Handler, SszStaticHandler, SszStaticTHCHandler, SszStaticWithSpecHandler, + }; use types::historical_summary::HistoricalSummary; use types::{ AttesterSlashingBase, AttesterSlashingElectra, ConsolidationRequest, DepositRequest, @@ -622,23 +624,21 @@ mod ssz_static { SszStaticHandler::::capella_and_later().run(); } - /* FIXME(das): re-enable #[test] fn data_column_sidecar() { SszStaticHandler::, MinimalEthSpec>::deneb_only() - .run_for_feature(FeatureName::Eip7594); + .run_for_feature(FeatureName::Fulu); SszStaticHandler::, MainnetEthSpec>::deneb_only() - .run_for_feature(FeatureName::Eip7594); + .run_for_feature(FeatureName::Fulu); } #[test] fn data_column_identifier() { SszStaticHandler::::deneb_only() - .run_for_feature(FeatureName::Eip7594); + .run_for_feature(FeatureName::Fulu); SszStaticHandler::::deneb_only() - .run_for_feature(FeatureName::Eip7594); + .run_for_feature(FeatureName::Fulu); } - */ #[test] fn consolidation() { @@ -899,25 +899,23 @@ fn kzg_verify_kzg_proof() { KZGVerifyKZGProofHandler::::default().run(); } -/* FIXME(das): re-enable these tests #[test] fn kzg_compute_cells_and_proofs() { KZGComputeCellsAndKZGProofHandler::::default() - .run_for_feature(FeatureName::Eip7594); + .run_for_feature(FeatureName::Fulu); } #[test] fn kzg_verify_cell_proof_batch() { KZGVerifyCellKZGProofBatchHandler::::default() - .run_for_feature(FeatureName::Eip7594); + .run_for_feature(FeatureName::Fulu); } #[test] fn kzg_recover_cells_and_proofs() { KZGRecoverCellsAndKZGProofHandler::::default() - .run_for_feature(FeatureName::Eip7594); + .run_for_feature(FeatureName::Fulu); } -*/ #[test] fn beacon_state_merkle_proof_validity() { @@ -949,10 +947,16 @@ fn rewards() { } } -/* FIXME(das): re-enable these tests #[test] -fn get_custody_columns() { - GetCustodyColumnsHandler::::default().run_for_feature(FeatureName::Eip7594); - GetCustodyColumnsHandler::::default().run_for_feature(FeatureName::Eip7594); +fn get_custody_groups() { + GetCustodyGroupsHandler::::default().run_for_feature(FeatureName::Fulu); + GetCustodyGroupsHandler::::default().run_for_feature(FeatureName::Fulu); +} + +#[test] +fn compute_columns_for_custody_group() { + ComputeColumnsForCustodyGroupHandler::::default() + .run_for_feature(FeatureName::Fulu); + ComputeColumnsForCustodyGroupHandler::::default() + .run_for_feature(FeatureName::Fulu); } -*/ From b1a19a8b20b26f2efc527238318149dd9d18dab6 Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Wed, 15 Jan 2025 20:43:00 +0800 Subject: [PATCH 078/254] Remove ineffectual block RPC limits post merge (#6798) * Remove ineffectual block RPC limits post merge * Remove more things --- .../lighthouse_network/src/rpc/protocol.rs | 89 +------- consensus/types/src/beacon_block.rs | 194 +----------------- consensus/types/src/execution_payload.rs | 52 ----- 3 files changed, 9 insertions(+), 326 deletions(-) diff --git a/beacon_node/lighthouse_network/src/rpc/protocol.rs b/beacon_node/lighthouse_network/src/rpc/protocol.rs index 780dff937d..80f15c9445 100644 --- a/beacon_node/lighthouse_network/src/rpc/protocol.rs +++ b/beacon_node/lighthouse_network/src/rpc/protocol.rs @@ -17,12 +17,11 @@ use tokio_util::{ compat::{Compat, FuturesAsyncReadCompatExt}, }; use types::{ - BeaconBlock, BeaconBlockAltair, BeaconBlockBase, BeaconBlockCapella, BeaconBlockElectra, - BeaconBlockFulu, BlobSidecar, ChainSpec, DataColumnSidecar, EmptyBlock, EthSpec, EthSpecId, - ForkContext, ForkName, LightClientBootstrap, LightClientBootstrapAltair, - LightClientFinalityUpdate, LightClientFinalityUpdateAltair, LightClientOptimisticUpdate, - LightClientOptimisticUpdateAltair, LightClientUpdate, MainnetEthSpec, MinimalEthSpec, - Signature, SignedBeaconBlock, + BeaconBlock, BeaconBlockAltair, BeaconBlockBase, BlobSidecar, ChainSpec, DataColumnSidecar, + EmptyBlock, EthSpec, EthSpecId, ForkContext, ForkName, LightClientBootstrap, + LightClientBootstrapAltair, LightClientFinalityUpdate, LightClientFinalityUpdateAltair, + LightClientOptimisticUpdate, LightClientOptimisticUpdateAltair, LightClientUpdate, + MainnetEthSpec, MinimalEthSpec, Signature, SignedBeaconBlock, }; // Note: Hardcoding the `EthSpec` type for `SignedBeaconBlock` as min/max values is @@ -55,74 +54,16 @@ pub static SIGNED_BEACON_BLOCK_ALTAIR_MAX: LazyLock = LazyLock::new(|| { .len() }); -pub static SIGNED_BEACON_BLOCK_CAPELLA_MAX_WITHOUT_PAYLOAD: LazyLock = LazyLock::new(|| { - SignedBeaconBlock::::from_block( - BeaconBlock::Capella(BeaconBlockCapella::full(&MainnetEthSpec::default_spec())), - Signature::empty(), - ) - .as_ssz_bytes() - .len() -}); - -pub static SIGNED_BEACON_BLOCK_ELECTRA_MAX_WITHOUT_PAYLOAD: LazyLock = LazyLock::new(|| { - SignedBeaconBlock::::from_block( - BeaconBlock::Electra(BeaconBlockElectra::full(&MainnetEthSpec::default_spec())), - Signature::empty(), - ) - .as_ssz_bytes() - .len() -}); - -pub static SIGNED_BEACON_BLOCK_FULU_MAX_WITHOUT_PAYLOAD: LazyLock = LazyLock::new(|| { - SignedBeaconBlock::::from_block( - BeaconBlock::Fulu(BeaconBlockFulu::full(&MainnetEthSpec::default_spec())), - Signature::empty(), - ) - .as_ssz_bytes() - .len() -}); - /// The `BeaconBlockBellatrix` block has an `ExecutionPayload` field which has a max size ~16 GiB for future proofing. /// We calculate the value from its fields instead of constructing the block and checking the length. /// Note: This is only the theoretical upper bound. We further bound the max size we receive over the network /// with `max_chunk_size`. -/// -/// FIXME: Given that these limits are useless we should probably delete them. See: -/// -/// https://github.com/sigp/lighthouse/issues/6790 pub static SIGNED_BEACON_BLOCK_BELLATRIX_MAX: LazyLock = LazyLock::new(|| // Size of a full altair block *SIGNED_BEACON_BLOCK_ALTAIR_MAX + types::ExecutionPayload::::max_execution_payload_bellatrix_size() // adding max size of execution payload (~16gb) + ssz::BYTES_PER_LENGTH_OFFSET); // Adding the additional ssz offset for the `ExecutionPayload` field -pub static SIGNED_BEACON_BLOCK_CAPELLA_MAX: LazyLock = LazyLock::new(|| { - *SIGNED_BEACON_BLOCK_CAPELLA_MAX_WITHOUT_PAYLOAD - + types::ExecutionPayload::::max_execution_payload_capella_size() // adding max size of execution payload (~16gb) - + ssz::BYTES_PER_LENGTH_OFFSET -}); // Adding the additional ssz offset for the `ExecutionPayload` field - -pub static SIGNED_BEACON_BLOCK_DENEB_MAX: LazyLock = LazyLock::new(|| { - *SIGNED_BEACON_BLOCK_CAPELLA_MAX_WITHOUT_PAYLOAD - + types::ExecutionPayload::::max_execution_payload_deneb_size() // adding max size of execution payload (~16gb) - + ssz::BYTES_PER_LENGTH_OFFSET // Adding the additional offsets for the `ExecutionPayload` - + ssz::BYTES_PER_LENGTH_OFFSET -}); // Length offset for the blob commitments field. - // -pub static SIGNED_BEACON_BLOCK_ELECTRA_MAX: LazyLock = LazyLock::new(|| { - *SIGNED_BEACON_BLOCK_ELECTRA_MAX_WITHOUT_PAYLOAD - + types::ExecutionPayload::::max_execution_payload_electra_size() // adding max size of execution payload (~16gb) - + ssz::BYTES_PER_LENGTH_OFFSET // Adding the additional ssz offset for the `ExecutionPayload` field - + ssz::BYTES_PER_LENGTH_OFFSET -}); // Length offset for the blob commitments field. - -pub static SIGNED_BEACON_BLOCK_FULU_MAX: LazyLock = LazyLock::new(|| { - *SIGNED_BEACON_BLOCK_FULU_MAX_WITHOUT_PAYLOAD - + types::ExecutionPayload::::max_execution_payload_fulu_size() - + ssz::BYTES_PER_LENGTH_OFFSET - + ssz::BYTES_PER_LENGTH_OFFSET -}); - pub static BLOB_SIDECAR_SIZE: LazyLock = LazyLock::new(BlobSidecar::::max_size); @@ -203,26 +144,12 @@ pub fn rpc_block_limits_by_fork(current_fork: ForkName) -> RpcLimits { *SIGNED_BEACON_BLOCK_BASE_MIN, // Base block is smaller than altair blocks *SIGNED_BEACON_BLOCK_ALTAIR_MAX, // Altair block is larger than base blocks ), - ForkName::Bellatrix => RpcLimits::new( + // After the merge the max SSZ size of a block is absurdly big. The size is actually + // bound by other constants, so here we default to the bellatrix's max value + _ => RpcLimits::new( *SIGNED_BEACON_BLOCK_BASE_MIN, // Base block is smaller than altair and bellatrix blocks *SIGNED_BEACON_BLOCK_BELLATRIX_MAX, // Bellatrix block is larger than base and altair blocks ), - ForkName::Capella => RpcLimits::new( - *SIGNED_BEACON_BLOCK_BASE_MIN, // Base block is smaller than altair and bellatrix blocks - *SIGNED_BEACON_BLOCK_CAPELLA_MAX, // Capella block is larger than base, altair and merge blocks - ), - ForkName::Deneb => RpcLimits::new( - *SIGNED_BEACON_BLOCK_BASE_MIN, // Base block is smaller than altair and bellatrix blocks - *SIGNED_BEACON_BLOCK_DENEB_MAX, // Deneb block is larger than all prior fork blocks - ), - ForkName::Electra => RpcLimits::new( - *SIGNED_BEACON_BLOCK_BASE_MIN, // Base block is smaller than altair and bellatrix blocks - *SIGNED_BEACON_BLOCK_ELECTRA_MAX, // Electra block is larger than Deneb block - ), - ForkName::Fulu => RpcLimits::new( - *SIGNED_BEACON_BLOCK_BASE_MIN, // Base block is smaller than all other blocks - *SIGNED_BEACON_BLOCK_FULU_MAX, // Fulu block is largest - ), } } diff --git a/consensus/types/src/beacon_block.rs b/consensus/types/src/beacon_block.rs index d72550aa12..6ea897cf1a 100644 --- a/consensus/types/src/beacon_block.rs +++ b/consensus/types/src/beacon_block.rs @@ -12,7 +12,7 @@ use test_random_derive::TestRandom; use tree_hash::TreeHash; use tree_hash_derive::TreeHash; -use self::indexed_attestation::{IndexedAttestationBase, IndexedAttestationElectra}; +use self::indexed_attestation::IndexedAttestationBase; /// A block of the `BeaconChain`. #[superstruct( @@ -499,52 +499,6 @@ impl> EmptyBlock for BeaconBlockBell } } -impl> BeaconBlockCapella { - /// Return a Capella block where the block has maximum size. - pub fn full(spec: &ChainSpec) -> Self { - let base_block: BeaconBlockBase<_, Payload> = BeaconBlockBase::full(spec); - let bls_to_execution_changes = vec![ - SignedBlsToExecutionChange { - message: BlsToExecutionChange { - validator_index: 0, - from_bls_pubkey: PublicKeyBytes::empty(), - to_execution_address: Address::ZERO, - }, - signature: Signature::empty() - }; - E::max_bls_to_execution_changes() - ] - .into(); - let sync_aggregate = SyncAggregate { - sync_committee_signature: AggregateSignature::empty(), - sync_committee_bits: BitVector::default(), - }; - BeaconBlockCapella { - slot: spec.genesis_slot, - proposer_index: 0, - parent_root: Hash256::zero(), - state_root: Hash256::zero(), - body: BeaconBlockBodyCapella { - proposer_slashings: base_block.body.proposer_slashings, - attester_slashings: base_block.body.attester_slashings, - attestations: base_block.body.attestations, - deposits: base_block.body.deposits, - voluntary_exits: base_block.body.voluntary_exits, - bls_to_execution_changes, - sync_aggregate, - randao_reveal: Signature::empty(), - eth1_data: Eth1Data { - deposit_root: Hash256::zero(), - block_hash: Hash256::zero(), - deposit_count: 0, - }, - graffiti: Graffiti::default(), - execution_payload: Payload::Capella::default(), - }, - } - } -} - impl> EmptyBlock for BeaconBlockCapella { /// Returns an empty Capella block to be used during genesis. fn empty(spec: &ChainSpec) -> Self { @@ -604,79 +558,6 @@ impl> EmptyBlock for BeaconBlockDene } } -impl> BeaconBlockElectra { - /// Return a Electra block where the block has maximum size. - pub fn full(spec: &ChainSpec) -> Self { - let base_block: BeaconBlockBase<_, Payload> = BeaconBlockBase::full(spec); - let indexed_attestation: IndexedAttestationElectra = IndexedAttestationElectra { - attesting_indices: VariableList::new(vec![0_u64; E::MaxValidatorsPerSlot::to_usize()]) - .unwrap(), - data: AttestationData::default(), - signature: AggregateSignature::empty(), - }; - let attester_slashings = vec![ - AttesterSlashingElectra { - attestation_1: indexed_attestation.clone(), - attestation_2: indexed_attestation, - }; - E::max_attester_slashings_electra() - ] - .into(); - let attestation = AttestationElectra { - aggregation_bits: BitList::with_capacity(E::MaxValidatorsPerSlot::to_usize()).unwrap(), - data: AttestationData::default(), - signature: AggregateSignature::empty(), - committee_bits: BitVector::new(), - }; - let mut attestations_electra = vec![]; - for _ in 0..E::MaxAttestationsElectra::to_usize() { - attestations_electra.push(attestation.clone()); - } - - let bls_to_execution_changes = vec![ - SignedBlsToExecutionChange { - message: BlsToExecutionChange { - validator_index: 0, - from_bls_pubkey: PublicKeyBytes::empty(), - to_execution_address: Address::ZERO, - }, - signature: Signature::empty() - }; - E::max_bls_to_execution_changes() - ] - .into(); - let sync_aggregate = SyncAggregate { - sync_committee_signature: AggregateSignature::empty(), - sync_committee_bits: BitVector::default(), - }; - BeaconBlockElectra { - slot: spec.genesis_slot, - proposer_index: 0, - parent_root: Hash256::zero(), - state_root: Hash256::zero(), - body: BeaconBlockBodyElectra { - proposer_slashings: base_block.body.proposer_slashings, - attester_slashings, - attestations: attestations_electra.into(), - deposits: base_block.body.deposits, - voluntary_exits: base_block.body.voluntary_exits, - bls_to_execution_changes, - sync_aggregate, - randao_reveal: Signature::empty(), - eth1_data: Eth1Data { - deposit_root: Hash256::zero(), - block_hash: Hash256::zero(), - deposit_count: 0, - }, - graffiti: Graffiti::default(), - execution_payload: Payload::Electra::default(), - blob_kzg_commitments: VariableList::empty(), - execution_requests: ExecutionRequests::default(), - }, - } - } -} - impl> EmptyBlock for BeaconBlockElectra { /// Returns an empty Electra block to be used during genesis. fn empty(spec: &ChainSpec) -> Self { @@ -708,79 +589,6 @@ impl> EmptyBlock for BeaconBlockElec } } -impl> BeaconBlockFulu { - /// Return a Fulu block where the block has maximum size. - pub fn full(spec: &ChainSpec) -> Self { - let base_block: BeaconBlockBase<_, Payload> = BeaconBlockBase::full(spec); - let indexed_attestation: IndexedAttestationElectra = IndexedAttestationElectra { - attesting_indices: VariableList::new(vec![0_u64; E::MaxValidatorsPerSlot::to_usize()]) - .unwrap(), - data: AttestationData::default(), - signature: AggregateSignature::empty(), - }; - let attester_slashings = vec![ - AttesterSlashingElectra { - attestation_1: indexed_attestation.clone(), - attestation_2: indexed_attestation, - }; - E::max_attester_slashings_electra() - ] - .into(); - let attestation = AttestationElectra { - aggregation_bits: BitList::with_capacity(E::MaxValidatorsPerSlot::to_usize()).unwrap(), - data: AttestationData::default(), - signature: AggregateSignature::empty(), - committee_bits: BitVector::new(), - }; - let mut attestations_electra = vec![]; - for _ in 0..E::MaxAttestationsElectra::to_usize() { - attestations_electra.push(attestation.clone()); - } - - let bls_to_execution_changes = vec![ - SignedBlsToExecutionChange { - message: BlsToExecutionChange { - validator_index: 0, - from_bls_pubkey: PublicKeyBytes::empty(), - to_execution_address: Address::ZERO, - }, - signature: Signature::empty() - }; - E::max_bls_to_execution_changes() - ] - .into(); - let sync_aggregate = SyncAggregate { - sync_committee_signature: AggregateSignature::empty(), - sync_committee_bits: BitVector::default(), - }; - BeaconBlockFulu { - slot: spec.genesis_slot, - proposer_index: 0, - parent_root: Hash256::zero(), - state_root: Hash256::zero(), - body: BeaconBlockBodyFulu { - proposer_slashings: base_block.body.proposer_slashings, - attester_slashings, - attestations: attestations_electra.into(), - deposits: base_block.body.deposits, - voluntary_exits: base_block.body.voluntary_exits, - bls_to_execution_changes, - sync_aggregate, - randao_reveal: Signature::empty(), - eth1_data: Eth1Data { - deposit_root: Hash256::zero(), - block_hash: Hash256::zero(), - deposit_count: 0, - }, - graffiti: Graffiti::default(), - execution_payload: Payload::Fulu::default(), - blob_kzg_commitments: VariableList::empty(), - execution_requests: ExecutionRequests::default(), - }, - } - } -} - impl> EmptyBlock for BeaconBlockFulu { /// Returns an empty Fulu block to be used during genesis. fn empty(spec: &ChainSpec) -> Self { diff --git a/consensus/types/src/execution_payload.rs b/consensus/types/src/execution_payload.rs index c619d61487..2df66343af 100644 --- a/consensus/types/src/execution_payload.rs +++ b/consensus/types/src/execution_payload.rs @@ -128,58 +128,6 @@ impl ExecutionPayload { // Max size of variable length `transactions` field + (E::max_transactions_per_payload() * (ssz::BYTES_PER_LENGTH_OFFSET + E::max_bytes_per_transaction())) } - - #[allow(clippy::arithmetic_side_effects)] - /// Returns the maximum size of an execution payload. - pub fn max_execution_payload_capella_size() -> usize { - // Fixed part - ExecutionPayloadCapella::::default().as_ssz_bytes().len() - // Max size of variable length `extra_data` field - + (E::max_extra_data_bytes() * ::ssz_fixed_len()) - // Max size of variable length `transactions` field - + (E::max_transactions_per_payload() * (ssz::BYTES_PER_LENGTH_OFFSET + E::max_bytes_per_transaction())) - // Max size of variable length `withdrawals` field - + (E::max_withdrawals_per_payload() * ::ssz_fixed_len()) - } - - #[allow(clippy::arithmetic_side_effects)] - /// Returns the maximum size of an execution payload. - pub fn max_execution_payload_deneb_size() -> usize { - // Fixed part - ExecutionPayloadDeneb::::default().as_ssz_bytes().len() - // Max size of variable length `extra_data` field - + (E::max_extra_data_bytes() * ::ssz_fixed_len()) - // Max size of variable length `transactions` field - + (E::max_transactions_per_payload() * (ssz::BYTES_PER_LENGTH_OFFSET + E::max_bytes_per_transaction())) - // Max size of variable length `withdrawals` field - + (E::max_withdrawals_per_payload() * ::ssz_fixed_len()) - } - - #[allow(clippy::arithmetic_side_effects)] - /// Returns the maximum size of an execution payload. - pub fn max_execution_payload_electra_size() -> usize { - // Fixed part - ExecutionPayloadElectra::::default().as_ssz_bytes().len() - // Max size of variable length `extra_data` field - + (E::max_extra_data_bytes() * ::ssz_fixed_len()) - // Max size of variable length `transactions` field - + (E::max_transactions_per_payload() * (ssz::BYTES_PER_LENGTH_OFFSET + E::max_bytes_per_transaction())) - // Max size of variable length `withdrawals` field - + (E::max_withdrawals_per_payload() * ::ssz_fixed_len()) - } - - #[allow(clippy::arithmetic_side_effects)] - /// Returns the maximum size of an execution payload. - pub fn max_execution_payload_fulu_size() -> usize { - // Fixed part - ExecutionPayloadFulu::::default().as_ssz_bytes().len() - // Max size of variable length `extra_data` field - + (E::max_extra_data_bytes() * ::ssz_fixed_len()) - // Max size of variable length `transactions` field - + (E::max_transactions_per_payload() * (ssz::BYTES_PER_LENGTH_OFFSET + E::max_bytes_per_transaction())) - // Max size of variable length `withdrawals` field - + (E::max_withdrawals_per_payload() * ::ssz_fixed_len()) - } } impl ForkVersionDeserialize for ExecutionPayload { From 669932aa671c69013f6133cdda7cb6c19b0832ae Mon Sep 17 00:00:00 2001 From: Daniel Knopik <107140945+dknopik@users.noreply.github.com> Date: Thu, 16 Jan 2025 02:48:50 +0100 Subject: [PATCH 079/254] Misc. dependency cleanup (#6810) * remove ensure_dir_exists (2 deps saved) * group UNHANDLED_ERRORs into a generic (2 deps saved) * Introduce separate `health_metrics` crate * separate health_metrics crate * remove metrics from warp_utils * move ProcessHealth::observe and SystemHealth::observe to health_metrics * fix errors * nitpick `Cargo.toml`s --------- Co-authored-by: Daniel Knopik # Conflicts: # Cargo.toml --- Cargo.lock | 21 ++- Cargo.toml | 2 + account_manager/src/validator/create.rs | 11 +- account_manager/src/validator/recover.rs | 8 +- account_manager/src/wallet/mod.rs | 5 +- beacon_node/http_api/Cargo.toml | 1 + .../http_api/src/attestation_performance.rs | 18 +-- beacon_node/http_api/src/attester_duties.rs | 18 ++- beacon_node/http_api/src/block_id.rs | 28 ++-- .../http_api/src/block_packing_efficiency.rs | 12 +- beacon_node/http_api/src/block_rewards.rs | 22 +-- .../http_api/src/build_block_contents.rs | 2 +- beacon_node/http_api/src/lib.rs | 51 ++++--- beacon_node/http_api/src/produce_block.rs | 4 +- beacon_node/http_api/src/proposer_duties.rs | 20 +-- .../http_api/src/standard_block_rewards.rs | 4 +- beacon_node/http_api/src/state_id.rs | 24 ++-- .../http_api/src/sync_committee_rewards.rs | 6 +- beacon_node/http_api/src/sync_committees.rs | 8 +- beacon_node/http_api/src/ui.rs | 6 +- beacon_node/http_metrics/Cargo.toml | 1 + beacon_node/http_metrics/src/metrics.rs | 2 +- common/account_utils/Cargo.toml | 1 - .../src/validator_definitions.rs | 5 +- common/directory/src/lib.rs | 13 +- common/eth2/Cargo.toml | 6 +- common/eth2/src/lighthouse.rs | 122 ----------------- common/health_metrics/Cargo.toml | 12 ++ common/health_metrics/src/lib.rs | 2 + .../src/metrics.rs | 1 + common/health_metrics/src/observe.rs | 127 ++++++++++++++++++ common/monitoring_api/Cargo.toml | 1 + common/monitoring_api/src/gather.rs | 1 + common/monitoring_api/src/lib.rs | 1 + common/validator_dir/Cargo.toml | 1 - common/validator_dir/src/builder.rs | 5 +- common/warp_utils/Cargo.toml | 2 - common/warp_utils/src/lib.rs | 1 - common/warp_utils/src/reject.rs | 38 +----- validator_client/http_api/Cargo.toml | 1 + validator_client/http_api/src/lib.rs | 1 + validator_client/http_metrics/Cargo.toml | 1 + validator_client/http_metrics/src/lib.rs | 2 +- 43 files changed, 303 insertions(+), 315 deletions(-) create mode 100644 common/health_metrics/Cargo.toml create mode 100644 common/health_metrics/src/lib.rs rename common/{warp_utils => health_metrics}/src/metrics.rs (99%) create mode 100644 common/health_metrics/src/observe.rs diff --git a/Cargo.lock b/Cargo.lock index c62e9fbc87..aa9bdd2afc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -43,7 +43,6 @@ dependencies = [ name = "account_utils" version = "0.1.0" dependencies = [ - "directory", "eth2_keystore", "eth2_wallet", "filesystem", @@ -2572,9 +2571,7 @@ dependencies = [ "lighthouse_network", "mediatype", "pretty_reqwest_error", - "procfs", "proto_array", - "psutil", "reqwest", "reqwest-eventsource", "sensitive_url", @@ -3710,6 +3707,16 @@ dependencies = [ "http 0.2.12", ] +[[package]] +name = "health_metrics" +version = "0.1.0" +dependencies = [ + "eth2", + "metrics", + "procfs", + "psutil", +] + [[package]] name = "heck" version = "0.4.1" @@ -3951,6 +3958,7 @@ dependencies = [ "execution_layer", "futures", "genesis", + "health_metrics", "hex", "lighthouse_network", "lighthouse_version", @@ -3986,6 +3994,7 @@ name = "http_metrics" version = "0.1.0" dependencies = [ "beacon_chain", + "health_metrics", "lighthouse_network", "lighthouse_version", "logging", @@ -5716,6 +5725,7 @@ name = "monitoring_api" version = "0.1.0" dependencies = [ "eth2", + "health_metrics", "lighthouse_version", "metrics", "regex", @@ -9561,7 +9571,6 @@ dependencies = [ "bls", "deposit_contract", "derivative", - "directory", "eth2_keystore", "filesystem", "hex", @@ -9589,6 +9598,7 @@ dependencies = [ "filesystem", "futures", "graffiti_file", + "health_metrics", "initialized_validators", "itertools 0.10.5", "lighthouse_version", @@ -9621,6 +9631,7 @@ dependencies = [ name = "validator_http_metrics" version = "0.1.0" dependencies = [ + "health_metrics", "lighthouse_version", "malloc_utils", "metrics", @@ -9799,7 +9810,6 @@ dependencies = [ name = "warp_utils" version = "0.1.0" dependencies = [ - "beacon_chain", "bytes", "eth2", "headers", @@ -9808,7 +9818,6 @@ dependencies = [ "serde", "serde_array_query", "serde_json", - "state_processing", "tokio", "types", "warp", diff --git a/Cargo.toml b/Cargo.toml index 233e5fa775..e30b6aa2b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ members = [ "common/eth2_network_config", "common/eth2_wallet_manager", "common/filesystem", + "common/health_metrics", "common/lighthouse_version", "common/lockfile", "common/logging", @@ -252,6 +253,7 @@ filesystem = { path = "common/filesystem" } fork_choice = { path = "consensus/fork_choice" } genesis = { path = "beacon_node/genesis" } gossipsub = { path = "beacon_node/lighthouse_network/gossipsub/" } +health_metrics = { path = "common/health_metrics" } http_api = { path = "beacon_node/http_api" } initialized_validators = { path = "validator_client/initialized_validators" } int_to_bytes = { path = "consensus/int_to_bytes" } diff --git a/account_manager/src/validator/create.rs b/account_manager/src/validator/create.rs index 73e0ad54d4..3db8c3f152 100644 --- a/account_manager/src/validator/create.rs +++ b/account_manager/src/validator/create.rs @@ -6,14 +6,13 @@ use account_utils::{ }; use clap::{Arg, ArgAction, ArgMatches, Command}; use clap_utils::FLAG_HEADER; -use directory::{ - ensure_dir_exists, parse_path_or_default_with_flag, DEFAULT_SECRET_DIR, DEFAULT_WALLET_DIR, -}; +use directory::{parse_path_or_default_with_flag, DEFAULT_SECRET_DIR, DEFAULT_WALLET_DIR}; use environment::Environment; use eth2_wallet_manager::WalletManager; use slashing_protection::{SlashingDatabase, SLASHING_PROTECTION_FILENAME}; use std::ffi::OsStr; use std::fs; +use std::fs::create_dir_all; use std::path::{Path, PathBuf}; use types::EthSpec; use validator_dir::Builder as ValidatorDirBuilder; @@ -156,8 +155,10 @@ pub fn cli_run( )); } - ensure_dir_exists(&validator_dir)?; - ensure_dir_exists(&secrets_dir)?; + create_dir_all(&validator_dir) + .map_err(|e| format!("Could not create validator dir at {validator_dir:?}: {e:?}"))?; + create_dir_all(&secrets_dir) + .map_err(|e| format!("Could not create secrets dir at {secrets_dir:?}: {e:?}"))?; eprintln!("secrets-dir path {:?}", secrets_dir); eprintln!("wallets-dir path {:?}", wallet_base_dir); diff --git a/account_manager/src/validator/recover.rs b/account_manager/src/validator/recover.rs index ddf754edac..19d161a468 100644 --- a/account_manager/src/validator/recover.rs +++ b/account_manager/src/validator/recover.rs @@ -5,10 +5,10 @@ use account_utils::eth2_keystore::{keypair_from_secret, Keystore, KeystoreBuilde use account_utils::{random_password, read_mnemonic_from_cli, STDIN_INPUTS_FLAG}; use clap::{Arg, ArgAction, ArgMatches, Command}; use clap_utils::FLAG_HEADER; -use directory::ensure_dir_exists; use directory::{parse_path_or_default_with_flag, DEFAULT_SECRET_DIR}; use eth2_wallet::bip39::Seed; use eth2_wallet::{recover_validator_secret_from_mnemonic, KeyType, ValidatorKeystores}; +use std::fs::create_dir_all; use std::path::PathBuf; use validator_dir::Builder as ValidatorDirBuilder; pub const CMD: &str = "recover"; @@ -91,8 +91,10 @@ pub fn cli_run(matches: &ArgMatches, validator_dir: PathBuf) -> Result<(), Strin eprintln!("secrets-dir path: {:?}", secrets_dir); - ensure_dir_exists(&validator_dir)?; - ensure_dir_exists(&secrets_dir)?; + create_dir_all(&validator_dir) + .map_err(|e| format!("Could not create validator dir at {validator_dir:?}: {e:?}"))?; + create_dir_all(&secrets_dir) + .map_err(|e| format!("Could not create secrets dir at {secrets_dir:?}: {e:?}"))?; eprintln!(); eprintln!("WARNING: KEY RECOVERY CAN LEAD TO DUPLICATING VALIDATORS KEYS, WHICH CAN LEAD TO SLASHING."); diff --git a/account_manager/src/wallet/mod.rs b/account_manager/src/wallet/mod.rs index 020858db77..c34f0363a4 100644 --- a/account_manager/src/wallet/mod.rs +++ b/account_manager/src/wallet/mod.rs @@ -5,7 +5,8 @@ pub mod recover; use crate::WALLETS_DIR_FLAG; use clap::{Arg, ArgAction, ArgMatches, Command}; use clap_utils::FLAG_HEADER; -use directory::{ensure_dir_exists, parse_path_or_default_with_flag, DEFAULT_WALLET_DIR}; +use directory::{parse_path_or_default_with_flag, DEFAULT_WALLET_DIR}; +use std::fs::create_dir_all; use std::path::PathBuf; pub const CMD: &str = "wallet"; @@ -44,7 +45,7 @@ pub fn cli_run(matches: &ArgMatches) -> Result<(), String> { } else { parse_path_or_default_with_flag(matches, WALLETS_DIR_FLAG, DEFAULT_WALLET_DIR)? }; - ensure_dir_exists(&wallet_base_dir)?; + create_dir_all(&wallet_base_dir).map_err(|_| "Could not create wallet base dir")?; eprintln!("wallet-dir path: {:?}", wallet_base_dir); diff --git a/beacon_node/http_api/Cargo.toml b/beacon_node/http_api/Cargo.toml index 5d601008bc..0ced27e446 100644 --- a/beacon_node/http_api/Cargo.toml +++ b/beacon_node/http_api/Cargo.toml @@ -17,6 +17,7 @@ ethereum_serde_utils = { workspace = true } ethereum_ssz = { workspace = true } execution_layer = { workspace = true } futures = { workspace = true } +health_metrics = { workspace = true } hex = { workspace = true } lighthouse_network = { workspace = true } lighthouse_version = { workspace = true } diff --git a/beacon_node/http_api/src/attestation_performance.rs b/beacon_node/http_api/src/attestation_performance.rs index d4f9916814..2f3f340445 100644 --- a/beacon_node/http_api/src/attestation_performance.rs +++ b/beacon_node/http_api/src/attestation_performance.rs @@ -7,7 +7,7 @@ use state_processing::{ }; use std::sync::Arc; use types::{BeaconState, BeaconStateError, EthSpec, Hash256}; -use warp_utils::reject::{beacon_chain_error, custom_bad_request, custom_server_error}; +use warp_utils::reject::{custom_bad_request, custom_server_error, unhandled_error}; const MAX_REQUEST_RANGE_EPOCHS: usize = 100; const BLOCK_ROOT_CHUNK_SIZE: usize = 100; @@ -50,7 +50,7 @@ pub fn get_attestation_performance( let end_slot = end_epoch.end_slot(T::EthSpec::slots_per_epoch()); // Ensure end_epoch is smaller than the current epoch - 1. - let current_epoch = chain.epoch().map_err(beacon_chain_error)?; + let current_epoch = chain.epoch().map_err(unhandled_error)?; if query.end_epoch >= current_epoch - 1 { return Err(custom_bad_request(format!( "end_epoch must be less than the current epoch - 1. current: {}, end: {}", @@ -83,7 +83,7 @@ pub fn get_attestation_performance( let index_range = if target.to_lowercase() == "global" { chain .with_head(|head| Ok((0..head.beacon_state.validators().len() as u64).collect())) - .map_err(beacon_chain_error)? + .map_err(unhandled_error::)? } else { vec![target.parse::().map_err(|_| { custom_bad_request(format!( @@ -96,10 +96,10 @@ pub fn get_attestation_performance( // Load block roots. let mut block_roots: Vec = chain .forwards_iter_block_roots_until(start_slot, end_slot) - .map_err(beacon_chain_error)? + .map_err(unhandled_error)? .map(|res| res.map(|(root, _)| root)) .collect::, _>>() - .map_err(beacon_chain_error)?; + .map_err(unhandled_error)?; block_roots.dedup(); // Load first block so we can get its parent. @@ -113,7 +113,7 @@ pub fn get_attestation_performance( .and_then(|maybe_block| { maybe_block.ok_or(BeaconChainError::MissingBeaconBlock(*first_block_root)) }) - .map_err(beacon_chain_error)?; + .map_err(unhandled_error)?; // Load the block of the prior slot which will be used to build the starting state. let prior_block = chain @@ -122,14 +122,14 @@ pub fn get_attestation_performance( maybe_block .ok_or_else(|| BeaconChainError::MissingBeaconBlock(first_block.parent_root())) }) - .map_err(beacon_chain_error)?; + .map_err(unhandled_error)?; // Load state for block replay. let state_root = prior_block.state_root(); let state = chain .get_state(&state_root, Some(prior_slot)) .and_then(|maybe_state| maybe_state.ok_or(BeaconChainError::MissingBeaconState(state_root))) - .map_err(beacon_chain_error)?; + .map_err(unhandled_error)?; // Allocate an AttestationPerformance vector for each validator in the range. let mut perfs: Vec = @@ -198,7 +198,7 @@ pub fn get_attestation_performance( .and_then(|maybe_block| { maybe_block.ok_or(BeaconChainError::MissingBeaconBlock(*root)) }) - .map_err(beacon_chain_error) + .map_err(unhandled_error) }) .collect::, _>>()?; diff --git a/beacon_node/http_api/src/attester_duties.rs b/beacon_node/http_api/src/attester_duties.rs index 6c7dc3348c..8905b24cde 100644 --- a/beacon_node/http_api/src/attester_duties.rs +++ b/beacon_node/http_api/src/attester_duties.rs @@ -16,9 +16,7 @@ pub fn attester_duties( request_indices: &[u64], chain: &BeaconChain, ) -> Result { - let current_epoch = chain - .epoch() - .map_err(warp_utils::reject::beacon_chain_error)?; + let current_epoch = chain.epoch().map_err(warp_utils::reject::unhandled_error)?; // Determine what the current epoch would be if we fast-forward our system clock by // `MAXIMUM_GOSSIP_CLOCK_DISPARITY`. @@ -57,7 +55,7 @@ fn cached_attestation_duties( let (duties, dependent_root, execution_status) = chain .validator_attestation_duties(request_indices, request_epoch, head_block_root) - .map_err(warp_utils::reject::beacon_chain_error)?; + .map_err(warp_utils::reject::unhandled_error)?; convert_to_api_response( duties, @@ -82,7 +80,7 @@ fn compute_historic_attester_duties( let (cached_head, execution_status) = chain .canonical_head .head_and_execution_status() - .map_err(warp_utils::reject::beacon_chain_error)?; + .map_err(warp_utils::reject::unhandled_error)?; let head = &cached_head.snapshot; if head.beacon_state.current_epoch() <= request_epoch { @@ -131,13 +129,13 @@ fn compute_historic_attester_duties( state .build_committee_cache(relative_epoch, &chain.spec) .map_err(BeaconChainError::from) - .map_err(warp_utils::reject::beacon_chain_error)?; + .map_err(warp_utils::reject::unhandled_error)?; let dependent_root = state // The only block which decides its own shuffling is the genesis block. .attester_shuffling_decision_root(chain.genesis_block_root, relative_epoch) .map_err(BeaconChainError::from) - .map_err(warp_utils::reject::beacon_chain_error)?; + .map_err(warp_utils::reject::unhandled_error)?; let duties = request_indices .iter() @@ -147,7 +145,7 @@ fn compute_historic_attester_duties( .map_err(BeaconChainError::from) }) .collect::>() - .map_err(warp_utils::reject::beacon_chain_error)?; + .map_err(warp_utils::reject::unhandled_error)?; convert_to_api_response( duties, @@ -181,7 +179,7 @@ fn ensure_state_knows_attester_duties_for_epoch( // A "partial" state advance is adequate since attester duties don't rely on state roots. partial_state_advance(state, Some(state_root), target_slot, spec) .map_err(BeaconChainError::from) - .map_err(warp_utils::reject::beacon_chain_error)?; + .map_err(warp_utils::reject::unhandled_error)?; } Ok(()) @@ -208,7 +206,7 @@ fn convert_to_api_response( let usize_indices = indices.iter().map(|i| *i as usize).collect::>(); let index_to_pubkey_map = chain .validator_pubkey_bytes_many(&usize_indices) - .map_err(warp_utils::reject::beacon_chain_error)?; + .map_err(warp_utils::reject::unhandled_error)?; let data = duties .into_iter() diff --git a/beacon_node/http_api/src/block_id.rs b/beacon_node/http_api/src/block_id.rs index be70f615e3..cdef1521ec 100644 --- a/beacon_node/http_api/src/block_id.rs +++ b/beacon_node/http_api/src/block_id.rs @@ -38,7 +38,7 @@ impl BlockId { let (cached_head, execution_status) = chain .canonical_head .head_and_execution_status() - .map_err(warp_utils::reject::beacon_chain_error)?; + .map_err(warp_utils::reject::unhandled_error)?; Ok(( cached_head.head_block_root(), execution_status.is_optimistic_or_invalid(), @@ -63,10 +63,10 @@ impl BlockId { CoreBlockId::Slot(slot) => { let execution_optimistic = chain .is_optimistic_or_invalid_head() - .map_err(warp_utils::reject::beacon_chain_error)?; + .map_err(warp_utils::reject::unhandled_error)?; let root = chain .block_root_at_slot(*slot, WhenSlotSkipped::None) - .map_err(warp_utils::reject::beacon_chain_error) + .map_err(warp_utils::reject::unhandled_error) .and_then(|root_opt| { root_opt.ok_or_else(|| { warp_utils::reject::custom_not_found(format!( @@ -96,17 +96,17 @@ impl BlockId { .store .block_exists(root) .map_err(BeaconChainError::DBError) - .map_err(warp_utils::reject::beacon_chain_error)? + .map_err(warp_utils::reject::unhandled_error)? { let execution_optimistic = chain .canonical_head .fork_choice_read_lock() .is_optimistic_or_invalid_block(root) .map_err(BeaconChainError::ForkChoiceError) - .map_err(warp_utils::reject::beacon_chain_error)?; + .map_err(warp_utils::reject::unhandled_error)?; let blinded_block = chain .get_blinded_block(root) - .map_err(warp_utils::reject::beacon_chain_error)? + .map_err(warp_utils::reject::unhandled_error)? .ok_or_else(|| { warp_utils::reject::custom_not_found(format!( "beacon block with root {}", @@ -116,7 +116,7 @@ impl BlockId { let block_slot = blinded_block.slot(); let finalized = chain .is_finalized_block(root, block_slot) - .map_err(warp_utils::reject::beacon_chain_error)?; + .map_err(warp_utils::reject::unhandled_error)?; Ok((*root, execution_optimistic, finalized)) } else { Err(warp_utils::reject::custom_not_found(format!( @@ -134,7 +134,7 @@ impl BlockId { ) -> Result>, warp::Rejection> { chain .get_blinded_block(root) - .map_err(warp_utils::reject::beacon_chain_error) + .map_err(warp_utils::reject::unhandled_error) } /// Return the `SignedBeaconBlock` identified by `self`. @@ -154,7 +154,7 @@ impl BlockId { let (cached_head, execution_status) = chain .canonical_head .head_and_execution_status() - .map_err(warp_utils::reject::beacon_chain_error)?; + .map_err(warp_utils::reject::unhandled_error)?; Ok(( cached_head.snapshot.beacon_block.clone_as_blinded(), execution_status.is_optimistic_or_invalid(), @@ -211,7 +211,7 @@ impl BlockId { let (cached_head, execution_status) = chain .canonical_head .head_and_execution_status() - .map_err(warp_utils::reject::beacon_chain_error)?; + .map_err(warp_utils::reject::unhandled_error)?; Ok(( cached_head.snapshot.beacon_block.clone(), execution_status.is_optimistic_or_invalid(), @@ -223,7 +223,7 @@ impl BlockId { chain .get_block(&root) .await - .map_err(warp_utils::reject::beacon_chain_error) + .map_err(warp_utils::reject::unhandled_error) .and_then(|block_opt| match block_opt { Some(block) => { if block.slot() != *slot { @@ -245,7 +245,7 @@ impl BlockId { chain .get_block(&root) .await - .map_err(warp_utils::reject::beacon_chain_error) + .map_err(warp_utils::reject::unhandled_error) .and_then(|block_opt| { block_opt .map(|block| (Arc::new(block), execution_optimistic, finalized)) @@ -311,7 +311,7 @@ impl BlockId { let blob_sidecar_list = chain .store .get_blobs(&root) - .map_err(|e| warp_utils::reject::beacon_chain_error(e.into()))? + .map_err(|e| warp_utils::reject::unhandled_error(BeaconChainError::from(e)))? .blobs() .ok_or_else(|| { warp_utils::reject::custom_not_found(format!("no blobs stored for block {root}")) @@ -356,7 +356,7 @@ impl BlockId { |column_index| match chain.get_data_column(&root, &column_index) { Ok(Some(data_column)) => Some(Ok(data_column)), Ok(None) => None, - Err(e) => Some(Err(warp_utils::reject::beacon_chain_error(e))), + Err(e) => Some(Err(warp_utils::reject::unhandled_error(e))), }, ) .collect::, _>>()?; diff --git a/beacon_node/http_api/src/block_packing_efficiency.rs b/beacon_node/http_api/src/block_packing_efficiency.rs index 66c7187278..431547f10b 100644 --- a/beacon_node/http_api/src/block_packing_efficiency.rs +++ b/beacon_node/http_api/src/block_packing_efficiency.rs @@ -13,7 +13,7 @@ use types::{ AttestationRef, BeaconCommittee, BeaconState, BeaconStateError, BlindedPayload, ChainSpec, Epoch, EthSpec, Hash256, OwnedBeaconCommittee, RelativeEpoch, SignedBeaconBlock, Slot, }; -use warp_utils::reject::{beacon_chain_error, custom_bad_request, custom_server_error}; +use warp_utils::reject::{custom_bad_request, custom_server_error, unhandled_error}; /// Load blocks from block roots in chunks to reduce load on memory. const BLOCK_ROOT_CHUNK_SIZE: usize = 100; @@ -263,9 +263,9 @@ pub fn get_block_packing_efficiency( // Load block roots. let mut block_roots: Vec = chain .forwards_iter_block_roots_until(start_slot_of_prior_epoch, end_slot) - .map_err(beacon_chain_error)? + .map_err(unhandled_error)? .collect::, _>>() - .map_err(beacon_chain_error)? + .map_err(unhandled_error)? .iter() .map(|(root, _)| *root) .collect(); @@ -280,7 +280,7 @@ pub fn get_block_packing_efficiency( .and_then(|maybe_block| { maybe_block.ok_or(BeaconChainError::MissingBeaconBlock(*first_block_root)) }) - .map_err(beacon_chain_error)?; + .map_err(unhandled_error)?; // Load state for block replay. let starting_state_root = first_block.state_root(); @@ -290,7 +290,7 @@ pub fn get_block_packing_efficiency( .and_then(|maybe_state| { maybe_state.ok_or(BeaconChainError::MissingBeaconState(starting_state_root)) }) - .map_err(beacon_chain_error)?; + .map_err(unhandled_error)?; // Initialize response vector. let mut response = Vec::new(); @@ -392,7 +392,7 @@ pub fn get_block_packing_efficiency( .and_then(|maybe_block| { maybe_block.ok_or(BeaconChainError::MissingBeaconBlock(*root)) }) - .map_err(beacon_chain_error) + .map_err(unhandled_error) }) .collect::, _>>()?; diff --git a/beacon_node/http_api/src/block_rewards.rs b/beacon_node/http_api/src/block_rewards.rs index ad71e9e9d0..0cc878bb48 100644 --- a/beacon_node/http_api/src/block_rewards.rs +++ b/beacon_node/http_api/src/block_rewards.rs @@ -7,7 +7,7 @@ use std::num::NonZeroUsize; use std::sync::Arc; use types::beacon_block::BlindedBeaconBlock; use types::non_zero_usize::new_non_zero_usize; -use warp_utils::reject::{beacon_chain_error, beacon_state_error, custom_bad_request}; +use warp_utils::reject::{beacon_state_error, custom_bad_request, unhandled_error}; const STATE_CACHE_SIZE: NonZeroUsize = new_non_zero_usize(2); @@ -30,23 +30,23 @@ pub fn get_block_rewards( let end_block_root = chain .block_root_at_slot(end_slot, WhenSlotSkipped::Prev) - .map_err(beacon_chain_error)? + .map_err(unhandled_error)? .ok_or_else(|| custom_bad_request(format!("block at end slot {} unknown", end_slot)))?; let blocks = chain .store .load_blocks_to_replay(start_slot, end_slot, end_block_root) - .map_err(|e| beacon_chain_error(e.into()))?; + .map_err(|e| unhandled_error(BeaconChainError::from(e)))?; let state_root = chain .state_root_at_slot(prior_slot) - .map_err(beacon_chain_error)? + .map_err(unhandled_error)? .ok_or_else(|| custom_bad_request(format!("prior state at slot {} unknown", prior_slot)))?; let mut state = chain .get_state(&state_root, Some(prior_slot)) .and_then(|maybe_state| maybe_state.ok_or(BeaconChainError::MissingBeaconState(state_root))) - .map_err(beacon_chain_error)?; + .map_err(unhandled_error)?; state .build_caches(&chain.spec) @@ -73,12 +73,12 @@ pub fn get_block_rewards( .state_root_iter( chain .forwards_iter_state_roots_until(prior_slot, end_slot) - .map_err(beacon_chain_error)?, + .map_err(unhandled_error)?, ) .no_signature_verification() .minimal_block_root_verification() .apply_blocks(blocks, None) - .map_err(beacon_chain_error)?; + .map_err(unhandled_error)?; if block_replayer.state_root_miss() { warn!( @@ -125,7 +125,7 @@ pub fn compute_block_rewards( ); let parent_block = chain .get_blinded_block(&parent_root) - .map_err(beacon_chain_error)? + .map_err(unhandled_error)? .ok_or_else(|| { custom_bad_request(format!( "parent block not known or not canonical: {:?}", @@ -135,7 +135,7 @@ pub fn compute_block_rewards( let parent_state = chain .get_state(&parent_block.state_root(), Some(parent_block.slot())) - .map_err(beacon_chain_error)? + .map_err(unhandled_error)? .ok_or_else(|| { custom_bad_request(format!( "no state known for parent block: {:?}", @@ -148,7 +148,7 @@ pub fn compute_block_rewards( .state_root_iter([Ok((parent_block.state_root(), parent_block.slot()))].into_iter()) .minimal_block_root_verification() .apply_blocks(vec![], Some(block.slot())) - .map_err(beacon_chain_error)?; + .map_err(unhandled_error::)?; if block_replayer.state_root_miss() { warn!( @@ -176,7 +176,7 @@ pub fn compute_block_rewards( &mut reward_cache, true, ) - .map_err(beacon_chain_error)?; + .map_err(unhandled_error)?; block_rewards.push(block_reward); } diff --git a/beacon_node/http_api/src/build_block_contents.rs b/beacon_node/http_api/src/build_block_contents.rs index c2ccb6695e..fb8fba0731 100644 --- a/beacon_node/http_api/src/build_block_contents.rs +++ b/beacon_node/http_api/src/build_block_contents.rs @@ -23,7 +23,7 @@ pub fn build_block_contents( } = block; let Some((kzg_proofs, blobs)) = blob_items else { - return Err(warp_utils::reject::block_production_error( + return Err(warp_utils::reject::unhandled_error( BlockProductionError::MissingBlobs, )); }; diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index febdf69259..d5c6c11567 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -50,6 +50,7 @@ use eth2::types::{ ValidatorStatus, ValidatorsRequestBody, }; use eth2::{CONSENSUS_VERSION_HEADER, CONTENT_TYPE_HEADER, SSZ_CONTENT_TYPE_HEADER}; +use health_metrics::observe::Observe; use lighthouse_network::{types::SyncState, EnrExt, NetworkGlobals, PeerId, PubsubMessage}; use lighthouse_version::version_with_platform; use logging::SSELoggingComponents; @@ -938,9 +939,9 @@ pub fn serve( ) } } - _ => { - warp_utils::reject::beacon_chain_error(e.into()) - } + _ => warp_utils::reject::unhandled_error( + BeaconChainError::from(e), + ), } })?; @@ -1067,7 +1068,7 @@ pub fn serve( let validators = chain .validator_indices(sync_committee.pubkeys.iter()) - .map_err(warp_utils::reject::beacon_chain_error)?; + .map_err(warp_utils::reject::unhandled_error)?; let validator_aggregates = validators .chunks_exact(T::EthSpec::sync_subcommittee_size()) @@ -1147,7 +1148,7 @@ pub fn serve( let (cached_head, execution_status) = chain .canonical_head .head_and_execution_status() - .map_err(warp_utils::reject::beacon_chain_error)?; + .map_err(warp_utils::reject::unhandled_error)?; ( cached_head.head_block_root(), cached_head.snapshot.beacon_block.clone_as_blinded(), @@ -1161,13 +1162,13 @@ pub fn serve( BlockId::from_root(parent_root).blinded_block(&chain)?; let (root, _slot) = chain .forwards_iter_block_roots(parent.slot()) - .map_err(warp_utils::reject::beacon_chain_error)? + .map_err(warp_utils::reject::unhandled_error)? // Ignore any skip-slots immediately following the parent. .find(|res| { res.as_ref().is_ok_and(|(root, _)| *root != parent_root) }) .transpose() - .map_err(warp_utils::reject::beacon_chain_error)? + .map_err(warp_utils::reject::unhandled_error)? .ok_or_else(|| { warp_utils::reject::custom_not_found(format!( "child of block with root {}", @@ -1248,7 +1249,7 @@ pub fn serve( let canonical = chain .block_root_at_slot(block.slot(), WhenSlotSkipped::None) - .map_err(warp_utils::reject::beacon_chain_error)? + .map_err(warp_utils::reject::unhandled_error)? .is_some_and(|canonical| root == canonical); let data = api_types::BlockHeaderData { @@ -2932,7 +2933,7 @@ pub fn serve( let (head, head_execution_status) = chain .canonical_head .head_and_execution_status() - .map_err(warp_utils::reject::beacon_chain_error)?; + .map_err(warp_utils::reject::unhandled_error)?; let head_slot = head.head_slot(); let current_slot = chain.slot_clock.now_or_genesis().ok_or_else(|| { @@ -2992,7 +2993,7 @@ pub fn serve( .blocking_response_task(Priority::P0, move || { let is_optimistic = chain .is_optimistic_or_invalid_head() - .map_err(warp_utils::reject::beacon_chain_error)?; + .map_err(warp_utils::reject::unhandled_error)?; let is_syncing = !network_globals.sync_state.read().is_synced(); @@ -3302,9 +3303,7 @@ pub fn serve( task_spawner.blocking_json_task(Priority::P0, move || { not_synced_filter?; - let current_slot = chain - .slot() - .map_err(warp_utils::reject::beacon_chain_error)?; + let current_slot = chain.slot().map_err(warp_utils::reject::unhandled_error)?; // allow a tolerance of one slot to account for clock skew if query.slot > current_slot + 1 { @@ -3318,7 +3317,7 @@ pub fn serve( .produce_unaggregated_attestation(query.slot, query.committee_index) .map(|attestation| attestation.data().clone()) .map(api_types::GenericResponse::from) - .map_err(warp_utils::reject::beacon_chain_error) + .map_err(warp_utils::reject::unhandled_error) }) }, ); @@ -3690,11 +3689,9 @@ pub fn serve( .execution_layer .as_ref() .ok_or(BeaconChainError::ExecutionLayerMissing) - .map_err(warp_utils::reject::beacon_chain_error)?; + .map_err(warp_utils::reject::unhandled_error)?; - let current_slot = chain - .slot() - .map_err(warp_utils::reject::beacon_chain_error)?; + let current_slot = chain.slot().map_err(warp_utils::reject::unhandled_error)?; let current_epoch = current_slot.epoch(T::EthSpec::slots_per_epoch()); debug!( @@ -3747,12 +3744,12 @@ pub fn serve( .execution_layer .as_ref() .ok_or(BeaconChainError::ExecutionLayerMissing) - .map_err(warp_utils::reject::beacon_chain_error)?; + .map_err(warp_utils::reject::unhandled_error)?; let current_slot = chain .slot_clock .now_or_genesis() .ok_or(BeaconChainError::UnableToReadSlot) - .map_err(warp_utils::reject::beacon_chain_error)?; + .map_err(warp_utils::reject::unhandled_error)?; let current_epoch = current_slot.epoch(T::EthSpec::slots_per_epoch()); debug!( @@ -3848,12 +3845,12 @@ pub fn serve( .execution_layer .as_ref() .ok_or(BeaconChainError::ExecutionLayerMissing) - .map_err(warp_utils::reject::beacon_chain_error)? + .map_err(warp_utils::reject::unhandled_error)? .builder(); let builder = arc_builder .as_ref() .ok_or(BeaconChainError::BuilderMissing) - .map_err(warp_utils::reject::beacon_chain_error)?; + .map_err(warp_utils::reject::unhandled_error)?; builder .post_builder_validators(&filtered_registration_data) .await @@ -3969,9 +3966,8 @@ pub fn serve( chain: Arc>| { task_spawner.blocking_json_task(Priority::P0, move || { // Ensure the request is for either the current, previous or next epoch. - let current_epoch = chain - .epoch() - .map_err(warp_utils::reject::beacon_chain_error)?; + let current_epoch = + chain.epoch().map_err(warp_utils::reject::unhandled_error)?; let prev_epoch = current_epoch.saturating_sub(Epoch::new(1)); let next_epoch = current_epoch.saturating_add(Epoch::new(1)); @@ -4010,9 +4006,8 @@ pub fn serve( chain: Arc>| { task_spawner.blocking_json_task(Priority::P0, move || { // Ensure the request is for either the current, previous or next epoch. - let current_epoch = chain - .epoch() - .map_err(warp_utils::reject::beacon_chain_error)?; + let current_epoch = + chain.epoch().map_err(warp_utils::reject::unhandled_error)?; let prev_epoch = current_epoch.saturating_sub(Epoch::new(1)); let next_epoch = current_epoch.saturating_add(Epoch::new(1)); diff --git a/beacon_node/http_api/src/produce_block.rs b/beacon_node/http_api/src/produce_block.rs index ed30da7362..0e24e8f175 100644 --- a/beacon_node/http_api/src/produce_block.rs +++ b/beacon_node/http_api/src/produce_block.rs @@ -153,7 +153,7 @@ pub async fn produce_blinded_block_v2( BlockProductionVersion::BlindedV2, ) .await - .map_err(warp_utils::reject::block_production_error)?; + .map_err(warp_utils::reject::unhandled_error)?; build_response_v2(chain, block_response_type, endpoint_version, accept_header) } @@ -184,7 +184,7 @@ pub async fn produce_block_v2( BlockProductionVersion::FullV2, ) .await - .map_err(warp_utils::reject::block_production_error)?; + .map_err(warp_utils::reject::unhandled_error)?; build_response_v2(chain, block_response_type, endpoint_version, accept_header) } diff --git a/beacon_node/http_api/src/proposer_duties.rs b/beacon_node/http_api/src/proposer_duties.rs index 515599ce88..c4945df9d7 100644 --- a/beacon_node/http_api/src/proposer_duties.rs +++ b/beacon_node/http_api/src/proposer_duties.rs @@ -26,7 +26,7 @@ pub fn proposer_duties( .now_or_genesis() .map(|slot| slot.epoch(T::EthSpec::slots_per_epoch())) .ok_or(BeaconChainError::UnableToReadSlot) - .map_err(warp_utils::reject::beacon_chain_error)?; + .map_err(warp_utils::reject::unhandled_error)?; // Determine what the current epoch would be if we fast-forward our system clock by // `MAXIMUM_GOSSIP_CLOCK_DISPARITY`. @@ -66,7 +66,7 @@ pub fn proposer_duties( { let (proposers, dependent_root, execution_status, _fork) = compute_proposer_duties_from_head(request_epoch, chain) - .map_err(warp_utils::reject::beacon_chain_error)?; + .map_err(warp_utils::reject::unhandled_error)?; convert_to_api_response( chain, request_epoch, @@ -114,7 +114,7 @@ fn try_proposer_duties_from_cache( .map_err(warp_utils::reject::beacon_state_error)?; let execution_optimistic = chain .is_optimistic_or_invalid_head_block(head_block) - .map_err(warp_utils::reject::beacon_chain_error)?; + .map_err(warp_utils::reject::unhandled_error)?; let dependent_root = match head_epoch.cmp(&request_epoch) { // head_epoch == request_epoch @@ -163,7 +163,7 @@ fn compute_and_cache_proposer_duties( ) -> Result { let (indices, dependent_root, execution_status, fork) = compute_proposer_duties_from_head(current_epoch, chain) - .map_err(warp_utils::reject::beacon_chain_error)?; + .map_err(warp_utils::reject::unhandled_error)?; // Prime the proposer shuffling cache with the newly-learned value. chain @@ -171,7 +171,7 @@ fn compute_and_cache_proposer_duties( .lock() .insert(current_epoch, dependent_root, indices.clone(), fork) .map_err(BeaconChainError::from) - .map_err(warp_utils::reject::beacon_chain_error)?; + .map_err(warp_utils::reject::unhandled_error)?; convert_to_api_response( chain, @@ -195,7 +195,7 @@ fn compute_historic_proposer_duties( let (cached_head, execution_status) = chain .canonical_head .head_and_execution_status() - .map_err(warp_utils::reject::beacon_chain_error)?; + .map_err(warp_utils::reject::unhandled_error)?; let head = &cached_head.snapshot; if head.beacon_state.current_epoch() <= epoch { @@ -214,7 +214,7 @@ fn compute_historic_proposer_duties( // If we've loaded the head state it might be from a previous epoch, ensure it's in a // suitable epoch. ensure_state_is_in_epoch(&mut state, state_root, epoch, &chain.spec) - .map_err(warp_utils::reject::beacon_chain_error)?; + .map_err(warp_utils::reject::unhandled_error)?; (state, execution_optimistic) } else { let (state, execution_optimistic, _finalized) = @@ -234,14 +234,14 @@ fn compute_historic_proposer_duties( let indices = state .get_beacon_proposer_indices(&chain.spec) .map_err(BeaconChainError::from) - .map_err(warp_utils::reject::beacon_chain_error)?; + .map_err(warp_utils::reject::unhandled_error)?; // We can supply the genesis block root as the block root since we know that the only block that // decides its own root is the genesis block. let dependent_root = state .proposer_shuffling_decision_root(chain.genesis_block_root) .map_err(BeaconChainError::from) - .map_err(warp_utils::reject::beacon_chain_error)?; + .map_err(warp_utils::reject::unhandled_error)?; convert_to_api_response(chain, epoch, dependent_root, execution_optimistic, indices) } @@ -257,7 +257,7 @@ fn convert_to_api_response( ) -> Result { let index_to_pubkey_map = chain .validator_pubkey_bytes_many(&indices) - .map_err(warp_utils::reject::beacon_chain_error)?; + .map_err(warp_utils::reject::unhandled_error)?; // Map our internal data structure into the API structure. let proposer_data = indices diff --git a/beacon_node/http_api/src/standard_block_rewards.rs b/beacon_node/http_api/src/standard_block_rewards.rs index 1ab75374ea..372a2765da 100644 --- a/beacon_node/http_api/src/standard_block_rewards.rs +++ b/beacon_node/http_api/src/standard_block_rewards.rs @@ -4,7 +4,7 @@ use crate::ExecutionOptimistic; use beacon_chain::{BeaconChain, BeaconChainTypes}; use eth2::lighthouse::StandardBlockReward; use std::sync::Arc; -use warp_utils::reject::beacon_chain_error; +use warp_utils::reject::unhandled_error; /// The difference between block_rewards and beacon_block_rewards is the later returns block /// reward format that satisfies beacon-api specs pub fn compute_beacon_block_rewards( @@ -19,7 +19,7 @@ pub fn compute_beacon_block_rewards( let rewards = chain .compute_beacon_block_reward(block_ref, &mut state) - .map_err(beacon_chain_error)?; + .map_err(unhandled_error)?; Ok((rewards, execution_optimistic, finalized)) } diff --git a/beacon_node/http_api/src/state_id.rs b/beacon_node/http_api/src/state_id.rs index ddacde9a3f..353390cdad 100644 --- a/beacon_node/http_api/src/state_id.rs +++ b/beacon_node/http_api/src/state_id.rs @@ -30,7 +30,7 @@ impl StateId { let (cached_head, execution_status) = chain .canonical_head .head_and_execution_status() - .map_err(warp_utils::reject::beacon_chain_error)?; + .map_err(warp_utils::reject::unhandled_error)?; return Ok(( cached_head.head_state_root(), execution_status.is_optimistic_or_invalid(), @@ -56,7 +56,7 @@ impl StateId { *slot, chain .is_optimistic_or_invalid_head() - .map_err(warp_utils::reject::beacon_chain_error)?, + .map_err(warp_utils::reject::unhandled_error)?, *slot <= chain .canonical_head @@ -70,11 +70,11 @@ impl StateId { .store .load_hot_state_summary(root) .map_err(BeaconChainError::DBError) - .map_err(warp_utils::reject::beacon_chain_error)? + .map_err(warp_utils::reject::unhandled_error)? { let finalization_status = chain .state_finalization_and_canonicity(root, hot_summary.slot) - .map_err(warp_utils::reject::beacon_chain_error)?; + .map_err(warp_utils::reject::unhandled_error)?; let finalized = finalization_status.is_finalized(); let fork_choice = chain.canonical_head.fork_choice_read_lock(); let execution_optimistic = if finalization_status.slot_is_finalized @@ -94,14 +94,14 @@ impl StateId { fork_choice .is_optimistic_or_invalid_block(&hot_summary.latest_block_root) .map_err(BeaconChainError::ForkChoiceError) - .map_err(warp_utils::reject::beacon_chain_error)? + .map_err(warp_utils::reject::unhandled_error)? }; return Ok((*root, execution_optimistic, finalized)); } else if let Some(_cold_state_slot) = chain .store .load_cold_state_slot(root) .map_err(BeaconChainError::DBError) - .map_err(warp_utils::reject::beacon_chain_error)? + .map_err(warp_utils::reject::unhandled_error)? { let fork_choice = chain.canonical_head.fork_choice_read_lock(); let finalized_root = fork_choice @@ -111,7 +111,7 @@ impl StateId { let execution_optimistic = fork_choice .is_optimistic_or_invalid_block_no_fallback(&finalized_root) .map_err(BeaconChainError::ForkChoiceError) - .map_err(warp_utils::reject::beacon_chain_error)?; + .map_err(warp_utils::reject::unhandled_error)?; return Ok((*root, execution_optimistic, true)); } else { return Err(warp_utils::reject::custom_not_found(format!( @@ -124,7 +124,7 @@ impl StateId { let root = chain .state_root_at_slot(slot) - .map_err(warp_utils::reject::beacon_chain_error)? + .map_err(warp_utils::reject::unhandled_error)? .ok_or_else(|| { warp_utils::reject::custom_not_found(format!("beacon state at slot {}", slot)) })?; @@ -178,7 +178,7 @@ impl StateId { let (cached_head, execution_status) = chain .canonical_head .head_and_execution_status() - .map_err(warp_utils::reject::beacon_chain_error)?; + .map_err(warp_utils::reject::unhandled_error)?; return Ok(( cached_head.snapshot.beacon_state.clone(), execution_status.is_optimistic_or_invalid(), @@ -191,7 +191,7 @@ impl StateId { let state = chain .get_state(&state_root, slot_opt) - .map_err(warp_utils::reject::beacon_chain_error) + .map_err(warp_utils::reject::unhandled_error) .and_then(|opt| { opt.ok_or_else(|| { warp_utils::reject::custom_not_found(format!( @@ -224,7 +224,7 @@ impl StateId { let (head, execution_status) = chain .canonical_head .head_and_execution_status() - .map_err(warp_utils::reject::beacon_chain_error)?; + .map_err(warp_utils::reject::unhandled_error)?; return func( &head.snapshot.beacon_state, execution_status.is_optimistic_or_invalid(), @@ -273,7 +273,7 @@ pub fn checkpoint_slot_and_execution_optimistic( let execution_optimistic = fork_choice .is_optimistic_or_invalid_block_no_fallback(root) .map_err(BeaconChainError::ForkChoiceError) - .map_err(warp_utils::reject::beacon_chain_error)?; + .map_err(warp_utils::reject::unhandled_error)?; Ok((slot, execution_optimistic)) } diff --git a/beacon_node/http_api/src/sync_committee_rewards.rs b/beacon_node/http_api/src/sync_committee_rewards.rs index 68a06b1ce8..ec63372406 100644 --- a/beacon_node/http_api/src/sync_committee_rewards.rs +++ b/beacon_node/http_api/src/sync_committee_rewards.rs @@ -6,7 +6,7 @@ use slog::{debug, Logger}; use state_processing::BlockReplayer; use std::sync::Arc; use types::{BeaconState, SignedBlindedBeaconBlock}; -use warp_utils::reject::{beacon_chain_error, custom_not_found}; +use warp_utils::reject::{custom_not_found, unhandled_error}; pub fn compute_sync_committee_rewards( chain: Arc>, @@ -20,7 +20,7 @@ pub fn compute_sync_committee_rewards( let reward_payload = chain .compute_sync_committee_rewards(block.message(), &mut state) - .map_err(beacon_chain_error)?; + .map_err(unhandled_error)?; let data = if reward_payload.is_empty() { debug!(log, "compute_sync_committee_rewards returned empty"); @@ -71,7 +71,7 @@ pub fn get_state_before_applying_block( .state_root_iter([Ok((parent_block.state_root(), parent_block.slot()))].into_iter()) .minimal_block_root_verification() .apply_blocks(vec![], Some(block.slot())) - .map_err(beacon_chain_error)?; + .map_err(unhandled_error::)?; Ok(replayer.into_state()) } diff --git a/beacon_node/http_api/src/sync_committees.rs b/beacon_node/http_api/src/sync_committees.rs index 3e5b1dc524..da9f9b7a06 100644 --- a/beacon_node/http_api/src/sync_committees.rs +++ b/beacon_node/http_api/src/sync_committees.rs @@ -39,7 +39,7 @@ pub fn sync_committee_duties( // still dependent on the head. So using `is_optimistic_head` is fine for both cases. let execution_optimistic = chain .is_optimistic_or_invalid_head() - .map_err(warp_utils::reject::beacon_chain_error)?; + .map_err(warp_utils::reject::unhandled_error)?; // Try using the head's sync committees to satisfy the request. This should be sufficient for // the vast majority of requests. Rather than checking if we think the request will succeed in a @@ -55,7 +55,7 @@ pub fn sync_committee_duties( .. })) | Err(BeaconChainError::SyncDutiesError(BeaconStateError::IncorrectStateVariant)) => (), - Err(e) => return Err(warp_utils::reject::beacon_chain_error(e)), + Err(e) => return Err(warp_utils::reject::unhandled_error(e)), } let duties = duties_from_state_load(request_epoch, request_indices, altair_fork_epoch, chain) @@ -67,7 +67,7 @@ pub fn sync_committee_duties( "invalid epoch: {}, current epoch: {}", request_epoch, current_epoch )), - e => warp_utils::reject::beacon_chain_error(e), + e => warp_utils::reject::unhandled_error(e), })?; Ok(convert_to_response( verify_unknown_validators(duties, request_epoch, chain)?, @@ -164,7 +164,7 @@ fn verify_unknown_validators( BeaconChainError::SyncDutiesError(BeaconStateError::UnknownValidator(idx)) => { warp_utils::reject::custom_bad_request(format!("invalid validator index: {idx}")) } - e => warp_utils::reject::beacon_chain_error(e), + e => warp_utils::reject::unhandled_error(e), }) } diff --git a/beacon_node/http_api/src/ui.rs b/beacon_node/http_api/src/ui.rs index 616745dbef..80a9ed896d 100644 --- a/beacon_node/http_api/src/ui.rs +++ b/beacon_node/http_api/src/ui.rs @@ -5,7 +5,7 @@ use eth2::types::{Epoch, ValidatorStatus}; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; use std::sync::Arc; -use warp_utils::reject::beacon_chain_error; +use warp_utils::reject::unhandled_error; #[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] pub struct ValidatorCountResponse { @@ -58,7 +58,7 @@ pub fn get_validator_count( } Ok::<(), BeaconChainError>(()) }) - .map_err(beacon_chain_error)?; + .map_err(unhandled_error)?; Ok(ValidatorCountResponse { active_ongoing, @@ -101,7 +101,7 @@ pub fn get_validator_info( request_data: ValidatorInfoRequestData, chain: Arc>, ) -> Result { - let current_epoch = chain.epoch().map_err(beacon_chain_error)?; + let current_epoch = chain.epoch().map_err(unhandled_error)?; let epochs = current_epoch.saturating_sub(HISTORIC_EPOCHS).as_u64()..=current_epoch.as_u64(); diff --git a/beacon_node/http_metrics/Cargo.toml b/beacon_node/http_metrics/Cargo.toml index d92f986440..9ad073439d 100644 --- a/beacon_node/http_metrics/Cargo.toml +++ b/beacon_node/http_metrics/Cargo.toml @@ -7,6 +7,7 @@ edition = { workspace = true } [dependencies] beacon_chain = { workspace = true } +health_metrics = { workspace = true } lighthouse_network = { workspace = true } lighthouse_version = { workspace = true } malloc_utils = { workspace = true } diff --git a/beacon_node/http_metrics/src/metrics.rs b/beacon_node/http_metrics/src/metrics.rs index d751c51e4c..bcfb8e4c9c 100644 --- a/beacon_node/http_metrics/src/metrics.rs +++ b/beacon_node/http_metrics/src/metrics.rs @@ -39,7 +39,7 @@ pub fn gather_prometheus_metrics( lighthouse_network::scrape_discovery_metrics(); - warp_utils::metrics::scrape_health_metrics(); + health_metrics::metrics::scrape_health_metrics(); // It's important to ensure these metrics are explicitly enabled in the case that users aren't // using glibc and this function causes panics. diff --git a/common/account_utils/Cargo.toml b/common/account_utils/Cargo.toml index dece975d37..3ab6034688 100644 --- a/common/account_utils/Cargo.toml +++ b/common/account_utils/Cargo.toml @@ -6,7 +6,6 @@ edition = { workspace = true } # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -directory = { workspace = true } eth2_keystore = { workspace = true } eth2_wallet = { workspace = true } filesystem = { workspace = true } diff --git a/common/account_utils/src/validator_definitions.rs b/common/account_utils/src/validator_definitions.rs index 24f6861daa..7337d6dfb4 100644 --- a/common/account_utils/src/validator_definitions.rs +++ b/common/account_utils/src/validator_definitions.rs @@ -4,13 +4,12 @@ //! attempt) to load into the `crate::intialized_validators::InitializedValidators` struct. use crate::{default_keystore_password_path, read_password_string, write_file_via_temporary}; -use directory::ensure_dir_exists; use eth2_keystore::Keystore; use regex::Regex; use serde::{Deserialize, Serialize}; use slog::{error, Logger}; use std::collections::HashSet; -use std::fs::{self, File}; +use std::fs::{self, create_dir_all, File}; use std::io; use std::path::{Path, PathBuf}; use types::{graffiti::GraffitiString, Address, PublicKey}; @@ -229,7 +228,7 @@ impl From> for ValidatorDefinitions { impl ValidatorDefinitions { /// Open an existing file or create a new, empty one if it does not exist. pub fn open_or_create>(validators_dir: P) -> Result { - ensure_dir_exists(validators_dir.as_ref()).map_err(|_| { + create_dir_all(validators_dir.as_ref()).map_err(|_| { Error::UnableToCreateValidatorDir(PathBuf::from(validators_dir.as_ref())) })?; let config_path = validators_dir.as_ref().join(CONFIG_FILENAME); diff --git a/common/directory/src/lib.rs b/common/directory/src/lib.rs index df03b4f9a4..d042f8dfad 100644 --- a/common/directory/src/lib.rs +++ b/common/directory/src/lib.rs @@ -1,6 +1,6 @@ use clap::ArgMatches; pub use eth2_network_config::DEFAULT_HARDCODED_NETWORK; -use std::fs::{self, create_dir_all}; +use std::fs; use std::path::{Path, PathBuf}; /// Names for the default directories. @@ -30,17 +30,6 @@ pub fn get_network_dir(matches: &ArgMatches) -> String { } } -/// Checks if a directory exists in the given path and creates a directory if it does not exist. -pub fn ensure_dir_exists>(path: P) -> Result<(), String> { - let path = path.as_ref(); - - if !path.exists() { - create_dir_all(path).map_err(|e| format!("Unable to create {:?}: {:?}", path, e))?; - } - - Ok(()) -} - /// If `arg` is in `matches`, parses the value as a path. /// /// Otherwise, attempts to find the default directory for the `testnet` from the `matches`. diff --git a/common/eth2/Cargo.toml b/common/eth2/Cargo.toml index 9d6dea100d..ca7fa7ccdb 100644 --- a/common/eth2/Cargo.toml +++ b/common/eth2/Cargo.toml @@ -31,10 +31,6 @@ zeroize = { workspace = true } [dev-dependencies] tokio = { workspace = true } -[target.'cfg(target_os = "linux")'.dependencies] -psutil = { version = "3.3.0", optional = true } -procfs = { version = "0.15.1", optional = true } - [features] default = ["lighthouse"] -lighthouse = ["psutil", "procfs"] +lighthouse = [] diff --git a/common/eth2/src/lighthouse.rs b/common/eth2/src/lighthouse.rs index 66dd5d779b..badc4857c4 100644 --- a/common/eth2/src/lighthouse.rs +++ b/common/eth2/src/lighthouse.rs @@ -88,12 +88,6 @@ pub struct ValidatorInclusionData { pub is_previous_epoch_head_attester: bool, } -#[cfg(target_os = "linux")] -use { - psutil::cpu::os::linux::CpuTimesExt, psutil::memory::os::linux::VirtualMemoryExt, - psutil::process::Process, -}; - /// Reports on the health of the Lighthouse instance. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Health { @@ -164,69 +158,6 @@ pub struct SystemHealth { pub misc_os: String, } -impl SystemHealth { - #[cfg(not(target_os = "linux"))] - pub fn observe() -> Result { - Err("Health is only available on Linux".into()) - } - - #[cfg(target_os = "linux")] - pub fn observe() -> Result { - let vm = psutil::memory::virtual_memory() - .map_err(|e| format!("Unable to get virtual memory: {:?}", e))?; - let loadavg = - psutil::host::loadavg().map_err(|e| format!("Unable to get loadavg: {:?}", e))?; - - let cpu = - psutil::cpu::cpu_times().map_err(|e| format!("Unable to get cpu times: {:?}", e))?; - - let disk_usage = psutil::disk::disk_usage("/") - .map_err(|e| format!("Unable to disk usage info: {:?}", e))?; - - let disk = psutil::disk::DiskIoCountersCollector::default() - .disk_io_counters() - .map_err(|e| format!("Unable to get disk counters: {:?}", e))?; - - let net = psutil::network::NetIoCountersCollector::default() - .net_io_counters() - .map_err(|e| format!("Unable to get network io counters: {:?}", e))?; - - let boot_time = psutil::host::boot_time() - .map_err(|e| format!("Unable to get system boot time: {:?}", e))? - .duration_since(std::time::UNIX_EPOCH) - .map_err(|e| format!("Boot time is lower than unix epoch: {}", e))? - .as_secs(); - - Ok(Self { - sys_virt_mem_total: vm.total(), - sys_virt_mem_available: vm.available(), - sys_virt_mem_used: vm.used(), - sys_virt_mem_free: vm.free(), - sys_virt_mem_cached: vm.cached(), - sys_virt_mem_buffers: vm.buffers(), - sys_virt_mem_percent: vm.percent(), - sys_loadavg_1: loadavg.one, - sys_loadavg_5: loadavg.five, - sys_loadavg_15: loadavg.fifteen, - cpu_cores: psutil::cpu::cpu_count_physical(), - cpu_threads: psutil::cpu::cpu_count(), - system_seconds_total: cpu.system().as_secs(), - cpu_time_total: cpu.total().as_secs(), - user_seconds_total: cpu.user().as_secs(), - iowait_seconds_total: cpu.iowait().as_secs(), - idle_seconds_total: cpu.idle().as_secs(), - disk_node_bytes_total: disk_usage.total(), - disk_node_bytes_free: disk_usage.free(), - disk_node_reads_total: disk.read_count(), - disk_node_writes_total: disk.write_count(), - network_node_bytes_total_received: net.bytes_recv(), - network_node_bytes_total_transmit: net.bytes_sent(), - misc_node_boot_ts_seconds: boot_time, - misc_os: std::env::consts::OS.to_string(), - }) - } -} - /// Process specific health #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct ProcessHealth { @@ -244,59 +175,6 @@ pub struct ProcessHealth { pub pid_process_seconds_total: u64, } -impl ProcessHealth { - #[cfg(not(target_os = "linux"))] - pub fn observe() -> Result { - Err("Health is only available on Linux".into()) - } - - #[cfg(target_os = "linux")] - pub fn observe() -> Result { - let process = - Process::current().map_err(|e| format!("Unable to get current process: {:?}", e))?; - - let process_mem = process - .memory_info() - .map_err(|e| format!("Unable to get process memory info: {:?}", e))?; - - let me = procfs::process::Process::myself() - .map_err(|e| format!("Unable to get process: {:?}", e))?; - let stat = me - .stat() - .map_err(|e| format!("Unable to get stat: {:?}", e))?; - - let process_times = process - .cpu_times() - .map_err(|e| format!("Unable to get process cpu times : {:?}", e))?; - - Ok(Self { - pid: process.pid(), - pid_num_threads: stat.num_threads, - pid_mem_resident_set_size: process_mem.rss(), - pid_mem_virtual_memory_size: process_mem.vms(), - pid_mem_shared_memory_size: process_mem.shared(), - pid_process_seconds_total: process_times.busy().as_secs() - + process_times.children_system().as_secs() - + process_times.children_system().as_secs(), - }) - } -} - -impl Health { - #[cfg(not(target_os = "linux"))] - pub fn observe() -> Result { - Err("Health is only available on Linux".into()) - } - - #[cfg(target_os = "linux")] - pub fn observe() -> Result { - Ok(Self { - process: ProcessHealth::observe()?, - system: SystemHealth::observe()?, - }) - } -} - /// Indicates how up-to-date the Eth1 caches are. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Eth1SyncStatusData { diff --git a/common/health_metrics/Cargo.toml b/common/health_metrics/Cargo.toml new file mode 100644 index 0000000000..08591471b2 --- /dev/null +++ b/common/health_metrics/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "health_metrics" +version = "0.1.0" +edition = { workspace = true } + +[dependencies] +eth2 = { workspace = true } +metrics = { workspace = true } + +[target.'cfg(target_os = "linux")'.dependencies] +psutil = "3.3.0" +procfs = "0.15.1" diff --git a/common/health_metrics/src/lib.rs b/common/health_metrics/src/lib.rs new file mode 100644 index 0000000000..bab80fb912 --- /dev/null +++ b/common/health_metrics/src/lib.rs @@ -0,0 +1,2 @@ +pub mod metrics; +pub mod observe; diff --git a/common/warp_utils/src/metrics.rs b/common/health_metrics/src/metrics.rs similarity index 99% rename from common/warp_utils/src/metrics.rs rename to common/health_metrics/src/metrics.rs index fabcf93650..c216426b7d 100644 --- a/common/warp_utils/src/metrics.rs +++ b/common/health_metrics/src/metrics.rs @@ -1,3 +1,4 @@ +use crate::observe::Observe; use eth2::lighthouse::{ProcessHealth, SystemHealth}; use metrics::*; use std::sync::LazyLock; diff --git a/common/health_metrics/src/observe.rs b/common/health_metrics/src/observe.rs new file mode 100644 index 0000000000..81bb8e6f7e --- /dev/null +++ b/common/health_metrics/src/observe.rs @@ -0,0 +1,127 @@ +use eth2::lighthouse::{Health, ProcessHealth, SystemHealth}; + +#[cfg(target_os = "linux")] +use { + psutil::cpu::os::linux::CpuTimesExt, psutil::memory::os::linux::VirtualMemoryExt, + psutil::process::Process, +}; + +pub trait Observe: Sized { + fn observe() -> Result; +} + +impl Observe for Health { + #[cfg(not(target_os = "linux"))] + fn observe() -> Result { + Err("Health is only available on Linux".into()) + } + + #[cfg(target_os = "linux")] + fn observe() -> Result { + Ok(Self { + process: ProcessHealth::observe()?, + system: SystemHealth::observe()?, + }) + } +} + +impl Observe for SystemHealth { + #[cfg(not(target_os = "linux"))] + fn observe() -> Result { + Err("Health is only available on Linux".into()) + } + + #[cfg(target_os = "linux")] + fn observe() -> Result { + let vm = psutil::memory::virtual_memory() + .map_err(|e| format!("Unable to get virtual memory: {:?}", e))?; + let loadavg = + psutil::host::loadavg().map_err(|e| format!("Unable to get loadavg: {:?}", e))?; + + let cpu = + psutil::cpu::cpu_times().map_err(|e| format!("Unable to get cpu times: {:?}", e))?; + + let disk_usage = psutil::disk::disk_usage("/") + .map_err(|e| format!("Unable to disk usage info: {:?}", e))?; + + let disk = psutil::disk::DiskIoCountersCollector::default() + .disk_io_counters() + .map_err(|e| format!("Unable to get disk counters: {:?}", e))?; + + let net = psutil::network::NetIoCountersCollector::default() + .net_io_counters() + .map_err(|e| format!("Unable to get network io counters: {:?}", e))?; + + let boot_time = psutil::host::boot_time() + .map_err(|e| format!("Unable to get system boot time: {:?}", e))? + .duration_since(std::time::UNIX_EPOCH) + .map_err(|e| format!("Boot time is lower than unix epoch: {}", e))? + .as_secs(); + + Ok(Self { + sys_virt_mem_total: vm.total(), + sys_virt_mem_available: vm.available(), + sys_virt_mem_used: vm.used(), + sys_virt_mem_free: vm.free(), + sys_virt_mem_cached: vm.cached(), + sys_virt_mem_buffers: vm.buffers(), + sys_virt_mem_percent: vm.percent(), + sys_loadavg_1: loadavg.one, + sys_loadavg_5: loadavg.five, + sys_loadavg_15: loadavg.fifteen, + cpu_cores: psutil::cpu::cpu_count_physical(), + cpu_threads: psutil::cpu::cpu_count(), + system_seconds_total: cpu.system().as_secs(), + cpu_time_total: cpu.total().as_secs(), + user_seconds_total: cpu.user().as_secs(), + iowait_seconds_total: cpu.iowait().as_secs(), + idle_seconds_total: cpu.idle().as_secs(), + disk_node_bytes_total: disk_usage.total(), + disk_node_bytes_free: disk_usage.free(), + disk_node_reads_total: disk.read_count(), + disk_node_writes_total: disk.write_count(), + network_node_bytes_total_received: net.bytes_recv(), + network_node_bytes_total_transmit: net.bytes_sent(), + misc_node_boot_ts_seconds: boot_time, + misc_os: std::env::consts::OS.to_string(), + }) + } +} + +impl Observe for ProcessHealth { + #[cfg(not(target_os = "linux"))] + fn observe() -> Result { + Err("Health is only available on Linux".into()) + } + + #[cfg(target_os = "linux")] + fn observe() -> Result { + let process = + Process::current().map_err(|e| format!("Unable to get current process: {:?}", e))?; + + let process_mem = process + .memory_info() + .map_err(|e| format!("Unable to get process memory info: {:?}", e))?; + + let me = procfs::process::Process::myself() + .map_err(|e| format!("Unable to get process: {:?}", e))?; + let stat = me + .stat() + .map_err(|e| format!("Unable to get stat: {:?}", e))?; + + let process_times = process + .cpu_times() + .map_err(|e| format!("Unable to get process cpu times : {:?}", e))?; + + Ok(Self { + pid: process.pid(), + pid_num_threads: stat.num_threads, + pid_mem_resident_set_size: process_mem.rss(), + pid_mem_virtual_memory_size: process_mem.vms(), + pid_mem_shared_memory_size: process_mem.shared(), + pid_process_seconds_total: process_times.busy().as_secs() + + process_times.children_system().as_secs() + + process_times.children_system().as_secs(), + }) + } +} diff --git a/common/monitoring_api/Cargo.toml b/common/monitoring_api/Cargo.toml index 5008c86e85..cb52cff29a 100644 --- a/common/monitoring_api/Cargo.toml +++ b/common/monitoring_api/Cargo.toml @@ -7,6 +7,7 @@ edition = { workspace = true } [dependencies] eth2 = { workspace = true } +health_metrics = { workspace = true } lighthouse_version = { workspace = true } metrics = { workspace = true } regex = { workspace = true } diff --git a/common/monitoring_api/src/gather.rs b/common/monitoring_api/src/gather.rs index 2f6c820f56..43bea35a93 100644 --- a/common/monitoring_api/src/gather.rs +++ b/common/monitoring_api/src/gather.rs @@ -1,4 +1,5 @@ use super::types::{BeaconProcessMetrics, ValidatorProcessMetrics}; +use health_metrics::observe::Observe; use metrics::{MetricFamily, MetricType}; use serde_json::json; use std::collections::HashMap; diff --git a/common/monitoring_api/src/lib.rs b/common/monitoring_api/src/lib.rs index 9592c50a40..6f919971b0 100644 --- a/common/monitoring_api/src/lib.rs +++ b/common/monitoring_api/src/lib.rs @@ -4,6 +4,7 @@ use std::{path::PathBuf, time::Duration}; use eth2::lighthouse::SystemHealth; use gather::{gather_beacon_metrics, gather_validator_metrics}; +use health_metrics::observe::Observe; use reqwest::{IntoUrl, Response}; pub use reqwest::{StatusCode, Url}; use sensitive_url::SensitiveUrl; diff --git a/common/validator_dir/Cargo.toml b/common/validator_dir/Cargo.toml index 773431c93c..4c03b7662e 100644 --- a/common/validator_dir/Cargo.toml +++ b/common/validator_dir/Cargo.toml @@ -12,7 +12,6 @@ insecure_keys = [] bls = { workspace = true } deposit_contract = { workspace = true } derivative = { workspace = true } -directory = { workspace = true } eth2_keystore = { workspace = true } filesystem = { workspace = true } hex = { workspace = true } diff --git a/common/validator_dir/src/builder.rs b/common/validator_dir/src/builder.rs index 3d5d149608..2e971a8b1a 100644 --- a/common/validator_dir/src/builder.rs +++ b/common/validator_dir/src/builder.rs @@ -1,7 +1,6 @@ use crate::{Error as DirError, ValidatorDir}; use bls::get_withdrawal_credentials; use deposit_contract::{encode_eth1_tx_data, Error as DepositError}; -use directory::ensure_dir_exists; use eth2_keystore::{Error as KeystoreError, Keystore, KeystoreBuilder, PlainText}; use filesystem::create_with_600_perms; use rand::{distributions::Alphanumeric, Rng}; @@ -42,7 +41,7 @@ pub enum Error { #[cfg(feature = "insecure_keys")] InsecureKeysError(String), MissingPasswordDir, - UnableToCreatePasswordDir(String), + UnableToCreatePasswordDir(io::Error), } impl From for Error { @@ -163,7 +162,7 @@ impl<'a> Builder<'a> { } if let Some(password_dir) = &self.password_dir { - ensure_dir_exists(password_dir).map_err(Error::UnableToCreatePasswordDir)?; + create_dir_all(password_dir).map_err(Error::UnableToCreatePasswordDir)?; } // The withdrawal keystore must be initialized in order to store it or create an eth1 diff --git a/common/warp_utils/Cargo.toml b/common/warp_utils/Cargo.toml index 4a3cde54a9..ec2d23686b 100644 --- a/common/warp_utils/Cargo.toml +++ b/common/warp_utils/Cargo.toml @@ -6,7 +6,6 @@ edition = { workspace = true } # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -beacon_chain = { workspace = true } bytes = { workspace = true } eth2 = { workspace = true } headers = "0.3.2" @@ -15,7 +14,6 @@ safe_arith = { workspace = true } serde = { workspace = true } serde_array_query = "0.1.0" serde_json = { workspace = true } -state_processing = { workspace = true } tokio = { workspace = true } types = { workspace = true } warp = { workspace = true } diff --git a/common/warp_utils/src/lib.rs b/common/warp_utils/src/lib.rs index 55ee423fa4..c10adbac0d 100644 --- a/common/warp_utils/src/lib.rs +++ b/common/warp_utils/src/lib.rs @@ -3,7 +3,6 @@ pub mod cors; pub mod json; -pub mod metrics; pub mod query; pub mod reject; pub mod task; diff --git a/common/warp_utils/src/reject.rs b/common/warp_utils/src/reject.rs index bbd5274a7e..3c7ef5e4fa 100644 --- a/common/warp_utils/src/reject.rs +++ b/common/warp_utils/src/reject.rs @@ -2,6 +2,7 @@ use eth2::types::{ErrorMessage, Failure, IndexedErrorMessage}; use std::convert::Infallible; use std::error::Error; use std::fmt; +use std::fmt::Debug; use warp::{http::StatusCode, reject::Reject, reply::Response, Reply}; #[derive(Debug)] @@ -19,15 +20,6 @@ pub fn server_sent_event_error(s: String) -> ServerSentEventError { ServerSentEventError(s) } -#[derive(Debug)] -pub struct BeaconChainError(pub beacon_chain::BeaconChainError); - -impl Reject for BeaconChainError {} - -pub fn beacon_chain_error(e: beacon_chain::BeaconChainError) -> warp::reject::Rejection { - warp::reject::custom(BeaconChainError(e)) -} - #[derive(Debug)] pub struct BeaconStateError(pub types::BeaconStateError); @@ -47,21 +39,12 @@ pub fn arith_error(e: safe_arith::ArithError) -> warp::reject::Rejection { } #[derive(Debug)] -pub struct SlotProcessingError(pub state_processing::SlotProcessingError); +pub struct UnhandledError(pub Box); -impl Reject for SlotProcessingError {} +impl Reject for UnhandledError {} -pub fn slot_processing_error(e: state_processing::SlotProcessingError) -> warp::reject::Rejection { - warp::reject::custom(SlotProcessingError(e)) -} - -#[derive(Debug)] -pub struct BlockProductionError(pub beacon_chain::BlockProductionError); - -impl Reject for BlockProductionError {} - -pub fn block_production_error(e: beacon_chain::BlockProductionError) -> warp::reject::Rejection { - warp::reject::custom(BlockProductionError(e)) +pub fn unhandled_error(e: D) -> warp::reject::Rejection { + warp::reject::custom(UnhandledError(Box::new(e))) } #[derive(Debug)] @@ -191,16 +174,7 @@ pub async fn handle_rejection(err: warp::Rejection) -> Result() { code = StatusCode::BAD_REQUEST; message = format!("BAD_REQUEST: invalid query: {}", e); - } else if let Some(e) = err.find::() { - code = StatusCode::INTERNAL_SERVER_ERROR; - message = format!("UNHANDLED_ERROR: {:?}", e.0); - } else if let Some(e) = err.find::() { - code = StatusCode::INTERNAL_SERVER_ERROR; - message = format!("UNHANDLED_ERROR: {:?}", e.0); - } else if let Some(e) = err.find::() { - code = StatusCode::INTERNAL_SERVER_ERROR; - message = format!("UNHANDLED_ERROR: {:?}", e.0); - } else if let Some(e) = err.find::() { + } else if let Some(e) = err.find::() { code = StatusCode::INTERNAL_SERVER_ERROR; message = format!("UNHANDLED_ERROR: {:?}", e.0); } else if let Some(e) = err.find::() { diff --git a/validator_client/http_api/Cargo.toml b/validator_client/http_api/Cargo.toml index 76a021ab8c..651e658a7a 100644 --- a/validator_client/http_api/Cargo.toml +++ b/validator_client/http_api/Cargo.toml @@ -21,6 +21,7 @@ eth2_keystore = { workspace = true } ethereum_serde_utils = { workspace = true } filesystem = { workspace = true } graffiti_file = { workspace = true } +health_metrics = { workspace = true } initialized_validators = { workspace = true } lighthouse_version = { workspace = true } logging = { workspace = true } diff --git a/validator_client/http_api/src/lib.rs b/validator_client/http_api/src/lib.rs index 73ebe717af..9c3e3da63d 100644 --- a/validator_client/http_api/src/lib.rs +++ b/validator_client/http_api/src/lib.rs @@ -32,6 +32,7 @@ use eth2::lighthouse_vc::{ PublicKeyBytes, SetGraffitiRequest, }, }; +use health_metrics::observe::Observe; use lighthouse_version::version_with_platform; use logging::SSELoggingComponents; use parking_lot::RwLock; diff --git a/validator_client/http_metrics/Cargo.toml b/validator_client/http_metrics/Cargo.toml index c29a4d18fa..a3432410bc 100644 --- a/validator_client/http_metrics/Cargo.toml +++ b/validator_client/http_metrics/Cargo.toml @@ -5,6 +5,7 @@ edition = { workspace = true } authors = ["Sigma Prime "] [dependencies] +health_metrics = { workspace = true } lighthouse_version = { workspace = true } malloc_utils = { workspace = true } metrics = { workspace = true } diff --git a/validator_client/http_metrics/src/lib.rs b/validator_client/http_metrics/src/lib.rs index 984b752e5a..f1c6d4ed8a 100644 --- a/validator_client/http_metrics/src/lib.rs +++ b/validator_client/http_metrics/src/lib.rs @@ -206,7 +206,7 @@ pub fn gather_prometheus_metrics( scrape_allocator_metrics(); } - warp_utils::metrics::scrape_health_metrics(); + health_metrics::metrics::scrape_health_metrics(); encoder .encode(&metrics::gather(), &mut buffer) From 06329ec2d105fc60c4c7218561cff11fb6b398a3 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Fri, 17 Jan 2025 01:27:08 +0700 Subject: [PATCH 080/254] `SingleAttestation` implementation (#6488) * First pass * Add restrictions to RuntimeVariableList api * Use empty_uninitialized and fix warnings * Fix some todos * Merge branch 'unstable' into max-blobs-preset * Fix take impl on RuntimeFixedList * cleanup * Fix test compilations * Fix some more tests * Fix test from unstable * Merge branch 'unstable' into max-blobs-preset * SingleAttestation * Add post attestation v2 endpoint logic to attestation service * Merge branch 'unstable' of https://github.com/sigp/lighthouse into single_attestation * Implement "Bugfix and more withdrawal tests" * Implement "Add missed exit checks to consolidation processing" * Implement "Update initial earliest_exit_epoch calculation" * Implement "Limit consolidating balance by validator.effective_balance" * Implement "Use 16-bit random value in validator filter" * Implement "Do not change creds type on consolidation" * some tests and fixed attestqtion calc * Merge branch 'unstable' of https://github.com/sigp/lighthouse into single_attestation * Rename PendingPartialWithdraw index field to validator_index * Skip slots to get test to pass and add TODO * Implement "Synchronously check all transactions to have non-zero length" * Merge remote-tracking branch 'origin/unstable' into max-blobs-preset * Remove footgun function * Minor simplifications * Move from preset to config * Fix typo * Revert "Remove footgun function" This reverts commit de01f923c7452355c87f50c0e8031ca94fa00d36. * Try fixing tests * Implement "bump minimal preset MAX_BLOB_COMMITMENTS_PER_BLOCK and KZG_COMMITMENT_INCLUSION_PROOF_DEPTH" * Thread through ChainSpec * Fix release tests * Move RuntimeFixedVector into module and rename * Add test * Merge branch 'unstable' of https://github.com/sigp/lighthouse into single_attestation * Added more test coverage, simplified Attestation conversion, and other minor refactors * Removed unusued codepaths * Fix failing test * Implement "Remove post-altair `initialize_beacon_state_from_eth1` from specs" * Update preset YAML * Remove empty RuntimeVarList awefullness * Make max_blobs_per_block a config parameter (#6329) Squashed commit of the following: commit 04b3743ec1e0b650269dd8e58b540c02430d1c0d Author: Michael Sproul Date: Mon Jan 6 17:36:58 2025 +1100 Add test commit 440e85419940d4daba406d910e7908dd1fe78668 Author: Michael Sproul Date: Mon Jan 6 17:24:50 2025 +1100 Move RuntimeFixedVector into module and rename commit f66e179a40c3917eee39a93534ecf75480172699 Author: Michael Sproul Date: Mon Jan 6 17:17:17 2025 +1100 Fix release tests commit e4bfe71cd1f0a2784d0bd57f85b2f5d8cf503ac1 Author: Michael Sproul Date: Mon Jan 6 17:05:30 2025 +1100 Thread through ChainSpec commit 063b79c16abd3f6df47b85efcf3858177bc933b9 Author: Michael Sproul Date: Mon Jan 6 15:32:16 2025 +1100 Try fixing tests commit 88bedf09bc647de66bd1ff944bbc8fb13e2b7590 Author: Michael Sproul Date: Mon Jan 6 15:04:37 2025 +1100 Revert "Remove footgun function" This reverts commit de01f923c7452355c87f50c0e8031ca94fa00d36. commit 32483d385b66f252d50cee5b524e2924157bdcd4 Author: Michael Sproul Date: Mon Jan 6 15:04:32 2025 +1100 Fix typo commit 2e86585b478c012f6e3483989c87e38161227674 Author: Michael Sproul Date: Mon Jan 6 15:04:15 2025 +1100 Move from preset to config commit 1095d60a40be20dd3c229b759fc3c228b51e51e3 Author: Michael Sproul Date: Mon Jan 6 14:38:40 2025 +1100 Minor simplifications commit de01f923c7452355c87f50c0e8031ca94fa00d36 Author: Michael Sproul Date: Mon Jan 6 14:06:57 2025 +1100 Remove footgun function commit 0c2c8c42245c25b8cf17885faf20acd3b81140ec Merge: 21ecb58ff f51a292f7 Author: Michael Sproul Date: Mon Jan 6 14:02:50 2025 +1100 Merge remote-tracking branch 'origin/unstable' into max-blobs-preset commit f51a292f77575a1786af34271fb44954f141c377 Author: Daniel Knopik <107140945+dknopik@users.noreply.github.com> Date: Fri Jan 3 20:27:21 2025 +0100 fully lint only explicitly to avoid unnecessary rebuilds (#6753) * fully lint only explicitly to avoid unnecessary rebuilds commit 7e0cddef321c2a069582c65b58e5f46590d60c49 Author: Akihito Nakano Date: Tue Dec 24 10:38:56 2024 +0900 Make sure we have fanout peers when publish (#6738) * Ensure that `fanout_peers` is always non-empty if it's `Some` commit 21ecb58ff88b86435ab62d9ac227394c10fdcd22 Merge: 2fcb2935e 9aefb5539 Author: Pawan Dhananjay Date: Mon Oct 21 14:46:00 2024 -0700 Merge branch 'unstable' into max-blobs-preset commit 2fcb2935ec7ef4cd18bbdd8aedb7de61fac69e61 Author: Pawan Dhananjay Date: Fri Sep 6 18:28:31 2024 -0700 Fix test from unstable commit 12c6ef118a1a6d910c48d9d4b23004f3609264c7 Author: Pawan Dhananjay Date: Wed Sep 4 16:16:36 2024 -0700 Fix some more tests commit d37733b846ce58e318e976d6503ca394b4901141 Author: Pawan Dhananjay Date: Wed Sep 4 12:47:36 2024 -0700 Fix test compilations commit 52bb581e071d5f474d519366e860a4b3a0b52f78 Author: Pawan Dhananjay Date: Tue Sep 3 18:38:19 2024 -0700 cleanup commit e71020e3e613910e0315f558ead661b490a0ff20 Author: Pawan Dhananjay Date: Tue Sep 3 17:16:10 2024 -0700 Fix take impl on RuntimeFixedList commit 13f9bba6470b2140e5c34f14aed06dab2b062c1c Merge: 60100fc6b 4e675cf5d Author: Pawan Dhananjay Date: Tue Sep 3 16:08:59 2024 -0700 Merge branch 'unstable' into max-blobs-preset commit 60100fc6be72792ff33913d7e5a53434c792aacf Author: Pawan Dhananjay Date: Fri Aug 30 16:04:11 2024 -0700 Fix some todos commit a9cb329a221a809f7dd818984753826f91c2e26b Author: Pawan Dhananjay Date: Fri Aug 30 15:54:00 2024 -0700 Use empty_uninitialized and fix warnings commit 4dc6e6515ecf75cefa4de840edc7b57e76a8fc9e Author: Pawan Dhananjay Date: Fri Aug 30 15:53:18 2024 -0700 Add restrictions to RuntimeVariableList api commit 25feedfde348b530c4fa2348cc71a06b746898ed Author: Pawan Dhananjay Date: Thu Aug 29 16:11:19 2024 -0700 First pass * Fix tests * Implement max_blobs_per_block_electra * Fix config issues * Simplify BlobSidecarListFromRoot * Disable PeerDAS tests * Cleanup single attestation imports * Fix some single attestation network plumbing * Merge remote-tracking branch 'origin/unstable' into max-blobs-preset * Bump quota to account for new target (6) * Remove clone * Fix issue from review * Try to remove ugliness * Merge branch 'unstable' into max-blobs-preset * Merge remote-tracking branch 'origin/unstable' into electra-alpha10 * Merge commit '04b3743ec1e0b650269dd8e58b540c02430d1c0d' into electra-alpha10 * Merge remote-tracking branch 'pawan/max-blobs-preset' into electra-alpha10 * Update tests to v1.5.0-beta.0 * Merge remote-tracking branch 'origin/electra-alpha10' into single_attestation * Fix some tests * Cargo fmt * lint * fmt * Resolve merge conflicts * Merge branch 'electra-alpha10' of https://github.com/sigp/lighthouse into single_attestation * lint * Linting * fmt * Merge branch 'electra-alpha10' of https://github.com/sigp/lighthouse into single_attestation * Fmt * Fix test and add TODO * Gracefully handle slashed proposers in fork choice tests * Merge remote-tracking branch 'origin/unstable' into electra-alpha10 * Keep latest changes from max_blobs_per_block PR in codec.rs * Revert a few more regressions and add a comment * Merge branch 'electra-alpha10' of https://github.com/sigp/lighthouse into single_attestation * Disable more DAS tests * Improve validator monitor test a little * Make test more robust * Fix sync test that didn't understand blobs * Fill out cropped comment * Merge remote-tracking branch 'origin/electra-alpha10' into single_attestation * Merge remote-tracking branch 'origin/unstable' into single_attestation * Merge remote-tracking branch 'origin/unstable' into single_attestation * Merge branch 'unstable' of https://github.com/sigp/lighthouse into single_attestation * publish_attestations should accept Either * log an error when failing to convert to SingleAttestation * Use Cow to avoid clone * Avoid reconverting to SingleAttestation * Tweak VC error message * update comments * update comments * pass in single attestation as ref to subnetid calculation method * Improved API, new error variants and other minor tweaks * Fix single_attestation event topic boilerplate * fix sse event failure * Add single_attestation event topic test coverage --- Cargo.lock | 1 + .../src/attestation_verification.rs | 13 +- beacon_node/beacon_chain/src/beacon_chain.rs | 26 +- beacon_node/beacon_chain/src/events.rs | 15 ++ beacon_node/beacon_chain/src/test_utils.rs | 245 ++++++++++++++++++ beacon_node/http_api/Cargo.toml | 1 + beacon_node/http_api/src/lib.rs | 59 ++++- .../http_api/src/publish_attestations.rs | 95 +++++-- beacon_node/http_api/tests/fork_tests.rs | 4 - .../http_api/tests/interactive_tests.rs | 57 ++-- beacon_node/http_api/tests/tests.rs | 99 ++++++- .../lighthouse_network/src/types/pubsub.rs | 83 +++--- .../src/network_beacon_processor/mod.rs | 52 ++++ beacon_node/network/src/router.rs | 11 + beacon_node/network/src/service.rs | 18 +- beacon_node/network/src/subnet_service/mod.rs | 6 +- common/eth2/src/lib.rs | 4 +- common/eth2/src/types.rs | 10 + consensus/types/src/attestation.rs | 94 ++++++- consensus/types/src/lib.rs | 2 +- consensus/types/src/subnet_id.rs | 16 ++ .../src/attestation_service.rs | 24 +- 22 files changed, 831 insertions(+), 104 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aa9bdd2afc..29ffdc49ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3951,6 +3951,7 @@ dependencies = [ "bs58 0.4.0", "bytes", "directory", + "either", "eth1", "eth2", "ethereum_serde_utils", diff --git a/beacon_node/beacon_chain/src/attestation_verification.rs b/beacon_node/beacon_chain/src/attestation_verification.rs index c3dea3dbb4..ffaf61e41a 100644 --- a/beacon_node/beacon_chain/src/attestation_verification.rs +++ b/beacon_node/beacon_chain/src/attestation_verification.rs @@ -62,7 +62,7 @@ use tree_hash::TreeHash; use types::{ Attestation, AttestationRef, BeaconCommittee, BeaconStateError::NoCommitteeFound, ChainSpec, CommitteeIndex, Epoch, EthSpec, Hash256, IndexedAttestation, SelectionProof, - SignedAggregateAndProof, Slot, SubnetId, + SignedAggregateAndProof, SingleAttestation, Slot, SubnetId, }; pub use batch::{batch_verify_aggregated_attestations, batch_verify_unaggregated_attestations}; @@ -317,12 +317,22 @@ pub struct VerifiedUnaggregatedAttestation<'a, T: BeaconChainTypes> { attestation: AttestationRef<'a, T::EthSpec>, indexed_attestation: IndexedAttestation, subnet_id: SubnetId, + validator_index: usize, } impl VerifiedUnaggregatedAttestation<'_, T> { pub fn into_indexed_attestation(self) -> IndexedAttestation { self.indexed_attestation } + + pub fn single_attestation(&self) -> Option { + Some(SingleAttestation { + committee_index: self.attestation.committee_index()? as usize, + attester_index: self.validator_index, + data: self.attestation.data().clone(), + signature: self.attestation.signature().clone(), + }) + } } /// Custom `Clone` implementation is to avoid the restrictive trait bounds applied by the usual derive @@ -1035,6 +1045,7 @@ impl<'a, T: BeaconChainTypes> VerifiedUnaggregatedAttestation<'a, T> { attestation, indexed_attestation, subnet_id, + validator_index: validator_index as usize, }) } diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index a6da610c0e..d0c294b44f 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -2035,10 +2035,30 @@ impl BeaconChain { |v| { // This method is called for API and gossip attestations, so this covers all unaggregated attestation events if let Some(event_handler) = self.event_handler.as_ref() { + if event_handler.has_single_attestation_subscribers() { + let current_fork = self + .spec + .fork_name_at_slot::(v.attestation().data().slot); + if current_fork.electra_enabled() { + // I don't see a situation where this could return None. The upstream unaggregated attestation checks + // should have already verified that this is an attestation with a single committee bit set. + if let Some(single_attestation) = v.single_attestation() { + event_handler.register(EventKind::SingleAttestation(Box::new( + single_attestation, + ))); + } + } + } + if event_handler.has_attestation_subscribers() { - event_handler.register(EventKind::Attestation(Box::new( - v.attestation().clone_as_attestation(), - ))); + let current_fork = self + .spec + .fork_name_at_slot::(v.attestation().data().slot); + if !current_fork.electra_enabled() { + event_handler.register(EventKind::Attestation(Box::new( + v.attestation().clone_as_attestation(), + ))); + } } } metrics::inc_counter(&metrics::UNAGGREGATED_ATTESTATION_PROCESSING_SUCCESSES); diff --git a/beacon_node/beacon_chain/src/events.rs b/beacon_node/beacon_chain/src/events.rs index 267d56220c..8c342893ae 100644 --- a/beacon_node/beacon_chain/src/events.rs +++ b/beacon_node/beacon_chain/src/events.rs @@ -8,6 +8,7 @@ const DEFAULT_CHANNEL_CAPACITY: usize = 16; pub struct ServerSentEventHandler { attestation_tx: Sender>, + single_attestation_tx: Sender>, block_tx: Sender>, blob_sidecar_tx: Sender>, finalized_tx: Sender>, @@ -37,6 +38,7 @@ impl ServerSentEventHandler { pub fn new_with_capacity(log: Logger, capacity: usize) -> Self { let (attestation_tx, _) = broadcast::channel(capacity); + let (single_attestation_tx, _) = broadcast::channel(capacity); let (block_tx, _) = broadcast::channel(capacity); let (blob_sidecar_tx, _) = broadcast::channel(capacity); let (finalized_tx, _) = broadcast::channel(capacity); @@ -56,6 +58,7 @@ impl ServerSentEventHandler { Self { attestation_tx, + single_attestation_tx, block_tx, blob_sidecar_tx, finalized_tx, @@ -90,6 +93,10 @@ impl ServerSentEventHandler { .attestation_tx .send(kind) .map(|count| log_count("attestation", count)), + EventKind::SingleAttestation(_) => self + .single_attestation_tx + .send(kind) + .map(|count| log_count("single_attestation", count)), EventKind::Block(_) => self .block_tx .send(kind) @@ -164,6 +171,10 @@ impl ServerSentEventHandler { self.attestation_tx.subscribe() } + pub fn subscribe_single_attestation(&self) -> Receiver> { + self.single_attestation_tx.subscribe() + } + pub fn subscribe_block(&self) -> Receiver> { self.block_tx.subscribe() } @@ -232,6 +243,10 @@ impl ServerSentEventHandler { self.attestation_tx.receiver_count() > 0 } + pub fn has_single_attestation_subscribers(&self) -> bool { + self.single_attestation_tx.receiver_count() > 0 + } + pub fn has_block_subscribers(&self) -> bool { self.block_tx.receiver_count() > 0 } diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index fd3cc49626..443cc686eb 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -669,10 +669,16 @@ pub struct BeaconChainHarness { pub rng: Mutex, } +pub type CommitteeSingleAttestations = Vec<(SingleAttestation, SubnetId)>; pub type CommitteeAttestations = Vec<(Attestation, SubnetId)>; pub type HarnessAttestations = Vec<(CommitteeAttestations, Option>)>; +pub type HarnessSingleAttestations = Vec<( + CommitteeSingleAttestations, + Option>, +)>; + pub type HarnessSyncContributions = Vec<( Vec<(SyncCommitteeMessage, usize)>, Option>, @@ -1024,6 +1030,99 @@ where ) } + #[allow(clippy::too_many_arguments)] + pub fn produce_single_attestation_for_block( + &self, + slot: Slot, + index: CommitteeIndex, + beacon_block_root: Hash256, + mut state: Cow>, + state_root: Hash256, + aggregation_bit_index: usize, + validator_index: usize, + ) -> Result { + let epoch = slot.epoch(E::slots_per_epoch()); + + if state.slot() > slot { + return Err(BeaconChainError::CannotAttestToFutureState); + } else if state.current_epoch() < epoch { + let mut_state = state.to_mut(); + complete_state_advance( + mut_state, + Some(state_root), + epoch.start_slot(E::slots_per_epoch()), + &self.spec, + )?; + mut_state.build_committee_cache(RelativeEpoch::Current, &self.spec)?; + } + + let committee_len = state.get_beacon_committee(slot, index)?.committee.len(); + + let target_slot = epoch.start_slot(E::slots_per_epoch()); + let target_root = if state.slot() <= target_slot { + beacon_block_root + } else { + *state.get_block_root(target_slot)? + }; + + let attestation: Attestation = Attestation::empty_for_signing( + index, + committee_len, + slot, + beacon_block_root, + state.current_justified_checkpoint(), + Checkpoint { + epoch, + root: target_root, + }, + &self.spec, + )?; + + let attestation = match attestation { + Attestation::Electra(mut attn) => { + attn.aggregation_bits + .set(aggregation_bit_index, true) + .unwrap(); + attn + } + Attestation::Base(_) => panic!("Must be an Electra attestation"), + }; + + let aggregation_bits = attestation.get_aggregation_bits(); + + if aggregation_bits.len() != 1 { + panic!("Must be an unaggregated attestation") + } + + let aggregation_bit = *aggregation_bits.first().unwrap(); + + let committee = state.get_beacon_committee(slot, index).unwrap(); + + let attester_index = committee + .committee + .iter() + .enumerate() + .find_map(|(i, &index)| { + if aggregation_bit as usize == i { + return Some(index); + } + None + }) + .unwrap(); + + let single_attestation = + attestation.to_single_attestation_with_attester_index(attester_index)?; + + let attestation: Attestation = single_attestation.to_attestation(committee.committee)?; + + assert_eq!( + single_attestation.committee_index, + attestation.committee_index().unwrap() as usize + ); + assert_eq!(single_attestation.attester_index, validator_index); + Ok(single_attestation) + } + /// Produces an "unaggregated" attestation for the given `slot` and `index` that attests to /// `beacon_block_root`. The provided `state` should match the `block.state_root` for the /// `block` identified by `beacon_block_root`. @@ -1081,6 +1180,33 @@ where )?) } + /// A list of attestations for each committee for the given slot. + /// + /// The first layer of the Vec is organised per committee. For example, if the return value is + /// called `all_attestations`, then all attestations in `all_attestations[0]` will be for + /// committee 0, whilst all in `all_attestations[1]` will be for committee 1. + pub fn make_single_attestations( + &self, + attesting_validators: &[usize], + state: &BeaconState, + state_root: Hash256, + head_block_root: SignedBeaconBlockHash, + attestation_slot: Slot, + ) -> Vec { + let fork = self + .spec + .fork_at_epoch(attestation_slot.epoch(E::slots_per_epoch())); + self.make_single_attestations_with_opts( + attesting_validators, + state, + state_root, + head_block_root, + attestation_slot, + MakeAttestationOptions { limit: None, fork }, + ) + .0 + } + /// A list of attestations for each committee for the given slot. /// /// The first layer of the Vec is organised per committee. For example, if the return value is @@ -1108,6 +1234,99 @@ where .0 } + pub fn make_single_attestations_with_opts( + &self, + attesting_validators: &[usize], + state: &BeaconState, + state_root: Hash256, + head_block_root: SignedBeaconBlockHash, + attestation_slot: Slot, + opts: MakeAttestationOptions, + ) -> (Vec, Vec) { + let MakeAttestationOptions { limit, fork } = opts; + let committee_count = state.get_committee_count_at_slot(state.slot()).unwrap(); + let num_attesters = AtomicUsize::new(0); + + let (attestations, split_attesters) = state + .get_beacon_committees_at_slot(attestation_slot) + .expect("should get committees") + .iter() + .map(|bc| { + bc.committee + .par_iter() + .enumerate() + .filter_map(|(i, validator_index)| { + if !attesting_validators.contains(validator_index) { + return None; + } + + if let Some(limit) = limit { + // This atomics stuff is necessary because we're under a par_iter, + // and Rayon will deadlock if we use a mutex. + if num_attesters.fetch_add(1, Ordering::Relaxed) >= limit { + num_attesters.fetch_sub(1, Ordering::Relaxed); + return None; + } + } + + let mut attestation = self + .produce_single_attestation_for_block( + attestation_slot, + bc.index, + head_block_root.into(), + Cow::Borrowed(state), + state_root, + i, + *validator_index, + ) + .unwrap(); + + attestation.signature = { + let domain = self.spec.get_domain( + attestation.data.target.epoch, + Domain::BeaconAttester, + &fork, + state.genesis_validators_root(), + ); + + let message = attestation.data.signing_root(domain); + + let mut agg_sig = AggregateSignature::infinity(); + + agg_sig.add_assign( + &self.validator_keypairs[*validator_index].sk.sign(message), + ); + + agg_sig + }; + + let subnet_id = SubnetId::compute_subnet_for_single_attestation::( + &attestation, + committee_count, + &self.chain.spec, + ) + .unwrap(); + + Some(((attestation, subnet_id), validator_index)) + }) + .unzip::<_, _, Vec<_>, Vec<_>>() + }) + .unzip::<_, _, Vec<_>, Vec<_>>(); + + // Flatten attesters. + let attesters = split_attesters.into_iter().flatten().collect::>(); + + if let Some(limit) = limit { + assert_eq!(limit, num_attesters.load(Ordering::Relaxed)); + assert_eq!( + limit, + attesters.len(), + "failed to generate `limit` attestations" + ); + } + (attestations, attesters) + } + pub fn make_unaggregated_attestations_with_opts( &self, attesting_validators: &[usize], @@ -1288,6 +1507,32 @@ where ) } + /// A list of attestations for each committee for the given slot. + /// + /// The first layer of the Vec is organised per committee. For example, if the return value is + /// called `all_attestations`, then all attestations in `all_attestations[0]` will be for + /// committee 0, whilst all in `all_attestations[1]` will be for committee 1. + pub fn get_single_attestations( + &self, + attestation_strategy: &AttestationStrategy, + state: &BeaconState, + state_root: Hash256, + head_block_root: Hash256, + attestation_slot: Slot, + ) -> Vec> { + let validators: Vec = match attestation_strategy { + AttestationStrategy::AllValidators => self.get_all_validators(), + AttestationStrategy::SomeValidators(vals) => vals.clone(), + }; + self.make_single_attestations( + &validators, + state, + state_root, + head_block_root.into(), + attestation_slot, + ) + } + pub fn make_attestations( &self, attesting_validators: &[usize], diff --git a/beacon_node/http_api/Cargo.toml b/beacon_node/http_api/Cargo.toml index 0ced27e446..61f3370c70 100644 --- a/beacon_node/http_api/Cargo.toml +++ b/beacon_node/http_api/Cargo.toml @@ -11,6 +11,7 @@ beacon_processor = { workspace = true } bs58 = "0.4.0" bytes = { workspace = true } directory = { workspace = true } +either = { workspace = true } eth1 = { workspace = true } eth2 = { workspace = true } ethereum_serde_utils = { workspace = true } diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index d5c6c11567..5dc9055c6c 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -44,6 +44,7 @@ pub use block_id::BlockId; use builder_states::get_next_withdrawals; use bytes::Bytes; use directory::DEFAULT_ROOT_DIR; +use either::Either; use eth2::types::{ self as api_types, BroadcastValidation, EndpointVersion, ForkChoice, ForkChoiceNode, LightClientUpdatesQuery, PublishBlockRequest, ValidatorBalancesRequestBody, ValidatorId, @@ -86,8 +87,8 @@ use types::{ AttesterSlashing, BeaconStateError, CommitteeCache, ConfigAndPreset, Epoch, EthSpec, ForkName, ForkVersionedResponse, Hash256, ProposerPreparationData, ProposerSlashing, RelativeEpoch, SignedAggregateAndProof, SignedBlindedBeaconBlock, SignedBlsToExecutionChange, - SignedContributionAndProof, SignedValidatorRegistrationData, SignedVoluntaryExit, Slot, - SyncCommitteeMessage, SyncContributionData, + SignedContributionAndProof, SignedValidatorRegistrationData, SignedVoluntaryExit, + SingleAttestation, Slot, SyncCommitteeMessage, SyncContributionData, }; use validator::pubkey_to_validator_index; use version::{ @@ -1832,8 +1833,47 @@ pub fn serve( .and(task_spawner_filter.clone()) .and(chain_filter.clone()); + let beacon_pool_path_v2 = eth_v2 + .and(warp::path("beacon")) + .and(warp::path("pool")) + .and(task_spawner_filter.clone()) + .and(chain_filter.clone()); + // POST beacon/pool/attestations - let post_beacon_pool_attestations = beacon_pool_path_any + let post_beacon_pool_attestations = beacon_pool_path + .clone() + .and(warp::path("attestations")) + .and(warp::path::end()) + .and(warp_utils::json::json()) + .and(network_tx_filter.clone()) + .and(reprocess_send_filter.clone()) + .and(log_filter.clone()) + .then( + // V1 and V2 are identical except V2 has a consensus version header in the request. + // We only require this header for SSZ deserialization, which isn't supported for + // this endpoint presently. + |task_spawner: TaskSpawner, + chain: Arc>, + attestations: Vec>, + network_tx: UnboundedSender>, + reprocess_tx: Option>, + log: Logger| async move { + let attestations = attestations.into_iter().map(Either::Left).collect(); + let result = crate::publish_attestations::publish_attestations( + task_spawner, + chain, + attestations, + network_tx, + reprocess_tx, + log, + ) + .await + .map(|()| warp::reply::json(&())); + convert_rejection(result).await + }, + ); + + let post_beacon_pool_attestations_v2 = beacon_pool_path_v2 .clone() .and(warp::path("attestations")) .and(warp::path::end()) @@ -1842,16 +1882,13 @@ pub fn serve( .and(reprocess_send_filter) .and(log_filter.clone()) .then( - // V1 and V2 are identical except V2 has a consensus version header in the request. - // We only require this header for SSZ deserialization, which isn't supported for - // this endpoint presently. - |_endpoint_version: EndpointVersion, - task_spawner: TaskSpawner, + |task_spawner: TaskSpawner, chain: Arc>, - attestations: Vec>, + attestations: Vec, network_tx: UnboundedSender>, reprocess_tx: Option>, log: Logger| async move { + let attestations = attestations.into_iter().map(Either::Right).collect(); let result = crate::publish_attestations::publish_attestations( task_spawner, chain, @@ -4509,6 +4546,9 @@ pub fn serve( api_types::EventTopic::Attestation => { event_handler.subscribe_attestation() } + api_types::EventTopic::SingleAttestation => { + event_handler.subscribe_single_attestation() + } api_types::EventTopic::VoluntaryExit => { event_handler.subscribe_exit() } @@ -4736,6 +4776,7 @@ pub fn serve( .uor(post_beacon_blocks_v2) .uor(post_beacon_blinded_blocks_v2) .uor(post_beacon_pool_attestations) + .uor(post_beacon_pool_attestations_v2) .uor(post_beacon_pool_attester_slashings) .uor(post_beacon_pool_proposer_slashings) .uor(post_beacon_pool_voluntary_exits) diff --git a/beacon_node/http_api/src/publish_attestations.rs b/beacon_node/http_api/src/publish_attestations.rs index 0065476532..111dee3cff 100644 --- a/beacon_node/http_api/src/publish_attestations.rs +++ b/beacon_node/http_api/src/publish_attestations.rs @@ -40,17 +40,19 @@ use beacon_chain::{ BeaconChainTypes, }; use beacon_processor::work_reprocessing_queue::{QueuedUnaggregate, ReprocessQueueMessage}; +use either::Either; use eth2::types::Failure; use lighthouse_network::PubsubMessage; use network::NetworkMessage; use slog::{debug, error, warn, Logger}; +use std::borrow::Cow; use std::sync::Arc; use std::time::Duration; use tokio::sync::{ mpsc::{Sender, UnboundedSender}, oneshot, }; -use types::Attestation; +use types::{Attestation, EthSpec, SingleAttestation}; // Error variants are only used in `Debug` and considered `dead_code` by the compiler. #[derive(Debug)] @@ -62,6 +64,7 @@ enum Error { ReprocessDisabled, ReprocessFull, ReprocessTimeout, + FailedConversion(#[allow(dead_code)] BeaconChainError), } enum PublishAttestationResult { @@ -73,24 +76,39 @@ enum PublishAttestationResult { fn verify_and_publish_attestation( chain: &Arc>, - attestation: &Attestation, + either_attestation: &Either, SingleAttestation>, seen_timestamp: Duration, network_tx: &UnboundedSender>, log: &Logger, ) -> Result<(), Error> { - let attestation = chain - .verify_unaggregated_attestation_for_gossip(attestation, None) + let attestation = convert_to_attestation(chain, either_attestation)?; + let verified_attestation = chain + .verify_unaggregated_attestation_for_gossip(&attestation, None) .map_err(Error::Validation)?; - // Publish. - network_tx - .send(NetworkMessage::Publish { - messages: vec![PubsubMessage::Attestation(Box::new(( - attestation.subnet_id(), - attestation.attestation().clone_as_attestation(), - )))], - }) - .map_err(|_| Error::Publication)?; + match either_attestation { + Either::Left(attestation) => { + // Publish. + network_tx + .send(NetworkMessage::Publish { + messages: vec![PubsubMessage::Attestation(Box::new(( + verified_attestation.subnet_id(), + attestation.clone(), + )))], + }) + .map_err(|_| Error::Publication)?; + } + Either::Right(single_attestation) => { + network_tx + .send(NetworkMessage::Publish { + messages: vec![PubsubMessage::SingleAttestation(Box::new(( + verified_attestation.subnet_id(), + single_attestation.clone(), + )))], + }) + .map_err(|_| Error::Publication)?; + } + } // Notify the validator monitor. chain @@ -98,12 +116,12 @@ fn verify_and_publish_attestation( .read() .register_api_unaggregated_attestation( seen_timestamp, - attestation.indexed_attestation(), + verified_attestation.indexed_attestation(), &chain.slot_clock, ); - let fc_result = chain.apply_attestation_to_fork_choice(&attestation); - let naive_aggregation_result = chain.add_to_naive_aggregation_pool(&attestation); + let fc_result = chain.apply_attestation_to_fork_choice(&verified_attestation); + let naive_aggregation_result = chain.add_to_naive_aggregation_pool(&verified_attestation); if let Err(e) = &fc_result { warn!( @@ -129,10 +147,48 @@ fn verify_and_publish_attestation( } } +fn convert_to_attestation<'a, T: BeaconChainTypes>( + chain: &Arc>, + attestation: &'a Either, SingleAttestation>, +) -> Result>, Error> { + let a = match attestation { + Either::Left(a) => Cow::Borrowed(a), + Either::Right(single_attestation) => chain + .with_committee_cache( + single_attestation.data.target.root, + single_attestation + .data + .slot + .epoch(T::EthSpec::slots_per_epoch()), + |committee_cache, _| { + let Some(committee) = committee_cache.get_beacon_committee( + single_attestation.data.slot, + single_attestation.committee_index as u64, + ) else { + return Err(BeaconChainError::AttestationError( + types::AttestationError::NoCommitteeForSlotAndIndex { + slot: single_attestation.data.slot, + index: single_attestation.committee_index as u64, + }, + )); + }; + + let attestation = + single_attestation.to_attestation::(committee.committee)?; + + Ok(Cow::Owned(attestation)) + }, + ) + .map_err(Error::FailedConversion)?, + }; + + Ok(a) +} + pub async fn publish_attestations( task_spawner: TaskSpawner, chain: Arc>, - attestations: Vec>, + attestations: Vec, SingleAttestation>>, network_tx: UnboundedSender>, reprocess_send: Option>, log: Logger, @@ -141,7 +197,10 @@ pub async fn publish_attestations( // move the `attestations` vec into the blocking task, so this small overhead is unavoidable. let attestation_metadata = attestations .iter() - .map(|att| (att.data().slot, att.committee_index())) + .map(|att| match att { + Either::Left(att) => (att.data().slot, att.committee_index()), + Either::Right(att) => (att.data.slot, Some(att.committee_index as u64)), + }) .collect::>(); // Gossip validate and publish attestations that can be immediately processed. diff --git a/beacon_node/http_api/tests/fork_tests.rs b/beacon_node/http_api/tests/fork_tests.rs index 8cb6053e9f..d6b8df33b3 100644 --- a/beacon_node/http_api/tests/fork_tests.rs +++ b/beacon_node/http_api/tests/fork_tests.rs @@ -155,10 +155,6 @@ async fn attestations_across_fork_with_skip_slots() { .post_beacon_pool_attestations_v1(&unaggregated_attestations) .await .unwrap(); - client - .post_beacon_pool_attestations_v2(&unaggregated_attestations, fork_name) - .await - .unwrap(); let signed_aggregates = attestations .into_iter() diff --git a/beacon_node/http_api/tests/interactive_tests.rs b/beacon_node/http_api/tests/interactive_tests.rs index 8cfcf5d93e..60a4c50783 100644 --- a/beacon_node/http_api/tests/interactive_tests.rs +++ b/beacon_node/http_api/tests/interactive_tests.rs @@ -890,27 +890,48 @@ async fn queue_attestations_from_http() { let pre_state = harness.get_current_state(); let (block, post_state) = harness.make_block(pre_state, attestation_slot).await; let block_root = block.0.canonical_root(); + let fork_name = tester.harness.spec.fork_name_at_slot::(attestation_slot); // Make attestations to the block and POST them to the beacon node on a background thread. - let attestations = harness - .make_unaggregated_attestations( - &all_validators, - &post_state, - block.0.state_root(), - block_root.into(), - attestation_slot, - ) - .into_iter() - .flat_map(|attestations| attestations.into_iter().map(|(att, _subnet)| att)) - .collect::>(); + let attestation_future = if fork_name.electra_enabled() { + let single_attestations = harness + .make_single_attestations( + &all_validators, + &post_state, + block.0.state_root(), + block_root.into(), + attestation_slot, + ) + .into_iter() + .flat_map(|attestations| attestations.into_iter().map(|(att, _subnet)| att)) + .collect::>(); - let fork_name = tester.harness.spec.fork_name_at_slot::(attestation_slot); - let attestation_future = tokio::spawn(async move { - client - .post_beacon_pool_attestations_v2(&attestations, fork_name) - .await - .expect("attestations should be processed successfully") - }); + tokio::spawn(async move { + client + .post_beacon_pool_attestations_v2(&single_attestations, fork_name) + .await + .expect("attestations should be processed successfully") + }) + } else { + let attestations = harness + .make_unaggregated_attestations( + &all_validators, + &post_state, + block.0.state_root(), + block_root.into(), + attestation_slot, + ) + .into_iter() + .flat_map(|attestations| attestations.into_iter().map(|(att, _subnet)| att)) + .collect::>(); + + tokio::spawn(async move { + client + .post_beacon_pool_attestations_v1(&attestations) + .await + .expect("attestations should be processed successfully") + }) + }; // In parallel, apply the block. We need to manually notify the reprocess queue, because the // `beacon_chain` does not know about the queue and will not update it for us. diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 85d3b4e9ba..dd6a92603a 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -40,7 +40,8 @@ use tree_hash::TreeHash; use types::application_domain::ApplicationDomain; use types::{ attestation::AttestationBase, AggregateSignature, BitList, Domain, EthSpec, ExecutionBlockHash, - Hash256, Keypair, MainnetEthSpec, RelativeEpoch, SelectionProof, SignedRoot, Slot, + Hash256, Keypair, MainnetEthSpec, RelativeEpoch, SelectionProof, SignedRoot, SingleAttestation, + Slot, }; type E = MainnetEthSpec; @@ -71,6 +72,7 @@ struct ApiTester { next_block: PublishBlockRequest, reorg_block: PublishBlockRequest, attestations: Vec>, + single_attestations: Vec, contribution_and_proofs: Vec>, attester_slashing: AttesterSlashing, proposer_slashing: ProposerSlashing, @@ -203,6 +205,27 @@ impl ApiTester { "precondition: attestations for testing" ); + let fork_name = harness + .chain + .spec + .fork_name_at_slot::(harness.chain.slot().unwrap()); + + let single_attestations = if fork_name.electra_enabled() { + harness + .get_single_attestations( + &AttestationStrategy::AllValidators, + &head.beacon_state, + head_state_root, + head.beacon_block_root, + harness.chain.slot().unwrap(), + ) + .into_iter() + .flat_map(|vec| vec.into_iter().map(|(attestation, _subnet_id)| attestation)) + .collect::>() + } else { + vec![] + }; + let current_epoch = harness .chain .slot() @@ -294,6 +317,7 @@ impl ApiTester { next_block, reorg_block, attestations, + single_attestations, contribution_and_proofs, attester_slashing, proposer_slashing, @@ -381,6 +405,7 @@ impl ApiTester { next_block, reorg_block, attestations, + single_attestations: vec![], contribution_and_proofs: vec![], attester_slashing, proposer_slashing, @@ -1800,13 +1825,16 @@ impl ApiTester { } pub async fn test_post_beacon_pool_attestations_valid_v2(mut self) -> Self { + if self.single_attestations.is_empty() { + return self; + } let fork_name = self - .attestations + .single_attestations .first() - .map(|att| self.chain.spec.fork_name_at_slot::(att.data().slot)) + .map(|att| self.chain.spec.fork_name_at_slot::(att.data.slot)) .unwrap(); self.client - .post_beacon_pool_attestations_v2(self.attestations.as_slice(), fork_name) + .post_beacon_pool_attestations_v2(self.single_attestations.as_slice(), fork_name) .await .unwrap(); assert!( @@ -1854,10 +1882,13 @@ impl ApiTester { self } pub async fn test_post_beacon_pool_attestations_invalid_v2(mut self) -> Self { + if self.single_attestations.is_empty() { + return self; + } let mut attestations = Vec::new(); - for attestation in &self.attestations { + for attestation in &self.single_attestations { let mut invalid_attestation = attestation.clone(); - invalid_attestation.data_mut().slot += 1; + invalid_attestation.data.slot += 1; // add both to ensure we only fail on invalid attestations attestations.push(attestation.clone()); @@ -6011,6 +6042,48 @@ impl ApiTester { self } + pub async fn test_get_events_electra(self) -> Self { + let topics = vec![EventTopic::SingleAttestation]; + let mut events_future = self + .client + .get_events::(topics.as_slice()) + .await + .unwrap(); + + let expected_attestation_len = self.single_attestations.len(); + + let fork_name = self + .chain + .spec + .fork_name_at_slot::(self.chain.slot().unwrap()); + + self.client + .post_beacon_pool_attestations_v2(&self.single_attestations, fork_name) + .await + .unwrap(); + + let attestation_events = poll_events( + &mut events_future, + expected_attestation_len, + Duration::from_millis(10000), + ) + .await; + + assert_eq!( + attestation_events.as_slice(), + self.single_attestations + .clone() + .into_iter() + .map(|single_attestation| EventKind::SingleAttestation(Box::new( + single_attestation + ))) + .collect::>() + .as_slice() + ); + + self + } + pub async fn test_get_events_altair(self) -> Self { let topics = vec![EventTopic::ContributionAndProof]; let mut events_future = self @@ -6158,6 +6231,20 @@ async fn get_events_altair() { .await; } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn get_events_electra() { + let mut config = ApiTesterConfig::default(); + config.spec.altair_fork_epoch = Some(Epoch::new(0)); + config.spec.bellatrix_fork_epoch = Some(Epoch::new(0)); + config.spec.capella_fork_epoch = Some(Epoch::new(0)); + config.spec.deneb_fork_epoch = Some(Epoch::new(0)); + config.spec.electra_fork_epoch = Some(Epoch::new(0)); + ApiTester::new_from_config(config) + .await + .test_get_events_electra() + .await; +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn get_events_from_genesis() { ApiTester::new_from_genesis() diff --git a/beacon_node/lighthouse_network/src/types/pubsub.rs b/beacon_node/lighthouse_network/src/types/pubsub.rs index c976959470..1e1f3efa18 100644 --- a/beacon_node/lighthouse_network/src/types/pubsub.rs +++ b/beacon_node/lighthouse_network/src/types/pubsub.rs @@ -7,15 +7,14 @@ use ssz::{Decode, Encode}; use std::io::{Error, ErrorKind}; use std::sync::Arc; use types::{ - Attestation, AttestationBase, AttestationElectra, AttesterSlashing, AttesterSlashingBase, - AttesterSlashingElectra, BlobSidecar, DataColumnSidecar, DataColumnSubnetId, EthSpec, - ForkContext, ForkName, LightClientFinalityUpdate, LightClientOptimisticUpdate, - ProposerSlashing, SignedAggregateAndProof, SignedAggregateAndProofBase, - SignedAggregateAndProofElectra, SignedBeaconBlock, SignedBeaconBlockAltair, - SignedBeaconBlockBase, SignedBeaconBlockBellatrix, SignedBeaconBlockCapella, - SignedBeaconBlockDeneb, SignedBeaconBlockElectra, SignedBeaconBlockFulu, - SignedBlsToExecutionChange, SignedContributionAndProof, SignedVoluntaryExit, SubnetId, - SyncCommitteeMessage, SyncSubnetId, + Attestation, AttestationBase, AttesterSlashing, AttesterSlashingBase, AttesterSlashingElectra, + BlobSidecar, DataColumnSidecar, DataColumnSubnetId, EthSpec, ForkContext, ForkName, + LightClientFinalityUpdate, LightClientOptimisticUpdate, ProposerSlashing, + SignedAggregateAndProof, SignedAggregateAndProofBase, SignedAggregateAndProofElectra, + SignedBeaconBlock, SignedBeaconBlockAltair, SignedBeaconBlockBase, SignedBeaconBlockBellatrix, + SignedBeaconBlockCapella, SignedBeaconBlockDeneb, SignedBeaconBlockElectra, + SignedBeaconBlockFulu, SignedBlsToExecutionChange, SignedContributionAndProof, + SignedVoluntaryExit, SingleAttestation, SubnetId, SyncCommitteeMessage, SyncSubnetId, }; #[derive(Debug, Clone, PartialEq)] @@ -28,8 +27,10 @@ pub enum PubsubMessage { 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. + /// Gossipsub message providing notification of a raw un-aggregated attestation with its subnet id. Attestation(Box<(SubnetId, Attestation)>), + /// Gossipsub message providing notification of a `SingleAttestation`` with its subnet id. + SingleAttestation(Box<(SubnetId, SingleAttestation)>), /// Gossipsub message providing notification of a voluntary exit. VoluntaryExit(Box), /// Gossipsub message providing notification of a new proposer slashing. @@ -129,6 +130,9 @@ impl PubsubMessage { PubsubMessage::Attestation(attestation_data) => { GossipKind::Attestation(attestation_data.0) } + PubsubMessage::SingleAttestation(attestation_data) => { + GossipKind::Attestation(attestation_data.0) + } PubsubMessage::VoluntaryExit(_) => GossipKind::VoluntaryExit, PubsubMessage::ProposerSlashing(_) => GossipKind::ProposerSlashing, PubsubMessage::AttesterSlashing(_) => GossipKind::AttesterSlashing, @@ -189,32 +193,32 @@ impl PubsubMessage { ))) } GossipKind::Attestation(subnet_id) => { - let attestation = - match fork_context.from_context_bytes(gossip_topic.fork_digest) { - Some(&fork_name) => { - if fork_name.electra_enabled() { - Attestation::Electra( - AttestationElectra::from_ssz_bytes(data) - .map_err(|e| format!("{:?}", e))?, - ) - } else { - Attestation::Base( - AttestationBase::from_ssz_bytes(data) - .map_err(|e| format!("{:?}", e))?, - ) - } + match fork_context.from_context_bytes(gossip_topic.fork_digest) { + Some(&fork_name) => { + if fork_name.electra_enabled() { + let single_attestation = + SingleAttestation::from_ssz_bytes(data) + .map_err(|e| format!("{:?}", e))?; + Ok(PubsubMessage::SingleAttestation(Box::new(( + *subnet_id, + single_attestation, + )))) + } else { + let attestation = Attestation::Base( + AttestationBase::from_ssz_bytes(data) + .map_err(|e| format!("{:?}", e))?, + ); + Ok(PubsubMessage::Attestation(Box::new(( + *subnet_id, + attestation, + )))) } - None => { - return Err(format!( - "Unknown gossipsub fork digest: {:?}", - gossip_topic.fork_digest - )) - } - }; - Ok(PubsubMessage::Attestation(Box::new(( - *subnet_id, - attestation, - )))) + } + None => Err(format!( + "Unknown gossipsub fork digest: {:?}", + gossip_topic.fork_digest + )), + } } GossipKind::BeaconBlock => { let beacon_block = @@ -416,6 +420,7 @@ impl PubsubMessage { PubsubMessage::ProposerSlashing(data) => data.as_ssz_bytes(), PubsubMessage::AttesterSlashing(data) => data.as_ssz_bytes(), PubsubMessage::Attestation(data) => data.1.as_ssz_bytes(), + PubsubMessage::SingleAttestation(data) => data.1.as_ssz_bytes(), PubsubMessage::SignedContributionAndProof(data) => data.as_ssz_bytes(), PubsubMessage::SyncCommitteeMessage(data) => data.1.as_ssz_bytes(), PubsubMessage::BlsToExecutionChange(data) => data.as_ssz_bytes(), @@ -460,6 +465,14 @@ impl std::fmt::Display for PubsubMessage { data.1.data().slot, data.1.committee_index(), ), + PubsubMessage::SingleAttestation(data) => write!( + f, + "SingleAttestation: subnet_id: {}, attestation_slot: {}, committee_index: {:?}, attester_index: {:?}", + *data.0, + data.1.data.slot, + data.1.committee_index, + data.1.attester_index, + ), PubsubMessage::VoluntaryExit(_data) => write!(f, "Voluntary Exit"), PubsubMessage::ProposerSlashing(_data) => write!(f, "Proposer Slashing"), PubsubMessage::AttesterSlashing(_data) => write!(f, "Attester Slashing"), diff --git a/beacon_node/network/src/network_beacon_processor/mod.rs b/beacon_node/network/src/network_beacon_processor/mod.rs index 2d15d39c6f..4a3fb28e10 100644 --- a/beacon_node/network/src/network_beacon_processor/mod.rs +++ b/beacon_node/network/src/network_beacon_processor/mod.rs @@ -84,6 +84,58 @@ impl NetworkBeaconProcessor { .map_err(Into::into) } + /// Create a new `Work` event for some `SingleAttestation`. + pub fn send_single_attestation( + self: &Arc, + message_id: MessageId, + peer_id: PeerId, + single_attestation: SingleAttestation, + subnet_id: SubnetId, + should_import: bool, + seen_timestamp: Duration, + ) -> Result<(), Error> { + let result = self.chain.with_committee_cache( + single_attestation.data.target.root, + single_attestation + .data + .slot + .epoch(T::EthSpec::slots_per_epoch()), + |committee_cache, _| { + let Some(committee) = committee_cache.get_beacon_committee( + single_attestation.data.slot, + single_attestation.committee_index as u64, + ) else { + warn!( + self.log, + "No beacon committee for slot and index"; + "slot" => single_attestation.data.slot, + "index" => single_attestation.committee_index + ); + return Ok(Ok(())); + }; + + let attestation = single_attestation.to_attestation(committee.committee)?; + + Ok(self.send_unaggregated_attestation( + message_id.clone(), + peer_id, + attestation, + subnet_id, + should_import, + seen_timestamp, + )) + }, + ); + + match result { + Ok(result) => result, + Err(e) => { + warn!(self.log, "Failed to send SingleAttestation"; "error" => ?e); + Ok(()) + } + } + } + /// Create a new `Work` event for some unaggregated attestation. pub fn send_unaggregated_attestation( self: &Arc, diff --git a/beacon_node/network/src/router.rs b/beacon_node/network/src/router.rs index 0a99b6af0c..d3da341e1c 100644 --- a/beacon_node/network/src/router.rs +++ b/beacon_node/network/src/router.rs @@ -398,6 +398,17 @@ impl Router { timestamp_now(), ), ), + PubsubMessage::SingleAttestation(subnet_attestation) => self + .handle_beacon_processor_send_result( + self.network_beacon_processor.send_single_attestation( + message_id, + peer_id, + subnet_attestation.1, + subnet_attestation.0, + should_process, + timestamp_now(), + ), + ), PubsubMessage::BeaconBlock(block) => self.handle_beacon_processor_send_result( self.network_beacon_processor.send_gossip_beacon_block( message_id, diff --git a/beacon_node/network/src/service.rs b/beacon_node/network/src/service.rs index 7826807e03..f89241b4ae 100644 --- a/beacon_node/network/src/service.rs +++ b/beacon_node/network/src/service.rs @@ -549,7 +549,23 @@ impl NetworkService { // the attestation, else we just just propagate the Attestation. let should_process = self.subnet_service.should_process_attestation( Subnet::Attestation(subnet_id), - attestation, + attestation.data(), + ); + self.send_to_router(RouterMessage::PubsubMessage( + id, + source, + message, + should_process, + )); + } + PubsubMessage::SingleAttestation(ref subnet_and_attestation) => { + let subnet_id = subnet_and_attestation.0; + let single_attestation = &subnet_and_attestation.1; + // checks if we have an aggregator for the slot. If so, we should process + // the attestation, else we just just propagate the Attestation. + let should_process = self.subnet_service.should_process_attestation( + Subnet::Attestation(subnet_id), + &single_attestation.data, ); self.send_to_router(RouterMessage::PubsubMessage( id, diff --git a/beacon_node/network/src/subnet_service/mod.rs b/beacon_node/network/src/subnet_service/mod.rs index da1f220f04..33ae567eb3 100644 --- a/beacon_node/network/src/subnet_service/mod.rs +++ b/beacon_node/network/src/subnet_service/mod.rs @@ -17,7 +17,7 @@ use lighthouse_network::{discv5::enr::NodeId, NetworkConfig, Subnet, SubnetDisco use slog::{debug, error, o, warn}; use slot_clock::SlotClock; use types::{ - Attestation, EthSpec, Slot, SubnetId, SyncCommitteeSubscription, SyncSubnetId, + AttestationData, EthSpec, Slot, SubnetId, SyncCommitteeSubscription, SyncSubnetId, ValidatorSubscription, }; @@ -363,7 +363,7 @@ impl SubnetService { pub fn should_process_attestation( &self, subnet: Subnet, - attestation: &Attestation, + attestation_data: &AttestationData, ) -> bool { // Proposer-only mode does not need to process attestations if self.proposer_only { @@ -374,7 +374,7 @@ impl SubnetService { .map(|tracked_vals| { tracked_vals.contains_key(&ExactSubnet { subnet, - slot: attestation.data().slot, + slot: attestation_data.slot, }) }) .unwrap_or(true) diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index 12b1538984..af8573a578 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -1324,9 +1324,9 @@ impl BeaconNodeHttpClient { } /// `POST v2/beacon/pool/attestations` - pub async fn post_beacon_pool_attestations_v2( + pub async fn post_beacon_pool_attestations_v2( &self, - attestations: &[Attestation], + attestations: &[SingleAttestation], fork_name: ForkName, ) -> Result<(), Error> { let mut path = self.eth_path(V2)?; diff --git a/common/eth2/src/types.rs b/common/eth2/src/types.rs index 695d536944..6d76101cb6 100644 --- a/common/eth2/src/types.rs +++ b/common/eth2/src/types.rs @@ -1113,6 +1113,7 @@ impl ForkVersionDeserialize for SseExtendedPayloadAttributes { #[serde(bound = "E: EthSpec", untagged)] pub enum EventKind { Attestation(Box>), + SingleAttestation(Box), Block(SseBlock), BlobSidecar(SseBlobSidecar), FinalizedCheckpoint(SseFinalizedCheckpoint), @@ -1139,6 +1140,7 @@ impl EventKind { EventKind::Block(_) => "block", EventKind::BlobSidecar(_) => "blob_sidecar", EventKind::Attestation(_) => "attestation", + EventKind::SingleAttestation(_) => "single_attestation", EventKind::VoluntaryExit(_) => "voluntary_exit", EventKind::FinalizedCheckpoint(_) => "finalized_checkpoint", EventKind::ChainReorg(_) => "chain_reorg", @@ -1161,6 +1163,11 @@ impl EventKind { "attestation" => Ok(EventKind::Attestation(serde_json::from_str(data).map_err( |e| ServerError::InvalidServerSentEvent(format!("Attestation: {:?}", e)), )?)), + "single_attestation" => Ok(EventKind::SingleAttestation( + serde_json::from_str(data).map_err(|e| { + ServerError::InvalidServerSentEvent(format!("SingleAttestation: {:?}", e)) + })?, + )), "block" => Ok(EventKind::Block(serde_json::from_str(data).map_err( |e| ServerError::InvalidServerSentEvent(format!("Block: {:?}", e)), )?)), @@ -1255,6 +1262,7 @@ pub enum EventTopic { Block, BlobSidecar, Attestation, + SingleAttestation, VoluntaryExit, FinalizedCheckpoint, ChainReorg, @@ -1280,6 +1288,7 @@ impl FromStr for EventTopic { "block" => Ok(EventTopic::Block), "blob_sidecar" => Ok(EventTopic::BlobSidecar), "attestation" => Ok(EventTopic::Attestation), + "single_attestation" => Ok(EventTopic::SingleAttestation), "voluntary_exit" => Ok(EventTopic::VoluntaryExit), "finalized_checkpoint" => Ok(EventTopic::FinalizedCheckpoint), "chain_reorg" => Ok(EventTopic::ChainReorg), @@ -1306,6 +1315,7 @@ impl fmt::Display for EventTopic { EventTopic::Block => write!(f, "block"), EventTopic::BlobSidecar => write!(f, "blob_sidecar"), EventTopic::Attestation => write!(f, "attestation"), + EventTopic::SingleAttestation => write!(f, "single_attestation"), EventTopic::VoluntaryExit => write!(f, "voluntary_exit"), EventTopic::FinalizedCheckpoint => write!(f, "finalized_checkpoint"), EventTopic::ChainReorg => write!(f, "chain_reorg"), diff --git a/consensus/types/src/attestation.rs b/consensus/types/src/attestation.rs index 190964736f..47e41acb5b 100644 --- a/consensus/types/src/attestation.rs +++ b/consensus/types/src/attestation.rs @@ -12,8 +12,8 @@ use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; use super::{ - AggregateSignature, AttestationData, BitList, ChainSpec, Domain, EthSpec, Fork, SecretKey, - Signature, SignedRoot, + AggregateSignature, AttestationData, BitList, ChainSpec, CommitteeIndex, Domain, EthSpec, Fork, + SecretKey, Signature, SignedRoot, }; #[derive(Debug, PartialEq)] @@ -24,6 +24,10 @@ pub enum Error { IncorrectStateVariant, InvalidCommitteeLength, InvalidCommitteeIndex, + AttesterNotInCommittee(usize), + InvalidCommittee, + MissingCommittee, + NoCommitteeForSlotAndIndex { slot: Slot, index: CommitteeIndex }, } impl From for Error { @@ -231,6 +235,16 @@ impl Attestation { Attestation::Electra(att) => att.aggregation_bits.get(index), } } + + pub fn to_single_attestation_with_attester_index( + &self, + attester_index: usize, + ) -> Result { + match self { + Self::Base(_) => Err(Error::IncorrectStateVariant), + Self::Electra(attn) => attn.to_single_attestation_with_attester_index(attester_index), + } + } } impl AttestationRef<'_, E> { @@ -287,6 +301,14 @@ impl AttestationElectra { self.get_committee_indices().first().cloned() } + pub fn get_aggregation_bits(&self) -> Vec { + self.aggregation_bits + .iter() + .enumerate() + .filter_map(|(index, bit)| if bit { Some(index as u64) } else { None }) + .collect() + } + pub fn get_committee_indices(&self) -> Vec { self.committee_bits .iter() @@ -350,6 +372,22 @@ impl AttestationElectra { Ok(()) } } + + pub fn to_single_attestation_with_attester_index( + &self, + attester_index: usize, + ) -> Result { + let Some(committee_index) = self.committee_index() else { + return Err(Error::InvalidCommitteeIndex); + }; + + Ok(SingleAttestation { + committee_index: committee_index as usize, + attester_index, + data: self.data.clone(), + signature: self.signature.clone(), + }) + } } impl AttestationBase { @@ -527,6 +565,58 @@ impl ForkVersionDeserialize for Vec> { } } +#[derive( + Debug, + Clone, + Serialize, + Deserialize, + Decode, + Encode, + TestRandom, + Derivative, + arbitrary::Arbitrary, + TreeHash, + PartialEq, +)] +pub struct SingleAttestation { + pub committee_index: usize, + pub attester_index: usize, + pub data: AttestationData, + pub signature: AggregateSignature, +} + +impl SingleAttestation { + pub fn to_attestation(&self, committee: &[usize]) -> Result, Error> { + let aggregation_bit = committee + .iter() + .enumerate() + .find_map(|(i, &validator_index)| { + if self.attester_index == validator_index { + return Some(i); + } + None + }) + .ok_or(Error::AttesterNotInCommittee(self.attester_index))?; + + let mut committee_bits: BitVector = BitVector::default(); + committee_bits + .set(self.committee_index, true) + .map_err(|_| Error::InvalidCommitteeIndex)?; + + let mut aggregation_bits = + BitList::with_capacity(committee.len()).map_err(|_| Error::InvalidCommitteeLength)?; + + aggregation_bits.set(aggregation_bit, true)?; + + Ok(Attestation::Electra(AttestationElectra { + aggregation_bits, + committee_bits, + data: self.data.clone(), + signature: self.signature.clone(), + })) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/consensus/types/src/lib.rs b/consensus/types/src/lib.rs index dcfa918146..11d1f5271b 100644 --- a/consensus/types/src/lib.rs +++ b/consensus/types/src/lib.rs @@ -118,7 +118,7 @@ pub use crate::aggregate_and_proof::{ }; pub use crate::attestation::{ Attestation, AttestationBase, AttestationElectra, AttestationRef, AttestationRefMut, - Error as AttestationError, + Error as AttestationError, SingleAttestation, }; pub use crate::attestation_data::AttestationData; pub use crate::attestation_duty::AttestationDuty; diff --git a/consensus/types/src/subnet_id.rs b/consensus/types/src/subnet_id.rs index 187b070d29..981d6d5653 100644 --- a/consensus/types/src/subnet_id.rs +++ b/consensus/types/src/subnet_id.rs @@ -1,4 +1,5 @@ //! Identifies each shard by an integer identifier. +use crate::SingleAttestation; use crate::{AttestationRef, ChainSpec, CommitteeIndex, EthSpec, Slot}; use alloy_primitives::{bytes::Buf, U256}; use safe_arith::{ArithError, SafeArith}; @@ -57,6 +58,21 @@ impl SubnetId { ) } + /// Compute the subnet for an attestation where each slot in the + /// attestation epoch contains `committee_count_per_slot` committees. + pub fn compute_subnet_for_single_attestation( + attestation: &SingleAttestation, + committee_count_per_slot: u64, + spec: &ChainSpec, + ) -> Result { + Self::compute_subnet::( + attestation.data.slot, + attestation.committee_index as u64, + committee_count_per_slot, + spec, + ) + } + /// Compute the subnet for an attestation with `attestation.data.slot == slot` and /// `attestation.data.index == committee_index` where each slot in the attestation epoch /// contains `committee_count_at_slot` committees. diff --git a/validator_client/validator_services/src/attestation_service.rs b/validator_client/validator_services/src/attestation_service.rs index e31ad4f661..58c6ea3298 100644 --- a/validator_client/validator_services/src/attestation_service.rs +++ b/validator_client/validator_services/src/attestation_service.rs @@ -457,8 +457,30 @@ impl AttestationService { &[validator_metrics::ATTESTATIONS_HTTP_POST], ); if fork_name.electra_enabled() { + let single_attestations = attestations + .iter() + .zip(validator_indices) + .filter_map(|(a, i)| { + match a.to_single_attestation_with_attester_index(*i as usize) { + Ok(a) => Some(a), + Err(e) => { + // This shouldn't happen unless BN and VC are out of sync with + // respect to the Electra fork. + error!( + log, + "Unable to convert to SingleAttestation"; + "error" => ?e, + "committee_index" => attestation_data.index, + "slot" => slot.as_u64(), + "type" => "unaggregated", + ); + None + } + } + }) + .collect::>(); beacon_node - .post_beacon_pool_attestations_v2(attestations, fork_name) + .post_beacon_pool_attestations_v2(&single_attestations, fork_name) .await } else { beacon_node From 6ce33c4d1d8a7b3545181a7c8b0721ddf4c1bf38 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Mon, 20 Jan 2025 20:07:47 +1100 Subject: [PATCH 081/254] Do not send column requests if there is no blob for the block. (#6814) * Do not send column requests if there is no blob for the block. * Address review comments * Replace fix - the previous solution didnt work. --- .../network/src/sync/block_lookups/single_block_lookup.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 9bbd2bf295..a096efcbb2 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 @@ -215,8 +215,7 @@ impl SingleBlockLookup { let block_epoch = block.slot().epoch(T::EthSpec::slots_per_epoch()); if expected_blobs == 0 { self.component_requests = ComponentRequests::NotNeeded("no data"); - } - if cx.chain.should_fetch_blobs(block_epoch) { + } else if cx.chain.should_fetch_blobs(block_epoch) { self.component_requests = ComponentRequests::ActiveBlobRequest( BlobRequestState::new(self.block_root), expected_blobs, From 7a0388ef2aa8ea6de5cfa0be26dd000f011a6484 Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Mon, 20 Jan 2025 19:31:18 +0700 Subject: [PATCH 082/254] Fix custodial peer assumption on lookup custody requests (#6815) * Fix custodial peer assumption on lookup custody requests * lint --- .../network/src/sync/block_lookups/common.rs | 17 ++++--- .../network/src/sync/block_lookups/mod.rs | 13 ++--- .../sync/block_lookups/single_block_lookup.rs | 49 ++++++------------- .../network/src/sync/network_context.rs | 41 ++++++++++++++-- .../src/sync/network_context/custody.rs | 20 ++++++-- 5 files changed, 80 insertions(+), 60 deletions(-) diff --git a/beacon_node/network/src/sync/block_lookups/common.rs b/beacon_node/network/src/sync/block_lookups/common.rs index 5e336d9c38..8eefb2d675 100644 --- a/beacon_node/network/src/sync/block_lookups/common.rs +++ b/beacon_node/network/src/sync/block_lookups/common.rs @@ -9,6 +9,8 @@ use crate::sync::network_context::{LookupRequestResult, SyncNetworkContext}; use beacon_chain::block_verification_types::RpcBlock; use beacon_chain::BeaconChainTypes; use lighthouse_network::service::api_types::Id; +use parking_lot::RwLock; +use std::collections::HashSet; use std::sync::Arc; use types::blob_sidecar::FixedBlobSidecarList; use types::{DataColumnSidecarList, SignedBeaconBlock}; @@ -41,7 +43,7 @@ pub trait RequestState { fn make_request( &self, id: Id, - peer_id: PeerId, + lookup_peers: Arc>>, expected_blobs: usize, cx: &mut SyncNetworkContext, ) -> Result; @@ -76,11 +78,11 @@ impl RequestState for BlockRequestState { fn make_request( &self, id: SingleLookupId, - peer_id: PeerId, + lookup_peers: Arc>>, _: usize, cx: &mut SyncNetworkContext, ) -> Result { - cx.block_lookup_request(id, peer_id, self.requested_block_root) + cx.block_lookup_request(id, lookup_peers, self.requested_block_root) .map_err(LookupRequestError::SendFailedNetwork) } @@ -124,11 +126,11 @@ impl RequestState for BlobRequestState { fn make_request( &self, id: Id, - peer_id: PeerId, + lookup_peers: Arc>>, expected_blobs: usize, cx: &mut SyncNetworkContext, ) -> Result { - cx.blob_lookup_request(id, peer_id, self.block_root, expected_blobs) + cx.blob_lookup_request(id, lookup_peers, self.block_root, expected_blobs) .map_err(LookupRequestError::SendFailedNetwork) } @@ -172,12 +174,11 @@ impl RequestState for CustodyRequestState { fn make_request( &self, id: Id, - // TODO(das): consider selecting peers that have custody but are in this set - _peer_id: PeerId, + lookup_peers: Arc>>, _: usize, cx: &mut SyncNetworkContext, ) -> Result { - cx.custody_lookup_request(id, self.block_root) + cx.custody_lookup_request(id, self.block_root, lookup_peers) .map_err(LookupRequestError::SendFailedNetwork) } diff --git a/beacon_node/network/src/sync/block_lookups/mod.rs b/beacon_node/network/src/sync/block_lookups/mod.rs index 5a11bca481..ac4df42a4e 100644 --- a/beacon_node/network/src/sync/block_lookups/mod.rs +++ b/beacon_node/network/src/sync/block_lookups/mod.rs @@ -153,14 +153,7 @@ impl BlockLookups { pub(crate) fn active_single_lookups(&self) -> Vec { self.single_block_lookups .iter() - .map(|(id, l)| { - ( - *id, - l.block_root(), - l.awaiting_parent(), - l.all_peers().copied().collect(), - ) - }) + .map(|(id, l)| (*id, l.block_root(), l.awaiting_parent(), l.all_peers())) .collect() } @@ -283,7 +276,7 @@ impl BlockLookups { .find(|(_, l)| l.block_root() == parent_chain_tip) { cx.send_sync_message(SyncMessage::AddPeersForceRangeSync { - peers: lookup.all_peers().copied().collect(), + peers: lookup.all_peers(), head_slot: tip_lookup.peek_downloaded_block_slot(), head_root: parent_chain_tip, }); @@ -682,7 +675,7 @@ impl BlockLookups { lookup.continue_requests(cx) } Action::ParentUnknown { parent_root } => { - let peers = lookup.all_peers().copied().collect::>(); + let peers = lookup.all_peers(); 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 a096efcbb2..3789dbe91e 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 @@ -7,7 +7,7 @@ use crate::sync::network_context::{ use beacon_chain::{BeaconChainTypes, BlockProcessStatus}; use derivative::Derivative; use lighthouse_network::service::api_types::Id; -use rand::seq::IteratorRandom; +use parking_lot::RwLock; use std::collections::HashSet; use std::fmt::Debug; use std::sync::Arc; @@ -33,8 +33,6 @@ pub enum LookupRequestError { /// The failed attempts were primarily due to processing failures. cannot_process: bool, }, - /// No peers left to serve this lookup - NoPeers, /// Error sending event to network SendFailedNetwork(RpcRequestSendError), /// Error sending event to processor @@ -63,9 +61,12 @@ pub struct SingleBlockLookup { pub id: Id, pub block_request_state: BlockRequestState, pub component_requests: ComponentRequests, - /// Peers that claim to have imported this set of block components + /// Peers that claim to have imported this set of block components. This state is shared with + /// the custody request to have an updated view of the peers that claim to have imported the + /// block associated with this lookup. The peer set of a lookup can change rapidly, and faster + /// than the lifetime of a custody request. #[derivative(Debug(format_with = "fmt_peer_set_as_len"))] - peers: HashSet, + peers: Arc>>, block_root: Hash256, awaiting_parent: Option, created: Instant, @@ -92,7 +93,7 @@ impl SingleBlockLookup { id, block_request_state: BlockRequestState::new(requested_block_root), component_requests: ComponentRequests::WaitingForBlock, - peers: HashSet::from_iter(peers.iter().copied()), + peers: Arc::new(RwLock::new(HashSet::from_iter(peers.iter().copied()))), block_root: requested_block_root, awaiting_parent, created: Instant::now(), @@ -282,24 +283,11 @@ impl SingleBlockLookup { return Err(LookupRequestError::TooManyAttempts { cannot_process }); } - let Some(peer_id) = self.use_rand_available_peer() else { - // Allow lookup to not have any peers and do nothing. This is an optimization to not - // lose progress of lookups created from a block with unknown parent before we receive - // attestations for said block. - // Lookup sync event safety: If a lookup requires peers to make progress, and does - // not receive any new peers for some time it will be dropped. If it receives a new - // peer it must attempt to make progress. - R::request_state_mut(self) - .map_err(|e| LookupRequestError::BadState(e.to_owned()))? - .get_state_mut() - .update_awaiting_download_status("no peers"); - return Ok(()); - }; - + let peers = self.peers.clone(); let request = R::request_state_mut(self) .map_err(|e| LookupRequestError::BadState(e.to_owned()))?; - match request.make_request(id, peer_id, expected_blobs, cx)? { + match request.make_request(id, peers, expected_blobs, cx)? { LookupRequestResult::RequestSent(req_id) => { // Lookup sync event safety: If make_request returns `RequestSent`, we are // guaranteed that `BlockLookups::on_download_response` will be called exactly @@ -347,29 +335,24 @@ impl SingleBlockLookup { } /// Get all unique peers that claim to have imported this set of block components - pub fn all_peers(&self) -> impl Iterator + '_ { - self.peers.iter() + pub fn all_peers(&self) -> Vec { + self.peers.read().iter().copied().collect() } /// 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 { - self.peers.insert(peer_id) + self.peers.write().insert(peer_id) } /// Remove peer from available peers. pub fn remove_peer(&mut self, peer_id: &PeerId) { - self.peers.remove(peer_id); + self.peers.write().remove(peer_id); } /// Returns true if this lookup has zero peers pub fn has_no_peers(&self) -> bool { - self.peers.is_empty() - } - - /// Selects a random peer from available peers if any - fn use_rand_available_peer(&mut self) -> Option { - self.peers.iter().choose(&mut rand::thread_rng()).copied() + self.peers.read().is_empty() } } @@ -688,8 +671,8 @@ impl std::fmt::Debug for State { } fn fmt_peer_set_as_len( - peer_set: &HashSet, + peer_set: &Arc>>, f: &mut std::fmt::Formatter, ) -> Result<(), std::fmt::Error> { - write!(f, "{}", peer_set.len()) + write!(f, "{}", peer_set.read().len()) } diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index 0a6bc8961f..f899936128 100644 --- a/beacon_node/network/src/sync/network_context.rs +++ b/beacon_node/network/src/sync/network_context.rs @@ -27,7 +27,8 @@ use lighthouse_network::service::api_types::{ DataColumnsByRootRequester, Id, SingleLookupReqId, SyncRequestId, }; use lighthouse_network::{Client, NetworkGlobals, PeerAction, PeerId, ReportSource}; -use rand::seq::SliceRandom; +use parking_lot::RwLock; +use rand::prelude::IteratorRandom; use rand::thread_rng; pub use requests::LookupVerifyError; use requests::{ @@ -308,8 +309,8 @@ impl SyncNetworkContext { pub fn get_random_custodial_peer(&self, column_index: ColumnIndex) -> Option { self.get_custodial_peers(column_index) + .into_iter() .choose(&mut thread_rng()) - .cloned() } pub fn network_globals(&self) -> &NetworkGlobals { @@ -562,9 +563,24 @@ impl SyncNetworkContext { pub fn block_lookup_request( &mut self, lookup_id: SingleLookupId, - peer_id: PeerId, + lookup_peers: Arc>>, block_root: Hash256, ) -> Result { + let Some(peer_id) = lookup_peers + .read() + .iter() + .choose(&mut rand::thread_rng()) + .copied() + else { + // Allow lookup to not have any peers and do nothing. This is an optimization to not + // lose progress of lookups created from a block with unknown parent before we receive + // attestations for said block. + // Lookup sync event safety: If a lookup requires peers to make progress, and does + // not receive any new peers for some time it will be dropped. If it receives a new + // peer it must attempt to make progress. + return Ok(LookupRequestResult::Pending("no peers")); + }; + match self.chain.get_block_process_status(&block_root) { // Unknown block, continue request to download BlockProcessStatus::Unknown => {} @@ -634,10 +650,25 @@ impl SyncNetworkContext { pub fn blob_lookup_request( &mut self, lookup_id: SingleLookupId, - peer_id: PeerId, + lookup_peers: Arc>>, block_root: Hash256, expected_blobs: usize, ) -> Result { + let Some(peer_id) = lookup_peers + .read() + .iter() + .choose(&mut rand::thread_rng()) + .copied() + else { + // Allow lookup to not have any peers and do nothing. This is an optimization to not + // lose progress of lookups created from a block with unknown parent before we receive + // attestations for said block. + // Lookup sync event safety: If a lookup requires peers to make progress, and does + // not receive any new peers for some time it will be dropped. If it receives a new + // peer it must attempt to make progress. + return Ok(LookupRequestResult::Pending("no peers")); + }; + let imported_blob_indexes = self .chain .data_availability_checker @@ -740,6 +771,7 @@ impl SyncNetworkContext { &mut self, lookup_id: SingleLookupId, block_root: Hash256, + lookup_peers: Arc>>, ) -> Result { let custody_indexes_imported = self .chain @@ -777,6 +809,7 @@ impl SyncNetworkContext { block_root, CustodyId { requester }, &custody_indexes_to_fetch, + lookup_peers, self.log.clone(), ); diff --git a/beacon_node/network/src/sync/network_context/custody.rs b/beacon_node/network/src/sync/network_context/custody.rs index e4bce3dafc..8a29545c21 100644 --- a/beacon_node/network/src/sync/network_context/custody.rs +++ b/beacon_node/network/src/sync/network_context/custody.rs @@ -7,8 +7,10 @@ use fnv::FnvHashMap; use lighthouse_network::service::api_types::{CustodyId, DataColumnsByRootRequester}; use lighthouse_network::PeerId; use lru_cache::LRUTimeCache; +use parking_lot::RwLock; use rand::Rng; use slog::{debug, warn}; +use std::collections::HashSet; use std::time::{Duration, Instant}; use std::{collections::HashMap, marker::PhantomData, sync::Arc}; use types::EthSpec; @@ -32,6 +34,8 @@ pub struct ActiveCustodyRequest { /// 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, + /// Set of peers that claim to have imported this block and their custody columns + lookup_peers: Arc>>, /// Logger for the `SyncNetworkContext`. pub log: slog::Logger, _phantom: PhantomData, @@ -64,6 +68,7 @@ impl ActiveCustodyRequest { block_root: Hash256, custody_id: CustodyId, column_indices: &[ColumnIndex], + lookup_peers: Arc>>, log: slog::Logger, ) -> Self { Self { @@ -76,6 +81,7 @@ impl ActiveCustodyRequest { ), active_batch_columns_requests: <_>::default(), failed_peers: LRUTimeCache::new(Duration::from_secs(FAILED_PEERS_CACHE_EXPIRY_SECONDS)), + lookup_peers, log, _phantom: PhantomData, } @@ -215,6 +221,7 @@ impl ActiveCustodyRequest { } let mut columns_to_request_by_peer = HashMap::>::new(); + let lookup_peers = self.lookup_peers.read(); // Need to: // - track how many active requests a peer has for load balancing @@ -244,6 +251,8 @@ impl ActiveCustodyRequest { .iter() .map(|peer| { ( + // Prioritize peers that claim to know have imported this block + if lookup_peers.contains(peer) { 0 } else { 1 }, // De-prioritize peers that have failed to successfully respond to // requests recently self.failed_peers.contains(peer), @@ -257,7 +266,7 @@ impl ActiveCustodyRequest { .collect::>(); priorized_peers.sort_unstable(); - if let Some((_, _, _, peer_id)) = priorized_peers.first() { + if let Some((_, _, _, _, peer_id)) = priorized_peers.first() { columns_to_request_by_peer .entry(*peer_id) .or_default() @@ -283,10 +292,11 @@ impl ActiveCustodyRequest { block_root: self.block_root, indices: indices.clone(), }, - // true = enforce max_requests are returned data_columns_by_root. We only issue requests - // for blocks after we know the block has data, and only request peers after they claim to - // have imported the block+columns and claim to be custodians - true, + // If peer is in the lookup peer set, it claims to have imported the block and + // must have its columns in custody. In that case, set `true = enforce max_requests` + // and downscore if data_columns_by_root does not returned the expected custody + // columns. For the rest of peers, don't downscore if columns are missing. + lookup_peers.contains(&peer_id), ) .map_err(Error::SendFailed)?; From 33c1648022e85d3443ed4c2fb72a3742975e9adb Mon Sep 17 00:00:00 2001 From: JKinc <73645805+JKincorperated@users.noreply.github.com> Date: Tue, 21 Jan 2025 00:24:14 +0000 Subject: [PATCH 083/254] Add EIP-7636 support (#6793) * Add eip7636 support * Add `version()` to the `lighthouse_version` crate and make the `enr.rs` file use it. * Hardcode version, Add `client_name()`, remove unneeded flag. * Make it use the new function. * Make cargo fmt zip it --- beacon_node/lighthouse_network/src/config.rs | 3 ++- .../lighthouse_network/src/discovery/enr.rs | 6 +++++ common/lighthouse_version/src/lib.rs | 26 +++++++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/beacon_node/lighthouse_network/src/config.rs b/beacon_node/lighthouse_network/src/config.rs index 8a93b1185d..55c1dbf491 100644 --- a/beacon_node/lighthouse_network/src/config.rs +++ b/beacon_node/lighthouse_network/src/config.rs @@ -116,7 +116,8 @@ pub struct Config { pub network_load: u8, /// Indicates if the user has set the network to be in private mode. Currently this - /// prevents sending client identifying information over identify. + /// prevents sending client identifying information over identify and prevents + /// EIP-7636 indentifiable information being provided in the ENR. pub private: bool, /// Shutdown beacon node after sync is completed. diff --git a/beacon_node/lighthouse_network/src/discovery/enr.rs b/beacon_node/lighthouse_network/src/discovery/enr.rs index 8946c7753c..062a119e0d 100644 --- a/beacon_node/lighthouse_network/src/discovery/enr.rs +++ b/beacon_node/lighthouse_network/src/discovery/enr.rs @@ -8,6 +8,7 @@ use crate::types::{Enr, EnrAttestationBitfield, EnrSyncCommitteeBitfield}; use crate::NetworkConfig; use alloy_rlp::bytes::Bytes; use libp2p::identity::Keypair; +use lighthouse_version::{client_name, version}; use slog::{debug, warn}; use ssz::{Decode, Encode}; use ssz_types::BitVector; @@ -188,6 +189,11 @@ pub fn build_enr( builder.udp6(udp6_port.get()); } + // Add EIP 7636 client information + if !config.private { + builder.client_info(client_name().to_string(), version().to_string(), None); + } + // Add QUIC fields to the ENR. // Since QUIC is used as an alternative transport for the libp2p protocols, // the related fields should only be added when both QUIC and libp2p are enabled diff --git a/common/lighthouse_version/src/lib.rs b/common/lighthouse_version/src/lib.rs index 0751bdadff..a35b8c42c1 100644 --- a/common/lighthouse_version/src/lib.rs +++ b/common/lighthouse_version/src/lib.rs @@ -48,6 +48,22 @@ pub fn version_with_platform() -> String { format!("{}/{}-{}", VERSION, Target::arch(), Target::os()) } +/// Returns semantic versioning information only. +/// +/// ## Example +/// +/// `1.5.1` +pub fn version() -> &'static str { + "6.0.1" +} + +/// Returns the name of the current client running. +/// +/// This will usually be "Lighthouse" +pub fn client_name() -> &'static str { + "Lighthouse" +} + #[cfg(test)] mod test { use super::*; @@ -64,4 +80,14 @@ mod test { VERSION ); } + + #[test] + fn semantic_version_formatting() { + let re = Regex::new(r"^[0-9]+\.[0-9]+\.[0-9]+").unwrap(); + assert!( + re.is_match(version()), + "semantic version doesn't match regex: {}", + version() + ); + } } From c33307d70287fd3b7a70785f89dadcb737214903 Mon Sep 17 00:00:00 2001 From: Pawan Dhananjay Date: Tue, 21 Jan 2025 12:23:21 -0800 Subject: [PATCH 084/254] Refactor mock builder (#6735) * Update builder api for electra * Refactor mock builder to separate functionality * Return a higher payload value for builder by default * Add additional methods * Cleanup * Add a flag for always returning a max bid * Add logs for debugging * Take builder secret key as an argument * Merge branch 'unstable' into refactor-mock-builder * Change return type for submit_blinded_blocks * Merge branch 'unstable' into refactor-mock-builder * Respect gas_limit from validator registration * Revert "Respect gas_limit from validator registration" This reverts commit 1f7b4a327e95d0c7aea3e28dfd3666c093033d89. * Merge branch 'unstable' into refactor-mock-builder * Remove unnecessary derive --- beacon_node/execution_layer/src/lib.rs | 5 +- .../src/test_utils/mock_builder.rs | 944 +++++++++++------- consensus/types/src/builder_bid.rs | 6 +- 3 files changed, 611 insertions(+), 344 deletions(-) diff --git a/beacon_node/execution_layer/src/lib.rs b/beacon_node/execution_layer/src/lib.rs index f7abe73543..d5fef4c5aa 100644 --- a/beacon_node/execution_layer/src/lib.rs +++ b/beacon_node/execution_layer/src/lib.rs @@ -121,8 +121,7 @@ impl TryFrom> for ProvenancedPayload BlockProposalContents::PayloadAndBlobs { payload: ExecutionPayloadHeader::Fulu(builder_bid.header).into(), @@ -330,7 +329,7 @@ impl> BlockProposalContents { pub parent_hash: ExecutionBlockHash, pub parent_gas_limit: u64, diff --git a/beacon_node/execution_layer/src/test_utils/mock_builder.rs b/beacon_node/execution_layer/src/test_utils/mock_builder.rs index 65181dcf4f..3540909fe4 100644 --- a/beacon_node/execution_layer/src/test_utils/mock_builder.rs +++ b/beacon_node/execution_layer/src/test_utils/mock_builder.rs @@ -1,10 +1,15 @@ use crate::test_utils::{DEFAULT_BUILDER_PAYLOAD_VALUE_WEI, DEFAULT_JWT_SECRET}; use crate::{Config, ExecutionLayer, PayloadAttributes, PayloadParameters}; -use eth2::types::{BlobsBundle, BlockId, StateId, ValidatorId}; +use eth2::types::PublishBlockRequest; +use eth2::types::{ + BlobsBundle, BlockId, BroadcastValidation, EventKind, EventTopic, FullPayloadContents, + ProposerData, StateId, ValidatorId, +}; use eth2::{BeaconNodeHttpClient, Timeouts, CONSENSUS_VERSION_HEADER}; use fork_choice::ForkchoiceUpdateParameters; use parking_lot::RwLock; use sensitive_url::SensitiveUrl; +use slog::{debug, error, info, warn, Logger}; use std::collections::HashMap; use std::fmt::Debug; use std::future::Future; @@ -13,20 +18,26 @@ use std::sync::Arc; use std::time::Duration; use task_executor::TaskExecutor; use tempfile::NamedTempFile; +use tokio_stream::StreamExt; use tree_hash::TreeHash; use types::builder_bid::{ BuilderBid, BuilderBidBellatrix, BuilderBidCapella, BuilderBidDeneb, BuilderBidElectra, BuilderBidFulu, SignedBuilderBid, }; use types::{ - Address, BeaconState, ChainSpec, EthSpec, ExecPayload, ExecutionPayload, - ExecutionPayloadHeaderRefMut, ExecutionRequests, FixedBytesExtended, ForkName, - ForkVersionedResponse, Hash256, PublicKeyBytes, Signature, SignedBlindedBeaconBlock, - SignedRoot, SignedValidatorRegistrationData, Slot, Uint256, + Address, BeaconState, ChainSpec, Epoch, EthSpec, ExecPayload, ExecutionPayload, + ExecutionPayloadHeaderRefMut, ExecutionRequests, ForkName, ForkVersionedResponse, Hash256, + PublicKeyBytes, Signature, SignedBlindedBeaconBlock, SignedRoot, + SignedValidatorRegistrationData, Slot, Uint256, }; use types::{ExecutionBlockHash, SecretKey}; use warp::{Filter, Rejection}; +pub const DEFAULT_FEE_RECIPIENT: Address = Address::repeat_byte(42); +pub const DEFAULT_GAS_LIMIT: u64 = 30_000_000; +pub const DEFAULT_BUILDER_PRIVATE_KEY: &str = + "607a11b45a7219cc61a3d9c5fd08c7eebd602a6a19a977f8d3771d5711a550f2"; + #[derive(Clone)] pub enum Operation { FeeRecipient(Address), @@ -259,6 +270,17 @@ impl BidStuff for BuilderBid { } } +// Non referenced version of `PayloadParameters` +#[derive(Clone)] +pub struct PayloadParametersCloned { + pub parent_hash: ExecutionBlockHash, + pub parent_gas_limit: u64, + pub proposer_gas_limit: Option, + pub payload_attributes: PayloadAttributes, + pub forkchoice_update_params: ForkchoiceUpdateParameters, + pub current_fork: ForkName, +} + #[derive(Clone)] pub struct MockBuilder { el: ExecutionLayer, @@ -268,6 +290,20 @@ pub struct MockBuilder { builder_sk: SecretKey, operations: Arc>>, invalidate_signatures: Arc>, + genesis_time: Option, + /// Only returns bids for registered validators if set to true. `true` by default. + validate_pubkey: bool, + /// Do not apply any operations if set to `false`. + /// Applying operations might modify the cached header in the execution layer. + /// Use this if you want get_header to return a valid bid that can be eventually submitted as + /// a valid block. + apply_operations: bool, + payload_id_cache: Arc>>, + /// If set to `true`, sets the bid returned by `get_header` to Uint256::MAX + max_bid: bool, + /// A cache that stores the proposers index for a given epoch + proposers_cache: Arc>>>, + log: Logger, } impl MockBuilder { @@ -295,7 +331,12 @@ impl MockBuilder { let builder = MockBuilder::new( el, BeaconNodeHttpClient::new(beacon_url, Timeouts::set_all(Duration::from_secs(1))), + true, + true, + false, spec, + None, + executor.log().clone(), ); let host: Ipv4Addr = Ipv4Addr::LOCALHOST; let port = 0; @@ -303,21 +344,47 @@ impl MockBuilder { (builder, server) } + #[allow(clippy::too_many_arguments)] pub fn new( el: ExecutionLayer, beacon_client: BeaconNodeHttpClient, + validate_pubkey: bool, + apply_operations: bool, + max_bid: bool, spec: Arc, + sk: Option<&[u8]>, + log: Logger, ) -> Self { - let sk = SecretKey::random(); + let builder_sk = if let Some(sk_bytes) = sk { + match SecretKey::deserialize(sk_bytes) { + Ok(sk) => sk, + Err(_) => { + error!( + log, + "Invalid sk_bytes provided, generating random secret key" + ); + SecretKey::random() + } + } + } else { + SecretKey::deserialize(&hex::decode(DEFAULT_BUILDER_PRIVATE_KEY).unwrap()).unwrap() + }; Self { el, beacon_client, // Should keep spec and context consistent somehow spec, val_registration_cache: Arc::new(RwLock::new(HashMap::new())), - builder_sk: sk, + builder_sk, + validate_pubkey, operations: Arc::new(RwLock::new(vec![])), invalidate_signatures: Arc::new(RwLock::new(false)), + payload_id_cache: Arc::new(RwLock::new(HashMap::new())), + proposers_cache: Arc::new(RwLock::new(HashMap::new())), + apply_operations, + max_bid, + genesis_time: None, + log, } } @@ -342,8 +409,523 @@ impl MockBuilder { } bid.stamp_payload(); } + + /// Return the public key of the builder + pub fn public_key(&self) -> PublicKeyBytes { + self.builder_sk.public_key().compress() + } + + pub async fn register_validators( + &self, + registrations: Vec, + ) -> Result<(), String> { + info!( + self.log, + "Registering validators"; + "count" => registrations.len(), + ); + for registration in registrations { + if !registration.verify_signature(&self.spec) { + error!( + self.log, + "Failed to register validator"; + "error" => "invalid signature", + "validator" => %registration.message.pubkey + ); + return Err("invalid signature".to_string()); + } + self.val_registration_cache + .write() + .insert(registration.message.pubkey, registration); + } + Ok(()) + } + + pub async fn submit_blinded_block( + &self, + block: SignedBlindedBeaconBlock, + ) -> Result, String> { + let root = match &block { + SignedBlindedBeaconBlock::Base(_) | types::SignedBeaconBlock::Altair(_) => { + return Err("invalid fork".to_string()); + } + SignedBlindedBeaconBlock::Bellatrix(block) => { + block.message.body.execution_payload.tree_hash_root() + } + SignedBlindedBeaconBlock::Capella(block) => { + block.message.body.execution_payload.tree_hash_root() + } + SignedBlindedBeaconBlock::Deneb(block) => { + block.message.body.execution_payload.tree_hash_root() + } + SignedBlindedBeaconBlock::Electra(block) => { + block.message.body.execution_payload.tree_hash_root() + } + SignedBlindedBeaconBlock::Fulu(block) => { + block.message.body.execution_payload.tree_hash_root() + } + }; + info!( + self.log, + "Submitting blinded beacon block to builder"; + "block_hash" => %root + ); + let payload = self + .el + .get_payload_by_root(&root) + .ok_or_else(|| "missing payload for tx root".to_string())?; + + let (payload, blobs) = payload.deconstruct(); + let full_block = block + .try_into_full_block(Some(payload.clone())) + .ok_or("Internal error, just provided a payload")?; + debug!( + self.log, + "Got full payload, sending to local beacon node for propagation"; + "txs_count" => payload.transactions().len(), + "blob_count" => blobs.as_ref().map(|b| b.commitments.len()) + ); + let publish_block_request = PublishBlockRequest::new( + Arc::new(full_block), + blobs.clone().map(|b| (b.proofs, b.blobs)), + ); + self.beacon_client + .post_beacon_blocks_v2(&publish_block_request, Some(BroadcastValidation::Gossip)) + .await + .map_err(|e| format!("Failed to post blinded block {:?}", e))?; + Ok(FullPayloadContents::new(payload, blobs)) + } + + pub async fn get_header( + &self, + slot: Slot, + parent_hash: ExecutionBlockHash, + pubkey: PublicKeyBytes, + ) -> Result, String> { + info!(self.log, "In get_header"); + // Check if the pubkey has registered with the builder if required + if self.validate_pubkey && !self.val_registration_cache.read().contains_key(&pubkey) { + return Err("validator not registered with builder".to_string()); + } + let payload_parameters = { + let mut guard = self.payload_id_cache.write(); + guard.remove(&parent_hash) + }; + + let payload_parameters = match payload_parameters { + Some(params) => params, + None => { + warn!( + self.log, + "Payload params not cached for parent_hash {}", parent_hash + ); + self.get_payload_params(slot, None, pubkey, None).await? + } + }; + + info!(self.log, "Got payload params"); + + let fork = self.fork_name_at_slot(slot); + let payload_response_type = self + .el + .get_full_payload_caching(PayloadParameters { + parent_hash: payload_parameters.parent_hash, + parent_gas_limit: payload_parameters.parent_gas_limit, + proposer_gas_limit: payload_parameters.proposer_gas_limit, + payload_attributes: &payload_parameters.payload_attributes, + forkchoice_update_params: &payload_parameters.forkchoice_update_params, + current_fork: payload_parameters.current_fork, + }) + .await + .map_err(|e| format!("couldn't get payload {:?}", e))?; + + info!(self.log, "Got payload message, fork {}", fork); + + let mut message = match payload_response_type { + crate::GetPayloadResponseType::Full(payload_response) => { + #[allow(clippy::type_complexity)] + let (payload, value, maybe_blobs_bundle, maybe_requests): ( + ExecutionPayload, + Uint256, + Option>, + Option>, + ) = payload_response.into(); + + match fork { + ForkName::Fulu => BuilderBid::Fulu(BuilderBidFulu { + header: payload + .as_fulu() + .map_err(|_| "incorrect payload variant".to_string())? + .into(), + blob_kzg_commitments: maybe_blobs_bundle + .map(|b| b.commitments) + .unwrap_or_default(), + value: self.get_bid_value(value), + pubkey: self.builder_sk.public_key().compress(), + execution_requests: maybe_requests.unwrap_or_default(), + }), + ForkName::Electra => BuilderBid::Electra(BuilderBidElectra { + header: payload + .as_electra() + .map_err(|_| "incorrect payload variant".to_string())? + .into(), + blob_kzg_commitments: maybe_blobs_bundle + .map(|b| b.commitments) + .unwrap_or_default(), + value: self.get_bid_value(value), + pubkey: self.builder_sk.public_key().compress(), + execution_requests: maybe_requests.unwrap_or_default(), + }), + ForkName::Deneb => BuilderBid::Deneb(BuilderBidDeneb { + header: payload + .as_deneb() + .map_err(|_| "incorrect payload variant".to_string())? + .into(), + blob_kzg_commitments: maybe_blobs_bundle + .map(|b| b.commitments) + .unwrap_or_default(), + value: self.get_bid_value(value), + pubkey: self.builder_sk.public_key().compress(), + }), + ForkName::Capella => BuilderBid::Capella(BuilderBidCapella { + header: payload + .as_capella() + .map_err(|_| "incorrect payload variant".to_string())? + .into(), + value: self.get_bid_value(value), + pubkey: self.builder_sk.public_key().compress(), + }), + ForkName::Bellatrix => BuilderBid::Bellatrix(BuilderBidBellatrix { + header: payload + .as_bellatrix() + .map_err(|_| "incorrect payload variant".to_string())? + .into(), + value: self.get_bid_value(value), + pubkey: self.builder_sk.public_key().compress(), + }), + ForkName::Base | ForkName::Altair => return Err("invalid fork".to_string()), + } + } + _ => panic!("just requested full payload, cannot get blinded"), + }; + + if self.apply_operations { + info!(self.log, "Applying operations"); + self.apply_operations(&mut message); + } + info!(self.log, "Signing builder message"); + + let mut signature = message.sign_builder_message(&self.builder_sk, &self.spec); + + if *self.invalidate_signatures.read() { + signature = Signature::empty(); + }; + let signed_bid = SignedBuilderBid { message, signature }; + info!(self.log, "Builder bid {:?}", &signed_bid.message.value()); + Ok(signed_bid) + } + + fn fork_name_at_slot(&self, slot: Slot) -> ForkName { + self.spec.fork_name_at_slot::(slot) + } + + fn get_bid_value(&self, value: Uint256) -> Uint256 { + if self.max_bid { + Uint256::MAX + } else if !self.apply_operations { + value + } else { + Uint256::from(DEFAULT_BUILDER_PAYLOAD_VALUE_WEI) + } + } + + /// Prepare the execution layer for payload creation every slot for the correct + /// proposer index + pub async fn prepare_execution_layer(&self) -> Result<(), String> { + info!( + self.log, + "Starting a task to prepare the execution layer"; + ); + let mut head_event_stream = self + .beacon_client + .get_events::(&[EventTopic::Head]) + .await + .map_err(|e| format!("Failed to get head event {:?}", e))?; + + while let Some(Ok(event)) = head_event_stream.next().await { + match event { + EventKind::Head(head) => { + debug!( + self.log, + "Got a new head event"; + "block_hash" => %head.block + ); + let next_slot = head.slot + 1; + // Find the next proposer index from the cached data or through a beacon api call + let epoch = next_slot.epoch(E::slots_per_epoch()); + let position_in_slot = next_slot.as_u64() % E::slots_per_epoch(); + let proposer_data = { + let proposers_opt = { + let proposers_cache = self.proposers_cache.read(); + proposers_cache.get(&epoch).cloned() + }; + match proposers_opt { + Some(proposers) => proposers + .get(position_in_slot as usize) + .expect("position in slot is max epoch size") + .clone(), + None => { + // make a call to the beacon api and populate the cache + let duties: Vec<_> = self + .beacon_client + .get_validator_duties_proposer(epoch) + .await + .map_err(|e| { + format!( + "Failed to get proposer duties for epoch: {}, {:?}", + epoch, e + ) + })? + .data; + let proposer_data = duties + .get(position_in_slot as usize) + .expect("position in slot is max epoch size") + .clone(); + self.proposers_cache.write().insert(epoch, duties); + proposer_data + } + } + }; + self.prepare_execution_layer_internal( + head.slot, + head.block, + proposer_data.validator_index, + proposer_data.pubkey, + ) + .await?; + } + e => { + warn!( + self.log, + "Got an unexpected event"; + "event" => %e.topic_name() + ); + } + } + } + Ok(()) + } + + async fn prepare_execution_layer_internal( + &self, + current_slot: Slot, + head_block_root: Hash256, + validator_index: u64, + pubkey: PublicKeyBytes, + ) -> Result<(), String> { + let next_slot = current_slot + 1; + let payload_parameters = self + .get_payload_params( + next_slot, + Some(head_block_root), + pubkey, + Some(validator_index), + ) + .await?; + + self.payload_id_cache + .write() + .insert(payload_parameters.parent_hash, payload_parameters); + Ok(()) + } + + /// Get the `PayloadParameters` for requesting an ExecutionPayload for `slot` + /// for the given `validator_index` and `pubkey`. + async fn get_payload_params( + &self, + slot: Slot, + head_block_root: Option, + pubkey: PublicKeyBytes, + validator_index: Option, + ) -> Result { + let fork = self.fork_name_at_slot(slot); + + let block_id = match head_block_root { + Some(block_root) => BlockId::Root(block_root), + None => BlockId::Head, + }; + let head = self + .beacon_client + .get_beacon_blocks::(block_id) + .await + .map_err(|_| "couldn't get head".to_string())? + .ok_or_else(|| "missing head block".to_string())? + .data; + + let head_block_root = head_block_root.unwrap_or(head.canonical_root()); + + let head_execution_payload = head + .message() + .body() + .execution_payload() + .map_err(|_| "pre-merge block".to_string())?; + let head_execution_hash = head_execution_payload.block_hash(); + let head_gas_limit = head_execution_payload.gas_limit(); + + let finalized_execution_hash = self + .beacon_client + .get_beacon_blocks::(BlockId::Finalized) + .await + .map_err(|_| "couldn't get finalized block".to_string())? + .ok_or_else(|| "missing finalized block".to_string())? + .data + .message() + .body() + .execution_payload() + .map_err(|_| "pre-merge block".to_string())? + .block_hash(); + + let justified_execution_hash = self + .beacon_client + .get_beacon_blocks::(BlockId::Justified) + .await + .map_err(|_| "couldn't get justified block".to_string())? + .ok_or_else(|| "missing justified block".to_string())? + .data + .message() + .body() + .execution_payload() + .map_err(|_| "pre-merge block".to_string())? + .block_hash(); + + let (fee_recipient, proposer_gas_limit) = + match self.val_registration_cache.read().get(&pubkey) { + Some(cached_data) => ( + cached_data.message.fee_recipient, + cached_data.message.gas_limit, + ), + None => { + warn!( + self.log, + "Validator not registered {}, using default fee recipient and gas limits", + pubkey + ); + (DEFAULT_FEE_RECIPIENT, DEFAULT_GAS_LIMIT) + } + }; + let slots_since_genesis = slot.as_u64() - self.spec.genesis_slot.as_u64(); + + let genesis_time = if let Some(genesis_time) = self.genesis_time { + genesis_time + } else { + self.beacon_client + .get_beacon_genesis() + .await + .map_err(|_| "couldn't get beacon genesis".to_string())? + .data + .genesis_time + }; + let timestamp = (slots_since_genesis * self.spec.seconds_per_slot) + genesis_time; + + let head_state: BeaconState = self + .beacon_client + .get_debug_beacon_states(StateId::Head) + .await + .map_err(|_| "couldn't get state".to_string())? + .ok_or_else(|| "missing state".to_string())? + .data; + + let prev_randao = head_state + .get_randao_mix(head_state.current_epoch()) + .map_err(|_| "couldn't get prev randao".to_string())?; + + let expected_withdrawals = if fork.capella_enabled() { + Some( + self.beacon_client + .get_expected_withdrawals(&StateId::Head) + .await + .map_err(|e| format!("Failed to get expected withdrawals: {:?}", e))? + .data, + ) + } else { + None + }; + + let payload_attributes = match fork { + // the withdrawals root is filled in by operations, but we supply the valid withdrawals + // first to avoid polluting the execution block generator with invalid payload attributes + // NOTE: this was part of an effort to add payload attribute uniqueness checks, + // which was abandoned because it broke too many tests in subtle ways. + ForkName::Bellatrix | ForkName::Capella => PayloadAttributes::new( + timestamp, + *prev_randao, + fee_recipient, + expected_withdrawals, + None, + ), + ForkName::Deneb | ForkName::Electra | ForkName::Fulu => PayloadAttributes::new( + timestamp, + *prev_randao, + fee_recipient, + expected_withdrawals, + Some(head_block_root), + ), + ForkName::Base | ForkName::Altair => { + return Err("invalid fork".to_string()); + } + }; + + // Tells the execution layer that the `validator_index` is expected to propose + // a block on top of `head_block_root` for the given slot + let val_index = validator_index.unwrap_or( + self.beacon_client + .get_beacon_states_validator_id(StateId::Head, &ValidatorId::PublicKey(pubkey)) + .await + .map_err(|_| "couldn't get validator".to_string())? + .ok_or_else(|| "missing validator".to_string())? + .data + .index, + ); + + self.el + .insert_proposer(slot, head_block_root, val_index, payload_attributes.clone()) + .await; + + let forkchoice_update_params = ForkchoiceUpdateParameters { + head_hash: Some(head_execution_hash), + finalized_hash: Some(finalized_execution_hash), + justified_hash: Some(justified_execution_hash), + head_root: head_block_root, + }; + + let _status = self + .el + .notify_forkchoice_updated( + head_execution_hash, + justified_execution_hash, + finalized_execution_hash, + slot - 1, + head_block_root, + ) + .await + .map_err(|e| format!("fcu call failed : {:?}", e))?; + + let payload_parameters = PayloadParametersCloned { + parent_hash: head_execution_hash, + parent_gas_limit: head_gas_limit, + proposer_gas_limit: Some(proposer_gas_limit), + payload_attributes, + forkchoice_update_params, + current_fork: fork, + }; + Ok(payload_parameters) + } } +/// Serve the builder api using warp. Uses the functions defined in `MockBuilder` to serve +/// the requests. +/// +/// We should eventually move this to axum when we move everything else. pub fn serve( listen_addr: Ipv4Addr, listen_port: u16, @@ -362,19 +944,16 @@ pub fn serve( .and(warp::path::end()) .and(ctx_filter.clone()) .and_then( - |registrations: Vec, builder: MockBuilder| async move { - for registration in registrations { - if !registration.verify_signature(&builder.spec) { - return Err(reject("invalid signature")); - } - builder - .val_registration_cache - .write() - .insert(registration.message.pubkey, registration); - } - Ok(warp::reply()) + |registrations: Vec, + builder: MockBuilder| async move { + builder + .register_validators(registrations) + .await + .map_err(|e| warp::reject::custom(Custom(e)))?; + Ok::<_, Rejection>(warp::reply()) }, - ); + ) + .boxed(); let blinded_block = prefix @@ -387,30 +966,10 @@ pub fn serve( |block: SignedBlindedBeaconBlock, fork_name: ForkName, builder: MockBuilder| async move { - let root = match block { - SignedBlindedBeaconBlock::Base(_) | types::SignedBeaconBlock::Altair(_) => { - return Err(reject("invalid fork")); - } - SignedBlindedBeaconBlock::Bellatrix(block) => { - block.message.body.execution_payload.tree_hash_root() - } - SignedBlindedBeaconBlock::Capella(block) => { - block.message.body.execution_payload.tree_hash_root() - } - SignedBlindedBeaconBlock::Deneb(block) => { - block.message.body.execution_payload.tree_hash_root() - } - SignedBlindedBeaconBlock::Electra(block) => { - block.message.body.execution_payload.tree_hash_root() - } - SignedBlindedBeaconBlock::Fulu(block) => { - block.message.body.execution_payload.tree_hash_root() - } - }; let payload = builder - .el - .get_payload_by_root(&root) - .ok_or_else(|| reject("missing payload for tx root"))?; + .submit_blinded_block(block) + .await + .map_err(|e| warp::reject::custom(Custom(e)))?; let resp: ForkVersionedResponse<_> = ForkVersionedResponse { version: Some(fork_name), metadata: Default::default(), @@ -453,305 +1012,12 @@ pub fn serve( parent_hash: ExecutionBlockHash, pubkey: PublicKeyBytes, builder: MockBuilder| async move { - let fork = builder.spec.fork_name_at_slot::(slot); - let signed_cached_data = builder - .val_registration_cache - .read() - .get(&pubkey) - .ok_or_else(|| reject("missing registration"))? - .clone(); - let cached_data = signed_cached_data.message; - - let head = builder - .beacon_client - .get_beacon_blocks::(BlockId::Head) + let fork_name = builder.fork_name_at_slot(slot); + let signed_bid = builder + .get_header(slot, parent_hash, pubkey) .await - .map_err(|_| reject("couldn't get head"))? - .ok_or_else(|| reject("missing head block"))?; + .map_err(|e| warp::reject::custom(Custom(e)))?; - let block = head.data.message(); - let head_block_root = block.tree_hash_root(); - let head_execution_payload = block - .body() - .execution_payload() - .map_err(|_| reject("pre-merge block"))?; - let head_execution_hash = head_execution_payload.block_hash(); - let head_gas_limit = head_execution_payload.gas_limit(); - if head_execution_hash != parent_hash { - return Err(reject("head mismatch")); - } - - let finalized_execution_hash = builder - .beacon_client - .get_beacon_blocks::(BlockId::Finalized) - .await - .map_err(|_| reject("couldn't get finalized block"))? - .ok_or_else(|| reject("missing finalized block"))? - .data - .message() - .body() - .execution_payload() - .map_err(|_| reject("pre-merge block"))? - .block_hash(); - - let justified_execution_hash = builder - .beacon_client - .get_beacon_blocks::(BlockId::Justified) - .await - .map_err(|_| reject("couldn't get justified block"))? - .ok_or_else(|| reject("missing justified block"))? - .data - .message() - .body() - .execution_payload() - .map_err(|_| reject("pre-merge block"))? - .block_hash(); - - let val_index = builder - .beacon_client - .get_beacon_states_validator_id(StateId::Head, &ValidatorId::PublicKey(pubkey)) - .await - .map_err(|_| reject("couldn't get validator"))? - .ok_or_else(|| reject("missing validator"))? - .data - .index; - let fee_recipient = cached_data.fee_recipient; - let slots_since_genesis = slot.as_u64() - builder.spec.genesis_slot.as_u64(); - - let genesis_data = builder - .beacon_client - .get_beacon_genesis() - .await - .map_err(|_| reject("couldn't get beacon genesis"))? - .data; - let genesis_time = genesis_data.genesis_time; - let timestamp = - (slots_since_genesis * builder.spec.seconds_per_slot) + genesis_time; - - let head_state: BeaconState = builder - .beacon_client - .get_debug_beacon_states(StateId::Head) - .await - .map_err(|_| reject("couldn't get state"))? - .ok_or_else(|| reject("missing state"))? - .data; - let prev_randao = head_state - .get_randao_mix(head_state.current_epoch()) - .map_err(|_| reject("couldn't get prev randao"))?; - - let expected_withdrawals = if fork.capella_enabled() { - Some( - builder - .beacon_client - .get_expected_withdrawals(&StateId::Head) - .await - .unwrap() - .data, - ) - } else { - None - }; - - let payload_attributes = match fork { - // the withdrawals root is filled in by operations, but we supply the valid withdrawals - // first to avoid polluting the execution block generator with invalid payload attributes - // NOTE: this was part of an effort to add payload attribute uniqueness checks, - // which was abandoned because it broke too many tests in subtle ways. - ForkName::Bellatrix | ForkName::Capella => PayloadAttributes::new( - timestamp, - *prev_randao, - fee_recipient, - expected_withdrawals, - None, - ), - ForkName::Deneb | ForkName::Electra | ForkName::Fulu => PayloadAttributes::new( - timestamp, - *prev_randao, - fee_recipient, - expected_withdrawals, - Some(head_block_root), - ), - ForkName::Base | ForkName::Altair => { - return Err(reject("invalid fork")); - } - }; - - builder - .el - .insert_proposer(slot, head_block_root, val_index, payload_attributes.clone()) - .await; - - let forkchoice_update_params = ForkchoiceUpdateParameters { - head_root: Hash256::zero(), - head_hash: None, - justified_hash: Some(justified_execution_hash), - finalized_hash: Some(finalized_execution_hash), - }; - - let proposer_gas_limit = builder - .val_registration_cache - .read() - .get(&pubkey) - .map(|v| v.message.gas_limit); - - let payload_parameters = PayloadParameters { - parent_hash: head_execution_hash, - parent_gas_limit: head_gas_limit, - proposer_gas_limit, - payload_attributes: &payload_attributes, - forkchoice_update_params: &forkchoice_update_params, - current_fork: fork, - }; - - let payload_response_type = builder - .el - .get_full_payload_caching(payload_parameters) - .await - .map_err(|_| reject("couldn't get payload"))?; - - let mut message = match payload_response_type { - crate::GetPayloadResponseType::Full(payload_response) => { - #[allow(clippy::type_complexity)] - let (payload, _block_value, maybe_blobs_bundle, _maybe_requests): ( - ExecutionPayload, - Uint256, - Option>, - Option>, - ) = payload_response.into(); - - match fork { - ForkName::Fulu => BuilderBid::Fulu(BuilderBidFulu { - header: payload - .as_fulu() - .map_err(|_| reject("incorrect payload variant"))? - .into(), - blob_kzg_commitments: maybe_blobs_bundle - .map(|b| b.commitments) - .unwrap_or_default(), - value: Uint256::from(DEFAULT_BUILDER_PAYLOAD_VALUE_WEI), - pubkey: builder.builder_sk.public_key().compress(), - }), - ForkName::Electra => BuilderBid::Electra(BuilderBidElectra { - header: payload - .as_electra() - .map_err(|_| reject("incorrect payload variant"))? - .into(), - blob_kzg_commitments: maybe_blobs_bundle - .map(|b| b.commitments) - .unwrap_or_default(), - value: Uint256::from(DEFAULT_BUILDER_PAYLOAD_VALUE_WEI), - pubkey: builder.builder_sk.public_key().compress(), - }), - ForkName::Deneb => BuilderBid::Deneb(BuilderBidDeneb { - header: payload - .as_deneb() - .map_err(|_| reject("incorrect payload variant"))? - .into(), - blob_kzg_commitments: maybe_blobs_bundle - .map(|b| b.commitments) - .unwrap_or_default(), - value: Uint256::from(DEFAULT_BUILDER_PAYLOAD_VALUE_WEI), - pubkey: builder.builder_sk.public_key().compress(), - }), - ForkName::Capella => BuilderBid::Capella(BuilderBidCapella { - header: payload - .as_capella() - .map_err(|_| reject("incorrect payload variant"))? - .into(), - value: Uint256::from(DEFAULT_BUILDER_PAYLOAD_VALUE_WEI), - pubkey: builder.builder_sk.public_key().compress(), - }), - ForkName::Bellatrix => BuilderBid::Bellatrix(BuilderBidBellatrix { - header: payload - .as_bellatrix() - .map_err(|_| reject("incorrect payload variant"))? - .into(), - value: Uint256::from(DEFAULT_BUILDER_PAYLOAD_VALUE_WEI), - pubkey: builder.builder_sk.public_key().compress(), - }), - ForkName::Base | ForkName::Altair => { - return Err(reject("invalid fork")) - } - } - } - crate::GetPayloadResponseType::Blinded(payload_response) => { - #[allow(clippy::type_complexity)] - let (payload, _block_value, maybe_blobs_bundle, _maybe_requests): ( - ExecutionPayload, - Uint256, - Option>, - Option>, - ) = payload_response.into(); - match fork { - ForkName::Fulu => BuilderBid::Fulu(BuilderBidFulu { - header: payload - .as_fulu() - .map_err(|_| reject("incorrect payload variant"))? - .into(), - blob_kzg_commitments: maybe_blobs_bundle - .map(|b| b.commitments) - .unwrap_or_default(), - value: Uint256::from(DEFAULT_BUILDER_PAYLOAD_VALUE_WEI), - pubkey: builder.builder_sk.public_key().compress(), - }), - ForkName::Electra => BuilderBid::Electra(BuilderBidElectra { - header: payload - .as_electra() - .map_err(|_| reject("incorrect payload variant"))? - .into(), - blob_kzg_commitments: maybe_blobs_bundle - .map(|b| b.commitments) - .unwrap_or_default(), - value: Uint256::from(DEFAULT_BUILDER_PAYLOAD_VALUE_WEI), - pubkey: builder.builder_sk.public_key().compress(), - }), - ForkName::Deneb => BuilderBid::Deneb(BuilderBidDeneb { - header: payload - .as_deneb() - .map_err(|_| reject("incorrect payload variant"))? - .into(), - blob_kzg_commitments: maybe_blobs_bundle - .map(|b| b.commitments) - .unwrap_or_default(), - value: Uint256::from(DEFAULT_BUILDER_PAYLOAD_VALUE_WEI), - pubkey: builder.builder_sk.public_key().compress(), - }), - ForkName::Capella => BuilderBid::Capella(BuilderBidCapella { - header: payload - .as_capella() - .map_err(|_| reject("incorrect payload variant"))? - .into(), - value: Uint256::from(DEFAULT_BUILDER_PAYLOAD_VALUE_WEI), - pubkey: builder.builder_sk.public_key().compress(), - }), - ForkName::Bellatrix => BuilderBid::Bellatrix(BuilderBidBellatrix { - header: payload - .as_bellatrix() - .map_err(|_| reject("incorrect payload variant"))? - .into(), - value: Uint256::from(DEFAULT_BUILDER_PAYLOAD_VALUE_WEI), - pubkey: builder.builder_sk.public_key().compress(), - }), - ForkName::Base | ForkName::Altair => { - return Err(reject("invalid fork")) - } - } - } - }; - - builder.apply_operations(&mut message); - - let mut signature = - message.sign_builder_message(&builder.builder_sk, &builder.spec); - - if *builder.invalidate_signatures.read() { - signature = Signature::empty(); - } - - let fork_name = builder - .spec - .fork_name_at_epoch(slot.epoch(E::slots_per_epoch())); - let signed_bid = SignedBuilderBid { message, signature }; let resp: ForkVersionedResponse<_> = ForkVersionedResponse { version: Some(fork_name), metadata: Default::default(), diff --git a/consensus/types/src/builder_bid.rs b/consensus/types/src/builder_bid.rs index 2ce46ca704..ac53c41216 100644 --- a/consensus/types/src/builder_bid.rs +++ b/consensus/types/src/builder_bid.rs @@ -2,8 +2,8 @@ use crate::beacon_block_body::KzgCommitments; use crate::{ ChainSpec, EthSpec, ExecutionPayloadHeaderBellatrix, ExecutionPayloadHeaderCapella, ExecutionPayloadHeaderDeneb, ExecutionPayloadHeaderElectra, ExecutionPayloadHeaderFulu, - ExecutionPayloadHeaderRef, ExecutionPayloadHeaderRefMut, ForkName, ForkVersionDeserialize, - SignedRoot, Uint256, + ExecutionPayloadHeaderRef, ExecutionPayloadHeaderRefMut, ExecutionRequests, ForkName, + ForkVersionDeserialize, SignedRoot, Uint256, }; use bls::PublicKeyBytes; use bls::Signature; @@ -36,6 +36,8 @@ pub struct BuilderBid { pub header: ExecutionPayloadHeaderFulu, #[superstruct(only(Deneb, Electra, Fulu))] pub blob_kzg_commitments: KzgCommitments, + #[superstruct(only(Electra, Fulu))] + pub execution_requests: ExecutionRequests, #[serde(with = "serde_utils::quoted_u256")] pub value: Uint256, pub pubkey: PublicKeyBytes, From 2b6ec96b4c0cacf9d1a95bdfcc1ff071d2e2f2a0 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Wed, 22 Jan 2025 15:05:29 +1100 Subject: [PATCH 085/254] Add MetaData V3 support to `node/identity` API (#6827) * Add metadata v3 support to `node/identity` api. --- beacon_node/http_api/src/lib.rs | 61 ++++++++++++++++++++--------- beacon_node/http_api/tests/tests.rs | 4 +- common/eth2/src/types.rs | 8 ++++ 3 files changed, 52 insertions(+), 21 deletions(-) diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index 5dc9055c6c..29c27198c0 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -52,6 +52,7 @@ use eth2::types::{ }; use eth2::{CONSENSUS_VERSION_HEADER, CONTENT_TYPE_HEADER, SSZ_CONTENT_TYPE_HEADER}; use health_metrics::observe::Observe; +use lighthouse_network::rpc::methods::MetaData; use lighthouse_network::{types::SyncState, EnrExt, NetworkGlobals, PeerId, PubsubMessage}; use lighthouse_version::version_with_platform; use logging::SSELoggingComponents; @@ -82,6 +83,7 @@ use tokio_stream::{ wrappers::{errors::BroadcastStreamRecvError, BroadcastStream}, StreamExt, }; +use types::ChainSpec; use types::{ fork_versioned_response::EmptyMetadata, Attestation, AttestationData, AttestationShufflingId, AttesterSlashing, BeaconStateError, CommitteeCache, ConfigAndPreset, Epoch, EthSpec, ForkName, @@ -2898,36 +2900,24 @@ pub fn serve( .and(warp::path::end()) .and(task_spawner_filter.clone()) .and(network_globals.clone()) + .and(chain_filter.clone()) .then( |task_spawner: TaskSpawner, - network_globals: Arc>| { + network_globals: Arc>, + chain: Arc>| { task_spawner.blocking_json_task(Priority::P1, move || { let enr = network_globals.local_enr(); let p2p_addresses = enr.multiaddr_p2p_tcp(); let discovery_addresses = enr.multiaddr_p2p_udp(); - let meta_data = network_globals.local_metadata.read(); Ok(api_types::GenericResponse::from(api_types::IdentityData { peer_id: network_globals.local_peer_id().to_base58(), enr, p2p_addresses, discovery_addresses, - metadata: api_types::MetaData { - seq_number: *meta_data.seq_number(), - attnets: format!( - "0x{}", - hex::encode(meta_data.attnets().clone().into_bytes()), - ), - syncnets: format!( - "0x{}", - hex::encode( - meta_data - .syncnets() - .cloned() - .unwrap_or_default() - .into_bytes() - ) - ), - }, + metadata: from_meta_data::( + &network_globals.local_metadata, + &chain.spec, + ), })) }) }, @@ -4844,6 +4834,39 @@ pub fn serve( Ok(http_server) } +fn from_meta_data( + meta_data: &RwLock>, + spec: &ChainSpec, +) -> api_types::MetaData { + let meta_data = meta_data.read(); + let format_hex = |bytes: &[u8]| format!("0x{}", hex::encode(bytes)); + + let seq_number = *meta_data.seq_number(); + let attnets = format_hex(&meta_data.attnets().clone().into_bytes()); + let syncnets = format_hex( + &meta_data + .syncnets() + .cloned() + .unwrap_or_default() + .into_bytes(), + ); + + if spec.is_peer_das_scheduled() { + api_types::MetaData::V3(api_types::MetaDataV3 { + seq_number, + attnets, + syncnets, + custody_group_count: meta_data.custody_group_count().cloned().unwrap_or_default(), + }) + } else { + api_types::MetaData::V2(api_types::MetaDataV2 { + seq_number, + attnets, + syncnets, + }) + } +} + /// Publish a message to the libp2p pubsub network. fn publish_pubsub_message( network_tx: &UnboundedSender>, diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index dd6a92603a..d9b3c8556c 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -2362,11 +2362,11 @@ impl ApiTester { enr: self.local_enr.clone(), p2p_addresses: self.local_enr.multiaddr_p2p_tcp(), discovery_addresses: self.local_enr.multiaddr_p2p_udp(), - metadata: eth2::types::MetaData { + metadata: MetaData::V2(MetaDataV2 { seq_number: 0, attnets: "0x0000000000000000".to_string(), syncnets: "0x00".to_string(), - }, + }), }; assert_eq!(result, expected); diff --git a/common/eth2/src/types.rs b/common/eth2/src/types.rs index 6d76101cb6..c6e95e1ba3 100644 --- a/common/eth2/src/types.rs +++ b/common/eth2/src/types.rs @@ -584,12 +584,20 @@ pub struct IdentityData { pub metadata: MetaData, } +#[superstruct( + variants(V2, V3), + variant_attributes(derive(Clone, Debug, PartialEq, Serialize, Deserialize)) +)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] pub struct MetaData { #[serde(with = "serde_utils::quoted_u64")] pub seq_number: u64, pub attnets: String, pub syncnets: String, + #[superstruct(only(V3))] + #[serde(with = "serde_utils::quoted_u64")] + pub custody_group_count: u64, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] From f008b84079bbb6eb86de22bb3421dfc8263a5650 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Wed, 22 Jan 2025 15:05:32 +1100 Subject: [PATCH 086/254] Avoid computing columns from EL blobs if block has already been imported (#6816) * Avoid computing columns from EL blobs if block has already been imported. * Downgrade a `warn` log to `debug` and update handling. --- beacon_node/beacon_chain/src/fetch_blobs.rs | 31 ++++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/beacon_node/beacon_chain/src/fetch_blobs.rs b/beacon_node/beacon_chain/src/fetch_blobs.rs index 49e46a50fe..5bc2b92ec3 100644 --- a/beacon_node/beacon_chain/src/fetch_blobs.rs +++ b/beacon_node/beacon_chain/src/fetch_blobs.rs @@ -163,6 +163,20 @@ pub async fn fetch_and_process_engine_blobs( return Ok(None); } + if chain + .canonical_head + .fork_choice_read_lock() + .contains_block(&block_root) + { + // Avoid computing columns if block has already been imported. + debug!( + log, + "Ignoring EL blobs response"; + "info" => "block has already been imported", + ); + return Ok(None); + } + let data_columns_receiver = spawn_compute_and_publish_data_columns_task( &chain, block.clone(), @@ -248,18 +262,21 @@ fn spawn_compute_and_publish_data_columns_task( } }; - if let Err(e) = data_columns_sender.send(all_data_columns.clone()) { - error!(log, "Failed to send computed data columns"; "error" => ?e); + if data_columns_sender.send(all_data_columns.clone()).is_err() { + // Data column receiver have been dropped - block may have already been imported. + // This race condition exists because gossip columns may arrive and trigger block + // import during the computation. Here we just drop the computed columns. + debug!( + log, + "Failed to send computed data columns"; + ); + return; }; - // Check indices from cache before sending the columns, to make sure we don't - // publish components already seen on gossip. - let is_supernode = chain_cloned.data_availability_checker.is_supernode(); - // At the moment non supernodes are not required to publish any columns. // TODO(das): we could experiment with having full nodes publish their custodied // columns here. - if !is_supernode { + if !chain_cloned.data_availability_checker.is_supernode() { return; } From 54e37096b61c69991a9044be782bcd41ed5d9ad3 Mon Sep 17 00:00:00 2001 From: Age Manning Date: Wed, 22 Jan 2025 23:29:56 +1100 Subject: [PATCH 087/254] Update discv5 (#6836) * Update discv5 dep * Handle yanked crates --- Cargo.lock | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 29ffdc49ba..ae7861f44f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2192,9 +2192,9 @@ dependencies = [ [[package]] name = "discv5" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "898d136ecb64116ec68aecf14d889bd30f8b1fe0c19e262953f7388dbe77052e" +checksum = "c4b4e7798d2ff74e29cee344dc490af947ae657d6ab5273dde35d58ce06a4d71" dependencies = [ "aes 0.8.4", "aes-gcm", @@ -4361,7 +4361,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f" dependencies = [ - "parity-scale-codec 3.7.0", + "parity-scale-codec 3.6.12", ] [[package]] @@ -4793,7 +4793,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-targets 0.48.5", ] [[package]] @@ -6278,16 +6278,15 @@ dependencies = [ [[package]] name = "parity-scale-codec" -version = "3.7.0" +version = "3.6.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be4817d39f3272f69c59fe05d0535ae6456c2dc2fa1ba02910296c7e0a5c590" +checksum = "306800abfa29c7f16596b5970a588435e3d5b3149683d00c12b699cc19f895ee" dependencies = [ "arrayvec", "bitvec 1.0.1", "byte-slice-cast", "impl-trait-for-tuples", - "parity-scale-codec-derive 3.7.0", - "rustversion", + "parity-scale-codec-derive 3.6.12", "serde", ] @@ -6305,14 +6304,14 @@ dependencies = [ [[package]] name = "parity-scale-codec-derive" -version = "3.7.0" +version = "3.6.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8781a75c6205af67215f382092b6e0a4ff3734798523e69073d4bcd294ec767b" +checksum = "d830939c76d294956402033aee57a6da7b438f2294eb94864c37b0569053a42c" dependencies = [ "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 2.0.89", + "syn 1.0.109", ] [[package]] @@ -7357,7 +7356,7 @@ dependencies = [ "fastrlp", "num-bigint", "num-traits", - "parity-scale-codec 3.7.0", + "parity-scale-codec 3.6.12", "primitive-types 0.12.2", "proptest", "rand", @@ -7633,7 +7632,7 @@ checksum = "346a3b32eba2640d17a9cb5927056b08f3de90f65b72fe09402c2ad07d684d0b" dependencies = [ "cfg-if", "derive_more 1.0.0", - "parity-scale-codec 3.7.0", + "parity-scale-codec 3.6.12", "scale-info-derive", ] From dc73791f35dff0484a35ddedba4b58c6ca34c3c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Oliveira?= Date: Wed, 22 Jan 2025 22:55:55 +0000 Subject: [PATCH 088/254] update script for new mergify syntax (#6597) --- .github/mergify.yml | 42 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/.github/mergify.yml b/.github/mergify.yml index 4c4046cf67..ff08f2d349 100644 --- a/.github/mergify.yml +++ b/.github/mergify.yml @@ -1,3 +1,36 @@ +pull_request_rules: + - name: Ask to resolve conflict + conditions: + - conflict + - -author=dependabot[bot] + - or: + - -draft # Don't report conflicts on regular draft. + - and: # Do report conflicts on draft that are scheduled for the next major release. + - draft + - milestone~=v[0-9]\.[0-9]{2} + actions: + comment: + message: This pull request has merge conflicts. Could you please resolve them + @{{author}}? 🙏 + + - name: Approve trivial maintainer PRs + conditions: + - base=master + - label=trivial + - author=@sigp/lighthouse + actions: + review: + type: APPROVE + + - name: Add ready-to-merge labeled PRs to merge queue + conditions: + # All branch protection rules are implicit: https://docs.mergify.com/conditions/#about-branch-protection + - base=master + - label=send-it + actions: + queue: + + queue_rules: - name: default batch_size: 8 @@ -6,10 +39,11 @@ queue_rules: merge_method: squash commit_message_template: | {{ title }} (#{{ number }}) - - {% for commit in commits %} - * {{ commit.commit_message }} - {% endfor %} + + {{ body | get_section("## Issue Addressed", "") }} + + + {{ body | get_section("## Proposed Changes", "") }} queue_conditions: - "#approved-reviews-by >= 1" - "check-success=license/cla" From 6b6f2beb7d1736a07b68de8ef4ffa1c8e4b5feab Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Thu, 23 Jan 2025 11:01:11 +1100 Subject: [PATCH 089/254] Fix branch/tag names in mergify config (#6842) --- .github/mergify.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/mergify.yml b/.github/mergify.yml index ff08f2d349..9a74414e72 100644 --- a/.github/mergify.yml +++ b/.github/mergify.yml @@ -15,7 +15,7 @@ pull_request_rules: - name: Approve trivial maintainer PRs conditions: - - base=master + - base=unstable - label=trivial - author=@sigp/lighthouse actions: @@ -25,8 +25,8 @@ pull_request_rules: - name: Add ready-to-merge labeled PRs to merge queue conditions: # All branch protection rules are implicit: https://docs.mergify.com/conditions/#about-branch-protection - - base=master - - label=send-it + - base=unstable + - label=ready-to-merge actions: queue: From 266b24112306355bdfa64cdc0d7b63f2e3b4572a Mon Sep 17 00:00:00 2001 From: Pawan Dhananjay Date: Wed, 22 Jan 2025 16:34:22 -0800 Subject: [PATCH 090/254] Electra minor refactorings (#6839) N/A Fix some typos and other minor refactorings in the electra code. Thanks @jtraglia for bringing them up. Note to reviewiers: 47803496dedf1d0e6e4b11f527afff0119976ff0 is the commit that needs looking into in detail. The rest are very minor refactorings --- .../operation_pool/src/attestation_storage.rs | 2 +- .../src/per_block_processing.rs | 26 +++++------- .../src/per_epoch_processing/single_pass.rs | 10 ++--- .../state_processing/src/upgrade/electra.rs | 3 +- consensus/types/src/beacon_state.rs | 42 +++++-------------- consensus/types/src/validator.rs | 12 +++--- 6 files changed, 33 insertions(+), 62 deletions(-) diff --git a/beacon_node/operation_pool/src/attestation_storage.rs b/beacon_node/operation_pool/src/attestation_storage.rs index 083c1170f0..49ef5c279c 100644 --- a/beacon_node/operation_pool/src/attestation_storage.rs +++ b/beacon_node/operation_pool/src/attestation_storage.rs @@ -214,7 +214,7 @@ impl CompactIndexedAttestationElectra { .is_zero() } - /// Returns `true` if aggregated, otherwise `false`. + /// Returns `true` if aggregated, otherwise `false`. pub fn aggregate_same_committee(&mut self, other: &Self) -> bool { if self.committee_bits != other.committee_bits { return false; diff --git a/consensus/state_processing/src/per_block_processing.rs b/consensus/state_processing/src/per_block_processing.rs index 502ad25838..ef4799c245 100644 --- a/consensus/state_processing/src/per_block_processing.rs +++ b/consensus/state_processing/src/per_block_processing.rs @@ -523,9 +523,9 @@ pub fn get_expected_withdrawals( // [New in Electra:EIP7251] // Consume pending partial withdrawals let processed_partial_withdrawals_count = - if let Ok(partial_withdrawals) = state.pending_partial_withdrawals() { + if let Ok(pending_partial_withdrawals) = state.pending_partial_withdrawals() { let mut processed_partial_withdrawals_count = 0; - for withdrawal in partial_withdrawals { + for withdrawal in pending_partial_withdrawals { if withdrawal.withdrawable_epoch > epoch || withdrawals.len() == spec.max_pending_partials_per_withdrawals_sweep as usize { @@ -552,7 +552,7 @@ pub fn get_expected_withdrawals( validator_index: withdrawal.validator_index, address: validator .get_execution_withdrawal_address(spec) - .ok_or(BeaconStateError::NonExecutionAddresWithdrawalCredential)?, + .ok_or(BeaconStateError::NonExecutionAddressWithdrawalCredential)?, amount: withdrawable_balance, }); withdrawal_index.safe_add_assign(1)?; @@ -583,7 +583,7 @@ pub fn get_expected_withdrawals( validator_index as usize, ))? .safe_sub(partially_withdrawn_balance)?; - if validator.is_fully_withdrawable_at(balance, epoch, spec, fork_name) { + if validator.is_fully_withdrawable_validator(balance, epoch, spec, fork_name) { withdrawals.push(Withdrawal { index: withdrawal_index, validator_index, @@ -600,9 +600,7 @@ pub fn get_expected_withdrawals( address: validator .get_execution_withdrawal_address(spec) .ok_or(BlockProcessingError::WithdrawalCredentialsInvalid)?, - amount: balance.safe_sub( - validator.get_max_effective_balance(spec, state.fork_name_unchecked()), - )?, + amount: balance.safe_sub(validator.get_max_effective_balance(spec, fork_name))?, }); withdrawal_index.safe_add_assign(1)?; } @@ -624,7 +622,7 @@ pub fn process_withdrawals>( spec: &ChainSpec, ) -> Result<(), BlockProcessingError> { if state.fork_name_unchecked().capella_enabled() { - let (expected_withdrawals, partial_withdrawals_count) = + let (expected_withdrawals, processed_partial_withdrawals_count) = get_expected_withdrawals(state, spec)?; let expected_root = expected_withdrawals.tree_hash_root(); let withdrawals_root = payload.withdrawals_root()?; @@ -645,14 +643,10 @@ pub fn process_withdrawals>( } // Update pending partial withdrawals [New in Electra:EIP7251] - if let Some(partial_withdrawals_count) = partial_withdrawals_count { - // TODO(electra): Use efficient pop_front after milhouse release https://github.com/sigp/milhouse/pull/38 - let new_partial_withdrawals = state - .pending_partial_withdrawals()? - .iter_from(partial_withdrawals_count)? - .cloned() - .collect::>(); - *state.pending_partial_withdrawals_mut()? = List::new(new_partial_withdrawals)?; + if let Some(processed_partial_withdrawals_count) = processed_partial_withdrawals_count { + state + .pending_partial_withdrawals_mut()? + .pop_front(processed_partial_withdrawals_count)?; } // Update the next withdrawal index if this block contained withdrawals diff --git a/consensus/state_processing/src/per_epoch_processing/single_pass.rs b/consensus/state_processing/src/per_epoch_processing/single_pass.rs index a4a81c8eef..5c31669a60 100644 --- a/consensus/state_processing/src/per_epoch_processing/single_pass.rs +++ b/consensus/state_processing/src/per_epoch_processing/single_pass.rs @@ -1075,13 +1075,9 @@ fn process_pending_consolidations( next_pending_consolidation.safe_add_assign(1)?; } - let new_pending_consolidations = List::try_from_iter( - state - .pending_consolidations()? - .iter_from(next_pending_consolidation)? - .cloned(), - )?; - *state.pending_consolidations_mut()? = new_pending_consolidations; + state + .pending_consolidations_mut()? + .pop_front(next_pending_consolidation)?; // the spec tests require we don't perform effective balance updates when testing pending_consolidations if !perform_effective_balance_updates { diff --git a/consensus/state_processing/src/upgrade/electra.rs b/consensus/state_processing/src/upgrade/electra.rs index 0f32e1553d..258b28a45b 100644 --- a/consensus/state_processing/src/upgrade/electra.rs +++ b/consensus/state_processing/src/upgrade/electra.rs @@ -47,10 +47,11 @@ pub fn upgrade_to_electra( .enumerate() .filter(|(_, validator)| validator.activation_epoch == spec.far_future_epoch) .sorted_by_key(|(index, validator)| (validator.activation_eligibility_epoch, *index)) + .map(|(index, _)| index) .collect::>(); // Process validators to queue entire balance and reset them - for (index, _) in pre_activation { + for index in pre_activation { let balance = post .balances_mut() .get_mut(index) diff --git a/consensus/types/src/beacon_state.rs b/consensus/types/src/beacon_state.rs index 6f44998cdf..157271b227 100644 --- a/consensus/types/src/beacon_state.rs +++ b/consensus/types/src/beacon_state.rs @@ -161,7 +161,7 @@ pub enum Error { InvalidFlagIndex(usize), MerkleTreeError(merkle_proof::MerkleTreeError), PartialWithdrawalCountInvalid(usize), - NonExecutionAddresWithdrawalCredential, + NonExecutionAddressWithdrawalCredential, NoCommitteeFound(CommitteeIndex), InvalidCommitteeIndex(CommitteeIndex), InvalidSelectionProof { @@ -2214,7 +2214,7 @@ impl BeaconState { // ******* Electra accessors ******* - /// Return the churn limit for the current epoch. + /// Return the churn limit for the current epoch. pub fn get_balance_churn_limit(&self, spec: &ChainSpec) -> Result { let total_active_balance = self.get_total_active_balance()?; let churn = std::cmp::max( @@ -2329,21 +2329,12 @@ impl BeaconState { | BeaconState::Bellatrix(_) | BeaconState::Capella(_) | BeaconState::Deneb(_) => Err(Error::IncorrectStateVariant), - BeaconState::Electra(_) => { - let state = self.as_electra_mut()?; - + BeaconState::Electra(_) | BeaconState::Fulu(_) => { // Consume the balance and update state variables - state.exit_balance_to_consume = exit_balance_to_consume.safe_sub(exit_balance)?; - state.earliest_exit_epoch = earliest_exit_epoch; - Ok(state.earliest_exit_epoch) - } - BeaconState::Fulu(_) => { - let state = self.as_fulu_mut()?; - - // Consume the balance and update state variables - state.exit_balance_to_consume = exit_balance_to_consume.safe_sub(exit_balance)?; - state.earliest_exit_epoch = earliest_exit_epoch; - Ok(state.earliest_exit_epoch) + *self.exit_balance_to_consume_mut()? = + exit_balance_to_consume.safe_sub(exit_balance)?; + *self.earliest_exit_epoch_mut()? = earliest_exit_epoch; + self.earliest_exit_epoch() } } } @@ -2385,23 +2376,12 @@ impl BeaconState { | BeaconState::Bellatrix(_) | BeaconState::Capella(_) | BeaconState::Deneb(_) => Err(Error::IncorrectStateVariant), - BeaconState::Electra(_) => { - let state = self.as_electra_mut()?; - + BeaconState::Electra(_) | BeaconState::Fulu(_) => { // Consume the balance and update state variables. - state.consolidation_balance_to_consume = + *self.consolidation_balance_to_consume_mut()? = consolidation_balance_to_consume.safe_sub(consolidation_balance)?; - state.earliest_consolidation_epoch = earliest_consolidation_epoch; - Ok(state.earliest_consolidation_epoch) - } - BeaconState::Fulu(_) => { - let state = self.as_fulu_mut()?; - - // Consume the balance and update state variables. - state.consolidation_balance_to_consume = - consolidation_balance_to_consume.safe_sub(consolidation_balance)?; - state.earliest_consolidation_epoch = earliest_consolidation_epoch; - Ok(state.earliest_consolidation_epoch) + *self.earliest_consolidation_epoch_mut()? = earliest_consolidation_epoch; + self.earliest_consolidation_epoch() } } } diff --git a/consensus/types/src/validator.rs b/consensus/types/src/validator.rs index 222b9292a2..5aed90d2c1 100644 --- a/consensus/types/src/validator.rs +++ b/consensus/types/src/validator.rs @@ -56,7 +56,7 @@ impl Validator { }; let max_effective_balance = validator.get_max_effective_balance(spec, fork_name); - // safe math is unnecessary here since the spec.effecive_balance_increment is never <= 0 + // safe math is unnecessary here since the spec.effective_balance_increment is never <= 0 validator.effective_balance = std::cmp::min( amount - (amount % spec.effective_balance_increment), max_effective_balance, @@ -195,7 +195,7 @@ impl Validator { /// Returns `true` if the validator is fully withdrawable at some epoch. /// /// Calls the correct function depending on the provided `fork_name`. - pub fn is_fully_withdrawable_at( + pub fn is_fully_withdrawable_validator( &self, balance: u64, epoch: Epoch, @@ -203,14 +203,14 @@ impl Validator { current_fork: ForkName, ) -> bool { if current_fork.electra_enabled() { - self.is_fully_withdrawable_at_electra(balance, epoch, spec) + self.is_fully_withdrawable_validator_electra(balance, epoch, spec) } else { - self.is_fully_withdrawable_at_capella(balance, epoch, spec) + self.is_fully_withdrawable_validator_capella(balance, epoch, spec) } } /// Returns `true` if the validator is fully withdrawable at some epoch. - fn is_fully_withdrawable_at_capella( + fn is_fully_withdrawable_validator_capella( &self, balance: u64, epoch: Epoch, @@ -222,7 +222,7 @@ impl Validator { /// Returns `true` if the validator is fully withdrawable at some epoch. /// /// Modified in electra as part of EIP 7251. - fn is_fully_withdrawable_at_electra( + fn is_fully_withdrawable_validator_electra( &self, balance: u64, epoch: Epoch, From a1b7d616b47604ec0cd1afb5543e03e68b629f96 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Thu, 23 Jan 2025 09:12:16 +0700 Subject: [PATCH 091/254] Modularize beacon node backend (#4718) #4669 Modularize the beacon node backend to make it easier to add new database implementations --- Cargo.lock | 2 + Makefile | 2 +- .../overflow_lru_cache.rs | 11 +- .../beacon_chain/src/historical_blocks.rs | 11 +- .../src/schema_change/migration_schema_v21.rs | 8 +- .../src/schema_change/migration_schema_v22.rs | 17 +- beacon_node/beacon_chain/src/test_utils.rs | 15 +- .../beacon_chain/tests/op_verification.rs | 5 +- beacon_node/beacon_chain/tests/store_tests.rs | 17 +- beacon_node/client/src/builder.rs | 5 +- beacon_node/http_api/tests/tests.rs | 4 +- beacon_node/src/cli.rs | 9 + beacon_node/src/config.rs | 4 + beacon_node/src/lib.rs | 13 +- beacon_node/store/Cargo.toml | 8 +- beacon_node/store/src/chunked_vector.rs | 9 +- beacon_node/store/src/config.rs | 25 +- beacon_node/store/src/database.rs | 5 + beacon_node/store/src/database/interface.rs | 220 ++++++++++ .../store/src/database/leveldb_impl.rs | 304 +++++++++++++ beacon_node/store/src/database/redb_impl.rs | 314 ++++++++++++++ beacon_node/store/src/errors.rs | 74 +++- beacon_node/store/src/forwards_iter.rs | 1 - beacon_node/store/src/garbage_collection.rs | 28 +- beacon_node/store/src/hot_cold_store.rs | 407 ++++++++++-------- beacon_node/store/src/impls/beacon_state.rs | 9 +- beacon_node/store/src/leveldb_store.rs | 310 ------------- beacon_node/store/src/lib.rs | 79 ++-- beacon_node/store/src/memory_store.rs | 83 ++-- beacon_node/store/src/metrics.rs | 33 ++ beacon_node/store/src/partial_beacon_state.rs | 13 +- book/src/help_bn.md | 3 + book/src/installation-source.md | 5 +- database_manager/src/cli.rs | 9 + database_manager/src/lib.rs | 40 +- lighthouse/Cargo.toml | 8 +- lighthouse/tests/beacon_node.rs | 17 +- wordlist.txt | 2 + 38 files changed, 1479 insertions(+), 650 deletions(-) create mode 100644 beacon_node/store/src/database.rs create mode 100644 beacon_node/store/src/database/interface.rs create mode 100644 beacon_node/store/src/database/leveldb_impl.rs create mode 100644 beacon_node/store/src/database/redb_impl.rs delete mode 100644 beacon_node/store/src/leveldb_store.rs diff --git a/Cargo.lock b/Cargo.lock index ae7861f44f..899435a66b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5301,6 +5301,7 @@ dependencies = [ "slasher", "slashing_protection", "slog", + "store", "task_executor", "tempfile", "types", @@ -8429,6 +8430,7 @@ dependencies = [ "metrics", "parking_lot 0.12.3", "rand", + "redb", "safe_arith", "serde", "slog", diff --git a/Makefile b/Makefile index 4d95f50c5c..e8b44cb780 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ BUILD_PATH_AARCH64 = "target/$(AARCH64_TAG)/release" PINNED_NIGHTLY ?= nightly # List of features to use when cross-compiling. Can be overridden via the environment. -CROSS_FEATURES ?= gnosis,slasher-lmdb,slasher-mdbx,slasher-redb,jemalloc +CROSS_FEATURES ?= gnosis,slasher-lmdb,slasher-mdbx,slasher-redb,jemalloc,beacon-node-leveldb,beacon-node-redb # Cargo profile for Cross builds. Default is for local builds, CI uses an override. CROSS_PROFILE ?= release 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 c8e92f7e9f..cd793c8394 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 @@ -317,7 +317,6 @@ impl PendingComponents { None, ) }; - let executed_block = recover(diet_executed_block)?; let AvailabilityPendingExecutedBlock { @@ -732,7 +731,7 @@ mod test { use slog::{info, Logger}; use state_processing::ConsensusContext; use std::collections::VecDeque; - use store::{HotColdDB, ItemStore, LevelDB, StoreConfig}; + use store::{database::interface::BeaconNodeBackend, HotColdDB, ItemStore, StoreConfig}; use tempfile::{tempdir, TempDir}; use types::non_zero_usize::new_non_zero_usize; use types::{ExecPayload, MinimalEthSpec}; @@ -744,7 +743,7 @@ mod test { db_path: &TempDir, spec: Arc, log: Logger, - ) -> Arc, LevelDB>> { + ) -> Arc, BeaconNodeBackend>> { let hot_path = db_path.path().join("hot_db"); let cold_path = db_path.path().join("cold_db"); let blobs_path = db_path.path().join("blobs_db"); @@ -920,7 +919,11 @@ mod test { ) where E: EthSpec, - T: BeaconChainTypes, ColdStore = LevelDB, EthSpec = E>, + T: BeaconChainTypes< + HotStore = BeaconNodeBackend, + ColdStore = BeaconNodeBackend, + EthSpec = E, + >, { let log = test_logger(); let chain_db_path = tempdir().expect("should get temp dir"); diff --git a/beacon_node/beacon_chain/src/historical_blocks.rs b/beacon_node/beacon_chain/src/historical_blocks.rs index ddae54f464..e22ec95a79 100644 --- a/beacon_node/beacon_chain/src/historical_blocks.rs +++ b/beacon_node/beacon_chain/src/historical_blocks.rs @@ -10,10 +10,7 @@ use std::borrow::Cow; use std::iter; use std::time::Duration; use store::metadata::DataColumnInfo; -use store::{ - get_key_for_col, AnchorInfo, BlobInfo, DBColumn, Error as StoreError, KeyValueStore, - KeyValueStoreOp, -}; +use store::{AnchorInfo, BlobInfo, DBColumn, Error as StoreError, KeyValueStore, KeyValueStoreOp}; use strum::IntoStaticStr; use types::{FixedBytesExtended, Hash256, Slot}; @@ -153,7 +150,8 @@ impl BeaconChain { // Store block roots, including at all skip slots in the freezer DB. for slot in (block.slot().as_u64()..prev_block_slot.as_u64()).rev() { cold_batch.push(KeyValueStoreOp::PutKeyValue( - get_key_for_col(DBColumn::BeaconBlockRoots.into(), &slot.to_be_bytes()), + DBColumn::BeaconBlockRoots, + slot.to_be_bytes().to_vec(), block_root.as_slice().to_vec(), )); } @@ -169,7 +167,8 @@ impl BeaconChain { let genesis_slot = self.spec.genesis_slot; for slot in genesis_slot.as_u64()..prev_block_slot.as_u64() { cold_batch.push(KeyValueStoreOp::PutKeyValue( - get_key_for_col(DBColumn::BeaconBlockRoots.into(), &slot.to_be_bytes()), + DBColumn::BeaconBlockRoots, + slot.to_be_bytes().to_vec(), self.genesis_block_root.as_slice().to_vec(), )); } diff --git a/beacon_node/beacon_chain/src/schema_change/migration_schema_v21.rs b/beacon_node/beacon_chain/src/schema_change/migration_schema_v21.rs index fcc8b9884a..f02f5ee6f3 100644 --- a/beacon_node/beacon_chain/src/schema_change/migration_schema_v21.rs +++ b/beacon_node/beacon_chain/src/schema_change/migration_schema_v21.rs @@ -3,9 +3,7 @@ use crate::validator_pubkey_cache::DatabasePubkey; use slog::{info, Logger}; use ssz::{Decode, Encode}; use std::sync::Arc; -use store::{ - get_key_for_col, DBColumn, Error, HotColdDB, KeyValueStore, KeyValueStoreOp, StoreItem, -}; +use store::{DBColumn, Error, HotColdDB, KeyValueStore, KeyValueStoreOp, StoreItem}; use types::{Hash256, PublicKey}; const LOG_EVERY: usize = 200_000; @@ -62,9 +60,9 @@ pub fn downgrade_from_v21( message: format!("{e:?}"), })?; - let db_key = get_key_for_col(DBColumn::PubkeyCache.into(), key.as_slice()); ops.push(KeyValueStoreOp::PutKeyValue( - db_key, + DBColumn::PubkeyCache, + key.as_slice().to_vec(), pubkey_bytes.as_ssz_bytes(), )); diff --git a/beacon_node/beacon_chain/src/schema_change/migration_schema_v22.rs b/beacon_node/beacon_chain/src/schema_change/migration_schema_v22.rs index c34512eded..982c3ded46 100644 --- a/beacon_node/beacon_chain/src/schema_change/migration_schema_v22.rs +++ b/beacon_node/beacon_chain/src/schema_change/migration_schema_v22.rs @@ -4,7 +4,6 @@ use std::sync::Arc; use store::chunked_iter::ChunkedVectorIter; use store::{ chunked_vector::BlockRootsChunked, - get_key_for_col, metadata::{ SchemaVersion, ANCHOR_FOR_ARCHIVE_NODE, ANCHOR_UNINITIALIZED, STATE_UPPER_LIMIT_NO_RETAIN, }, @@ -21,7 +20,7 @@ fn load_old_schema_frozen_state( ) -> Result>, Error> { let Some(partial_state_bytes) = db .cold_db - .get_bytes(DBColumn::BeaconState.into(), state_root.as_slice())? + .get_bytes(DBColumn::BeaconState, state_root.as_slice())? else { return Ok(None); }; @@ -136,10 +135,7 @@ pub fn delete_old_schema_freezer_data( for column in columns { for res in db.cold_db.iter_column_keys::>(column) { let key = res?; - cold_ops.push(KeyValueStoreOp::DeleteKey(get_key_for_col( - column.as_str(), - &key, - ))); + cold_ops.push(KeyValueStoreOp::DeleteKey(column, key)); } } let delete_ops = cold_ops.len(); @@ -175,7 +171,8 @@ pub fn write_new_schema_block_roots( // Store the genesis block root if it would otherwise not be stored. if oldest_block_slot != 0 { cold_ops.push(KeyValueStoreOp::PutKeyValue( - get_key_for_col(DBColumn::BeaconBlockRoots.into(), &0u64.to_be_bytes()), + DBColumn::BeaconBlockRoots, + 0u64.to_be_bytes().to_vec(), genesis_block_root.as_slice().to_vec(), )); } @@ -192,10 +189,8 @@ pub fn write_new_schema_block_roots( // OK to hold these in memory (10M slots * 43 bytes per KV ~= 430 MB). for (i, (slot, block_root)) in block_root_iter.enumerate() { cold_ops.push(KeyValueStoreOp::PutKeyValue( - get_key_for_col( - DBColumn::BeaconBlockRoots.into(), - &(slot as u64).to_be_bytes(), - ), + DBColumn::BeaconBlockRoots, + slot.to_be_bytes().to_vec(), block_root.as_slice().to_vec(), )); diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index 443cc686eb..ba0a2159da 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -56,7 +56,8 @@ use std::str::FromStr; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, LazyLock}; use std::time::Duration; -use store::{config::StoreConfig, HotColdDB, ItemStore, LevelDB, MemoryStore}; +use store::database::interface::BeaconNodeBackend; +use store::{config::StoreConfig, HotColdDB, ItemStore, MemoryStore}; use task_executor::TaskExecutor; use task_executor::{test_utils::TestRuntime, ShutdownReason}; use tree_hash::TreeHash; @@ -116,7 +117,7 @@ pub fn get_kzg(spec: &ChainSpec) -> Arc { pub type BaseHarnessType = Witness, E, THotStore, TColdStore>; -pub type DiskHarnessType = BaseHarnessType, LevelDB>; +pub type DiskHarnessType = BaseHarnessType, BeaconNodeBackend>; pub type EphemeralHarnessType = BaseHarnessType, MemoryStore>; pub type BoxedMutator = Box< @@ -299,7 +300,10 @@ impl Builder> { impl Builder> { /// Disk store, start from genesis. - pub fn fresh_disk_store(mut self, store: Arc, LevelDB>>) -> Self { + pub fn fresh_disk_store( + mut self, + store: Arc, BeaconNodeBackend>>, + ) -> Self { let validator_keypairs = self .validator_keypairs .clone() @@ -324,7 +328,10 @@ impl Builder> { } /// Disk store, resume. - pub fn resumed_disk_store(mut self, store: Arc, LevelDB>>) -> Self { + pub fn resumed_disk_store( + mut self, + store: Arc, BeaconNodeBackend>>, + ) -> Self { let mutator = move |builder: BeaconChainBuilder<_>| { builder .resume_from_db() diff --git a/beacon_node/beacon_chain/tests/op_verification.rs b/beacon_node/beacon_chain/tests/op_verification.rs index df0d561e1c..44fb298d6c 100644 --- a/beacon_node/beacon_chain/tests/op_verification.rs +++ b/beacon_node/beacon_chain/tests/op_verification.rs @@ -14,7 +14,8 @@ use state_processing::per_block_processing::errors::{ AttesterSlashingInvalid, BlockOperationError, ExitInvalid, ProposerSlashingInvalid, }; use std::sync::{Arc, LazyLock}; -use store::{LevelDB, StoreConfig}; +use store::database::interface::BeaconNodeBackend; +use store::StoreConfig; use tempfile::{tempdir, TempDir}; use types::*; @@ -26,7 +27,7 @@ static KEYPAIRS: LazyLock> = type E = MinimalEthSpec; type TestHarness = BeaconChainHarness>; -type HotColdDB = store::HotColdDB, LevelDB>; +type HotColdDB = store::HotColdDB, BeaconNodeBackend>; fn get_store(db_path: &TempDir) -> Arc { let spec = Arc::new(test_spec::()); diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index 60d46e8269..d1a38b1cde 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -25,10 +25,11 @@ use std::collections::HashSet; use std::convert::TryInto; use std::sync::{Arc, LazyLock}; use std::time::Duration; +use store::database::interface::BeaconNodeBackend; use store::metadata::{SchemaVersion, CURRENT_SCHEMA_VERSION, STATE_UPPER_LIMIT_NO_RETAIN}; use store::{ iter::{BlockRootsIterator, StateRootsIterator}, - BlobInfo, DBColumn, HotColdDB, LevelDB, StoreConfig, + BlobInfo, DBColumn, HotColdDB, StoreConfig, }; use tempfile::{tempdir, TempDir}; use tokio::time::sleep; @@ -46,7 +47,7 @@ static KEYPAIRS: LazyLock> = type E = MinimalEthSpec; type TestHarness = BeaconChainHarness>; -fn get_store(db_path: &TempDir) -> Arc, LevelDB>> { +fn get_store(db_path: &TempDir) -> Arc, BeaconNodeBackend>> { get_store_generic(db_path, StoreConfig::default(), test_spec::()) } @@ -54,7 +55,7 @@ fn get_store_generic( db_path: &TempDir, config: StoreConfig, spec: ChainSpec, -) -> Arc, LevelDB>> { +) -> Arc, BeaconNodeBackend>> { let hot_path = db_path.path().join("chain_db"); let cold_path = db_path.path().join("freezer_db"); let blobs_path = db_path.path().join("blobs_db"); @@ -73,7 +74,7 @@ fn get_store_generic( } fn get_harness( - store: Arc, LevelDB>>, + store: Arc, BeaconNodeBackend>>, validator_count: usize, ) -> TestHarness { // Most tests expect to retain historic states, so we use this as the default. @@ -85,7 +86,7 @@ fn get_harness( } fn get_harness_generic( - store: Arc, LevelDB>>, + store: Arc, BeaconNodeBackend>>, validator_count: usize, chain_config: ChainConfig, ) -> TestHarness { @@ -244,7 +245,6 @@ async fn full_participation_no_skips() { AttestationStrategy::AllValidators, ) .await; - check_finalization(&harness, num_blocks_produced); check_split_slot(&harness, store); check_chain_dump(&harness, num_blocks_produced + 1); @@ -3508,7 +3508,10 @@ fn check_finalization(harness: &TestHarness, expected_slot: u64) { } /// Check that the HotColdDB's split_slot is equal to the start slot of the last finalized epoch. -fn check_split_slot(harness: &TestHarness, store: Arc, LevelDB>>) { +fn check_split_slot( + harness: &TestHarness, + store: Arc, BeaconNodeBackend>>, +) { let split_slot = store.get_split_slot(); assert_eq!( harness diff --git a/beacon_node/client/src/builder.rs b/beacon_node/client/src/builder.rs index 1cd9e89b96..e3bfd60a48 100644 --- a/beacon_node/client/src/builder.rs +++ b/beacon_node/client/src/builder.rs @@ -14,7 +14,7 @@ use beacon_chain::{ eth1_chain::{CachingEth1Backend, Eth1Chain}, slot_clock::{SlotClock, SystemTimeSlotClock}, state_advance_timer::spawn_state_advance_timer, - store::{HotColdDB, ItemStore, LevelDB, StoreConfig}, + store::{HotColdDB, ItemStore, StoreConfig}, BeaconChain, BeaconChainTypes, Eth1ChainBackend, MigratorConfig, ServerSentEventHandler, }; use beacon_chain::{Kzg, LightClientProducerEvent}; @@ -41,6 +41,7 @@ use std::path::{Path, PathBuf}; use std::sync::Arc; use std::time::Duration; use std::time::{SystemTime, UNIX_EPOCH}; +use store::database::interface::BeaconNodeBackend; use timer::spawn_timer; use tokio::sync::oneshot; use types::{ @@ -1030,7 +1031,7 @@ where } impl - ClientBuilder, LevelDB>> + ClientBuilder, BeaconNodeBackend>> where TSlotClock: SlotClock + 'static, TEth1Backend: Eth1ChainBackend + 'static, diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index d9b3c8556c..99b7696610 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -1933,7 +1933,7 @@ impl ApiTester { .sync_committee_period(&self.chain.spec) .unwrap(); - let result = match self + match self .client .get_beacon_light_client_updates::(current_sync_committee_period, 1) .await @@ -1954,7 +1954,6 @@ impl ApiTester { .unwrap(); assert_eq!(1, expected.len()); - assert_eq!(result.clone().unwrap().len(), expected.len()); self } @@ -1979,7 +1978,6 @@ impl ApiTester { .get_light_client_bootstrap(&self.chain.store, &block_root, 1u64, &self.chain.spec); assert!(expected.is_ok()); - assert_eq!(result.unwrap().data, expected.unwrap().unwrap().0); self diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index cecfcee868..1339c15825 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -1591,5 +1591,14 @@ pub fn cli_app() -> Command { .action(ArgAction::Set) .display_order(0) ) + .arg( + Arg::new("beacon-node-backend") + .long("beacon-node-backend") + .value_name("DATABASE") + .value_parser(store::config::DatabaseBackend::VARIANTS.to_vec()) + .help("Set the database backend to be used by the beacon node.") + .action(ArgAction::Set) + .display_order(0) + ) .group(ArgGroup::new("enable_http").args(["http", "gui", "staking"]).multiple(true)) } diff --git a/beacon_node/src/config.rs b/beacon_node/src/config.rs index 8d8a44a6fd..6d3c18d363 100644 --- a/beacon_node/src/config.rs +++ b/beacon_node/src/config.rs @@ -432,6 +432,10 @@ pub fn get_config( warn!(log, "The slots-per-restore-point flag is deprecated"); } + if let Some(backend) = clap_utils::parse_optional(cli_args, "beacon-node-backend")? { + client_config.store.backend = backend; + } + if let Some(hierarchy_config) = clap_utils::parse_optional(cli_args, "hierarchy-exponents")? { client_config.store.hierarchy_config = hierarchy_config; } diff --git a/beacon_node/src/lib.rs b/beacon_node/src/lib.rs index 0c4cbf0f57..e3802c837c 100644 --- a/beacon_node/src/lib.rs +++ b/beacon_node/src/lib.rs @@ -2,7 +2,6 @@ mod cli; mod config; pub use beacon_chain; -use beacon_chain::store::LevelDB; use beacon_chain::{ builder::Witness, eth1_chain::CachingEth1Backend, slot_clock::SystemTimeSlotClock, }; @@ -16,11 +15,19 @@ use slasher::{DatabaseBackendOverride, Slasher}; use slog::{info, warn}; use std::ops::{Deref, DerefMut}; use std::sync::Arc; +use store::database::interface::BeaconNodeBackend; use types::{ChainSpec, Epoch, EthSpec, ForkName}; /// A type-alias to the tighten the definition of a production-intended `Client`. -pub type ProductionClient = - Client, E, LevelDB, LevelDB>>; +pub type ProductionClient = Client< + Witness< + SystemTimeSlotClock, + CachingEth1Backend, + E, + BeaconNodeBackend, + BeaconNodeBackend, + >, +>; /// The beacon node `Client` that will be used in production. /// diff --git a/beacon_node/store/Cargo.toml b/beacon_node/store/Cargo.toml index 21d0cf8dec..d2f3a5c562 100644 --- a/beacon_node/store/Cargo.toml +++ b/beacon_node/store/Cargo.toml @@ -4,6 +4,11 @@ version = "0.2.0" authors = ["Paul Hauner "] edition = { workspace = true } +[features] +default = ["leveldb"] +leveldb = ["dep:leveldb"] +redb = ["dep:redb"] + [dev-dependencies] beacon_chain = { workspace = true } criterion = { workspace = true } @@ -17,11 +22,12 @@ directory = { workspace = true } ethereum_ssz = { workspace = true } ethereum_ssz_derive = { workspace = true } itertools = { workspace = true } -leveldb = { version = "0.8" } +leveldb = { version = "0.8.6", optional = true } logging = { workspace = true } lru = { workspace = true } metrics = { workspace = true } parking_lot = { workspace = true } +redb = { version = "2.1.3", optional = true } safe_arith = { workspace = true } serde = { workspace = true } slog = { workspace = true } diff --git a/beacon_node/store/src/chunked_vector.rs b/beacon_node/store/src/chunked_vector.rs index 83b8da2a18..90e8c17310 100644 --- a/beacon_node/store/src/chunked_vector.rs +++ b/beacon_node/store/src/chunked_vector.rs @@ -680,7 +680,7 @@ where key: &[u8], ) -> Result, Error> { store - .get_bytes(column.into(), key)? + .get_bytes(column, key)? .map(|bytes| Self::decode(&bytes)) .transpose() } @@ -691,8 +691,11 @@ where key: &[u8], ops: &mut Vec, ) -> Result<(), Error> { - let db_key = get_key_for_col(column.into(), key); - ops.push(KeyValueStoreOp::PutKeyValue(db_key, self.encode()?)); + ops.push(KeyValueStoreOp::PutKeyValue( + column, + key.to_vec(), + self.encode()?, + )); Ok(()) } diff --git a/beacon_node/store/src/config.rs b/beacon_node/store/src/config.rs index 4f67530570..64765fd66a 100644 --- a/beacon_node/store/src/config.rs +++ b/beacon_node/store/src/config.rs @@ -1,16 +1,23 @@ use crate::hdiff::HierarchyConfig; +use crate::superstruct; use crate::{AnchorInfo, DBColumn, Error, Split, StoreItem}; use serde::{Deserialize, Serialize}; use ssz::{Decode, Encode}; use ssz_derive::{Decode, Encode}; use std::io::Write; use std::num::NonZeroUsize; -use superstruct::superstruct; +use strum::{Display, EnumString, EnumVariantNames}; use types::non_zero_usize::new_non_zero_usize; use types::EthSpec; use zstd::Encoder; -// Only used in tests. Mainnet sets a higher default on the CLI. +#[cfg(all(feature = "redb", not(feature = "leveldb")))] +pub const DEFAULT_BACKEND: DatabaseBackend = DatabaseBackend::Redb; +#[cfg(feature = "leveldb")] +pub const DEFAULT_BACKEND: DatabaseBackend = DatabaseBackend::LevelDb; + +pub const PREV_DEFAULT_SLOTS_PER_RESTORE_POINT: u64 = 2048; +pub const DEFAULT_SLOTS_PER_RESTORE_POINT: u64 = 8192; pub const DEFAULT_EPOCHS_PER_STATE_DIFF: u64 = 8; pub const DEFAULT_BLOCK_CACHE_SIZE: NonZeroUsize = new_non_zero_usize(64); pub const DEFAULT_STATE_CACHE_SIZE: NonZeroUsize = new_non_zero_usize(128); @@ -40,6 +47,8 @@ pub struct StoreConfig { pub compact_on_prune: bool, /// Whether to prune payloads on initialization and finalization. pub prune_payloads: bool, + /// Database backend to use. + pub backend: DatabaseBackend, /// State diff hierarchy. pub hierarchy_config: HierarchyConfig, /// Whether to prune blobs older than the blob data availability boundary. @@ -104,6 +113,7 @@ impl Default for StoreConfig { compact_on_init: false, compact_on_prune: true, prune_payloads: true, + backend: DEFAULT_BACKEND, hierarchy_config: HierarchyConfig::default(), prune_blobs: true, epochs_per_blob_prune: DEFAULT_EPOCHS_PER_BLOB_PRUNE, @@ -340,3 +350,14 @@ mod test { assert_eq!(config_out, config); } } + +#[derive( + Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Display, EnumString, EnumVariantNames, +)] +#[strum(serialize_all = "lowercase")] +pub enum DatabaseBackend { + #[cfg(feature = "leveldb")] + LevelDb, + #[cfg(feature = "redb")] + Redb, +} diff --git a/beacon_node/store/src/database.rs b/beacon_node/store/src/database.rs new file mode 100644 index 0000000000..2232f73c5c --- /dev/null +++ b/beacon_node/store/src/database.rs @@ -0,0 +1,5 @@ +pub mod interface; +#[cfg(feature = "leveldb")] +pub mod leveldb_impl; +#[cfg(feature = "redb")] +pub mod redb_impl; diff --git a/beacon_node/store/src/database/interface.rs b/beacon_node/store/src/database/interface.rs new file mode 100644 index 0000000000..b213433241 --- /dev/null +++ b/beacon_node/store/src/database/interface.rs @@ -0,0 +1,220 @@ +#[cfg(feature = "leveldb")] +use crate::database::leveldb_impl; +#[cfg(feature = "redb")] +use crate::database::redb_impl; +use crate::{config::DatabaseBackend, KeyValueStoreOp, StoreConfig}; +use crate::{metrics, ColumnIter, ColumnKeyIter, DBColumn, Error, ItemStore, Key, KeyValueStore}; +use std::collections::HashSet; +use std::path::Path; +use types::EthSpec; + +pub enum BeaconNodeBackend { + #[cfg(feature = "leveldb")] + LevelDb(leveldb_impl::LevelDB), + #[cfg(feature = "redb")] + Redb(redb_impl::Redb), +} + +impl ItemStore for BeaconNodeBackend {} + +impl KeyValueStore for BeaconNodeBackend { + fn get_bytes(&self, column: DBColumn, key: &[u8]) -> Result>, Error> { + match self { + #[cfg(feature = "leveldb")] + BeaconNodeBackend::LevelDb(txn) => leveldb_impl::LevelDB::get_bytes(txn, column, key), + #[cfg(feature = "redb")] + BeaconNodeBackend::Redb(txn) => redb_impl::Redb::get_bytes(txn, column, key), + } + } + + fn put_bytes(&self, column: DBColumn, key: &[u8], value: &[u8]) -> Result<(), Error> { + match self { + #[cfg(feature = "leveldb")] + BeaconNodeBackend::LevelDb(txn) => leveldb_impl::LevelDB::put_bytes_with_options( + txn, + column, + key, + value, + txn.write_options(), + ), + #[cfg(feature = "redb")] + BeaconNodeBackend::Redb(txn) => redb_impl::Redb::put_bytes_with_options( + txn, + column, + key, + value, + txn.write_options(), + ), + } + } + + fn put_bytes_sync(&self, column: DBColumn, key: &[u8], value: &[u8]) -> Result<(), Error> { + match self { + #[cfg(feature = "leveldb")] + BeaconNodeBackend::LevelDb(txn) => leveldb_impl::LevelDB::put_bytes_with_options( + txn, + column, + key, + value, + txn.write_options_sync(), + ), + #[cfg(feature = "redb")] + BeaconNodeBackend::Redb(txn) => redb_impl::Redb::put_bytes_with_options( + txn, + column, + key, + value, + txn.write_options_sync(), + ), + } + } + + fn sync(&self) -> Result<(), Error> { + match self { + #[cfg(feature = "leveldb")] + BeaconNodeBackend::LevelDb(txn) => leveldb_impl::LevelDB::sync(txn), + #[cfg(feature = "redb")] + BeaconNodeBackend::Redb(txn) => redb_impl::Redb::sync(txn), + } + } + + fn key_exists(&self, column: DBColumn, key: &[u8]) -> Result { + match self { + #[cfg(feature = "leveldb")] + BeaconNodeBackend::LevelDb(txn) => leveldb_impl::LevelDB::key_exists(txn, column, key), + #[cfg(feature = "redb")] + BeaconNodeBackend::Redb(txn) => redb_impl::Redb::key_exists(txn, column, key), + } + } + + fn key_delete(&self, column: DBColumn, key: &[u8]) -> Result<(), Error> { + match self { + #[cfg(feature = "leveldb")] + BeaconNodeBackend::LevelDb(txn) => leveldb_impl::LevelDB::key_delete(txn, column, key), + #[cfg(feature = "redb")] + BeaconNodeBackend::Redb(txn) => redb_impl::Redb::key_delete(txn, column, key), + } + } + + fn do_atomically(&self, batch: Vec) -> Result<(), Error> { + match self { + #[cfg(feature = "leveldb")] + BeaconNodeBackend::LevelDb(txn) => leveldb_impl::LevelDB::do_atomically(txn, batch), + #[cfg(feature = "redb")] + BeaconNodeBackend::Redb(txn) => redb_impl::Redb::do_atomically(txn, batch), + } + } + + fn begin_rw_transaction(&self) -> parking_lot::MutexGuard<()> { + match self { + #[cfg(feature = "leveldb")] + BeaconNodeBackend::LevelDb(txn) => leveldb_impl::LevelDB::begin_rw_transaction(txn), + #[cfg(feature = "redb")] + BeaconNodeBackend::Redb(txn) => redb_impl::Redb::begin_rw_transaction(txn), + } + } + + fn compact(&self) -> Result<(), Error> { + match self { + #[cfg(feature = "leveldb")] + BeaconNodeBackend::LevelDb(txn) => leveldb_impl::LevelDB::compact(txn), + #[cfg(feature = "redb")] + BeaconNodeBackend::Redb(txn) => redb_impl::Redb::compact(txn), + } + } + + fn iter_column_keys_from(&self, _column: DBColumn, from: &[u8]) -> ColumnKeyIter { + match self { + #[cfg(feature = "leveldb")] + BeaconNodeBackend::LevelDb(txn) => { + leveldb_impl::LevelDB::iter_column_keys_from(txn, _column, from) + } + #[cfg(feature = "redb")] + BeaconNodeBackend::Redb(txn) => { + redb_impl::Redb::iter_column_keys_from(txn, _column, from) + } + } + } + + fn iter_column_keys(&self, column: DBColumn) -> ColumnKeyIter { + match self { + #[cfg(feature = "leveldb")] + BeaconNodeBackend::LevelDb(txn) => leveldb_impl::LevelDB::iter_column_keys(txn, column), + #[cfg(feature = "redb")] + BeaconNodeBackend::Redb(txn) => redb_impl::Redb::iter_column_keys(txn, column), + } + } + + fn iter_column_from(&self, column: DBColumn, from: &[u8]) -> ColumnIter { + match self { + #[cfg(feature = "leveldb")] + BeaconNodeBackend::LevelDb(txn) => { + leveldb_impl::LevelDB::iter_column_from(txn, column, from) + } + #[cfg(feature = "redb")] + BeaconNodeBackend::Redb(txn) => redb_impl::Redb::iter_column_from(txn, column, from), + } + } + + fn compact_column(&self, _column: DBColumn) -> Result<(), Error> { + match self { + #[cfg(feature = "leveldb")] + BeaconNodeBackend::LevelDb(txn) => leveldb_impl::LevelDB::compact_column(txn, _column), + #[cfg(feature = "redb")] + BeaconNodeBackend::Redb(txn) => redb_impl::Redb::compact(txn), + } + } + + fn delete_batch(&self, col: DBColumn, ops: HashSet<&[u8]>) -> Result<(), Error> { + match self { + #[cfg(feature = "leveldb")] + BeaconNodeBackend::LevelDb(txn) => leveldb_impl::LevelDB::delete_batch(txn, col, ops), + #[cfg(feature = "redb")] + BeaconNodeBackend::Redb(txn) => redb_impl::Redb::delete_batch(txn, col, ops), + } + } + + fn delete_if( + &self, + column: DBColumn, + f: impl FnMut(&[u8]) -> Result, + ) -> Result<(), Error> { + match self { + #[cfg(feature = "leveldb")] + BeaconNodeBackend::LevelDb(txn) => leveldb_impl::LevelDB::delete_if(txn, column, f), + #[cfg(feature = "redb")] + BeaconNodeBackend::Redb(txn) => redb_impl::Redb::delete_if(txn, column, f), + } + } +} + +impl BeaconNodeBackend { + pub fn open(config: &StoreConfig, path: &Path) -> Result { + metrics::inc_counter_vec(&metrics::DISK_DB_TYPE, &[&config.backend.to_string()]); + match config.backend { + #[cfg(feature = "leveldb")] + DatabaseBackend::LevelDb => { + leveldb_impl::LevelDB::open(path).map(BeaconNodeBackend::LevelDb) + } + #[cfg(feature = "redb")] + DatabaseBackend::Redb => redb_impl::Redb::open(path).map(BeaconNodeBackend::Redb), + } + } +} + +pub struct WriteOptions { + /// fsync before acknowledging a write operation. + pub sync: bool, +} + +impl WriteOptions { + pub fn new() -> Self { + WriteOptions { sync: false } + } +} + +impl Default for WriteOptions { + fn default() -> Self { + Self::new() + } +} diff --git a/beacon_node/store/src/database/leveldb_impl.rs b/beacon_node/store/src/database/leveldb_impl.rs new file mode 100644 index 0000000000..3d8bbe1473 --- /dev/null +++ b/beacon_node/store/src/database/leveldb_impl.rs @@ -0,0 +1,304 @@ +use crate::hot_cold_store::{BytesKey, HotColdDBError}; +use crate::Key; +use crate::{ + get_key_for_col, metrics, ColumnIter, ColumnKeyIter, DBColumn, Error, KeyValueStoreOp, +}; +use leveldb::{ + compaction::Compaction, + database::{ + batch::{Batch, Writebatch}, + kv::KV, + Database, + }, + iterator::{Iterable, LevelDBIterator}, + options::{Options, ReadOptions}, +}; +use parking_lot::{Mutex, MutexGuard}; +use std::collections::HashSet; +use std::marker::PhantomData; +use std::path::Path; +use types::{EthSpec, FixedBytesExtended, Hash256}; + +use super::interface::WriteOptions; + +pub struct LevelDB { + db: Database, + /// A mutex to synchronise sensitive read-write transactions. + transaction_mutex: Mutex<()>, + _phantom: PhantomData, +} + +impl From for leveldb::options::WriteOptions { + fn from(options: WriteOptions) -> Self { + let mut opts = leveldb::options::WriteOptions::new(); + opts.sync = options.sync; + opts + } +} + +impl LevelDB { + pub fn open(path: &Path) -> Result { + let mut options = Options::new(); + + options.create_if_missing = true; + + let db = Database::open(path, options)?; + let transaction_mutex = Mutex::new(()); + + Ok(Self { + db, + transaction_mutex, + _phantom: PhantomData, + }) + } + + pub fn read_options(&self) -> ReadOptions { + ReadOptions::new() + } + + pub fn write_options(&self) -> WriteOptions { + WriteOptions::new() + } + + pub fn write_options_sync(&self) -> WriteOptions { + let mut opts = WriteOptions::new(); + opts.sync = true; + opts + } + + pub fn put_bytes_with_options( + &self, + col: DBColumn, + key: &[u8], + val: &[u8], + opts: WriteOptions, + ) -> Result<(), Error> { + let column_key = get_key_for_col(col, key); + + metrics::inc_counter_vec(&metrics::DISK_DB_WRITE_COUNT, &[col.into()]); + metrics::inc_counter_vec_by( + &metrics::DISK_DB_WRITE_BYTES, + &[col.into()], + val.len() as u64, + ); + let timer = metrics::start_timer(&metrics::DISK_DB_WRITE_TIMES); + + self.db + .put(opts.into(), BytesKey::from_vec(column_key), val) + .map_err(Into::into) + .map(|()| { + metrics::stop_timer(timer); + }) + } + + /// Store some `value` in `column`, indexed with `key`. + pub fn put_bytes(&self, col: DBColumn, key: &[u8], val: &[u8]) -> Result<(), Error> { + self.put_bytes_with_options(col, key, val, self.write_options()) + } + + pub fn put_bytes_sync(&self, col: DBColumn, key: &[u8], val: &[u8]) -> Result<(), Error> { + self.put_bytes_with_options(col, key, val, self.write_options_sync()) + } + + pub fn sync(&self) -> Result<(), Error> { + self.put_bytes_sync(DBColumn::Dummy, b"sync", b"sync") + } + + // Retrieve some bytes in `column` with `key`. + pub fn get_bytes(&self, col: DBColumn, key: &[u8]) -> Result>, Error> { + let column_key = get_key_for_col(col, key); + + metrics::inc_counter_vec(&metrics::DISK_DB_READ_COUNT, &[col.into()]); + let timer = metrics::start_timer(&metrics::DISK_DB_READ_TIMES); + + self.db + .get(self.read_options(), BytesKey::from_vec(column_key)) + .map_err(Into::into) + .map(|opt| { + opt.inspect(|bytes| { + metrics::inc_counter_vec_by( + &metrics::DISK_DB_READ_BYTES, + &[col.into()], + bytes.len() as u64, + ); + metrics::stop_timer(timer); + }) + }) + } + + /// Return `true` if `key` exists in `column`. + pub fn key_exists(&self, col: DBColumn, key: &[u8]) -> Result { + let column_key = get_key_for_col(col, key); + + metrics::inc_counter_vec(&metrics::DISK_DB_EXISTS_COUNT, &[col.into()]); + + self.db + .get(self.read_options(), BytesKey::from_vec(column_key)) + .map_err(Into::into) + .map(|val| val.is_some()) + } + + /// Removes `key` from `column`. + pub fn key_delete(&self, col: DBColumn, key: &[u8]) -> Result<(), Error> { + let column_key = get_key_for_col(col, key); + + metrics::inc_counter_vec(&metrics::DISK_DB_DELETE_COUNT, &[col.into()]); + + self.db + .delete(self.write_options().into(), BytesKey::from_vec(column_key)) + .map_err(Into::into) + } + + pub fn do_atomically(&self, ops_batch: Vec) -> Result<(), Error> { + let mut leveldb_batch = Writebatch::new(); + for op in ops_batch { + match op { + KeyValueStoreOp::PutKeyValue(col, key, value) => { + let _timer = metrics::start_timer(&metrics::DISK_DB_WRITE_TIMES); + metrics::inc_counter_vec_by( + &metrics::DISK_DB_WRITE_BYTES, + &[col.into()], + value.len() as u64, + ); + metrics::inc_counter_vec(&metrics::DISK_DB_WRITE_COUNT, &[col.into()]); + let column_key = get_key_for_col(col, &key); + leveldb_batch.put(BytesKey::from_vec(column_key), &value); + } + + KeyValueStoreOp::DeleteKey(col, key) => { + let _timer = metrics::start_timer(&metrics::DISK_DB_DELETE_TIMES); + metrics::inc_counter_vec(&metrics::DISK_DB_DELETE_COUNT, &[col.into()]); + let column_key = get_key_for_col(col, &key); + leveldb_batch.delete(BytesKey::from_vec(column_key)); + } + } + } + self.db.write(self.write_options().into(), &leveldb_batch)?; + Ok(()) + } + + pub fn begin_rw_transaction(&self) -> MutexGuard<()> { + self.transaction_mutex.lock() + } + + /// Compact all values in the states and states flag columns. + pub fn compact(&self) -> Result<(), Error> { + let _timer = metrics::start_timer(&metrics::DISK_DB_COMPACT_TIMES); + let endpoints = |column: DBColumn| { + ( + BytesKey::from_vec(get_key_for_col(column, Hash256::zero().as_slice())), + BytesKey::from_vec(get_key_for_col( + column, + Hash256::repeat_byte(0xff).as_slice(), + )), + ) + }; + + for (start_key, end_key) in [ + endpoints(DBColumn::BeaconStateTemporary), + endpoints(DBColumn::BeaconState), + endpoints(DBColumn::BeaconStateSummary), + ] { + self.db.compact(&start_key, &end_key); + } + + Ok(()) + } + + pub fn compact_column(&self, column: DBColumn) -> Result<(), Error> { + // Use key-size-agnostic keys [] and 0xff..ff with a minimum of 32 bytes to account for + // columns that may change size between sub-databases or schema versions. + let start_key = BytesKey::from_vec(get_key_for_col(column, &[])); + let end_key = BytesKey::from_vec(get_key_for_col( + column, + &vec![0xff; std::cmp::max(column.key_size(), 32)], + )); + self.db.compact(&start_key, &end_key); + Ok(()) + } + + pub fn iter_column_from(&self, column: DBColumn, from: &[u8]) -> ColumnIter { + let start_key = BytesKey::from_vec(get_key_for_col(column, from)); + let iter = self.db.iter(self.read_options()); + iter.seek(&start_key); + + Box::new( + iter.take_while(move |(key, _)| key.matches_column(column)) + .map(move |(bytes_key, value)| { + metrics::inc_counter_vec(&metrics::DISK_DB_READ_COUNT, &[column.into()]); + metrics::inc_counter_vec_by( + &metrics::DISK_DB_READ_BYTES, + &[column.into()], + value.len() as u64, + ); + let key = bytes_key.remove_column_variable(column).ok_or_else(|| { + HotColdDBError::IterationError { + unexpected_key: bytes_key.clone(), + } + })?; + Ok((K::from_bytes(key)?, value)) + }), + ) + } + + pub fn iter_column_keys_from(&self, column: DBColumn, from: &[u8]) -> ColumnKeyIter { + let start_key = BytesKey::from_vec(get_key_for_col(column, from)); + + let iter = self.db.keys_iter(self.read_options()); + iter.seek(&start_key); + + Box::new( + iter.take_while(move |key| key.matches_column(column)) + .map(move |bytes_key| { + metrics::inc_counter_vec(&metrics::DISK_DB_KEY_READ_COUNT, &[column.into()]); + metrics::inc_counter_vec_by( + &metrics::DISK_DB_KEY_READ_BYTES, + &[column.into()], + bytes_key.key.len() as u64, + ); + let key = &bytes_key.key[column.as_bytes().len()..]; + K::from_bytes(key) + }), + ) + } + + /// Iterate through all keys and values in a particular column. + pub fn iter_column_keys(&self, column: DBColumn) -> ColumnKeyIter { + self.iter_column_keys_from(column, &vec![0; column.key_size()]) + } + + pub fn iter_column(&self, column: DBColumn) -> ColumnIter { + self.iter_column_from(column, &vec![0; column.key_size()]) + } + + pub fn delete_batch(&self, col: DBColumn, ops: HashSet<&[u8]>) -> Result<(), Error> { + let mut leveldb_batch = Writebatch::new(); + for op in ops { + let column_key = get_key_for_col(col, op); + leveldb_batch.delete(BytesKey::from_vec(column_key)); + } + self.db.write(self.write_options().into(), &leveldb_batch)?; + Ok(()) + } + + pub fn delete_if( + &self, + column: DBColumn, + mut f: impl FnMut(&[u8]) -> Result, + ) -> Result<(), Error> { + let mut leveldb_batch = Writebatch::new(); + let iter = self.db.iter(self.read_options()); + + iter.take_while(move |(key, _)| key.matches_column(column)) + .for_each(|(key, value)| { + if f(&value).unwrap_or(false) { + let _timer = metrics::start_timer(&metrics::DISK_DB_DELETE_TIMES); + metrics::inc_counter_vec(&metrics::DISK_DB_DELETE_COUNT, &[column.into()]); + leveldb_batch.delete(key); + } + }); + + self.db.write(self.write_options().into(), &leveldb_batch)?; + Ok(()) + } +} diff --git a/beacon_node/store/src/database/redb_impl.rs b/beacon_node/store/src/database/redb_impl.rs new file mode 100644 index 0000000000..6a776da7b1 --- /dev/null +++ b/beacon_node/store/src/database/redb_impl.rs @@ -0,0 +1,314 @@ +use crate::{metrics, ColumnIter, ColumnKeyIter, Key}; +use crate::{DBColumn, Error, KeyValueStoreOp}; +use parking_lot::{Mutex, MutexGuard, RwLock}; +use redb::TableDefinition; +use std::collections::HashSet; +use std::{borrow::BorrowMut, marker::PhantomData, path::Path}; +use strum::IntoEnumIterator; +use types::EthSpec; + +use super::interface::WriteOptions; + +pub const DB_FILE_NAME: &str = "database.redb"; + +pub struct Redb { + db: RwLock, + transaction_mutex: Mutex<()>, + _phantom: PhantomData, +} + +impl From for redb::Durability { + fn from(options: WriteOptions) -> Self { + if options.sync { + redb::Durability::Immediate + } else { + redb::Durability::Eventual + } + } +} + +impl Redb { + pub fn open(path: &Path) -> Result { + let db_file = path.join(DB_FILE_NAME); + let db = redb::Database::create(db_file)?; + let transaction_mutex = Mutex::new(()); + + for column in DBColumn::iter() { + Redb::::create_table(&db, column.into())?; + } + + Ok(Self { + db: db.into(), + transaction_mutex, + _phantom: PhantomData, + }) + } + + fn create_table(db: &redb::Database, table_name: &str) -> Result<(), Error> { + let table_definition: TableDefinition<'_, &[u8], &[u8]> = TableDefinition::new(table_name); + let tx = db.begin_write()?; + tx.open_table(table_definition)?; + tx.commit().map_err(Into::into) + } + + pub fn write_options(&self) -> WriteOptions { + WriteOptions::new() + } + + pub fn write_options_sync(&self) -> WriteOptions { + let mut opts = WriteOptions::new(); + opts.sync = true; + opts + } + + pub fn begin_rw_transaction(&self) -> MutexGuard<()> { + self.transaction_mutex.lock() + } + + pub fn put_bytes_with_options( + &self, + col: DBColumn, + key: &[u8], + val: &[u8], + opts: WriteOptions, + ) -> Result<(), Error> { + metrics::inc_counter_vec(&metrics::DISK_DB_WRITE_COUNT, &[col.into()]); + metrics::inc_counter_vec_by( + &metrics::DISK_DB_WRITE_BYTES, + &[col.into()], + val.len() as u64, + ); + let timer = metrics::start_timer(&metrics::DISK_DB_WRITE_TIMES); + + let table_definition: TableDefinition<'_, &[u8], &[u8]> = TableDefinition::new(col.into()); + let open_db = self.db.read(); + let mut tx = open_db.begin_write()?; + tx.set_durability(opts.into()); + let mut table = tx.open_table(table_definition)?; + + table.insert(key, val).map(|_| { + metrics::stop_timer(timer); + })?; + drop(table); + tx.commit().map_err(Into::into) + } + + /// Store some `value` in `column`, indexed with `key`. + pub fn put_bytes(&self, col: DBColumn, key: &[u8], val: &[u8]) -> Result<(), Error> { + self.put_bytes_with_options(col, key, val, self.write_options()) + } + + pub fn put_bytes_sync(&self, col: DBColumn, key: &[u8], val: &[u8]) -> Result<(), Error> { + self.put_bytes_with_options(col, key, val, self.write_options_sync()) + } + + pub fn sync(&self) -> Result<(), Error> { + self.put_bytes_sync(DBColumn::Dummy, b"sync", b"sync") + } + + // Retrieve some bytes in `column` with `key`. + pub fn get_bytes(&self, col: DBColumn, key: &[u8]) -> Result>, Error> { + metrics::inc_counter_vec(&metrics::DISK_DB_READ_COUNT, &[col.into()]); + let timer = metrics::start_timer(&metrics::DISK_DB_READ_TIMES); + + let table_definition: TableDefinition<'_, &[u8], &[u8]> = TableDefinition::new(col.into()); + let open_db = self.db.read(); + let tx = open_db.begin_read()?; + let table = tx.open_table(table_definition)?; + + let result = table.get(key)?; + + match result { + Some(access_guard) => { + let value = access_guard.value().to_vec(); + metrics::inc_counter_vec_by( + &metrics::DISK_DB_READ_BYTES, + &[col.into()], + value.len() as u64, + ); + metrics::stop_timer(timer); + Ok(Some(value)) + } + None => { + metrics::stop_timer(timer); + Ok(None) + } + } + } + + /// Return `true` if `key` exists in `column`. + pub fn key_exists(&self, col: DBColumn, key: &[u8]) -> Result { + metrics::inc_counter_vec(&metrics::DISK_DB_EXISTS_COUNT, &[col.into()]); + + let table_definition: TableDefinition<'_, &[u8], &[u8]> = TableDefinition::new(col.into()); + let open_db = self.db.read(); + let tx = open_db.begin_read()?; + let table = tx.open_table(table_definition)?; + + table + .get(key) + .map_err(Into::into) + .map(|access_guard| access_guard.is_some()) + } + + /// Removes `key` from `column`. + pub fn key_delete(&self, col: DBColumn, key: &[u8]) -> Result<(), Error> { + let table_definition: TableDefinition<'_, &[u8], &[u8]> = TableDefinition::new(col.into()); + let open_db = self.db.read(); + let tx = open_db.begin_write()?; + let mut table = tx.open_table(table_definition)?; + metrics::inc_counter_vec(&metrics::DISK_DB_DELETE_COUNT, &[col.into()]); + + table.remove(key).map(|_| ())?; + drop(table); + tx.commit().map_err(Into::into) + } + + pub fn do_atomically(&self, ops_batch: Vec) -> Result<(), Error> { + let open_db = self.db.read(); + let mut tx = open_db.begin_write()?; + tx.set_durability(self.write_options().into()); + for op in ops_batch { + match op { + KeyValueStoreOp::PutKeyValue(column, key, value) => { + let _timer = metrics::start_timer(&metrics::DISK_DB_WRITE_TIMES); + metrics::inc_counter_vec_by( + &metrics::DISK_DB_WRITE_BYTES, + &[column.into()], + value.len() as u64, + ); + metrics::inc_counter_vec(&metrics::DISK_DB_WRITE_COUNT, &[column.into()]); + let table_definition: TableDefinition<'_, &[u8], &[u8]> = + TableDefinition::new(column.into()); + + let mut table = tx.open_table(table_definition)?; + table.insert(key.as_slice(), value.as_slice())?; + drop(table); + } + + KeyValueStoreOp::DeleteKey(column, key) => { + metrics::inc_counter_vec(&metrics::DISK_DB_DELETE_COUNT, &[column.into()]); + let _timer = metrics::start_timer(&metrics::DISK_DB_DELETE_TIMES); + let table_definition: TableDefinition<'_, &[u8], &[u8]> = + TableDefinition::new(column.into()); + + let mut table = tx.open_table(table_definition)?; + table.remove(key.as_slice())?; + drop(table); + } + } + } + + tx.commit()?; + Ok(()) + } + + /// Compact all values in the states and states flag columns. + pub fn compact(&self) -> Result<(), Error> { + let _timer = metrics::start_timer(&metrics::DISK_DB_COMPACT_TIMES); + let mut open_db = self.db.write(); + let mut_db = open_db.borrow_mut(); + mut_db.compact().map_err(Into::into).map(|_| ()) + } + + pub fn iter_column_keys_from(&self, column: DBColumn, from: &[u8]) -> ColumnKeyIter { + let table_definition: TableDefinition<'_, &[u8], &[u8]> = + TableDefinition::new(column.into()); + + let iter = { + let open_db = self.db.read(); + let read_txn = open_db.begin_read()?; + let table = read_txn.open_table(table_definition)?; + table.range(from..)?.map(move |res| { + let (key, _) = res?; + metrics::inc_counter_vec(&metrics::DISK_DB_KEY_READ_COUNT, &[column.into()]); + metrics::inc_counter_vec_by( + &metrics::DISK_DB_KEY_READ_BYTES, + &[column.into()], + key.value().len() as u64, + ); + K::from_bytes(key.value()) + }) + }; + + Box::new(iter) + } + + /// Iterate through all keys and values in a particular column. + pub fn iter_column_keys(&self, column: DBColumn) -> ColumnKeyIter { + self.iter_column_keys_from(column, &vec![0; column.key_size()]) + } + + pub fn iter_column_from(&self, column: DBColumn, from: &[u8]) -> ColumnIter { + let table_definition: TableDefinition<'_, &[u8], &[u8]> = + TableDefinition::new(column.into()); + + let prefix = from.to_vec(); + + let iter = { + let open_db = self.db.read(); + let read_txn = open_db.begin_read()?; + let table = read_txn.open_table(table_definition)?; + + table + .range(from..)? + .take_while(move |res| match res.as_ref() { + Ok((_, _)) => true, + Err(_) => false, + }) + .map(move |res| { + let (key, value) = res?; + metrics::inc_counter_vec(&metrics::DISK_DB_READ_COUNT, &[column.into()]); + metrics::inc_counter_vec_by( + &metrics::DISK_DB_READ_BYTES, + &[column.into()], + value.value().len() as u64, + ); + Ok((K::from_bytes(key.value())?, value.value().to_vec())) + }) + }; + + Ok(Box::new(iter)) + } + + pub fn iter_column(&self, column: DBColumn) -> ColumnIter { + self.iter_column_from(column, &vec![0; column.key_size()], |_, _| true) + } + + pub fn delete_batch(&self, col: DBColumn, ops: HashSet<&[u8]>) -> Result<(), Error> { + let open_db = self.db.read(); + let mut tx = open_db.begin_write()?; + + tx.set_durability(redb::Durability::None); + + let table_definition: TableDefinition<'_, &[u8], &[u8]> = TableDefinition::new(col.into()); + + let mut table = tx.open_table(table_definition)?; + table.retain(|key, _| !ops.contains(key))?; + + drop(table); + tx.commit()?; + Ok(()) + } + + pub fn delete_if( + &self, + column: DBColumn, + mut f: impl FnMut(&[u8]) -> Result, + ) -> Result<(), Error> { + let open_db = self.db.read(); + let mut tx = open_db.begin_write()?; + + tx.set_durability(redb::Durability::None); + + let table_definition: TableDefinition<'_, &[u8], &[u8]> = + TableDefinition::new(column.into()); + + let mut table = tx.open_table(table_definition)?; + table.retain(|_, value| !f(value).unwrap_or(false))?; + + drop(table); + tx.commit()?; + Ok(()) + } +} diff --git a/beacon_node/store/src/errors.rs b/beacon_node/store/src/errors.rs index 6bb4edee6b..41fd17ef43 100644 --- a/beacon_node/store/src/errors.rs +++ b/beacon_node/store/src/errors.rs @@ -2,6 +2,8 @@ use crate::chunked_vector::ChunkError; use crate::config::StoreConfigError; use crate::hot_cold_store::HotColdDBError; use crate::{hdiff, DBColumn}; +#[cfg(feature = "leveldb")] +use leveldb::error::Error as LevelDBError; use ssz::DecodeError; use state_processing::BlockReplayError; use types::{milhouse, BeaconStateError, EpochCacheError, Hash256, InconsistentFork, Slot}; @@ -48,6 +50,16 @@ pub enum Error { MissingGenesisState, MissingSnapshot(Slot), BlockReplayError(BlockReplayError), + AddPayloadLogicError, + InvalidKey, + InvalidBytes, + InconsistentFork(InconsistentFork), + #[cfg(feature = "leveldb")] + LevelDbError(LevelDBError), + #[cfg(feature = "redb")] + RedbError(redb::Error), + CacheBuildError(EpochCacheError), + RandaoMixOutOfBounds, MilhouseError(milhouse::Error), Compression(std::io::Error), FinalizedStateDecreasingSlot, @@ -56,17 +68,11 @@ pub enum Error { state_root: Hash256, slot: Slot, }, - AddPayloadLogicError, - InvalidKey, - InvalidBytes, - InconsistentFork(InconsistentFork), Hdiff(hdiff::Error), - CacheBuildError(EpochCacheError), ForwardsIterInvalidColumn(DBColumn), ForwardsIterGap(DBColumn, Slot, Slot), StateShouldNotBeRequired(Slot), MissingBlock(Hash256), - RandaoMixOutOfBounds, GenesisStateUnknown, ArithError(safe_arith::ArithError), } @@ -145,6 +151,62 @@ impl From for Error { } } +#[cfg(feature = "leveldb")] +impl From for Error { + fn from(e: LevelDBError) -> Error { + Error::LevelDbError(e) + } +} + +#[cfg(feature = "redb")] +impl From for Error { + fn from(e: redb::Error) -> Self { + Error::RedbError(e) + } +} + +#[cfg(feature = "redb")] +impl From for Error { + fn from(e: redb::TableError) -> Self { + Error::RedbError(e.into()) + } +} + +#[cfg(feature = "redb")] +impl From for Error { + fn from(e: redb::TransactionError) -> Self { + Error::RedbError(e.into()) + } +} + +#[cfg(feature = "redb")] +impl From for Error { + fn from(e: redb::DatabaseError) -> Self { + Error::RedbError(e.into()) + } +} + +#[cfg(feature = "redb")] +impl From for Error { + fn from(e: redb::StorageError) -> Self { + Error::RedbError(e.into()) + } +} + +#[cfg(feature = "redb")] +impl From for Error { + fn from(e: redb::CommitError) -> Self { + Error::RedbError(e.into()) + } +} + +#[cfg(feature = "redb")] +impl From for Error { + fn from(e: redb::CompactionError) -> Self { + Error::RedbError(e.into()) + } +} + impl From for Error { fn from(e: EpochCacheError) -> Error { Error::CacheBuildError(e) diff --git a/beacon_node/store/src/forwards_iter.rs b/beacon_node/store/src/forwards_iter.rs index 955bd33b30..5300a74c06 100644 --- a/beacon_node/store/src/forwards_iter.rs +++ b/beacon_node/store/src/forwards_iter.rs @@ -4,7 +4,6 @@ use crate::{ColumnIter, DBColumn, HotColdDB, ItemStore}; use itertools::process_results; use std::marker::PhantomData; use types::{BeaconState, EthSpec, Hash256, Slot}; - pub type HybridForwardsBlockRootsIterator<'a, E, Hot, Cold> = HybridForwardsIterator<'a, E, Hot, Cold>; pub type HybridForwardsStateRootsIterator<'a, E, Hot, Cold> = diff --git a/beacon_node/store/src/garbage_collection.rs b/beacon_node/store/src/garbage_collection.rs index 5f8ed8f5e7..06393f2d21 100644 --- a/beacon_node/store/src/garbage_collection.rs +++ b/beacon_node/store/src/garbage_collection.rs @@ -1,10 +1,11 @@ //! Garbage collection process that runs at start-up to clean up the database. +use crate::database::interface::BeaconNodeBackend; use crate::hot_cold_store::HotColdDB; -use crate::{Error, LevelDB, StoreOp}; +use crate::{DBColumn, Error}; use slog::debug; use types::EthSpec; -impl HotColdDB, LevelDB> +impl HotColdDB, BeaconNodeBackend> where E: EthSpec, { @@ -16,21 +17,22 @@ where /// Delete the temporary states that were leftover by failed block imports. pub fn delete_temp_states(&self) -> Result<(), Error> { - let delete_ops = - self.iter_temporary_state_roots() - .try_fold(vec![], |mut ops, state_root| { - let state_root = state_root?; - ops.push(StoreOp::DeleteState(state_root, None)); - Result::<_, Error>::Ok(ops) - })?; - - if !delete_ops.is_empty() { + let mut ops = vec![]; + self.iter_temporary_state_roots().for_each(|state_root| { + if let Ok(state_root) = state_root { + ops.push(state_root); + } + }); + if !ops.is_empty() { debug!( self.log, "Garbage collecting {} temporary states", - delete_ops.len() + ops.len() ); - self.do_atomically_with_block_and_blobs_cache(delete_ops)?; + + self.delete_batch(DBColumn::BeaconState, ops.clone())?; + self.delete_batch(DBColumn::BeaconStateSummary, ops.clone())?; + self.delete_batch(DBColumn::BeaconStateTemporary, ops)?; } Ok(()) diff --git a/beacon_node/store/src/hot_cold_store.rs b/beacon_node/store/src/hot_cold_store.rs index c29305f983..75251cb5fb 100644 --- a/beacon_node/store/src/hot_cold_store.rs +++ b/beacon_node/store/src/hot_cold_store.rs @@ -1,10 +1,10 @@ use crate::config::{OnDiskStoreConfig, StoreConfig}; +use crate::database::interface::BeaconNodeBackend; use crate::forwards_iter::{HybridForwardsBlockRootsIterator, HybridForwardsStateRootsIterator}; use crate::hdiff::{HDiff, HDiffBuffer, HierarchyModuli, StorageStrategy}; use crate::historic_state_cache::HistoricStateCache; use crate::impls::beacon_state::{get_full_state, store_full_state}; use crate::iter::{BlockRootsIterator, ParentRootBlockIterator, RootsIterator}; -use crate::leveldb_store::{BytesKey, LevelDB}; use crate::memory_store::MemoryStore; use crate::metadata::{ AnchorInfo, BlobInfo, CompactionTimestamp, DataColumnInfo, PruningCheckpoint, SchemaVersion, @@ -14,12 +14,10 @@ use crate::metadata::{ }; use crate::state_cache::{PutStateOutcome, StateCache}; use crate::{ - get_data_column_key, get_key_for_col, BlobSidecarListFromRoot, DBColumn, DatabaseBlock, Error, - ItemStore, KeyValueStoreOp, StoreItem, StoreOp, + get_data_column_key, metrics, parse_data_column_key, BlobSidecarListFromRoot, DBColumn, + DatabaseBlock, Error, ItemStore, KeyValueStore, KeyValueStoreOp, StoreItem, StoreOp, }; -use crate::{metrics, parse_data_column_key}; use itertools::{process_results, Itertools}; -use leveldb::iterator::LevelDBIterator; use lru::LruCache; use parking_lot::{Mutex, RwLock}; use safe_arith::SafeArith; @@ -231,7 +229,7 @@ impl HotColdDB, MemoryStore> { } } -impl HotColdDB, LevelDB> { +impl HotColdDB, BeaconNodeBackend> { /// Open a new or existing database, with the given paths to the hot and cold DBs. /// /// The `migrate_schema` function is passed in so that the parent `BeaconChain` can provide @@ -249,7 +247,7 @@ impl HotColdDB, LevelDB> { let hierarchy = config.hierarchy_config.to_moduli()?; - let hot_db = LevelDB::open(hot_path)?; + let hot_db = BeaconNodeBackend::open(&config, hot_path)?; let anchor_info = RwLock::new(Self::load_anchor_info(&hot_db)?); let db = HotColdDB { @@ -257,8 +255,8 @@ impl HotColdDB, LevelDB> { anchor_info, 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)?, + blobs_db: BeaconNodeBackend::open(&config, blobs_db_path)?, + cold_db: BeaconNodeBackend::open(&config, cold_path)?, hot_db, block_cache: Mutex::new(BlockCache::new(config.block_cache_size)), state_cache: Mutex::new(StateCache::new(config.state_cache_size)), @@ -408,23 +406,8 @@ impl HotColdDB, LevelDB> { /// Return an iterator over the state roots of all temporary states. pub fn iter_temporary_state_roots(&self) -> impl Iterator> + '_ { - let column = DBColumn::BeaconStateTemporary; - let start_key = - BytesKey::from_vec(get_key_for_col(column.into(), Hash256::zero().as_slice())); - - let keys_iter = self.hot_db.keys_iter(); - keys_iter.seek(&start_key); - - keys_iter - .take_while(move |key| key.matches_column(column)) - .map(move |bytes_key| { - bytes_key.remove_column(column).ok_or_else(|| { - HotColdDBError::IterationError { - unexpected_key: bytes_key, - } - .into() - }) - }) + self.hot_db + .iter_column_keys::(DBColumn::BeaconStateTemporary) } } @@ -536,9 +519,9 @@ impl, Cold: ItemStore> HotColdDB blinded_block: &SignedBeaconBlock>, ops: &mut Vec, ) { - let db_key = get_key_for_col(DBColumn::BeaconBlock.into(), key.as_slice()); ops.push(KeyValueStoreOp::PutKeyValue( - db_key, + DBColumn::BeaconBlock, + key.as_slice().into(), blinded_block.as_ssz_bytes(), )); } @@ -660,7 +643,7 @@ impl, Cold: ItemStore> HotColdDB decoder: impl FnOnce(&[u8]) -> Result, ssz::DecodeError>, ) -> Result>, Error> { self.hot_db - .get_bytes(DBColumn::BeaconBlock.into(), block_root.as_slice())? + .get_bytes(DBColumn::BeaconBlock, block_root.as_slice())? .map(|block_bytes| decoder(&block_bytes)) .transpose() .map_err(|e| e.into()) @@ -673,10 +656,12 @@ impl, Cold: ItemStore> HotColdDB block_root: &Hash256, fork_name: ForkName, ) -> Result>, Error> { - let column = ExecutionPayload::::db_column().into(); let key = block_root.as_slice(); - match self.hot_db.get_bytes(column, key)? { + match self + .hot_db + .get_bytes(ExecutionPayload::::db_column(), key)? + { Some(bytes) => Ok(Some(ExecutionPayload::from_ssz_bytes(&bytes, fork_name)?)), None => Ok(None), } @@ -705,10 +690,7 @@ impl, Cold: ItemStore> HotColdDB ) -> Result, Error> { let column = DBColumn::SyncCommitteeBranch; - if let Some(bytes) = self - .hot_db - .get_bytes(column.into(), &block_root.as_ssz_bytes())? - { + if let Some(bytes) = self.hot_db.get_bytes(column, &block_root.as_ssz_bytes())? { let sync_committee_branch = Vec::::from_ssz_bytes(&bytes)?; return Ok(Some(sync_committee_branch)); } @@ -725,7 +707,7 @@ impl, Cold: ItemStore> HotColdDB if let Some(bytes) = self .hot_db - .get_bytes(column.into(), &sync_committee_period.as_ssz_bytes())? + .get_bytes(column, &sync_committee_period.as_ssz_bytes())? { let sync_committee: SyncCommittee = SyncCommittee::from_ssz_bytes(&bytes)?; return Ok(Some(sync_committee)); @@ -741,7 +723,7 @@ impl, Cold: ItemStore> HotColdDB ) -> Result<(), Error> { let column = DBColumn::SyncCommitteeBranch; self.hot_db.put_bytes( - column.into(), + column, &block_root.as_ssz_bytes(), &sync_committee_branch.as_ssz_bytes(), )?; @@ -755,7 +737,7 @@ impl, Cold: ItemStore> HotColdDB ) -> Result<(), Error> { let column = DBColumn::SyncCommittee; self.hot_db.put_bytes( - column.into(), + column, &sync_committee_period.to_le_bytes(), &sync_committee.as_ssz_bytes(), )?; @@ -767,10 +749,10 @@ impl, Cold: ItemStore> HotColdDB &self, sync_committee_period: u64, ) -> Result>, Error> { - let column = DBColumn::LightClientUpdate; - let res = self - .hot_db - .get_bytes(column.into(), &sync_committee_period.to_le_bytes())?; + let res = self.hot_db.get_bytes( + DBColumn::LightClientUpdate, + &sync_committee_period.to_le_bytes(), + )?; if let Some(light_client_update_bytes) = res { let epoch = sync_committee_period @@ -822,10 +804,8 @@ impl, Cold: ItemStore> HotColdDB sync_committee_period: u64, light_client_update: &LightClientUpdate, ) -> Result<(), Error> { - let column = DBColumn::LightClientUpdate; - self.hot_db.put_bytes( - column.into(), + DBColumn::LightClientUpdate, &sync_committee_period.to_le_bytes(), &light_client_update.as_ssz_bytes(), )?; @@ -836,29 +816,29 @@ impl, Cold: ItemStore> HotColdDB /// Check if the blobs for a block exists on disk. pub fn blobs_exist(&self, block_root: &Hash256) -> Result { self.blobs_db - .key_exists(DBColumn::BeaconBlob.into(), block_root.as_slice()) + .key_exists(DBColumn::BeaconBlob, block_root.as_slice()) } /// Determine whether a block exists in the database. pub fn block_exists(&self, block_root: &Hash256) -> Result { self.hot_db - .key_exists(DBColumn::BeaconBlock.into(), block_root.as_slice()) + .key_exists(DBColumn::BeaconBlock, block_root.as_slice()) } /// Delete a block from the store and the block cache. pub fn delete_block(&self, block_root: &Hash256) -> Result<(), Error> { self.block_cache.lock().delete(block_root); self.hot_db - .key_delete(DBColumn::BeaconBlock.into(), block_root.as_slice())?; + .key_delete(DBColumn::BeaconBlock, block_root.as_slice())?; self.hot_db - .key_delete(DBColumn::ExecPayload.into(), block_root.as_slice())?; + .key_delete(DBColumn::ExecPayload, block_root.as_slice())?; self.blobs_db - .key_delete(DBColumn::BeaconBlob.into(), block_root.as_slice()) + .key_delete(DBColumn::BeaconBlob, block_root.as_slice()) } pub fn put_blobs(&self, block_root: &Hash256, blobs: BlobSidecarList) -> Result<(), Error> { self.blobs_db.put_bytes( - DBColumn::BeaconBlob.into(), + DBColumn::BeaconBlob, block_root.as_slice(), &blobs.as_ssz_bytes(), )?; @@ -872,8 +852,11 @@ impl, Cold: ItemStore> HotColdDB blobs: BlobSidecarList, ops: &mut Vec, ) { - let db_key = get_key_for_col(DBColumn::BeaconBlob.into(), key.as_slice()); - ops.push(KeyValueStoreOp::PutKeyValue(db_key, blobs.as_ssz_bytes())); + ops.push(KeyValueStoreOp::PutKeyValue( + DBColumn::BeaconBlob, + key.as_slice().to_vec(), + blobs.as_ssz_bytes(), + )); } pub fn data_columns_as_kv_store_ops( @@ -883,12 +866,9 @@ impl, Cold: ItemStore> HotColdDB ops: &mut Vec, ) { 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, + DBColumn::BeaconDataColumn, + get_data_column_key(block_root, &data_column.index), data_column.as_ssz_bytes(), )); } @@ -1202,63 +1182,68 @@ impl, Cold: ItemStore> HotColdDB } StoreOp::DeleteStateTemporaryFlag(state_root) => { - let db_key = - get_key_for_col(TemporaryFlag::db_column().into(), state_root.as_slice()); - key_value_batch.push(KeyValueStoreOp::DeleteKey(db_key)); + key_value_batch.push(KeyValueStoreOp::DeleteKey( + TemporaryFlag::db_column(), + state_root.as_slice().to_vec(), + )); } StoreOp::DeleteBlock(block_root) => { - let key = get_key_for_col(DBColumn::BeaconBlock.into(), block_root.as_slice()); - key_value_batch.push(KeyValueStoreOp::DeleteKey(key)); + key_value_batch.push(KeyValueStoreOp::DeleteKey( + DBColumn::BeaconBlock, + block_root.as_slice().to_vec(), + )); } StoreOp::DeleteBlobs(block_root) => { - let key = get_key_for_col(DBColumn::BeaconBlob.into(), block_root.as_slice()); - key_value_batch.push(KeyValueStoreOp::DeleteKey(key)); + key_value_batch.push(KeyValueStoreOp::DeleteKey( + DBColumn::BeaconBlob, + block_root.as_slice().to_vec(), + )); } 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)); + let key = get_data_column_key(&block_root, &index); + key_value_batch + .push(KeyValueStoreOp::DeleteKey(DBColumn::BeaconDataColumn, key)); } } StoreOp::DeleteState(state_root, slot) => { // Delete the hot state summary. - let state_summary_key = - get_key_for_col(DBColumn::BeaconStateSummary.into(), state_root.as_slice()); - key_value_batch.push(KeyValueStoreOp::DeleteKey(state_summary_key)); + key_value_batch.push(KeyValueStoreOp::DeleteKey( + DBColumn::BeaconStateSummary, + state_root.as_slice().to_vec(), + )); // Delete the state temporary flag (if any). Temporary flags are commonly // created by the state advance routine. - let state_temp_key = get_key_for_col( - DBColumn::BeaconStateTemporary.into(), - state_root.as_slice(), - ); - key_value_batch.push(KeyValueStoreOp::DeleteKey(state_temp_key)); + key_value_batch.push(KeyValueStoreOp::DeleteKey( + DBColumn::BeaconStateTemporary, + state_root.as_slice().to_vec(), + )); if slot.map_or(true, |slot| slot % E::slots_per_epoch() == 0) { - let state_key = - get_key_for_col(DBColumn::BeaconState.into(), state_root.as_slice()); - key_value_batch.push(KeyValueStoreOp::DeleteKey(state_key)); + key_value_batch.push(KeyValueStoreOp::DeleteKey( + DBColumn::BeaconState, + state_root.as_slice().to_vec(), + )); } } StoreOp::DeleteExecutionPayload(block_root) => { - let key = get_key_for_col(DBColumn::ExecPayload.into(), block_root.as_slice()); - key_value_batch.push(KeyValueStoreOp::DeleteKey(key)); + key_value_batch.push(KeyValueStoreOp::DeleteKey( + DBColumn::ExecPayload, + block_root.as_slice().to_vec(), + )); } StoreOp::DeleteSyncCommitteeBranch(block_root) => { - let key = get_key_for_col( - DBColumn::SyncCommitteeBranch.into(), - block_root.as_slice(), - ); - key_value_batch.push(KeyValueStoreOp::DeleteKey(key)); + key_value_batch.push(KeyValueStoreOp::DeleteKey( + DBColumn::SyncCommitteeBranch, + block_root.as_slice().to_vec(), + )); } StoreOp::KeyValueOp(kv_op) => { @@ -1269,6 +1254,19 @@ impl, Cold: ItemStore> HotColdDB Ok(key_value_batch) } + pub fn delete_batch(&self, col: DBColumn, ops: Vec) -> Result<(), Error> { + let new_ops: HashSet<&[u8]> = ops.iter().map(|v| v.as_slice()).collect(); + self.hot_db.delete_batch(col, new_ops) + } + + pub fn delete_if( + &self, + column: DBColumn, + f: impl Fn(&[u8]) -> Result, + ) -> Result<(), Error> { + self.hot_db.delete_if(column, f) + } + pub fn do_atomically_with_block_and_blobs_cache( &self, batch: Vec>, @@ -1608,10 +1606,8 @@ impl, Cold: ItemStore> HotColdDB ) -> Result<(), Error> { ops.push(ColdStateSummary { slot }.as_kv_store_op(*state_root)); ops.push(KeyValueStoreOp::PutKeyValue( - get_key_for_col( - DBColumn::BeaconStateRoots.into(), - &slot.as_u64().to_be_bytes(), - ), + DBColumn::BeaconStateRoots, + slot.as_u64().to_be_bytes().to_vec(), state_root.as_slice().to_vec(), )); Ok(()) @@ -1678,19 +1674,19 @@ impl, Cold: ItemStore> HotColdDB out }; - let key = get_key_for_col( - DBColumn::BeaconStateSnapshot.into(), - &state.slot().as_u64().to_be_bytes(), - ); - ops.push(KeyValueStoreOp::PutKeyValue(key, compressed_value)); + ops.push(KeyValueStoreOp::PutKeyValue( + DBColumn::BeaconStateSnapshot, + state.slot().as_u64().to_be_bytes().to_vec(), + compressed_value, + )); Ok(()) } fn load_cold_state_bytes_as_snapshot(&self, slot: Slot) -> Result>, Error> { - match self.cold_db.get_bytes( - DBColumn::BeaconStateSnapshot.into(), - &slot.as_u64().to_be_bytes(), - )? { + match self + .cold_db + .get_bytes(DBColumn::BeaconStateSnapshot, &slot.as_u64().to_be_bytes())? + { Some(bytes) => { let _timer = metrics::start_timer(&metrics::STORE_BEACON_STATE_FREEZER_DECOMPRESS_TIME); @@ -1731,11 +1727,11 @@ impl, Cold: ItemStore> HotColdDB }; let diff_bytes = diff.as_ssz_bytes(); - let key = get_key_for_col( - DBColumn::BeaconStateDiff.into(), - &state.slot().as_u64().to_be_bytes(), - ); - ops.push(KeyValueStoreOp::PutKeyValue(key, diff_bytes)); + ops.push(KeyValueStoreOp::PutKeyValue( + DBColumn::BeaconStateDiff, + state.slot().as_u64().to_be_bytes().to_vec(), + diff_bytes, + )); Ok(()) } @@ -1858,10 +1854,7 @@ impl, Cold: ItemStore> HotColdDB let bytes = { let _t = metrics::start_timer(&metrics::BEACON_HDIFF_READ_TIMES); self.cold_db - .get_bytes( - DBColumn::BeaconStateDiff.into(), - &slot.as_u64().to_be_bytes(), - )? + .get_bytes(DBColumn::BeaconStateDiff, &slot.as_u64().to_be_bytes())? .ok_or(HotColdDBError::MissingHDiff(slot))? }; let hdiff = { @@ -2054,7 +2047,7 @@ impl, Cold: ItemStore> HotColdDB match self .blobs_db - .get_bytes(DBColumn::BeaconBlob.into(), block_root.as_slice())? + .get_bytes(DBColumn::BeaconBlob, block_root.as_slice())? { Some(ref blobs_bytes) => { // We insert a VariableList of BlobSidecars into the db, but retrieve @@ -2084,8 +2077,17 @@ impl, Cold: ItemStore> HotColdDB /// 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_slice()) - .map(|key| key.and_then(|key| parse_data_column_key(key).map(|key| key.1))) + .iter_column_from::>(DBColumn::BeaconDataColumn, block_root.as_slice()) + .take_while(|res| { + let Ok((key, _)) = res else { return false }; + + if !key.starts_with(block_root.as_slice()) { + return false; + } + + true + }) + .map(|key| key.and_then(|(key, _)| parse_data_column_key(key).map(|key| key.1))) .collect() } @@ -2106,7 +2108,7 @@ impl, Cold: ItemStore> HotColdDB } match self.blobs_db.get_bytes( - DBColumn::BeaconDataColumn.into(), + DBColumn::BeaconDataColumn, &get_data_column_key(block_root, column_index), )? { Some(ref data_column_bytes) => { @@ -2164,10 +2166,12 @@ impl, Cold: ItemStore> HotColdDB schema_version: SchemaVersion, mut ops: Vec, ) -> Result<(), Error> { - let column = SchemaVersion::db_column().into(); let key = SCHEMA_VERSION_KEY.as_slice(); - let db_key = get_key_for_col(column, key); - let op = KeyValueStoreOp::PutKeyValue(db_key, schema_version.as_store_bytes()); + let op = KeyValueStoreOp::PutKeyValue( + SchemaVersion::db_column(), + key.to_vec(), + schema_version.as_store_bytes(), + ); ops.push(op); self.hot_db.do_atomically(ops) @@ -2589,7 +2593,8 @@ impl, Cold: ItemStore> HotColdDB let mut ops = vec![]; for slot in start_slot.as_u64()..end_slot.as_u64() { ops.push(KeyValueStoreOp::PutKeyValue( - get_key_for_col(DBColumn::BeaconBlockRoots.into(), &slot.to_be_bytes()), + DBColumn::BeaconBlockRoots, + slot.to_be_bytes().to_vec(), block_root.as_slice().to_vec(), )); } @@ -2811,77 +2816,62 @@ impl, Cold: ItemStore> HotColdDB "data_availability_boundary" => data_availability_boundary, ); - let mut ops = vec![]; - let mut last_pruned_block_root = None; + // We collect block roots of deleted blobs in memory. Even for 10y of blob history this + // vec won't go beyond 1GB. We can probably optimise this out eventually. + let mut removed_block_roots = vec![]; - for res in self.forwards_block_roots_iterator_until(oldest_blob_slot, end_slot, || { - let (_, split_state) = self - .get_advanced_hot_state(split.block_root, split.slot, split.state_root)? - .ok_or(HotColdDBError::MissingSplitState( - split.state_root, - split.slot, - ))?; - - Ok((split_state, split.block_root)) - })? { - let (block_root, slot) = match res { - Ok(tuple) => tuple, - Err(e) => { - warn!( - self.log, - "Stopping blob pruning early"; - "error" => ?e, - ); - break; - } + let remove_blob_if = |blobs_bytes: &[u8]| { + let blobs = Vec::from_ssz_bytes(blobs_bytes)?; + let Some(blob): Option<&Arc>> = blobs.first() else { + return Ok(false); }; - 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 blob.slot() <= end_slot { + // Store the block root so we can delete from the blob cache + removed_block_roots.push(blob.block_root()); + // Delete from the on-disk db + return Ok(true); + }; + Ok(false) + }; - if slot >= end_slot { - break; - } + self.blobs_db + .delete_if(DBColumn::BeaconBlob, remove_blob_if)?; + + if self.spec.is_peer_das_enabled_for_epoch(start_epoch) { + let remove_data_column_if = |blobs_bytes: &[u8]| { + let data_column: DataColumnSidecar = + DataColumnSidecar::from_ssz_bytes(blobs_bytes)?; + + if data_column.slot() <= end_slot { + return Ok(true); + }; + + Ok(false) + }; + + self.blobs_db + .delete_if(DBColumn::BeaconDataColumn, remove_data_column_if)?; } - let blob_lists_pruned = ops.len(); + + // Remove deleted blobs from the cache. + let mut block_cache = self.block_cache.lock(); + for block_root in removed_block_roots { + block_cache.delete_blobs(&block_root); + } + drop(block_cache); + let new_blob_info = BlobInfo { oldest_blob_slot: Some(end_slot + 1), blobs_db: blob_info.blobs_db, }; - let update_blob_info = self.compare_and_set_blob_info(blob_info, new_blob_info)?; - ops.push(StoreOp::KeyValueOp(update_blob_info)); - self.do_atomically_with_block_and_blobs_cache(ops)?; + let op = self.compare_and_set_blob_info(blob_info, new_blob_info)?; + self.do_atomically_with_block_and_blobs_cache(vec![StoreOp::KeyValueOp(op)])?; + debug!( self.log, "Blob pruning complete"; - "blob_lists_pruned" => blob_lists_pruned, ); Ok(()) @@ -2944,10 +2934,7 @@ impl, Cold: ItemStore> HotColdDB for column in columns { for res in self.cold_db.iter_column_keys::>(column) { let key = res?; - cold_ops.push(KeyValueStoreOp::DeleteKey(get_key_for_col( - column.as_str(), - &key, - ))); + cold_ops.push(KeyValueStoreOp::DeleteKey(column, key)); } } let delete_ops = cold_ops.len(); @@ -3085,10 +3072,8 @@ pub fn migrate_database, Cold: ItemStore>( // Store the slot to block root mapping. cold_db_block_ops.push(KeyValueStoreOp::PutKeyValue( - get_key_for_col( - DBColumn::BeaconBlockRoots.into(), - &slot.as_u64().to_be_bytes(), - ), + DBColumn::BeaconBlockRoots, + slot.as_u64().to_be_bytes().to_vec(), block_root.as_slice().to_vec(), )); @@ -3339,3 +3324,57 @@ impl StoreItem for TemporaryFlag { Ok(TemporaryFlag) } } + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct BytesKey { + pub key: Vec, +} + +impl db_key::Key for BytesKey { + fn from_u8(key: &[u8]) -> Self { + Self { key: key.to_vec() } + } + + fn as_slice T>(&self, f: F) -> T { + f(self.key.as_slice()) + } +} + +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()) + } + + /// Remove the column from a key, returning its `Hash256` portion. + pub fn remove_column(&self, column: DBColumn) -> Option { + if self.matches_column(column) { + let subkey = &self.key[column.as_bytes().len()..]; + if subkey.len() == 32 { + return Some(Hash256::from_slice(subkey)); + } + } + None + } + + /// Remove the column from a key. + /// + /// Will return `None` if the value doesn't match the column or has the wrong length. + pub fn remove_column_variable(&self, column: DBColumn) -> Option<&[u8]> { + if self.matches_column(column) { + let subkey = &self.key[column.as_bytes().len()..]; + if subkey.len() == column.key_size() { + return Some(subkey); + } + } + None + } + + pub fn from_vec(key: Vec) -> Self { + Self { key } + } +} diff --git a/beacon_node/store/src/impls/beacon_state.rs b/beacon_node/store/src/impls/beacon_state.rs index 48c289f2b2..fd08e547f1 100644 --- a/beacon_node/store/src/impls/beacon_state.rs +++ b/beacon_node/store/src/impls/beacon_state.rs @@ -13,8 +13,11 @@ pub fn store_full_state( }; metrics::inc_counter_by(&metrics::BEACON_STATE_WRITE_BYTES, bytes.len() as u64); metrics::inc_counter(&metrics::BEACON_STATE_WRITE_COUNT); - let key = get_key_for_col(DBColumn::BeaconState.into(), state_root.as_slice()); - ops.push(KeyValueStoreOp::PutKeyValue(key, bytes)); + ops.push(KeyValueStoreOp::PutKeyValue( + DBColumn::BeaconState, + state_root.as_slice().to_vec(), + bytes, + )); Ok(()) } @@ -25,7 +28,7 @@ pub fn get_full_state, E: EthSpec>( ) -> Result>, Error> { let total_timer = metrics::start_timer(&metrics::BEACON_STATE_READ_TIMES); - match db.get_bytes(DBColumn::BeaconState.into(), state_root.as_slice())? { + match db.get_bytes(DBColumn::BeaconState, state_root.as_slice())? { Some(bytes) => { let overhead_timer = metrics::start_timer(&metrics::BEACON_STATE_READ_OVERHEAD_TIMES); let container = StorageContainer::from_ssz_bytes(&bytes, spec)?; diff --git a/beacon_node/store/src/leveldb_store.rs b/beacon_node/store/src/leveldb_store.rs deleted file mode 100644 index 720afd0f3f..0000000000 --- a/beacon_node/store/src/leveldb_store.rs +++ /dev/null @@ -1,310 +0,0 @@ -use super::*; -use crate::hot_cold_store::HotColdDBError; -use leveldb::compaction::Compaction; -use leveldb::database::batch::{Batch, Writebatch}; -use leveldb::database::kv::KV; -use leveldb::database::Database; -use leveldb::error::Error as LevelDBError; -use leveldb::iterator::{Iterable, KeyIterator, LevelDBIterator}; -use leveldb::options::{Options, ReadOptions, WriteOptions}; -use parking_lot::Mutex; -use std::marker::PhantomData; -use std::path::Path; - -/// A wrapped leveldb database. -pub struct LevelDB { - db: Database, - /// A mutex to synchronise sensitive read-write transactions. - transaction_mutex: Mutex<()>, - _phantom: PhantomData, -} - -impl LevelDB { - /// Open a database at `path`, creating a new database if one does not already exist. - pub fn open(path: &Path) -> Result { - let mut options = Options::new(); - - options.create_if_missing = true; - - let db = Database::open(path, options)?; - let transaction_mutex = Mutex::new(()); - - Ok(Self { - db, - transaction_mutex, - _phantom: PhantomData, - }) - } - - fn read_options(&self) -> ReadOptions { - ReadOptions::new() - } - - fn write_options(&self) -> WriteOptions { - WriteOptions::new() - } - - fn write_options_sync(&self) -> WriteOptions { - let mut opts = WriteOptions::new(); - opts.sync = true; - opts - } - - fn put_bytes_with_options( - &self, - col: &str, - key: &[u8], - val: &[u8], - opts: WriteOptions, - ) -> Result<(), Error> { - let column_key = get_key_for_col(col, key); - - metrics::inc_counter_vec(&metrics::DISK_DB_WRITE_COUNT, &[col]); - metrics::inc_counter_vec_by(&metrics::DISK_DB_WRITE_BYTES, &[col], val.len() as u64); - let _timer = metrics::start_timer(&metrics::DISK_DB_WRITE_TIMES); - - self.db - .put(opts, BytesKey::from_vec(column_key), val) - .map_err(Into::into) - } - - pub fn keys_iter(&self) -> KeyIterator { - self.db.keys_iter(self.read_options()) - } -} - -impl KeyValueStore for LevelDB { - /// Store some `value` in `column`, indexed with `key`. - fn put_bytes(&self, col: &str, key: &[u8], val: &[u8]) -> Result<(), Error> { - self.put_bytes_with_options(col, key, val, self.write_options()) - } - - fn put_bytes_sync(&self, col: &str, key: &[u8], val: &[u8]) -> Result<(), Error> { - self.put_bytes_with_options(col, key, val, self.write_options_sync()) - } - - fn sync(&self) -> Result<(), Error> { - self.put_bytes_sync("sync", b"sync", b"sync") - } - - /// Retrieve some bytes in `column` with `key`. - fn get_bytes(&self, col: &str, key: &[u8]) -> Result>, Error> { - let column_key = get_key_for_col(col, key); - - metrics::inc_counter_vec(&metrics::DISK_DB_READ_COUNT, &[col]); - let timer = metrics::start_timer(&metrics::DISK_DB_READ_TIMES); - - self.db - .get(self.read_options(), BytesKey::from_vec(column_key)) - .map_err(Into::into) - .map(|opt| { - opt.inspect(|bytes| { - metrics::inc_counter_vec_by( - &metrics::DISK_DB_READ_BYTES, - &[col], - bytes.len() as u64, - ); - metrics::stop_timer(timer); - }) - }) - } - - /// Return `true` if `key` exists in `column`. - fn key_exists(&self, col: &str, key: &[u8]) -> Result { - let column_key = get_key_for_col(col, key); - - metrics::inc_counter_vec(&metrics::DISK_DB_EXISTS_COUNT, &[col]); - - self.db - .get(self.read_options(), BytesKey::from_vec(column_key)) - .map_err(Into::into) - .map(|val| val.is_some()) - } - - /// Removes `key` from `column`. - fn key_delete(&self, col: &str, key: &[u8]) -> Result<(), Error> { - let column_key = get_key_for_col(col, key); - - metrics::inc_counter_vec(&metrics::DISK_DB_DELETE_COUNT, &[col]); - - self.db - .delete(self.write_options(), BytesKey::from_vec(column_key)) - .map_err(Into::into) - } - - fn do_atomically(&self, ops_batch: Vec) -> Result<(), Error> { - let mut leveldb_batch = Writebatch::new(); - for op in ops_batch { - match op { - KeyValueStoreOp::PutKeyValue(key, value) => { - let col = get_col_from_key(&key).unwrap_or("unknown".to_owned()); - metrics::inc_counter_vec(&metrics::DISK_DB_WRITE_COUNT, &[&col]); - metrics::inc_counter_vec_by( - &metrics::DISK_DB_WRITE_BYTES, - &[&col], - value.len() as u64, - ); - - leveldb_batch.put(BytesKey::from_vec(key), &value); - } - - KeyValueStoreOp::DeleteKey(key) => { - let col = get_col_from_key(&key).unwrap_or("unknown".to_owned()); - metrics::inc_counter_vec(&metrics::DISK_DB_DELETE_COUNT, &[&col]); - - leveldb_batch.delete(BytesKey::from_vec(key)); - } - } - } - - let _timer = metrics::start_timer(&metrics::DISK_DB_WRITE_TIMES); - - self.db.write(self.write_options(), &leveldb_batch)?; - Ok(()) - } - - fn begin_rw_transaction(&self) -> MutexGuard<()> { - self.transaction_mutex.lock() - } - - fn compact_column(&self, column: DBColumn) -> Result<(), Error> { - // Use key-size-agnostic keys [] and 0xff..ff with a minimum of 32 bytes to account for - // columns that may change size between sub-databases or schema versions. - let start_key = BytesKey::from_vec(get_key_for_col(column.as_str(), &[])); - let end_key = BytesKey::from_vec(get_key_for_col( - column.as_str(), - &vec![0xff; std::cmp::max(column.key_size(), 32)], - )); - self.db.compact(&start_key, &end_key); - Ok(()) - } - - fn iter_column_from(&self, column: DBColumn, from: &[u8]) -> ColumnIter { - let start_key = BytesKey::from_vec(get_key_for_col(column.into(), from)); - let iter = self.db.iter(self.read_options()); - iter.seek(&start_key); - - Box::new( - iter.take_while(move |(key, _)| key.matches_column(column)) - .map(move |(bytes_key, value)| { - let key = bytes_key.remove_column_variable(column).ok_or_else(|| { - HotColdDBError::IterationError { - unexpected_key: bytes_key.clone(), - } - })?; - Ok((K::from_bytes(key)?, value)) - }), - ) - } - - fn iter_raw_entries(&self, column: DBColumn, prefix: &[u8]) -> RawEntryIter { - let start_key = BytesKey::from_vec(get_key_for_col(column.into(), prefix)); - - let iter = self.db.iter(self.read_options()); - iter.seek(&start_key); - - Box::new( - iter.take_while(move |(key, _)| key.key.starts_with(start_key.key.as_slice())) - .map(move |(bytes_key, value)| { - let subkey = &bytes_key.key[column.as_bytes().len()..]; - Ok((Vec::from(subkey), value)) - }), - ) - } - - fn iter_raw_keys(&self, column: DBColumn, prefix: &[u8]) -> RawKeyIter { - let start_key = BytesKey::from_vec(get_key_for_col(column.into(), prefix)); - - let iter = self.db.keys_iter(self.read_options()); - iter.seek(&start_key); - - Box::new( - iter.take_while(move |key| key.key.starts_with(start_key.key.as_slice())) - .map(move |bytes_key| { - let subkey = &bytes_key.key[column.as_bytes().len()..]; - Ok(Vec::from(subkey)) - }), - ) - } - - /// Iterate through all keys and values in a particular column. - fn iter_column_keys(&self, column: DBColumn) -> ColumnKeyIter { - let start_key = - BytesKey::from_vec(get_key_for_col(column.into(), &vec![0; column.key_size()])); - - let iter = self.db.keys_iter(self.read_options()); - iter.seek(&start_key); - - Box::new( - iter.take_while(move |key| key.matches_column(column)) - .map(move |bytes_key| { - let key = bytes_key.remove_column_variable(column).ok_or_else(|| { - HotColdDBError::IterationError { - unexpected_key: bytes_key.clone(), - } - })?; - K::from_bytes(key) - }), - ) - } -} - -impl ItemStore for LevelDB {} - -/// Used for keying leveldb. -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub struct BytesKey { - key: Vec, -} - -impl db_key::Key for BytesKey { - fn from_u8(key: &[u8]) -> Self { - Self { key: key.to_vec() } - } - - fn as_slice T>(&self, f: F) -> T { - f(self.key.as_slice()) - } -} - -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()) - } - - /// Remove the column from a 32 byte key, yielding the `Hash256` key. - pub fn remove_column(&self, column: DBColumn) -> Option { - let key = self.remove_column_variable(column)?; - (column.key_size() == 32).then(|| Hash256::from_slice(key)) - } - - /// Remove the column from a key. - /// - /// Will return `None` if the value doesn't match the column or has the wrong length. - pub fn remove_column_variable(&self, column: DBColumn) -> Option<&[u8]> { - if self.matches_column(column) { - let subkey = &self.key[column.as_bytes().len()..]; - if subkey.len() == column.key_size() { - return Some(subkey); - } - } - None - } - - pub fn from_vec(key: Vec) -> Self { - Self { key } - } -} - -impl From for Error { - fn from(e: LevelDBError) -> Error { - Error::DBError { - message: format!("{:?}", e), - } - } -} diff --git a/beacon_node/store/src/lib.rs b/beacon_node/store/src/lib.rs index 1458fa846c..0cfc42ab15 100644 --- a/beacon_node/store/src/lib.rs +++ b/beacon_node/store/src/lib.rs @@ -19,7 +19,6 @@ pub mod hdiff; pub mod historic_state_cache; pub mod hot_cold_store; mod impls; -mod leveldb_store; mod memory_store; pub mod metadata; pub mod metrics; @@ -27,13 +26,13 @@ pub mod partial_beacon_state; pub mod reconstruct; pub mod state_cache; +pub mod database; pub mod iter; pub use self::blob_sidecar_list_from_root::BlobSidecarListFromRoot; pub use self::config::StoreConfig; pub use self::consensus_context::OnDiskConsensusContext; pub use self::hot_cold_store::{HotColdDB, HotStateSummary, Split}; -pub use self::leveldb_store::LevelDB; pub use self::memory_store::MemoryStore; pub use crate::metadata::BlobInfo; pub use errors::Error; @@ -41,8 +40,9 @@ pub use impls::beacon_state::StorageContainer as BeaconStateStorageContainer; pub use metadata::AnchorInfo; pub use metrics::scrape_for_metrics; use parking_lot::MutexGuard; +use std::collections::HashSet; use std::sync::Arc; -use strum::{EnumString, IntoStaticStr}; +use strum::{EnumIter, EnumString, IntoStaticStr}; pub use types::*; const DATA_COLUMN_DB_KEY_SIZE: usize = 32 + 8; @@ -50,18 +50,18 @@ const DATA_COLUMN_DB_KEY_SIZE: usize = 32 + 8; pub type ColumnIter<'a, K> = Box), Error>> + 'a>; pub type ColumnKeyIter<'a, K> = Box> + 'a>; -pub type RawEntryIter<'a> = Box, Vec), Error>> + 'a>; -pub type RawKeyIter<'a> = Box, Error>> + 'a>; +pub type RawEntryIter<'a> = + Result, Vec), Error>> + 'a>, Error>; pub trait KeyValueStore: Sync + Send + Sized + 'static { /// Retrieve some bytes in `column` with `key`. - fn get_bytes(&self, column: &str, key: &[u8]) -> Result>, Error>; + fn get_bytes(&self, column: DBColumn, key: &[u8]) -> Result>, Error>; /// Store some `value` in `column`, indexed with `key`. - fn put_bytes(&self, column: &str, key: &[u8], value: &[u8]) -> Result<(), Error>; + fn put_bytes(&self, column: DBColumn, key: &[u8], value: &[u8]) -> Result<(), Error>; /// Same as put_bytes() but also force a flush to disk - fn put_bytes_sync(&self, column: &str, key: &[u8], value: &[u8]) -> Result<(), Error>; + fn put_bytes_sync(&self, column: DBColumn, key: &[u8], value: &[u8]) -> Result<(), Error>; /// Flush to disk. See /// https://chromium.googlesource.com/external/leveldb/+/HEAD/doc/index.md#synchronous-writes @@ -69,10 +69,10 @@ pub trait KeyValueStore: Sync + Send + Sized + 'static { fn sync(&self) -> Result<(), Error>; /// Return `true` if `key` exists in `column`. - fn key_exists(&self, column: &str, key: &[u8]) -> Result; + fn key_exists(&self, column: DBColumn, key: &[u8]) -> Result; /// Removes `key` from `column`. - fn key_delete(&self, column: &str, key: &[u8]) -> Result<(), Error>; + fn key_delete(&self, column: DBColumn, key: &[u8]) -> Result<(), Error>; /// Execute either all of the operations in `batch` or none at all, returning an error. fn do_atomically(&self, batch: Vec) -> Result<(), Error>; @@ -105,17 +105,21 @@ pub trait KeyValueStore: Sync + Send + Sized + 'static { self.iter_column_from(column, &vec![0; column.key_size()]) } - /// Iterate through all keys and values in a column from a given starting point. + /// Iterate through all keys and values in a column from a given starting point that fulfill the given predicate. fn iter_column_from(&self, column: DBColumn, from: &[u8]) -> ColumnIter; - fn iter_raw_entries(&self, _column: DBColumn, _prefix: &[u8]) -> RawEntryIter { - Box::new(std::iter::empty()) - } - - fn iter_raw_keys(&self, column: DBColumn, prefix: &[u8]) -> RawKeyIter; + fn iter_column_keys(&self, column: DBColumn) -> ColumnKeyIter; /// Iterate through all keys in a particular column. - fn iter_column_keys(&self, column: DBColumn) -> ColumnKeyIter; + fn iter_column_keys_from(&self, column: DBColumn, from: &[u8]) -> ColumnKeyIter; + + fn delete_batch(&self, column: DBColumn, ops: HashSet<&[u8]>) -> Result<(), Error>; + + fn delete_if( + &self, + column: DBColumn, + f: impl FnMut(&[u8]) -> Result, + ) -> Result<(), Error>; } pub trait Key: Sized + 'static { @@ -138,7 +142,7 @@ impl Key for Vec { } } -pub fn get_key_for_col(column: &str, key: &[u8]) -> Vec { +pub fn get_key_for_col(column: DBColumn, key: &[u8]) -> Vec { let mut result = column.as_bytes().to_vec(); result.extend_from_slice(key); result @@ -176,14 +180,18 @@ pub fn parse_data_column_key(data: Vec) -> Result<(Hash256, ColumnIndex), Er #[must_use] #[derive(Clone)] pub enum KeyValueStoreOp { - PutKeyValue(Vec, Vec), - DeleteKey(Vec), + // Indicate that a PUT operation should be made + // to the db store for a (Column, Key, Value) + PutKeyValue(DBColumn, Vec, Vec), + // Indicate that a DELETE operation should be made + // to the db store for a (Column, Key) + DeleteKey(DBColumn, Vec), } pub trait ItemStore: KeyValueStore + Sync + Send + Sized + 'static { /// Store an item in `Self`. fn put(&self, key: &Hash256, item: &I) -> Result<(), Error> { - let column = I::db_column().into(); + let column = I::db_column(); let key = key.as_slice(); self.put_bytes(column, key, &item.as_store_bytes()) @@ -191,7 +199,7 @@ pub trait ItemStore: KeyValueStore + Sync + Send + Sized + 'stati } fn put_sync(&self, key: &Hash256, item: &I) -> Result<(), Error> { - let column = I::db_column().into(); + let column = I::db_column(); let key = key.as_slice(); self.put_bytes_sync(column, key, &item.as_store_bytes()) @@ -200,7 +208,7 @@ pub trait ItemStore: KeyValueStore + Sync + Send + Sized + 'stati /// Retrieve an item from `Self`. fn get(&self, key: &Hash256) -> Result, Error> { - let column = I::db_column().into(); + let column = I::db_column(); let key = key.as_slice(); match self.get_bytes(column, key)? { @@ -211,7 +219,7 @@ pub trait ItemStore: KeyValueStore + Sync + Send + Sized + 'stati /// Returns `true` if the given key represents an item in `Self`. fn exists(&self, key: &Hash256) -> Result { - let column = I::db_column().into(); + let column = I::db_column(); let key = key.as_slice(); self.key_exists(column, key) @@ -219,7 +227,7 @@ pub trait ItemStore: KeyValueStore + Sync + Send + Sized + 'stati /// Remove an item from `Self`. fn delete(&self, key: &Hash256) -> Result<(), Error> { - let column = I::db_column().into(); + let column = I::db_column(); let key = key.as_slice(); self.key_delete(column, key) @@ -247,7 +255,7 @@ pub enum StoreOp<'a, E: EthSpec> { } /// A unique column identifier. -#[derive(Debug, Clone, Copy, PartialEq, IntoStaticStr, EnumString)] +#[derive(Debug, Clone, Copy, PartialEq, IntoStaticStr, EnumString, EnumIter)] pub enum DBColumn { /// For data related to the database itself. #[strum(serialize = "bma")] @@ -351,6 +359,9 @@ pub enum DBColumn { /// For helping persist eagerly computed light client bootstrap data #[strum(serialize = "scm")] SyncCommittee, + /// The dummy table is used to force the db to sync + #[strum(serialize = "dmy")] + Dummy, } /// A block from the database, which might have an execution payload or not. @@ -401,7 +412,8 @@ impl DBColumn { | Self::BeaconStateDiff | Self::SyncCommittee | Self::SyncCommitteeBranch - | Self::LightClientUpdate => 8, + | Self::LightClientUpdate + | Self::Dummy => 8, Self::BeaconDataColumn => DATA_COLUMN_DB_KEY_SIZE, } } @@ -421,13 +433,18 @@ pub trait StoreItem: Sized { fn from_store_bytes(bytes: &[u8]) -> Result; fn as_kv_store_op(&self, key: Hash256) -> KeyValueStoreOp { - let db_key = get_key_for_col(Self::db_column().into(), key.as_slice()); - KeyValueStoreOp::PutKeyValue(db_key, self.as_store_bytes()) + KeyValueStoreOp::PutKeyValue( + Self::db_column(), + key.as_slice().to_vec(), + self.as_store_bytes(), + ) } } #[cfg(test)] mod tests { + use crate::database::interface::BeaconNodeBackend; + use super::*; use ssz::{Decode, Encode}; use ssz_derive::{Decode, Encode}; @@ -477,7 +494,7 @@ mod tests { fn simplediskdb() { let dir = tempdir().unwrap(); let path = dir.path(); - let store = LevelDB::open(path).unwrap(); + let store = BeaconNodeBackend::open(&StoreConfig::default(), path).unwrap(); test_impl(store); } @@ -508,7 +525,7 @@ mod tests { #[test] fn test_get_col_from_key() { - let key = get_key_for_col(DBColumn::BeaconBlock.into(), &[1u8; 32]); + let key = get_key_for_col(DBColumn::BeaconBlock, &[1u8; 32]); let col = get_col_from_key(&key).unwrap(); assert_eq!(col, "blk"); } diff --git a/beacon_node/store/src/memory_store.rs b/beacon_node/store/src/memory_store.rs index 4c7bfdf10f..6070a2d3f0 100644 --- a/beacon_node/store/src/memory_store.rs +++ b/beacon_node/store/src/memory_store.rs @@ -1,9 +1,9 @@ use crate::{ - get_key_for_col, leveldb_store::BytesKey, ColumnIter, ColumnKeyIter, DBColumn, Error, - ItemStore, Key, KeyValueStore, KeyValueStoreOp, RawKeyIter, + errors::Error as DBError, get_key_for_col, hot_cold_store::BytesKey, ColumnIter, ColumnKeyIter, + DBColumn, Error, ItemStore, Key, KeyValueStore, KeyValueStoreOp, }; use parking_lot::{Mutex, MutexGuard, RwLock}; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, HashSet}; use std::marker::PhantomData; use types::*; @@ -29,19 +29,19 @@ impl MemoryStore { impl KeyValueStore for MemoryStore { /// Get the value of some key from the database. Returns `None` if the key does not exist. - fn get_bytes(&self, col: &str, key: &[u8]) -> Result>, Error> { + fn get_bytes(&self, col: DBColumn, key: &[u8]) -> Result>, Error> { let column_key = BytesKey::from_vec(get_key_for_col(col, key)); Ok(self.db.read().get(&column_key).cloned()) } /// Puts a key in the database. - fn put_bytes(&self, col: &str, key: &[u8], val: &[u8]) -> Result<(), Error> { + fn put_bytes(&self, col: DBColumn, key: &[u8], val: &[u8]) -> Result<(), Error> { let column_key = BytesKey::from_vec(get_key_for_col(col, key)); self.db.write().insert(column_key, val.to_vec()); Ok(()) } - fn put_bytes_sync(&self, col: &str, key: &[u8], val: &[u8]) -> Result<(), Error> { + fn put_bytes_sync(&self, col: DBColumn, key: &[u8], val: &[u8]) -> Result<(), Error> { self.put_bytes(col, key, val) } @@ -51,13 +51,13 @@ impl KeyValueStore for MemoryStore { } /// Return true if some key exists in some column. - fn key_exists(&self, col: &str, key: &[u8]) -> Result { + fn key_exists(&self, col: DBColumn, key: &[u8]) -> Result { let column_key = BytesKey::from_vec(get_key_for_col(col, key)); Ok(self.db.read().contains_key(&column_key)) } /// Delete some key from the database. - fn key_delete(&self, col: &str, key: &[u8]) -> Result<(), Error> { + fn key_delete(&self, col: DBColumn, key: &[u8]) -> Result<(), Error> { let column_key = BytesKey::from_vec(get_key_for_col(col, key)); self.db.write().remove(&column_key); Ok(()) @@ -66,12 +66,16 @@ impl KeyValueStore for MemoryStore { fn do_atomically(&self, batch: Vec) -> Result<(), Error> { for op in batch { match op { - KeyValueStoreOp::PutKeyValue(key, value) => { - self.db.write().insert(BytesKey::from_vec(key), value); + KeyValueStoreOp::PutKeyValue(col, key, value) => { + let column_key = get_key_for_col(col, &key); + self.db + .write() + .insert(BytesKey::from_vec(column_key), value); } - KeyValueStoreOp::DeleteKey(key) => { - self.db.write().remove(&BytesKey::from_vec(key)); + KeyValueStoreOp::DeleteKey(col, key) => { + let column_key = get_key_for_col(col, &key); + self.db.write().remove(&BytesKey::from_vec(column_key)); } } } @@ -82,8 +86,7 @@ impl KeyValueStore for MemoryStore { // We use this awkward pattern because we can't lock the `self.db` field *and* maintain a // reference to the lock guard across calls to `.next()`. This would be require a // struct with a field (the iterator) which references another field (the lock guard). - let start_key = BytesKey::from_vec(get_key_for_col(column.as_str(), from)); - let col = column.as_str(); + let start_key = BytesKey::from_vec(get_key_for_col(column, from)); let keys = self .db .read() @@ -92,7 +95,7 @@ impl KeyValueStore for MemoryStore { .filter_map(|(k, _)| k.remove_column_variable(column).map(|k| k.to_vec())) .collect::>(); Box::new(keys.into_iter().filter_map(move |key| { - self.get_bytes(col, &key).transpose().map(|res| { + self.get_bytes(column, &key).transpose().map(|res| { let k = K::from_bytes(&key)?; let v = res?; Ok((k, v)) @@ -100,18 +103,6 @@ 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))) } @@ -123,6 +114,44 @@ impl KeyValueStore for MemoryStore { fn compact_column(&self, _column: DBColumn) -> Result<(), Error> { Ok(()) } + + fn iter_column_keys_from(&self, column: DBColumn, from: &[u8]) -> ColumnKeyIter { + // We use this awkward pattern because we can't lock the `self.db` field *and* maintain a + // reference to the lock guard across calls to `.next()`. This would be require a + // struct with a field (the iterator) which references another field (the lock guard). + let start_key = BytesKey::from_vec(get_key_for_col(column, from)); + let keys = self + .db + .read() + .range(start_key..) + .take_while(|(k, _)| k.remove_column_variable(column).is_some()) + .filter_map(|(k, _)| k.remove_column_variable(column).map(|k| k.to_vec())) + .collect::>(); + Box::new(keys.into_iter().map(move |key| K::from_bytes(&key))) + } + + fn delete_batch(&self, col: DBColumn, ops: HashSet<&[u8]>) -> Result<(), DBError> { + for op in ops { + let column_key = get_key_for_col(col, op); + self.db.write().remove(&BytesKey::from_vec(column_key)); + } + Ok(()) + } + + fn delete_if( + &self, + column: DBColumn, + mut f: impl FnMut(&[u8]) -> Result, + ) -> Result<(), Error> { + self.db.write().retain(|key, value| { + if key.remove_column_variable(column).is_some() { + !f(value).unwrap_or(false) + } else { + true + } + }); + Ok(()) + } } impl ItemStore for MemoryStore {} diff --git a/beacon_node/store/src/metrics.rs b/beacon_node/store/src/metrics.rs index f0dd061790..6f9f667917 100644 --- a/beacon_node/store/src/metrics.rs +++ b/beacon_node/store/src/metrics.rs @@ -33,6 +33,13 @@ pub static DISK_DB_READ_BYTES: LazyLock> = LazyLock::new(| &["col"], ) }); +pub static DISK_DB_KEY_READ_BYTES: LazyLock> = LazyLock::new(|| { + try_create_int_counter_vec( + "store_disk_db_key_read_bytes_total", + "Number of key bytes read from the hot on-disk DB", + &["col"], + ) +}); pub static DISK_DB_READ_COUNT: LazyLock> = LazyLock::new(|| { try_create_int_counter_vec( "store_disk_db_read_count_total", @@ -40,6 +47,13 @@ pub static DISK_DB_READ_COUNT: LazyLock> = LazyLock::new(| &["col"], ) }); +pub static DISK_DB_KEY_READ_COUNT: LazyLock> = LazyLock::new(|| { + try_create_int_counter_vec( + "store_disk_db_read_count_total", + "Total number of key reads to the hot on-disk DB", + &["col"], + ) +}); pub static DISK_DB_WRITE_COUNT: LazyLock> = LazyLock::new(|| { try_create_int_counter_vec( "store_disk_db_write_count_total", @@ -66,6 +80,12 @@ pub static DISK_DB_EXISTS_COUNT: LazyLock> = LazyLock::new &["col"], ) }); +pub static DISK_DB_DELETE_TIMES: LazyLock> = LazyLock::new(|| { + try_create_histogram( + "store_disk_db_delete_seconds", + "Time taken to delete bytes from the store.", + ) +}); pub static DISK_DB_DELETE_COUNT: LazyLock> = LazyLock::new(|| { try_create_int_counter_vec( "store_disk_db_delete_count_total", @@ -73,6 +93,19 @@ pub static DISK_DB_DELETE_COUNT: LazyLock> = LazyLock::new &["col"], ) }); +pub static DISK_DB_COMPACT_TIMES: LazyLock> = LazyLock::new(|| { + try_create_histogram( + "store_disk_db_compact_seconds", + "Time taken to run compaction on the DB.", + ) +}); +pub static DISK_DB_TYPE: LazyLock> = LazyLock::new(|| { + try_create_int_counter_vec( + "store_disk_db_type", + "The on-disk database type being used", + &["db_type"], + ) +}); /* * Anchor Info */ diff --git a/beacon_node/store/src/partial_beacon_state.rs b/beacon_node/store/src/partial_beacon_state.rs index 0b8bc2e0d4..d209512159 100644 --- a/beacon_node/store/src/partial_beacon_state.rs +++ b/beacon_node/store/src/partial_beacon_state.rs @@ -2,8 +2,8 @@ use crate::chunked_vector::{ load_variable_list_from_db, load_vector_from_db, BlockRootsChunked, HistoricalRoots, HistoricalSummaries, RandaoMixes, StateRootsChunked, }; -use crate::{Error, KeyValueStore}; -use ssz::{Decode, DecodeError}; +use crate::{DBColumn, Error, KeyValueStore, KeyValueStoreOp}; +use ssz::{Decode, DecodeError, Encode}; use ssz_derive::{Decode, Encode}; use std::sync::Arc; use types::historical_summary::HistoricalSummary; @@ -172,6 +172,15 @@ impl PartialBeaconState { )) } + /// Prepare the partial state for storage in the KV database. + pub fn as_kv_store_op(&self, state_root: Hash256) -> KeyValueStoreOp { + KeyValueStoreOp::PutKeyValue( + DBColumn::BeaconState, + state_root.as_slice().to_vec(), + self.as_ssz_bytes(), + ) + } + pub fn load_block_roots>( &mut self, store: &S, diff --git a/book/src/help_bn.md b/book/src/help_bn.md index a4ab44748c..2d12010094 100644 --- a/book/src/help_bn.md +++ b/book/src/help_bn.md @@ -11,6 +11,9 @@ Options: --auto-compact-db Enable or disable automatic compaction of the database on finalization. [default: true] + --beacon-node-backend + Set the database backend to be used by the beacon node. [possible + values: leveldb] --blob-prune-margin-epochs The margin for blob pruning in epochs. The oldest blobs are pruned up until data_availability_boundary - blob_prune_margin_epochs. [default: diff --git a/book/src/installation-source.md b/book/src/installation-source.md index 3c9f27d236..19098a5bc8 100644 --- a/book/src/installation-source.md +++ b/book/src/installation-source.md @@ -154,7 +154,7 @@ You can customise the features that Lighthouse is built with using the `FEATURES variable. E.g. ``` -FEATURES=gnosis,slasher-lmdb make +FEATURES=gnosis,slasher-lmdb,beacon-node-leveldb make ``` Commonly used features include: @@ -163,11 +163,12 @@ Commonly used features include: - `portable`: the default feature as Lighthouse now uses runtime detection of hardware CPU features. - `slasher-lmdb`: support for the LMDB slasher backend. Enabled by default. - `slasher-mdbx`: support for the MDBX slasher backend. +- `beacon-node-leveldb`: support for the leveldb backend. Enabled by default. - `jemalloc`: use [`jemalloc`][jemalloc] to allocate memory. Enabled by default on Linux and macOS. Not supported on Windows. - `spec-minimal`: support for the minimal preset (useful for testing). -Default features (e.g. `slasher-lmdb`) may be opted out of using the `--no-default-features` +Default features (e.g. `slasher-lmdb`, `beacon-node-leveldb`) may be opted out of using the `--no-default-features` argument for `cargo`, which can be plumbed in via the `CARGO_INSTALL_EXTRA_FLAGS` environment variable. E.g. diff --git a/database_manager/src/cli.rs b/database_manager/src/cli.rs index 4246a51f89..9db807df2c 100644 --- a/database_manager/src/cli.rs +++ b/database_manager/src/cli.rs @@ -57,6 +57,15 @@ pub struct DatabaseManager { )] pub blobs_dir: Option, + #[clap( + long, + value_name = "DATABASE", + help = "Set the database backend to be used by the beacon node.", + display_order = 0, + default_value_t = store::config::DatabaseBackend::LevelDb + )] + pub backend: store::config::DatabaseBackend, + #[clap( long, global = true, diff --git a/database_manager/src/lib.rs b/database_manager/src/lib.rs index fc15e98616..bed90df9df 100644 --- a/database_manager/src/lib.rs +++ b/database_manager/src/lib.rs @@ -16,10 +16,12 @@ use slog::{info, warn, Logger}; use std::fs; use std::io::Write; use std::path::PathBuf; +use store::KeyValueStore; use store::{ + database::interface::BeaconNodeBackend, errors::Error, metadata::{SchemaVersion, CURRENT_SCHEMA_VERSION}, - DBColumn, HotColdDB, KeyValueStore, LevelDB, + DBColumn, HotColdDB, }; use strum::{EnumString, EnumVariantNames}; use types::{BeaconState, EthSpec, Slot}; @@ -40,7 +42,7 @@ fn parse_client_config( .clone_from(&database_manager_config.blobs_dir); client_config.store.blob_prune_margin_epochs = database_manager_config.blob_prune_margin_epochs; client_config.store.hierarchy_config = database_manager_config.hierarchy_exponents.clone(); - + client_config.store.backend = database_manager_config.backend; Ok(client_config) } @@ -55,7 +57,7 @@ pub fn display_db_version( let blobs_path = client_config.get_blobs_db_path(); let mut version = CURRENT_SCHEMA_VERSION; - HotColdDB::, LevelDB>::open( + HotColdDB::, BeaconNodeBackend>::open( &hot_path, &cold_path, &blobs_path, @@ -145,11 +147,14 @@ pub fn inspect_db( let mut num_keys = 0; let sub_db = if inspect_config.freezer { - LevelDB::::open(&cold_path).map_err(|e| format!("Unable to open freezer DB: {e:?}"))? + BeaconNodeBackend::::open(&client_config.store, &cold_path) + .map_err(|e| format!("Unable to open freezer DB: {e:?}"))? } else if inspect_config.blobs_db { - LevelDB::::open(&blobs_path).map_err(|e| format!("Unable to open blobs DB: {e:?}"))? + BeaconNodeBackend::::open(&client_config.store, &blobs_path) + .map_err(|e| format!("Unable to open blobs DB: {e:?}"))? } else { - LevelDB::::open(&hot_path).map_err(|e| format!("Unable to open hot DB: {e:?}"))? + BeaconNodeBackend::::open(&client_config.store, &hot_path) + .map_err(|e| format!("Unable to open hot DB: {e:?}"))? }; let skip = inspect_config.skip.unwrap_or(0); @@ -263,11 +268,20 @@ pub fn compact_db( let column = compact_config.column; let (sub_db, db_name) = if compact_config.freezer { - (LevelDB::::open(&cold_path)?, "freezer_db") + ( + BeaconNodeBackend::::open(&client_config.store, &cold_path)?, + "freezer_db", + ) } else if compact_config.blobs_db { - (LevelDB::::open(&blobs_path)?, "blobs_db") + ( + BeaconNodeBackend::::open(&client_config.store, &blobs_path)?, + "blobs_db", + ) } else { - (LevelDB::::open(&hot_path)?, "hot_db") + ( + BeaconNodeBackend::::open(&client_config.store, &hot_path)?, + "hot_db", + ) }; info!( log, @@ -303,7 +317,7 @@ pub fn migrate_db( let mut from = CURRENT_SCHEMA_VERSION; let to = migrate_config.to; - let db = HotColdDB::, LevelDB>::open( + let db = HotColdDB::, BeaconNodeBackend>::open( &hot_path, &cold_path, &blobs_path, @@ -343,7 +357,7 @@ pub fn prune_payloads( let cold_path = client_config.get_freezer_db_path(); let blobs_path = client_config.get_blobs_db_path(); - let db = HotColdDB::, LevelDB>::open( + let db = HotColdDB::, BeaconNodeBackend>::open( &hot_path, &cold_path, &blobs_path, @@ -369,7 +383,7 @@ pub fn prune_blobs( let cold_path = client_config.get_freezer_db_path(); let blobs_path = client_config.get_blobs_db_path(); - let db = HotColdDB::, LevelDB>::open( + let db = HotColdDB::, BeaconNodeBackend>::open( &hot_path, &cold_path, &blobs_path, @@ -406,7 +420,7 @@ pub fn prune_states( let cold_path = client_config.get_freezer_db_path(); let blobs_path = client_config.get_blobs_db_path(); - let db = HotColdDB::, LevelDB>::open( + let db = HotColdDB::, BeaconNodeBackend>::open( &hot_path, &cold_path, &blobs_path, diff --git a/lighthouse/Cargo.toml b/lighthouse/Cargo.toml index eda9a2ebf2..c303511338 100644 --- a/lighthouse/Cargo.toml +++ b/lighthouse/Cargo.toml @@ -7,7 +7,7 @@ autotests = false rust-version = "1.80.0" [features] -default = ["slasher-lmdb"] +default = ["slasher-lmdb", "beacon-node-leveldb"] # Writes debugging .ssz files to /tmp during block processing. write_ssz_files = ["beacon_node/write_ssz_files"] # Compiles the BLS crypto code so that the binary is portable across machines. @@ -24,6 +24,11 @@ slasher-mdbx = ["slasher/mdbx"] slasher-lmdb = ["slasher/lmdb"] # Support slasher redb backend. slasher-redb = ["slasher/redb"] +# Supports beacon node leveldb backend. +beacon-node-leveldb = ["store/leveldb"] +# Supports beacon node redb backend. +beacon-node-redb = ["store/redb"] + # Deprecated. This is now enabled by default on non windows targets. jemalloc = [] @@ -56,6 +61,7 @@ serde_json = { workspace = true } serde_yaml = { workspace = true } slasher = { workspace = true } slog = { workspace = true } +store = { workspace = true } task_executor = { workspace = true } types = { workspace = true } unused_port = { workspace = true } diff --git a/lighthouse/tests/beacon_node.rs b/lighthouse/tests/beacon_node.rs index 88e05dfa12..1063a80ff4 100644 --- a/lighthouse/tests/beacon_node.rs +++ b/lighthouse/tests/beacon_node.rs @@ -1,11 +1,12 @@ -use beacon_node::ClientConfig as Config; - use crate::exec::{CommandLineTestExec, CompletedTest}; use beacon_node::beacon_chain::chain_config::{ DisallowedReOrgOffsets, DEFAULT_RE_ORG_CUTOFF_DENOMINATOR, DEFAULT_RE_ORG_HEAD_THRESHOLD, DEFAULT_RE_ORG_MAX_EPOCHS_SINCE_FINALIZATION, }; -use beacon_node::beacon_chain::graffiti_calculator::GraffitiOrigin; +use beacon_node::{ + beacon_chain::graffiti_calculator::GraffitiOrigin, + beacon_chain::store::config::DatabaseBackend as BeaconNodeBackend, ClientConfig as Config, +}; use beacon_processor::BeaconProcessorConfig; use eth1::Eth1Endpoint; use lighthouse_network::PeerId; @@ -2691,3 +2692,13 @@ fn genesis_state_url_value() { assert_eq!(config.genesis_state_url_timeout, Duration::from_secs(42)); }); } + +#[test] +fn beacon_node_backend_override() { + CommandLineTest::new() + .flag("beacon-node-backend", Some("leveldb")) + .run_with_zero_port() + .with_config(|config| { + assert_eq!(config.store.backend, BeaconNodeBackend::LevelDb); + }); +} diff --git a/wordlist.txt b/wordlist.txt index 6287366cbc..bb8b46b525 100644 --- a/wordlist.txt +++ b/wordlist.txt @@ -162,6 +162,7 @@ keypair keypairs keystore keystores +leveldb linter linux localhost @@ -191,6 +192,7 @@ pre pubkey pubkeys rc +redb reimport resync roadmap From 029b4f21047c37d1ffde51c554737b1aa0880f97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Oliveira?= Date: Fri, 24 Jan 2025 00:43:51 +0000 Subject: [PATCH 092/254] Improve mergify config (#6852) * improve mergify config * negate conflict --- .github/mergify.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/mergify.yml b/.github/mergify.yml index 9a74414e72..1aa24f8302 100644 --- a/.github/mergify.yml +++ b/.github/mergify.yml @@ -18,6 +18,7 @@ pull_request_rules: - base=unstable - label=trivial - author=@sigp/lighthouse + - -conflict actions: review: type: APPROVE @@ -26,11 +27,11 @@ pull_request_rules: conditions: # All branch protection rules are implicit: https://docs.mergify.com/conditions/#about-branch-protection - base=unstable - - label=ready-to-merge + - label=ready-for-merge + - label!=do-not-merge actions: queue: - queue_rules: - name: default batch_size: 8 @@ -48,6 +49,7 @@ queue_rules: - "#approved-reviews-by >= 1" - "check-success=license/cla" - "check-success=target-branch-check" + - "label!=do-not-merge" merge_conditions: - "check-success=test-suite-success" - "check-success=local-testnet-success" From 1781c5a75539e499dc5288246b22d06853f6b54f Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Tue, 28 Jan 2025 12:30:53 +1100 Subject: [PATCH 093/254] Update to EF tests v1.5.0-beta.1 (#6871) No substantial changes in v1.5.0-beta.1, this PR just updates the tests. The optimisation described in this PR is already implemented in our single-pass epoch processing: - https://github.com/ethereum/consensus-specs/pull/4081 --- testing/ef_tests/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/ef_tests/Makefile b/testing/ef_tests/Makefile index 7108e3e8f6..7b507f8c50 100644 --- a/testing/ef_tests/Makefile +++ b/testing/ef_tests/Makefile @@ -1,4 +1,4 @@ -TESTS_TAG := v1.5.0-beta.0 +TESTS_TAG := v1.5.0-beta.1 TESTS = general minimal mainnet TARBALLS = $(patsubst %,%-$(TESTS_TAG).tar.gz,$(TESTS)) From 33b8555d2c3ae00c48ab845b678384d643c9fbaa Mon Sep 17 00:00:00 2001 From: Pawan Dhananjay Date: Tue, 28 Jan 2025 00:48:36 -0800 Subject: [PATCH 094/254] Add tests for ExecutionRequests decoding errors (#6832) N/A Cover all error cases for decoding JsonExecutionRequests --- .../src/engine_api/json_structures.rs | 151 ++++++++++++++++++ 1 file changed, 151 insertions(+) diff --git a/beacon_node/execution_layer/src/engine_api/json_structures.rs b/beacon_node/execution_layer/src/engine_api/json_structures.rs index 95b4b50925..96615297d8 100644 --- a/beacon_node/execution_layer/src/engine_api/json_structures.rs +++ b/beacon_node/execution_layer/src/engine_api/json_structures.rs @@ -991,3 +991,154 @@ impl TryFrom for ClientVersionV1 { }) } } + +#[cfg(test)] +mod tests { + use ssz::Encode; + use types::{ + ConsolidationRequest, DepositRequest, MainnetEthSpec, PublicKeyBytes, RequestType, + SignatureBytes, WithdrawalRequest, + }; + + use super::*; + + fn create_request_string(prefix: u8, request_bytes: &T) -> String { + format!( + "0x{:02x}{}", + prefix, + hex::encode(request_bytes.as_ssz_bytes()) + ) + } + + /// Tests all error conditions except ssz decoding errors + /// + /// *** + /// Elements of the list MUST be ordered by request_type in ascending order. + /// Elements with empty request_data MUST be excluded from the list. + /// If any element is out of order, has a length of 1-byte or shorter, + /// or more than one element has the same type byte, client software MUST return -32602: Invalid params error. + /// *** + #[test] + fn test_invalid_execution_requests() { + let deposit_request = DepositRequest { + pubkey: PublicKeyBytes::empty(), + withdrawal_credentials: Hash256::random(), + amount: 32, + signature: SignatureBytes::empty(), + index: 0, + }; + + let consolidation_request = ConsolidationRequest { + source_address: Address::random(), + source_pubkey: PublicKeyBytes::empty(), + target_pubkey: PublicKeyBytes::empty(), + }; + + let withdrawal_request = WithdrawalRequest { + amount: 32, + source_address: Address::random(), + validator_pubkey: PublicKeyBytes::empty(), + }; + + // First check a valid request with all requests + assert!( + ExecutionRequests::::try_from(JsonExecutionRequests(vec![ + create_request_string(RequestType::Deposit.to_u8(), &deposit_request), + create_request_string(RequestType::Withdrawal.to_u8(), &withdrawal_request), + create_request_string(RequestType::Consolidation.to_u8(), &consolidation_request), + ])) + .is_ok() + ); + + // Single requests + assert!( + ExecutionRequests::::try_from(JsonExecutionRequests(vec![ + create_request_string(RequestType::Deposit.to_u8(), &deposit_request), + ])) + .is_ok() + ); + + assert!( + ExecutionRequests::::try_from(JsonExecutionRequests(vec![ + create_request_string(RequestType::Withdrawal.to_u8(), &withdrawal_request), + ])) + .is_ok() + ); + + assert!( + ExecutionRequests::::try_from(JsonExecutionRequests(vec![ + create_request_string(RequestType::Consolidation.to_u8(), &consolidation_request), + ])) + .is_ok() + ); + + // Out of order + assert!(matches!( + ExecutionRequests::::try_from(JsonExecutionRequests(vec![ + create_request_string(RequestType::Withdrawal.to_u8(), &withdrawal_request), + create_request_string(RequestType::Deposit.to_u8(), &deposit_request), + ])) + .unwrap_err(), + RequestsError::InvalidOrdering + )); + + assert!(matches!( + ExecutionRequests::::try_from(JsonExecutionRequests(vec![ + create_request_string(RequestType::Consolidation.to_u8(), &consolidation_request), + create_request_string(RequestType::Withdrawal.to_u8(), &withdrawal_request), + ])) + .unwrap_err(), + RequestsError::InvalidOrdering + )); + + assert!(matches!( + ExecutionRequests::::try_from(JsonExecutionRequests(vec![ + create_request_string(RequestType::Consolidation.to_u8(), &consolidation_request), + create_request_string(RequestType::Deposit.to_u8(), &deposit_request), + ])) + .unwrap_err(), + RequestsError::InvalidOrdering + )); + + // Multiple requests of same type + assert!(matches!( + ExecutionRequests::::try_from(JsonExecutionRequests(vec![ + create_request_string(RequestType::Deposit.to_u8(), &deposit_request), + create_request_string(RequestType::Deposit.to_u8(), &deposit_request), + ])) + .unwrap_err(), + RequestsError::InvalidOrdering + )); + + // Invalid prefix + assert!(matches!( + ExecutionRequests::::try_from(JsonExecutionRequests(vec![ + create_request_string(42, &deposit_request), + ])) + .unwrap_err(), + RequestsError::InvalidPrefix(42) + )); + + // Prefix followed by no data + assert!(matches!( + ExecutionRequests::::try_from(JsonExecutionRequests(vec![ + create_request_string(RequestType::Deposit.to_u8(), &deposit_request), + create_request_string( + RequestType::Consolidation.to_u8(), + &Vec::::new() + ), + ])) + .unwrap_err(), + RequestsError::EmptyRequest(1) + )); + // Empty request + assert!(matches!( + ExecutionRequests::::try_from(JsonExecutionRequests(vec![ + create_request_string(RequestType::Deposit.to_u8(), &deposit_request), + "0x".to_string() + ])) + .unwrap_err(), + RequestsError::EmptyRequest(1) + )); + } +} From c6ebaba8927086c0199b3b536f08c9146efb2606 Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Tue, 28 Jan 2025 16:01:26 -0300 Subject: [PATCH 095/254] Detect invalid proposer signature on RPC block processing (#6519) Complements - https://github.com/sigp/lighthouse/pull/6321 by detecting if the proposer signature is valid or not during RPC block processing. In lookup sync, if the invalid signature signature is the proposer signature, it's not deterministic on the block root. So we should only penalize the sending peer and retry. Otherwise, if it's on the body we should drop the lookup and penalize all peers that claim to have imported the block --- .../beacon_chain/src/block_verification.rs | 58 +++++++++++++----- beacon_node/beacon_chain/src/lib.rs | 2 +- .../beacon_chain/tests/block_verification.rs | 59 +++++++++++-------- .../gossip_methods.rs | 3 +- beacon_node/network/src/sync/tests/lookups.rs | 6 +- 5 files changed, 84 insertions(+), 44 deletions(-) diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 315105ac2b..1265276376 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -208,24 +208,18 @@ pub enum BlockError { /// /// The block is invalid and the peer is faulty. IncorrectBlockProposer { block: u64, local_shuffling: u64 }, - /// The proposal signature in invalid. - /// - /// ## Peer scoring - /// - /// The block is invalid and the peer is faulty. - ProposalSignatureInvalid, /// The `block.proposal_index` is not known. /// /// ## Peer scoring /// /// The block is invalid and the peer is faulty. UnknownValidator(u64), - /// A signature in the block is invalid (exactly which is unknown). + /// A signature in the block is invalid /// /// ## Peer scoring /// /// The block is invalid and the peer is faulty. - InvalidSignature, + InvalidSignature(InvalidSignature), /// The provided block is not from a later slot than its parent. /// /// ## Peer scoring @@ -329,6 +323,17 @@ pub enum BlockError { InternalError(String), } +/// Which specific signature(s) are invalid in a SignedBeaconBlock +#[derive(Debug)] +pub enum InvalidSignature { + // The outer signature in a SignedBeaconBlock + ProposerSignature, + // One or more signatures in BeaconBlockBody + BlockBodySignatures, + // One or more signatures in SignedBeaconBlock + Unknown, +} + impl From for BlockError { fn from(e: AvailabilityCheckError) -> Self { Self::AvailabilityCheck(e) @@ -523,7 +528,9 @@ pub enum BlockSlashInfo { impl BlockSlashInfo { pub fn from_early_error_block(header: SignedBeaconBlockHeader, e: BlockError) -> Self { match e { - BlockError::ProposalSignatureInvalid => BlockSlashInfo::SignatureInvalid(e), + BlockError::InvalidSignature(InvalidSignature::ProposerSignature) => { + BlockSlashInfo::SignatureInvalid(e) + } // `InvalidSignature` could indicate any signature in the block, so we want // to recheck the proposer signature alone. _ => BlockSlashInfo::SignatureNotChecked(header, e), @@ -652,7 +659,7 @@ pub fn signature_verify_chain_segment( } if signature_verifier.verify().is_err() { - return Err(BlockError::InvalidSignature); + return Err(BlockError::InvalidSignature(InvalidSignature::Unknown)); } drop(pubkey_cache); @@ -964,7 +971,9 @@ impl GossipVerifiedBlock { }; if !signature_is_valid { - return Err(BlockError::ProposalSignatureInvalid); + return Err(BlockError::InvalidSignature( + InvalidSignature::ProposerSignature, + )); } chain @@ -1098,7 +1107,26 @@ impl SignatureVerifiedBlock { parent: Some(parent), }) } else { - Err(BlockError::InvalidSignature) + // Re-verify the proposer signature in isolation to attribute fault + let pubkey = pubkey_cache + .get(block.message().proposer_index() as usize) + .ok_or_else(|| BlockError::UnknownValidator(block.message().proposer_index()))?; + if block.as_block().verify_signature( + Some(block_root), + pubkey, + &state.fork(), + chain.genesis_validators_root, + &chain.spec, + ) { + // Proposer signature is valid, the invalid signature must be in the body + Err(BlockError::InvalidSignature( + InvalidSignature::BlockBodySignatures, + )) + } else { + Err(BlockError::InvalidSignature( + InvalidSignature::ProposerSignature, + )) + } } } @@ -1153,7 +1181,9 @@ impl SignatureVerifiedBlock { consensus_context, }) } else { - Err(BlockError::InvalidSignature) + Err(BlockError::InvalidSignature( + InvalidSignature::BlockBodySignatures, + )) } } @@ -1981,7 +2011,7 @@ impl BlockBlobError for BlockError { } fn proposer_signature_invalid() -> Self { - BlockError::ProposalSignatureInvalid + BlockError::InvalidSignature(InvalidSignature::ProposerSignature) } } diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index 4783945eb1..456b3c0dd8 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -78,7 +78,7 @@ pub use beacon_fork_choice_store::{BeaconForkChoiceStore, Error as ForkChoiceSto pub use block_verification::{ build_blob_data_column_sidecars, get_block_root, BlockError, ExecutionPayloadError, ExecutionPendingBlock, GossipVerifiedBlock, IntoExecutionPendingBlock, IntoGossipVerifiedBlock, - PayloadVerificationOutcome, PayloadVerificationStatus, + InvalidSignature, PayloadVerificationOutcome, PayloadVerificationStatus, }; pub use block_verification_types::AvailabilityPendingExecutedBlock; pub use block_verification_types::ExecutedBlock; diff --git a/beacon_node/beacon_chain/tests/block_verification.rs b/beacon_node/beacon_chain/tests/block_verification.rs index 1a651332ad..46f5befbba 100644 --- a/beacon_node/beacon_chain/tests/block_verification.rs +++ b/beacon_node/beacon_chain/tests/block_verification.rs @@ -9,7 +9,7 @@ use beacon_chain::{ }; use beacon_chain::{ BeaconSnapshot, BlockError, ChainConfig, ChainSegmentResult, IntoExecutionPendingBlock, - NotifyExecutionLayer, + InvalidSignature, NotifyExecutionLayer, }; use logging::test_logger; use slasher::{Config as SlasherConfig, Slasher}; @@ -438,7 +438,7 @@ async fn assert_invalid_signature( .process_chain_segment(blocks, NotifyExecutionLayer::Yes) .await .into_block_error(), - Err(BlockError::InvalidSignature) + Err(BlockError::InvalidSignature(InvalidSignature::Unknown)) ), "should not import chain segment with an invalid {} signature", item @@ -480,7 +480,12 @@ async fn assert_invalid_signature( ) .await; assert!( - matches!(process_res, Err(BlockError::InvalidSignature)), + matches!( + process_res, + Err(BlockError::InvalidSignature( + InvalidSignature::BlockBodySignatures + )) + ), "should not import individual block with an invalid {} signature, got: {:?}", item, process_res @@ -536,21 +541,25 @@ async fn invalid_signature_gossip_block() { .into_block_error() .expect("should import all blocks prior to the one being tested"); let signed_block = SignedBeaconBlock::from_block(block, junk_signature()); + let process_res = harness + .chain + .process_block( + signed_block.canonical_root(), + Arc::new(signed_block), + NotifyExecutionLayer::Yes, + BlockImportSource::Lookup, + || Ok(()), + ) + .await; assert!( matches!( - harness - .chain - .process_block( - signed_block.canonical_root(), - Arc::new(signed_block), - NotifyExecutionLayer::Yes, - BlockImportSource::Lookup, - || Ok(()), - ) - .await, - Err(BlockError::InvalidSignature) + process_res, + Err(BlockError::InvalidSignature( + InvalidSignature::ProposerSignature + )) ), - "should not import individual block with an invalid gossip signature", + "should not import individual block with an invalid gossip signature, got: {:?}", + process_res ); } } @@ -578,16 +587,18 @@ async fn invalid_signature_block_proposal() { }) .collect::>(); // Ensure the block will be rejected if imported in a chain segment. + let process_res = harness + .chain + .process_chain_segment(blocks, NotifyExecutionLayer::Yes) + .await + .into_block_error(); assert!( matches!( - harness - .chain - .process_chain_segment(blocks, NotifyExecutionLayer::Yes) - .await - .into_block_error(), - Err(BlockError::InvalidSignature) + process_res, + Err(BlockError::InvalidSignature(InvalidSignature::Unknown)) ), - "should not import chain segment with an invalid block signature", + "should not import chain segment with an invalid block signature, got: {:?}", + process_res ); } } @@ -890,7 +901,7 @@ async fn invalid_signature_deposit() { .process_chain_segment(blocks, NotifyExecutionLayer::Yes) .await .into_block_error(), - Err(BlockError::InvalidSignature) + Err(BlockError::InvalidSignature(InvalidSignature::Unknown)) ), "should not throw an invalid signature error for a bad deposit signature" ); @@ -1086,7 +1097,7 @@ async fn block_gossip_verification() { ))) .await ), - BlockError::ProposalSignatureInvalid + BlockError::InvalidSignature(InvalidSignature::ProposerSignature) ), "should not import a block with an invalid proposal signature" ); 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 6b5753e96a..dc8d32800e 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -1290,13 +1290,12 @@ impl NetworkBeaconProcessor { Err(e @ BlockError::StateRootMismatch { .. }) | Err(e @ BlockError::IncorrectBlockProposer { .. }) | Err(e @ BlockError::BlockSlotLimitReached) - | Err(e @ BlockError::ProposalSignatureInvalid) | Err(e @ BlockError::NonLinearSlots) | Err(e @ BlockError::UnknownValidator(_)) | Err(e @ BlockError::PerBlockProcessingError(_)) | Err(e @ BlockError::NonLinearParentRoots) | Err(e @ BlockError::BlockIsNotLaterThanParent { .. }) - | Err(e @ BlockError::InvalidSignature) + | Err(e @ BlockError::InvalidSignature(_)) | Err(e @ BlockError::WeakSubjectivityConflict) | Err(e @ BlockError::InconsistentFork(_)) | Err(e @ BlockError::ExecutionPayloadError(_)) diff --git a/beacon_node/network/src/sync/tests/lookups.rs b/beacon_node/network/src/sync/tests/lookups.rs index f623aa2c12..f772010500 100644 --- a/beacon_node/network/src/sync/tests/lookups.rs +++ b/beacon_node/network/src/sync/tests/lookups.rs @@ -1677,7 +1677,7 @@ fn test_parent_lookup_too_many_processing_attempts_must_blacklist() { rig.assert_not_failed_chain(block_root); // send the right parent but fail processing rig.parent_lookup_block_response(id, peer_id, Some(parent.clone().into())); - rig.parent_block_processed(block_root, BlockError::InvalidSignature.into()); + rig.parent_block_processed(block_root, BlockError::BlockSlotLimitReached.into()); rig.parent_lookup_block_response(id, peer_id, None); rig.expect_penalty(peer_id, "lookup_block_processing_failure"); } @@ -2575,7 +2575,7 @@ mod deneb_only { fn invalid_parent_processed(mut self) -> Self { self.rig.parent_block_processed( self.block_root, - BlockProcessingResult::Err(BlockError::ProposalSignatureInvalid), + BlockProcessingResult::Err(BlockError::BlockSlotLimitReached), ); assert_eq!(self.rig.active_parent_lookups_count(), 1); self @@ -2584,7 +2584,7 @@ mod deneb_only { fn invalid_block_processed(mut self) -> Self { self.rig.single_block_component_processed( self.block_req_id.expect("block request id").lookup_id, - BlockProcessingResult::Err(BlockError::ProposalSignatureInvalid), + BlockProcessingResult::Err(BlockError::BlockSlotLimitReached), ); self.rig.assert_single_lookups_count(1); self From 6973184b06017f19894ab0925898a205a2cfdace Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Wed, 29 Jan 2025 12:22:21 +0300 Subject: [PATCH 096/254] Fix Redb implementation and add CI checks (#6856) --- Makefile | 2 +- beacon_node/store/src/database/redb_impl.rs | 33 ++++++++++++--------- beacon_node/store/src/forwards_iter.rs | 1 + beacon_node/store/src/hot_cold_store.rs | 6 ++-- 4 files changed, 24 insertions(+), 18 deletions(-) diff --git a/Makefile b/Makefile index e8b44cb780..0f08afd168 100644 --- a/Makefile +++ b/Makefile @@ -222,7 +222,7 @@ lint-fix: # Also run the lints on the optimized-only tests lint-full: - RUSTFLAGS="-C debug-assertions=no $(RUSTFLAGS)" $(MAKE) lint + TEST_FEATURES="beacon-node-leveldb,beacon-node-redb,${TEST_FEATURES}" RUSTFLAGS="-C debug-assertions=no $(RUSTFLAGS)" $(MAKE) lint # Runs the makefile in the `ef_tests` repo. # diff --git a/beacon_node/store/src/database/redb_impl.rs b/beacon_node/store/src/database/redb_impl.rs index 6a776da7b1..cbe575d184 100644 --- a/beacon_node/store/src/database/redb_impl.rs +++ b/beacon_node/store/src/database/redb_impl.rs @@ -215,11 +215,12 @@ impl Redb { let table_definition: TableDefinition<'_, &[u8], &[u8]> = TableDefinition::new(column.into()); - let iter = { + let result = (|| { let open_db = self.db.read(); let read_txn = open_db.begin_read()?; let table = read_txn.open_table(table_definition)?; - table.range(from..)?.map(move |res| { + let range = table.range(from..)?; + Ok(range.map(move |res| { let (key, _) = res?; metrics::inc_counter_vec(&metrics::DISK_DB_KEY_READ_COUNT, &[column.into()]); metrics::inc_counter_vec_by( @@ -228,10 +229,13 @@ impl Redb { key.value().len() as u64, ); K::from_bytes(key.value()) - }) - }; + })) + })(); - Box::new(iter) + match result { + Ok(iter) => Box::new(iter), + Err(err) => Box::new(std::iter::once(Err(err))), + } } /// Iterate through all keys and values in a particular column. @@ -243,15 +247,13 @@ impl Redb { let table_definition: TableDefinition<'_, &[u8], &[u8]> = TableDefinition::new(column.into()); - let prefix = from.to_vec(); - - let iter = { + let result = (|| { let open_db = self.db.read(); let read_txn = open_db.begin_read()?; let table = read_txn.open_table(table_definition)?; + let range = table.range(from..)?; - table - .range(from..)? + Ok(range .take_while(move |res| match res.as_ref() { Ok((_, _)) => true, Err(_) => false, @@ -265,14 +267,17 @@ impl Redb { value.value().len() as u64, ); Ok((K::from_bytes(key.value())?, value.value().to_vec())) - }) - }; + })) + })(); - Ok(Box::new(iter)) + match result { + Ok(iter) => Box::new(iter), + Err(err) => Box::new(std::iter::once(Err(err))), + } } pub fn iter_column(&self, column: DBColumn) -> ColumnIter { - self.iter_column_from(column, &vec![0; column.key_size()], |_, _| true) + self.iter_column_from(column, &vec![0; column.key_size()]) } pub fn delete_batch(&self, col: DBColumn, ops: HashSet<&[u8]>) -> Result<(), Error> { diff --git a/beacon_node/store/src/forwards_iter.rs b/beacon_node/store/src/forwards_iter.rs index 5300a74c06..255b7d8eac 100644 --- a/beacon_node/store/src/forwards_iter.rs +++ b/beacon_node/store/src/forwards_iter.rs @@ -158,6 +158,7 @@ impl, Cold: ItemStore> Iterator return None; } self.inner + .as_mut() .next()? .and_then(|(slot_bytes, root_bytes)| { let slot = slot_bytes diff --git a/beacon_node/store/src/hot_cold_store.rs b/beacon_node/store/src/hot_cold_store.rs index 75251cb5fb..45b1983492 100644 --- a/beacon_node/store/src/hot_cold_store.rs +++ b/beacon_node/store/src/hot_cold_store.rs @@ -14,8 +14,8 @@ use crate::metadata::{ }; use crate::state_cache::{PutStateOutcome, StateCache}; use crate::{ - get_data_column_key, metrics, parse_data_column_key, BlobSidecarListFromRoot, DBColumn, - DatabaseBlock, Error, ItemStore, KeyValueStore, KeyValueStoreOp, StoreItem, StoreOp, + get_data_column_key, metrics, parse_data_column_key, BlobSidecarListFromRoot, ColumnKeyIter, + DBColumn, DatabaseBlock, Error, ItemStore, KeyValueStore, KeyValueStoreOp, StoreItem, StoreOp, }; use itertools::{process_results, Itertools}; use lru::LruCache; @@ -405,7 +405,7 @@ impl HotColdDB, BeaconNodeBackend> { } /// Return an iterator over the state roots of all temporary states. - pub fn iter_temporary_state_roots(&self) -> impl Iterator> + '_ { + pub fn iter_temporary_state_roots(&self) -> ColumnKeyIter { self.hot_db .iter_column_keys::(DBColumn::BeaconStateTemporary) } From e7ea69647a4cb686d0c7a80f24b3f34d329a7e01 Mon Sep 17 00:00:00 2001 From: Pawan Dhananjay Date: Wed, 29 Jan 2025 11:42:10 -0800 Subject: [PATCH 097/254] More gossipsub metrics (#6873) N/A Add metrics that tell us if a duplicate message that we received was from a mesh peer or from a non mesh peer that we requested with iwant message. --- .../gossipsub/src/behaviour.rs | 24 ++++++++++++++ .../gossipsub/src/gossip_promises.rs | 7 ++++ .../gossipsub/src/metrics.rs | 32 +++++++++++++++++++ 3 files changed, 63 insertions(+) diff --git a/beacon_node/lighthouse_network/gossipsub/src/behaviour.rs b/beacon_node/lighthouse_network/gossipsub/src/behaviour.rs index 6528e737a3..7eb35cc49b 100644 --- a/beacon_node/lighthouse_network/gossipsub/src/behaviour.rs +++ b/beacon_node/lighthouse_network/gossipsub/src/behaviour.rs @@ -1841,6 +1841,30 @@ where peer_score.duplicated_message(propagation_source, &msg_id, &message.topic); } self.mcache.observe_duplicate(&msg_id, propagation_source); + // track metrics for the source of the duplicates + if let Some(metrics) = self.metrics.as_mut() { + if self + .mesh + .get(&message.topic) + .is_some_and(|peers| peers.contains(propagation_source)) + { + // duplicate was received from a mesh peer + metrics.mesh_duplicates(&message.topic); + } else if self + .gossip_promises + .contains_peer(&msg_id, propagation_source) + { + // duplicate was received from an iwant request + metrics.iwant_duplicates(&message.topic); + } else { + tracing::warn!( + messsage=%msg_id, + peer=%propagation_source, + topic=%message.topic, + "Peer should not have sent message" + ); + } + } return; } diff --git a/beacon_node/lighthouse_network/gossipsub/src/gossip_promises.rs b/beacon_node/lighthouse_network/gossipsub/src/gossip_promises.rs index 3f72709245..ce1dee2a72 100644 --- a/beacon_node/lighthouse_network/gossipsub/src/gossip_promises.rs +++ b/beacon_node/lighthouse_network/gossipsub/src/gossip_promises.rs @@ -41,6 +41,13 @@ impl GossipPromises { self.promises.contains_key(message) } + /// Returns true if the message id exists in the promises and contains the given peer. + pub(crate) fn contains_peer(&self, message: &MessageId, peer: &PeerId) -> bool { + self.promises + .get(message) + .is_some_and(|peers| peers.contains_key(peer)) + } + ///Get the peers we sent IWANT the input message id. pub(crate) fn peers_for_message(&self, message_id: &MessageId) -> Vec { self.promises diff --git a/beacon_node/lighthouse_network/gossipsub/src/metrics.rs b/beacon_node/lighthouse_network/gossipsub/src/metrics.rs index d3ca6c299e..2989f95a26 100644 --- a/beacon_node/lighthouse_network/gossipsub/src/metrics.rs +++ b/beacon_node/lighthouse_network/gossipsub/src/metrics.rs @@ -194,6 +194,12 @@ pub(crate) struct Metrics { /// Number of full messages we received that we previously sent a IDONTWANT for. idontwant_messages_ignored_per_topic: Family, + /// Count of duplicate messages we have received from mesh peers for a given topic. + mesh_duplicates: Family, + + /// Count of duplicate messages we have received from by requesting them over iwant for a given topic. + iwant_duplicates: Family, + /// The size of the priority queue. priority_queue_size: Histogram, /// The size of the non-priority queue. @@ -359,6 +365,16 @@ impl Metrics { "IDONTWANT messages that were sent but we received the full message regardless" ); + let mesh_duplicates = register_family!( + "mesh_duplicates_per_topic", + "Count of duplicate messages received from mesh peers per topic" + ); + + let iwant_duplicates = register_family!( + "iwant_duplicates_per_topic", + "Count of duplicate messages received from non-mesh peers that we sent iwants for" + ); + let idontwant_bytes = { let metric = Counter::default(); registry.register( @@ -425,6 +441,8 @@ impl Metrics { idontwant_msgs_ids, idontwant_messages_sent_per_topic, idontwant_messages_ignored_per_topic, + mesh_duplicates, + iwant_duplicates, priority_queue_size, non_priority_queue_size, } @@ -597,6 +615,20 @@ impl Metrics { } } + /// Register a duplicate message received from a mesh peer. + pub(crate) fn mesh_duplicates(&mut self, topic: &TopicHash) { + if self.register_topic(topic).is_ok() { + self.mesh_duplicates.get_or_create(topic).inc(); + } + } + + /// Register a duplicate message received from a non-mesh peer on an iwant request. + pub(crate) fn iwant_duplicates(&mut self, topic: &TopicHash) { + if self.register_topic(topic).is_ok() { + self.iwant_duplicates.get_or_create(topic).inc(); + } + } + pub(crate) fn register_msg_validation( &mut self, topic: &TopicHash, From 4a07c08c4f515f7094828f62c856f6268dafaa58 Mon Sep 17 00:00:00 2001 From: Pawan Dhananjay Date: Wed, 29 Jan 2025 11:42:13 -0800 Subject: [PATCH 098/254] Fork aware max values in rpc (#6847) N/A In https://github.com/sigp/lighthouse/pull/6329 we changed `max_blobs_per_block` from a preset to a config value. We weren't using the right value based on fork in that PR. This is a follow up PR to use the fork dependent values. In the proces, I also updated other places where we weren't using fork dependent values from the ChainSpec. Note to reviewer: easier to go through by commit --- .../lighthouse_network/src/rpc/codec.rs | 61 +++++++++---------- .../lighthouse_network/src/rpc/handler.rs | 39 ++++++++++++ .../lighthouse_network/src/rpc/methods.rs | 26 +++++--- .../lighthouse_network/src/service/mod.rs | 2 +- .../lighthouse_network/src/service/utils.rs | 6 +- .../lighthouse_network/src/types/topics.rs | 4 +- .../lighthouse_network/tests/rpc_tests.rs | 60 ++++++++++-------- .../network_beacon_processor/rpc_methods.rs | 18 ------ beacon_node/network/src/router.rs | 4 +- beacon_node/network/src/service.rs | 1 + beacon_node/network/src/sync/manager.rs | 10 ++- .../network/src/sync/network_context.rs | 13 ++-- .../network_context/requests/blobs_by_root.rs | 4 +- .../requests/blocks_by_root.rs | 6 +- beacon_node/network/src/sync/tests/lookups.rs | 7 +++ consensus/types/src/chain_spec.rs | 56 ++++++++++++++--- 16 files changed, 203 insertions(+), 114 deletions(-) diff --git a/beacon_node/lighthouse_network/src/rpc/codec.rs b/beacon_node/lighthouse_network/src/rpc/codec.rs index 8981a75aed..6a70eef9bd 100644 --- a/beacon_node/lighthouse_network/src/rpc/codec.rs +++ b/beacon_node/lighthouse_network/src/rpc/codec.rs @@ -576,7 +576,7 @@ fn handle_rpc_request( BlocksByRootRequest::V2(BlocksByRootRequestV2 { block_roots: RuntimeVariableList::from_ssz_bytes( decoded_buffer, - spec.max_request_blocks as usize, + spec.max_request_blocks(current_fork), )?, }), ))), @@ -584,32 +584,18 @@ fn handle_rpc_request( BlocksByRootRequest::V1(BlocksByRootRequestV1 { block_roots: RuntimeVariableList::from_ssz_bytes( decoded_buffer, - spec.max_request_blocks as usize, + spec.max_request_blocks(current_fork), )?, }), ))), - SupportedProtocol::BlobsByRangeV1 => { - let req = BlobsByRangeRequest::from_ssz_bytes(decoded_buffer)?; - let max_requested_blobs = req - .count - .saturating_mul(spec.max_blobs_per_block_by_fork(current_fork)); - // TODO(pawan): change this to max_blobs_per_rpc_request in the alpha10 PR - if max_requested_blobs > spec.max_request_blob_sidecars { - return Err(RPCError::ErrorResponse( - RpcErrorResponse::InvalidRequest, - format!( - "requested exceeded limit. allowed: {}, requested: {}", - spec.max_request_blob_sidecars, max_requested_blobs - ), - )); - } - Ok(Some(RequestType::BlobsByRange(req))) - } + SupportedProtocol::BlobsByRangeV1 => Ok(Some(RequestType::BlobsByRange( + BlobsByRangeRequest::from_ssz_bytes(decoded_buffer)?, + ))), SupportedProtocol::BlobsByRootV1 => { Ok(Some(RequestType::BlobsByRoot(BlobsByRootRequest { blob_ids: RuntimeVariableList::from_ssz_bytes( decoded_buffer, - spec.max_request_blob_sidecars as usize, + spec.max_request_blob_sidecars(current_fork), )?, }))) } @@ -1097,21 +1083,21 @@ mod tests { } } - fn bbroot_request_v1(spec: &ChainSpec) -> BlocksByRootRequest { - BlocksByRootRequest::new_v1(vec![Hash256::zero()], spec) + fn bbroot_request_v1(fork_name: ForkName) -> BlocksByRootRequest { + BlocksByRootRequest::new_v1(vec![Hash256::zero()], &fork_context(fork_name)) } - fn bbroot_request_v2(spec: &ChainSpec) -> BlocksByRootRequest { - BlocksByRootRequest::new(vec![Hash256::zero()], spec) + fn bbroot_request_v2(fork_name: ForkName) -> BlocksByRootRequest { + BlocksByRootRequest::new(vec![Hash256::zero()], &fork_context(fork_name)) } - fn blbroot_request(spec: &ChainSpec) -> BlobsByRootRequest { + fn blbroot_request(fork_name: ForkName) -> BlobsByRootRequest { BlobsByRootRequest::new( vec![BlobIdentifier { block_root: Hash256::zero(), index: 0, }], - spec, + &fork_context(fork_name), ) } @@ -1909,7 +1895,8 @@ mod tests { #[test] fn test_encode_then_decode_request() { - let chain_spec = Spec::default_spec(); + let fork_context = fork_context(ForkName::Electra); + let chain_spec = fork_context.spec.clone(); let requests: &[RequestType] = &[ RequestType::Ping(ping_message()), @@ -1917,21 +1904,33 @@ mod tests { RequestType::Goodbye(GoodbyeReason::Fault), RequestType::BlocksByRange(bbrange_request_v1()), RequestType::BlocksByRange(bbrange_request_v2()), - RequestType::BlocksByRoot(bbroot_request_v1(&chain_spec)), - RequestType::BlocksByRoot(bbroot_request_v2(&chain_spec)), RequestType::MetaData(MetadataRequest::new_v1()), RequestType::BlobsByRange(blbrange_request()), - RequestType::BlobsByRoot(blbroot_request(&chain_spec)), RequestType::DataColumnsByRange(dcbrange_request()), RequestType::DataColumnsByRoot(dcbroot_request(&chain_spec)), RequestType::MetaData(MetadataRequest::new_v2()), ]; - for req in requests.iter() { for fork_name in ForkName::list_all() { encode_then_decode_request(req.clone(), fork_name, &chain_spec); } } + + // Request types that have different length limits depending on the fork + // Handled separately to have consistent `ForkName` across request and responses + let fork_dependent_requests = |fork_name| { + [ + RequestType::BlobsByRoot(blbroot_request(fork_name)), + RequestType::BlocksByRoot(bbroot_request_v1(fork_name)), + RequestType::BlocksByRoot(bbroot_request_v2(fork_name)), + ] + }; + for fork_name in ForkName::list_all() { + let requests = fork_dependent_requests(fork_name); + for req in requests { + encode_then_decode_request(req.clone(), fork_name, &chain_spec); + } + } } /// Test a malicious snappy encoding for a V1 `Status` message where the attacker diff --git a/beacon_node/lighthouse_network/src/rpc/handler.rs b/beacon_node/lighthouse_network/src/rpc/handler.rs index 3a008df023..cb57a640bc 100644 --- a/beacon_node/lighthouse_network/src/rpc/handler.rs +++ b/beacon_node/lighthouse_network/src/rpc/handler.rs @@ -855,6 +855,45 @@ where } let (req, substream) = substream; + let current_fork = self.fork_context.current_fork(); + let spec = &self.fork_context.spec; + + match &req { + RequestType::BlocksByRange(request) => { + let max_allowed = spec.max_request_blocks(current_fork) as u64; + if *request.count() > max_allowed { + self.events_out.push(HandlerEvent::Err(HandlerErr::Inbound { + id: self.current_inbound_substream_id, + proto: Protocol::BlocksByRange, + error: RPCError::InvalidData(format!( + "requested exceeded limit. allowed: {}, requested: {}", + max_allowed, + request.count() + )), + })); + return self.shutdown(None); + } + } + RequestType::BlobsByRange(request) => { + let max_requested_blobs = request + .count + .saturating_mul(spec.max_blobs_per_block_by_fork(current_fork)); + let max_allowed = spec.max_request_blob_sidecars(current_fork) as u64; + if max_requested_blobs > max_allowed { + self.events_out.push(HandlerEvent::Err(HandlerErr::Inbound { + id: self.current_inbound_substream_id, + proto: Protocol::BlobsByRange, + error: RPCError::InvalidData(format!( + "requested exceeded limit. allowed: {}, requested: {}", + max_allowed, max_requested_blobs + )), + })); + return self.shutdown(None); + } + } + _ => {} + }; + let max_responses = req.max_responses(self.fork_context.current_fork(), &self.fork_context.spec); diff --git a/beacon_node/lighthouse_network/src/rpc/methods.rs b/beacon_node/lighthouse_network/src/rpc/methods.rs index 958041c53f..ad6bea455e 100644 --- a/beacon_node/lighthouse_network/src/rpc/methods.rs +++ b/beacon_node/lighthouse_network/src/rpc/methods.rs @@ -15,12 +15,12 @@ use strum::IntoStaticStr; use superstruct::superstruct; use types::blob_sidecar::BlobIdentifier; use types::light_client_update::MAX_REQUEST_LIGHT_CLIENT_UPDATES; -use types::ForkName; use types::{ blob_sidecar::BlobSidecar, ChainSpec, ColumnIndex, DataColumnIdentifier, DataColumnSidecar, Epoch, EthSpec, Hash256, LightClientBootstrap, LightClientFinalityUpdate, LightClientOptimisticUpdate, LightClientUpdate, RuntimeVariableList, SignedBeaconBlock, Slot, }; +use types::{ForkContext, ForkName}; /// Maximum length of error message. pub type MaxErrorLen = U256; @@ -420,15 +420,19 @@ pub struct BlocksByRootRequest { } impl BlocksByRootRequest { - pub fn new(block_roots: Vec, spec: &ChainSpec) -> Self { - let block_roots = - RuntimeVariableList::from_vec(block_roots, spec.max_request_blocks as usize); + pub fn new(block_roots: Vec, fork_context: &ForkContext) -> Self { + let max_request_blocks = fork_context + .spec + .max_request_blocks(fork_context.current_fork()); + let block_roots = RuntimeVariableList::from_vec(block_roots, max_request_blocks); Self::V2(BlocksByRootRequestV2 { block_roots }) } - pub fn new_v1(block_roots: Vec, spec: &ChainSpec) -> Self { - let block_roots = - RuntimeVariableList::from_vec(block_roots, spec.max_request_blocks as usize); + pub fn new_v1(block_roots: Vec, fork_context: &ForkContext) -> Self { + let max_request_blocks = fork_context + .spec + .max_request_blocks(fork_context.current_fork()); + let block_roots = RuntimeVariableList::from_vec(block_roots, max_request_blocks); Self::V1(BlocksByRootRequestV1 { block_roots }) } } @@ -441,9 +445,11 @@ pub struct BlobsByRootRequest { } impl BlobsByRootRequest { - pub fn new(blob_ids: Vec, spec: &ChainSpec) -> Self { - let blob_ids = - RuntimeVariableList::from_vec(blob_ids, spec.max_request_blob_sidecars as usize); + pub fn new(blob_ids: Vec, fork_context: &ForkContext) -> Self { + let max_request_blob_sidecars = fork_context + .spec + .max_request_blob_sidecars(fork_context.current_fork()); + let blob_ids = RuntimeVariableList::from_vec(blob_ids, max_request_blob_sidecars); Self { blob_ids } } } diff --git a/beacon_node/lighthouse_network/src/service/mod.rs b/beacon_node/lighthouse_network/src/service/mod.rs index 4738c76d0c..a18daa5791 100644 --- a/beacon_node/lighthouse_network/src/service/mod.rs +++ b/beacon_node/lighthouse_network/src/service/mod.rs @@ -282,7 +282,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_electra as usize + + ctx.chain_spec.blob_sidecar_subnet_count_max() as usize + ctx.chain_spec.data_column_sidecar_subnet_count as usize + BASE_CORE_TOPICS.len() + ALTAIR_CORE_TOPICS.len() diff --git a/beacon_node/lighthouse_network/src/service/utils.rs b/beacon_node/lighthouse_network/src/service/utils.rs index 5746c13c58..72c2b29102 100644 --- a/beacon_node/lighthouse_network/src/service/utils.rs +++ b/beacon_node/lighthouse_network/src/service/utils.rs @@ -263,11 +263,7 @@ pub(crate) fn create_whitelist_filter( for id in 0..sync_committee_subnet_count { add(SyncCommitteeMessage(SyncSubnetId::new(id))); } - let blob_subnet_count = if spec.electra_fork_epoch.is_some() { - spec.blob_sidecar_subnet_count_electra - } else { - spec.blob_sidecar_subnet_count - }; + let blob_subnet_count = spec.blob_sidecar_subnet_count_max(); for id in 0..blob_subnet_count { add(BlobSidecar(id)); } diff --git a/beacon_node/lighthouse_network/src/types/topics.rs b/beacon_node/lighthouse_network/src/types/topics.rs index 475b459ccb..2c79f93423 100644 --- a/beacon_node/lighthouse_network/src/types/topics.rs +++ b/beacon_node/lighthouse_network/src/types/topics.rs @@ -51,7 +51,7 @@ pub fn fork_core_topics(fork_name: &ForkName, spec: &ChainSpec) -> V ForkName::Deneb => { // All of deneb blob topics are core topics let mut deneb_blob_topics = Vec::new(); - for i in 0..spec.blob_sidecar_subnet_count { + for i in 0..spec.blob_sidecar_subnet_count(ForkName::Deneb) { deneb_blob_topics.push(GossipKind::BlobSidecar(i)); } deneb_blob_topics @@ -59,7 +59,7 @@ pub fn fork_core_topics(fork_name: &ForkName, spec: &ChainSpec) -> V ForkName::Electra => { // All of electra blob topics are core topics let mut electra_blob_topics = Vec::new(); - for i in 0..spec.blob_sidecar_subnet_count_electra { + for i in 0..spec.blob_sidecar_subnet_count(ForkName::Electra) { electra_blob_topics.push(GossipKind::BlobSidecar(i)); } electra_blob_topics diff --git a/beacon_node/lighthouse_network/tests/rpc_tests.rs b/beacon_node/lighthouse_network/tests/rpc_tests.rs index f721c8477c..4b54a24ddc 100644 --- a/beacon_node/lighthouse_network/tests/rpc_tests.rs +++ b/beacon_node/lighthouse_network/tests/rpc_tests.rs @@ -16,7 +16,7 @@ use tokio::time::sleep; use types::{ BeaconBlock, BeaconBlockAltair, BeaconBlockBase, BeaconBlockBellatrix, BlobSidecar, ChainSpec, EmptyBlock, Epoch, EthSpec, FixedBytesExtended, ForkContext, ForkName, Hash256, MinimalEthSpec, - Signature, SignedBeaconBlock, Slot, + RuntimeVariableList, Signature, SignedBeaconBlock, Slot, }; type E = MinimalEthSpec; @@ -810,17 +810,20 @@ fn test_tcp_blocks_by_root_chunked_rpc() { .await; // BlocksByRoot Request - let rpc_request = RequestType::BlocksByRoot(BlocksByRootRequest::new( - vec![ - Hash256::zero(), - Hash256::zero(), - Hash256::zero(), - Hash256::zero(), - Hash256::zero(), - Hash256::zero(), - ], - &spec, - )); + let rpc_request = + RequestType::BlocksByRoot(BlocksByRootRequest::V2(BlocksByRootRequestV2 { + block_roots: RuntimeVariableList::from_vec( + vec![ + Hash256::zero(), + Hash256::zero(), + Hash256::zero(), + Hash256::zero(), + Hash256::zero(), + Hash256::zero(), + ], + spec.max_request_blocks_upper_bound(), + ), + })); // BlocksByRoot Response let full_block = BeaconBlock::Base(BeaconBlockBase::::full(&spec)); @@ -953,21 +956,24 @@ fn test_tcp_blocks_by_root_chunked_rpc_terminates_correctly() { .await; // BlocksByRoot Request - let rpc_request = RequestType::BlocksByRoot(BlocksByRootRequest::new( - vec![ - Hash256::zero(), - Hash256::zero(), - Hash256::zero(), - Hash256::zero(), - Hash256::zero(), - Hash256::zero(), - Hash256::zero(), - Hash256::zero(), - Hash256::zero(), - Hash256::zero(), - ], - &spec, - )); + let rpc_request = + RequestType::BlocksByRoot(BlocksByRootRequest::V2(BlocksByRootRequestV2 { + block_roots: RuntimeVariableList::from_vec( + vec![ + Hash256::zero(), + Hash256::zero(), + Hash256::zero(), + Hash256::zero(), + Hash256::zero(), + Hash256::zero(), + Hash256::zero(), + Hash256::zero(), + Hash256::zero(), + Hash256::zero(), + ], + spec.max_request_blocks_upper_bound(), + ), + })); // BlocksByRoot Response let full_block = BeaconBlock::Base(BeaconBlockBase::::full(&spec)); 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 b4f19f668d..67a1570275 100644 --- a/beacon_node/network/src/network_beacon_processor/rpc_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/rpc_methods.rs @@ -659,24 +659,6 @@ impl NetworkBeaconProcessor { "start_slot" => req.start_slot(), ); - // Should not send more than max request blocks - let max_request_size = - self.chain - .epoch() - .map_or(self.chain.spec.max_request_blocks, |epoch| { - if self.chain.spec.fork_name_at_epoch(epoch).deneb_enabled() { - self.chain.spec.max_request_blocks_deneb - } else { - self.chain.spec.max_request_blocks - } - }); - if *req.count() > max_request_size { - return Err(( - RpcErrorResponse::InvalidRequest, - "Request exceeded max size", - )); - } - let forwards_block_root_iter = match self .chain .forwards_iter_block_roots(Slot::from(*req.start_slot())) diff --git a/beacon_node/network/src/router.rs b/beacon_node/network/src/router.rs index d3da341e1c..41b9f2c91e 100644 --- a/beacon_node/network/src/router.rs +++ b/beacon_node/network/src/router.rs @@ -28,7 +28,7 @@ use std::sync::Arc; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use tokio::sync::mpsc; use tokio_stream::wrappers::UnboundedReceiverStream; -use types::{BlobSidecar, DataColumnSidecar, EthSpec, SignedBeaconBlock}; +use types::{BlobSidecar, DataColumnSidecar, EthSpec, ForkContext, SignedBeaconBlock}; /// Handles messages from the network and routes them to the appropriate service to be handled. pub struct Router { @@ -90,6 +90,7 @@ impl Router { invalid_block_storage: InvalidBlockStorage, beacon_processor_send: BeaconProcessorSend, beacon_processor_reprocess_tx: mpsc::Sender, + fork_context: Arc, log: slog::Logger, ) -> Result>, String> { let message_handler_log = log.new(o!("service"=> "router")); @@ -122,6 +123,7 @@ impl Router { network_send.clone(), network_beacon_processor.clone(), sync_recv, + fork_context, sync_logger, ); diff --git a/beacon_node/network/src/service.rs b/beacon_node/network/src/service.rs index f89241b4ae..ab654ddf77 100644 --- a/beacon_node/network/src/service.rs +++ b/beacon_node/network/src/service.rs @@ -312,6 +312,7 @@ impl NetworkService { invalid_block_storage, beacon_processor_send, beacon_processor_reprocess_tx, + fork_context.clone(), network_log.clone(), )?; diff --git a/beacon_node/network/src/sync/manager.rs b/beacon_node/network/src/sync/manager.rs index 2df8b5f94c..fd91dc78b1 100644 --- a/beacon_node/network/src/sync/manager.rs +++ b/beacon_node/network/src/sync/manager.rs @@ -69,7 +69,9 @@ use std::ops::Sub; use std::sync::Arc; use std::time::Duration; use tokio::sync::mpsc; -use types::{BlobSidecar, DataColumnSidecar, EthSpec, Hash256, SignedBeaconBlock, Slot}; +use types::{ + BlobSidecar, DataColumnSidecar, EthSpec, ForkContext, Hash256, SignedBeaconBlock, Slot, +}; #[cfg(test)] use types::ColumnIndex; @@ -258,10 +260,11 @@ pub fn spawn( network_send: mpsc::UnboundedSender>, beacon_processor: Arc>, sync_recv: mpsc::UnboundedReceiver>, + fork_context: Arc, log: slog::Logger, ) { assert!( - beacon_chain.spec.max_request_blocks >= T::EthSpec::slots_per_epoch() * EPOCHS_PER_BATCH, + beacon_chain.spec.max_request_blocks(fork_context.current_fork()) as u64 >= T::EthSpec::slots_per_epoch() * EPOCHS_PER_BATCH, "Max blocks that can be requested in a single batch greater than max allowed blocks in a single request" ); @@ -272,6 +275,7 @@ pub fn spawn( beacon_processor, sync_recv, SamplingConfig::Default, + fork_context, log.clone(), ); @@ -287,6 +291,7 @@ impl SyncManager { beacon_processor: Arc>, sync_recv: mpsc::UnboundedReceiver>, sampling_config: SamplingConfig, + fork_context: Arc, log: slog::Logger, ) -> Self { let network_globals = beacon_processor.network_globals.clone(); @@ -297,6 +302,7 @@ impl SyncManager { network_send, beacon_processor.clone(), beacon_chain.clone(), + fork_context.clone(), log.clone(), ), range_sync: RangeSync::new( diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index f899936128..e21041192d 100644 --- a/beacon_node/network/src/sync/network_context.rs +++ b/beacon_node/network/src/sync/network_context.rs @@ -43,8 +43,8 @@ use std::time::Duration; use tokio::sync::mpsc; use types::blob_sidecar::FixedBlobSidecarList; use types::{ - BlobSidecar, ColumnIndex, DataColumnSidecar, DataColumnSidecarList, EthSpec, Hash256, - SignedBeaconBlock, Slot, + BlobSidecar, ColumnIndex, DataColumnSidecar, DataColumnSidecarList, EthSpec, ForkContext, + Hash256, SignedBeaconBlock, Slot, }; pub mod custody; @@ -216,6 +216,8 @@ pub struct SyncNetworkContext { pub chain: Arc>, + fork_context: Arc, + /// Logger for the `SyncNetworkContext`. pub log: slog::Logger, } @@ -244,6 +246,7 @@ impl SyncNetworkContext { network_send: mpsc::UnboundedSender>, network_beacon_processor: Arc>, chain: Arc>, + fork_context: Arc, log: slog::Logger, ) -> Self { SyncNetworkContext { @@ -257,6 +260,7 @@ impl SyncNetworkContext { range_block_components_requests: FnvHashMap::default(), network_beacon_processor, chain, + fork_context, log, } } @@ -455,7 +459,6 @@ impl SyncNetworkContext { (None, None) }; - // TODO(pawan): this would break if a batch contains multiple epochs let max_blobs_len = self.chain.spec.max_blobs_per_block(epoch); let info = RangeBlockComponentsRequest::new( expected_blobs, @@ -624,7 +627,7 @@ impl SyncNetworkContext { self.network_send .send(NetworkMessage::SendRequest { peer_id, - request: RequestType::BlocksByRoot(request.into_request(&self.chain.spec)), + request: RequestType::BlocksByRoot(request.into_request(&self.fork_context)), request_id: AppRequestId::Sync(SyncRequestId::SingleBlock { id }), }) .map_err(|_| RpcRequestSendError::NetworkSendError)?; @@ -706,7 +709,7 @@ impl SyncNetworkContext { self.network_send .send(NetworkMessage::SendRequest { peer_id, - request: RequestType::BlobsByRoot(request.clone().into_request(&self.chain.spec)), + request: RequestType::BlobsByRoot(request.clone().into_request(&self.fork_context)), request_id: AppRequestId::Sync(SyncRequestId::SingleBlob { id }), }) .map_err(|_| RpcRequestSendError::NetworkSendError)?; diff --git a/beacon_node/network/src/sync/network_context/requests/blobs_by_root.rs b/beacon_node/network/src/sync/network_context/requests/blobs_by_root.rs index fefb27a5ef..a670229884 100644 --- a/beacon_node/network/src/sync/network_context/requests/blobs_by_root.rs +++ b/beacon_node/network/src/sync/network_context/requests/blobs_by_root.rs @@ -1,6 +1,6 @@ use lighthouse_network::rpc::methods::BlobsByRootRequest; use std::sync::Arc; -use types::{blob_sidecar::BlobIdentifier, BlobSidecar, ChainSpec, EthSpec, Hash256}; +use types::{blob_sidecar::BlobIdentifier, BlobSidecar, EthSpec, ForkContext, Hash256}; use super::{ActiveRequestItems, LookupVerifyError}; @@ -11,7 +11,7 @@ pub struct BlobsByRootSingleBlockRequest { } impl BlobsByRootSingleBlockRequest { - pub fn into_request(self, spec: &ChainSpec) -> BlobsByRootRequest { + pub fn into_request(self, spec: &ForkContext) -> BlobsByRootRequest { BlobsByRootRequest::new( self.indices .into_iter() diff --git a/beacon_node/network/src/sync/network_context/requests/blocks_by_root.rs b/beacon_node/network/src/sync/network_context/requests/blocks_by_root.rs index f3cdcbe714..6d7eabf909 100644 --- a/beacon_node/network/src/sync/network_context/requests/blocks_by_root.rs +++ b/beacon_node/network/src/sync/network_context/requests/blocks_by_root.rs @@ -1,7 +1,7 @@ use beacon_chain::get_block_root; use lighthouse_network::rpc::BlocksByRootRequest; use std::sync::Arc; -use types::{ChainSpec, EthSpec, Hash256, SignedBeaconBlock}; +use types::{EthSpec, ForkContext, Hash256, SignedBeaconBlock}; use super::{ActiveRequestItems, LookupVerifyError}; @@ -9,8 +9,8 @@ use super::{ActiveRequestItems, LookupVerifyError}; pub struct BlocksByRootSingleRequest(pub Hash256); impl BlocksByRootSingleRequest { - pub fn into_request(self, spec: &ChainSpec) -> BlocksByRootRequest { - BlocksByRootRequest::new(vec![self.0], spec) + pub fn into_request(self, fork_context: &ForkContext) -> BlocksByRootRequest { + BlocksByRootRequest::new(vec![self.0], fork_context) } } diff --git a/beacon_node/network/src/sync/tests/lookups.rs b/beacon_node/network/src/sync/tests/lookups.rs index f772010500..341fe8667c 100644 --- a/beacon_node/network/src/sync/tests/lookups.rs +++ b/beacon_node/network/src/sync/tests/lookups.rs @@ -39,6 +39,7 @@ use lighthouse_network::{ use slog::info; use slot_clock::{SlotClock, TestingSlotClock}; use tokio::sync::mpsc; +use types::ForkContext; use types::{ data_column_sidecar::ColumnIndex, test_utils::{SeedableRng, TestRandom, XorShiftRng}, @@ -92,6 +93,11 @@ impl TestRig { .build(); let chain = harness.chain.clone(); + let fork_context = Arc::new(ForkContext::new::( + Slot::new(0), + chain.genesis_validators_root, + &chain.spec, + )); let (network_tx, network_rx) = mpsc::unbounded_channel(); let (sync_tx, sync_rx) = mpsc::unbounded_channel::>(); @@ -139,6 +145,7 @@ impl TestRig { SamplingConfig::Custom { required_successes: vec![SAMPLING_REQUIRED_SUCCESSES], }, + fork_context, log.clone(), ), harness, diff --git a/consensus/types/src/chain_spec.rs b/consensus/types/src/chain_spec.rs index 9177f66b94..91d64f5c8e 100644 --- a/consensus/types/src/chain_spec.rs +++ b/consensus/types/src/chain_spec.rs @@ -217,7 +217,7 @@ pub struct ChainSpec { pub network_id: u8, pub target_aggregators_per_committee: u64, pub gossip_max_size: u64, - pub max_request_blocks: u64, + max_request_blocks: u64, pub min_epochs_for_block_requests: u64, pub max_chunk_size: u64, pub ttfb_timeout: u64, @@ -233,19 +233,19 @@ pub struct ChainSpec { /* * Networking Deneb */ - pub max_request_blocks_deneb: u64, - pub max_request_blob_sidecars: u64, + max_request_blocks_deneb: u64, + 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 max_blobs_per_block: u64, + blob_sidecar_subnet_count: u64, + max_blobs_per_block: u64, /* * Networking Electra */ max_blobs_per_block_electra: u64, - pub blob_sidecar_subnet_count_electra: u64, - pub max_request_blob_sidecars_electra: u64, + blob_sidecar_subnet_count_electra: u64, + max_request_blob_sidecars_electra: u64, /* * Networking Derived @@ -625,6 +625,17 @@ impl ChainSpec { } } + /// Returns the highest possible value for max_request_blocks based on enabled forks. + /// + /// This is useful for upper bounds in testing. + pub fn max_request_blocks_upper_bound(&self) -> usize { + if self.deneb_fork_epoch.is_some() { + self.max_request_blocks_deneb as usize + } else { + self.max_request_blocks as usize + } + } + pub fn max_request_blob_sidecars(&self, fork_name: ForkName) -> usize { if fork_name.electra_enabled() { self.max_request_blob_sidecars_electra as usize @@ -633,6 +644,17 @@ impl ChainSpec { } } + /// Returns the highest possible value for max_request_blobs based on enabled forks. + /// + /// This is useful for upper bounds in testing. + pub fn max_request_blobs_upper_bound(&self) -> usize { + if self.electra_fork_epoch.is_some() { + self.max_request_blob_sidecars_electra as usize + } else { + self.max_request_blob_sidecars as usize + } + } + /// Return the value of `MAX_BLOBS_PER_BLOCK` appropriate for the fork at `epoch`. pub fn max_blobs_per_block(&self, epoch: Epoch) -> u64 { self.max_blobs_per_block_by_fork(self.fork_name_at_epoch(epoch)) @@ -647,6 +669,26 @@ impl ChainSpec { } } + /// Returns the `BLOB_SIDECAR_SUBNET_COUNT` at the given fork_name. + pub fn blob_sidecar_subnet_count(&self, fork_name: ForkName) -> u64 { + if fork_name.electra_enabled() { + self.blob_sidecar_subnet_count_electra + } else { + self.blob_sidecar_subnet_count + } + } + + /// Returns the highest possible value of blob sidecar subnet count based on enabled forks. + /// + /// This is useful for upper bounds for the subnet count during a given run of lighthouse. + pub fn blob_sidecar_subnet_count_max(&self) -> u64 { + if self.electra_fork_epoch.is_some() { + self.blob_sidecar_subnet_count_electra + } else { + self.blob_sidecar_subnet_count + } + } + /// Returns the number of data columns per custody group. pub fn data_columns_per_group(&self) -> u64 { self.number_of_columns From 66c6552e8cd5f20466b2717489ba91a333684361 Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Thu, 30 Jan 2025 00:09:48 -0300 Subject: [PATCH 099/254] Some sync/backfill format nits (#6861) When working on unrelated changes I noted: - An unnecessary closure left by a commit of some guy named @dapplion that can be removed - match statements that can be simplified with the new let else syntax - instead of mapping a result to ignore the Ok value, return --- .../network/src/sync/backfill_sync/mod.rs | 140 ++++++++---------- 1 file changed, 63 insertions(+), 77 deletions(-) diff --git a/beacon_node/network/src/sync/backfill_sync/mod.rs b/beacon_node/network/src/sync/backfill_sync/mod.rs index 5703ed3504..a3d2c82642 100644 --- a/beacon_node/network/src/sync/backfill_sync/mod.rs +++ b/beacon_node/network/src/sync/backfill_sync/mod.rs @@ -388,67 +388,59 @@ impl BackFillSync { blocks: Vec>, ) -> Result { // check if we have this batch - let batch = match self.batches.get_mut(&batch_id) { - None => { - if !matches!(self.state(), BackFillState::Failed) { - // A batch might get removed when the chain advances, so this is non fatal. - debug!(self.log, "Received a block for unknown batch"; "epoch" => batch_id); - } - return Ok(ProcessResult::Successful); - } - Some(batch) => { - // 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, and that the - // request_id matches - // 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 + let Some(batch) = self.batches.get_mut(&batch_id) else { + if !matches!(self.state(), BackFillState::Failed) { + // A batch might get removed when the chain advances, so this is non fatal. + debug!(self.log, "Received a block for unknown batch"; "epoch" => batch_id); } + return Ok(ProcessResult::Successful); }; - { - // A stream termination has been sent. This batch has ended. Process a completed batch. - // Remove the request from the peer's active batches - self.active_requests - .get_mut(peer_id) - .map(|active_requests| active_requests.remove(&batch_id)); + // 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, and that the + // request_id matches + // 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); + } - match batch.download_completed(blocks) { - Ok(received) => { - let awaiting_batches = - self.processing_target.saturating_sub(batch_id) / BACKFILL_EPOCHS_PER_BATCH; - debug!(self.log, "Completed batch received"; "epoch" => batch_id, "blocks" => received, "awaiting_batches" => awaiting_batches); + // A stream termination has been sent. This batch has ended. Process a completed batch. + // Remove the request from the peer's active batches + self.active_requests + .get_mut(peer_id) + .map(|active_requests| active_requests.remove(&batch_id)); - // pre-emptively request more blocks from peers whilst we process current blocks, - self.request_batches(network)?; - self.process_completed_batches(network) - } - Err(result) => { - let (expected_boundary, received_boundary, outcome) = match result { - Err(e) => { - return self - .fail_sync(BackFillError::BatchInvalidState(batch_id, e.0)) - .map(|_| ProcessResult::Successful); - } - Ok(v) => v, - }; - warn!(self.log, "Batch received out of range blocks"; "expected_boundary" => expected_boundary, "received_boundary" => received_boundary, + match batch.download_completed(blocks) { + Ok(received) => { + let awaiting_batches = + self.processing_target.saturating_sub(batch_id) / BACKFILL_EPOCHS_PER_BATCH; + debug!(self.log, "Completed batch received"; "epoch" => batch_id, "blocks" => received, "awaiting_batches" => awaiting_batches); + + // pre-emptively request more blocks from peers whilst we process current blocks, + self.request_batches(network)?; + self.process_completed_batches(network) + } + Err(result) => { + let (expected_boundary, received_boundary, outcome) = match result { + Err(e) => { + self.fail_sync(BackFillError::BatchInvalidState(batch_id, e.0))?; + return Ok(ProcessResult::Successful); + } + Ok(v) => v, + }; + warn!(self.log, "Batch received out of range blocks"; "expected_boundary" => expected_boundary, "received_boundary" => received_boundary, "peer_id" => %peer_id, batch); - if let BatchOperationOutcome::Failed { blacklist: _ } = outcome { - error!(self.log, "Backfill failed"; "epoch" => batch_id, "received_boundary" => received_boundary, "expected_boundary" => expected_boundary); - return self - .fail_sync(BackFillError::BatchDownloadFailed(batch_id)) - .map(|_| ProcessResult::Successful); - } - // this batch can't be used, so we need to request it again. - self.retry_batch_download(network, batch_id) - .map(|_| ProcessResult::Successful) + if let BatchOperationOutcome::Failed { blacklist: _ } = outcome { + error!(self.log, "Backfill failed"; "epoch" => batch_id, "received_boundary" => received_boundary, "expected_boundary" => expected_boundary); + self.fail_sync(BackFillError::BatchDownloadFailed(batch_id))?; + return Ok(ProcessResult::Successful); } + // this batch can't be used, so we need to request it again. + self.retry_batch_download(network, batch_id)?; + Ok(ProcessResult::Successful) } } } @@ -582,20 +574,16 @@ impl BackFillSync { } }; - let peer = match batch.current_peer() { - Some(v) => *v, - None => { - return self - .fail_sync(BackFillError::BatchInvalidState( - batch_id, - String::from("Peer does not exist"), - )) - .map(|_| ProcessResult::Successful) - } + let Some(peer) = batch.current_peer() else { + self.fail_sync(BackFillError::BatchInvalidState( + batch_id, + String::from("Peer does not exist"), + ))?; + return Ok(ProcessResult::Successful); }; debug!(self.log, "Backfill batch processed"; "result" => ?result, &batch, - "batch_epoch" => batch_id, "peer" => %peer, "client" => %network.client_type(&peer)); + "batch_epoch" => batch_id, "peer" => %peer, "client" => %network.client_type(peer)); match result { BatchProcessResult::Success { @@ -679,8 +667,8 @@ impl BackFillSync { { self.fail_sync(BackFillError::BatchInvalidState(batch_id, e.0))?; } - self.retry_batch_download(network, batch_id) - .map(|_| ProcessResult::Successful) + self.retry_batch_download(network, batch_id)?; + Ok(ProcessResult::Successful) } } } @@ -712,11 +700,10 @@ impl BackFillSync { // - AwaitingDownload -> A recoverable failed batch should have been // re-requested. // - Processing -> `self.current_processing_batch` is None - return self - .fail_sync(BackFillError::InvalidSyncState(String::from( - "Invalid expected batch state", - ))) - .map(|_| ProcessResult::Successful); + self.fail_sync(BackFillError::InvalidSyncState(String::from( + "Invalid expected batch state", + )))?; + return Ok(ProcessResult::Successful); } BatchState::AwaitingValidation(_) => { // TODO: I don't think this state is possible, log a CRIT just in case. @@ -731,12 +718,11 @@ impl BackFillSync { } } } else { - return self - .fail_sync(BackFillError::InvalidSyncState(format!( - "Batch not found for current processing target {}", - self.processing_target - ))) - .map(|_| ProcessResult::Successful); + self.fail_sync(BackFillError::InvalidSyncState(format!( + "Batch not found for current processing target {}", + self.processing_target + )))?; + return Ok(ProcessResult::Successful); } Ok(ProcessResult::Successful) } From d297d08c6b536cc1f33a7d6d5a5107eef0a9514f Mon Sep 17 00:00:00 2001 From: Janick Martinez Esturo Date: Thu, 30 Jan 2025 06:14:57 +0100 Subject: [PATCH 100/254] Increase jemalloc aarch64 page size limit (#5244) (#6831) #5244 Pass `JEMALLOC_SYS_WITH_LG_PAGE=16` env to aarch64 cross-compilation to support systems with up to 64-KiB page sizes. This is backwards-compatible for the current (most usual) 4-KiB systems. --- .cargo/config.toml | 1 - Cross.toml | 11 +++++++++++ Makefile | 10 ++++++++-- common/malloc_utils/src/jemalloc.rs | 17 ++++++++++++++++- common/malloc_utils/src/lib.rs | 4 ++-- lighthouse/src/main.rs | 14 +++++++++----- 6 files changed, 46 insertions(+), 11 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index dac0163003..a408305c4d 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,4 +1,3 @@ [env] # Set the number of arenas to 16 when using jemalloc. JEMALLOC_SYS_WITH_MALLOC_CONF = "abort_conf:true,narenas:16" - diff --git a/Cross.toml b/Cross.toml index 871391253d..8181967f32 100644 --- a/Cross.toml +++ b/Cross.toml @@ -3,3 +3,14 @@ pre-build = ["apt-get install -y cmake clang-5.0"] [target.aarch64-unknown-linux-gnu] pre-build = ["apt-get install -y cmake clang-5.0"] + +# Allow setting page size limits for jemalloc at build time: +# For certain architectures (like aarch64), we must compile +# jemalloc with support for large page sizes, otherwise the host's +# system page size will be used, which may not work on the target systems. +# JEMALLOC_SYS_WITH_LG_PAGE=16 tells jemalloc to support up to 64-KiB +# pages. See: https://github.com/sigp/lighthouse/issues/5244 +[build.env] +passthrough = [ + "JEMALLOC_SYS_WITH_LG_PAGE", +] diff --git a/Makefile b/Makefile index 0f08afd168..81477634fe 100644 --- a/Makefile +++ b/Makefile @@ -63,12 +63,18 @@ install-lcli: build-x86_64: cross build --bin lighthouse --target x86_64-unknown-linux-gnu --features "portable,$(CROSS_FEATURES)" --profile "$(CROSS_PROFILE)" --locked build-aarch64: - cross build --bin lighthouse --target aarch64-unknown-linux-gnu --features "portable,$(CROSS_FEATURES)" --profile "$(CROSS_PROFILE)" --locked + # JEMALLOC_SYS_WITH_LG_PAGE=16 tells jemalloc to support up to 64-KiB + # pages, which are commonly used by aarch64 systems. + # See: https://github.com/sigp/lighthouse/issues/5244 + JEMALLOC_SYS_WITH_LG_PAGE=16 cross build --bin lighthouse --target aarch64-unknown-linux-gnu --features "portable,$(CROSS_FEATURES)" --profile "$(CROSS_PROFILE)" --locked build-lcli-x86_64: cross build --bin lcli --target x86_64-unknown-linux-gnu --features "portable" --profile "$(CROSS_PROFILE)" --locked build-lcli-aarch64: - cross build --bin lcli --target aarch64-unknown-linux-gnu --features "portable" --profile "$(CROSS_PROFILE)" --locked + # JEMALLOC_SYS_WITH_LG_PAGE=16 tells jemalloc to support up to 64-KiB + # pages, which are commonly used by aarch64 systems. + # See: https://github.com/sigp/lighthouse/issues/5244 + JEMALLOC_SYS_WITH_LG_PAGE=16 cross build --bin lcli --target aarch64-unknown-linux-gnu --features "portable" --profile "$(CROSS_PROFILE)" --locked # Create a `.tar.gz` containing a binary for a specific target. define tarball_release_binary diff --git a/common/malloc_utils/src/jemalloc.rs b/common/malloc_utils/src/jemalloc.rs index 0e2e00cb0e..f3a35fc41c 100644 --- a/common/malloc_utils/src/jemalloc.rs +++ b/common/malloc_utils/src/jemalloc.rs @@ -9,7 +9,7 @@ //! B) `_RJEM_MALLOC_CONF` at runtime. use metrics::{set_gauge, try_create_int_gauge, IntGauge}; use std::sync::LazyLock; -use tikv_jemalloc_ctl::{arenas, epoch, stats, Error}; +use tikv_jemalloc_ctl::{arenas, epoch, stats, Access, AsName, Error}; #[global_allocator] static ALLOC: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc; @@ -52,3 +52,18 @@ pub fn scrape_jemalloc_metrics_fallible() -> Result<(), Error> { Ok(()) } + +pub fn page_size() -> Result { + // Full list of keys: https://jemalloc.net/jemalloc.3.html + "arenas.page\0".name().read() +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn page_size_ok() { + assert!(page_size().is_ok()); + } +} diff --git a/common/malloc_utils/src/lib.rs b/common/malloc_utils/src/lib.rs index 3bb242369f..50d2785a74 100644 --- a/common/malloc_utils/src/lib.rs +++ b/common/malloc_utils/src/lib.rs @@ -29,10 +29,10 @@ not(target_env = "musl"), not(feature = "jemalloc") ))] -mod glibc; +pub mod glibc; #[cfg(feature = "jemalloc")] -mod jemalloc; +pub mod jemalloc; pub use interface::*; diff --git a/lighthouse/src/main.rs b/lighthouse/src/main.rs index 43c5e1107c..dd7401d49e 100644 --- a/lighthouse/src/main.rs +++ b/lighthouse/src/main.rs @@ -66,11 +66,15 @@ fn bls_hardware_acceleration() -> bool { return std::arch::is_aarch64_feature_detected!("neon"); } -fn allocator_name() -> &'static str { - if cfg!(target_os = "windows") { - "system" - } else { - "jemalloc" +fn allocator_name() -> String { + #[cfg(target_os = "windows")] + { + "system".to_string() + } + #[cfg(not(target_os = "windows"))] + match malloc_utils::jemalloc::page_size() { + Ok(page_size) => format!("jemalloc ({}K)", page_size / 1024), + Err(e) => format!("jemalloc (error: {e:?})"), } } From 1fe0ac72be6e099a4e28994c81e62bd9ccd64dae Mon Sep 17 00:00:00 2001 From: Age Manning Date: Thu, 30 Jan 2025 17:22:59 +1100 Subject: [PATCH 101/254] Underflow and Typo (#6885) I was looking at sync and noticed a potential underflow and a typo, so just fixed those whilst I was in there. --- beacon_node/network/src/sync/peer_sync_info.rs | 4 ++-- beacon_node/network/src/sync/range_sync/chain_collection.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/beacon_node/network/src/sync/peer_sync_info.rs b/beacon_node/network/src/sync/peer_sync_info.rs index c01366f1be..5ea1533d35 100644 --- a/beacon_node/network/src/sync/peer_sync_info.rs +++ b/beacon_node/network/src/sync/peer_sync_info.rs @@ -30,8 +30,8 @@ pub fn remote_sync_type( ) -> PeerSyncType { // auxiliary variables for clarity: Inclusive boundaries of the range in which we consider a peer's // head "near" ours. - let near_range_start = local.head_slot - SLOT_IMPORT_TOLERANCE as u64; - let near_range_end = local.head_slot + SLOT_IMPORT_TOLERANCE as u64; + let near_range_start = local.head_slot.saturating_sub(SLOT_IMPORT_TOLERANCE); + let near_range_end = local.head_slot.saturating_add(SLOT_IMPORT_TOLERANCE); match remote.finalized_epoch.cmp(&local.finalized_epoch) { Ordering::Less => { diff --git a/beacon_node/network/src/sync/range_sync/chain_collection.rs b/beacon_node/network/src/sync/range_sync/chain_collection.rs index c030d0a19e..16dadb3660 100644 --- a/beacon_node/network/src/sync/range_sync/chain_collection.rs +++ b/beacon_node/network/src/sync/range_sync/chain_collection.rs @@ -86,7 +86,7 @@ impl ChainCollection { RangeSyncState::Head(syncing_head_ids) }; } else { - // we removed a head chain, or an stoped finalized chain + // we removed a head chain, or a stopped finalized chain debug_assert!(!was_syncing || sync_type != RangeSyncType::Finalized); } } From 7d54a43243905b62e1ced8f56cd5ad0575b8638b Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Thu, 30 Jan 2025 04:01:32 -0300 Subject: [PATCH 102/254] Make range sync chain Id sequential (#6868) Currently, we set the `chain_id` of range sync chains to `u64(hash(target_root, target_slot))`, which results in a long integer. ``` Jan 27 00:43:27.246 DEBG Batch downloaded, chain: 4223372036854775807, awaiting_batches: 0, batch_state: [p,E,E,E,E], blocks: 0, epoch: 0, service: range_sync ``` Instead, we can use `network_context.next_id()` as we do for all other sync items and get a unique sequential (not too big) integer as id. ``` Jan 27 00:43:27.246 DEBG Batch downloaded, chain: 4, awaiting_batches: 0, batch_state: [p,E,E,E,E], blocks: 0, epoch: 0, service: range_sync ``` Also, if a specific chain for the same target is retried later, it won't get the same ID so we can more clearly differentiate the logs associated with each attempt. --- .../network/src/sync/range_sync/chain.rs | 19 +++++------- .../src/sync/range_sync/chain_collection.rs | 31 +++++++++++-------- 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/beacon_node/network/src/sync/range_sync/chain.rs b/beacon_node/network/src/sync/range_sync/chain.rs index 51d9d9da37..4eb73f5483 100644 --- a/beacon_node/network/src/sync/range_sync/chain.rs +++ b/beacon_node/network/src/sync/range_sync/chain.rs @@ -15,7 +15,6 @@ 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}; use strum::IntoStaticStr; use types::{Epoch, EthSpec, Hash256, Slot}; @@ -56,7 +55,7 @@ pub enum RemoveChain { pub struct KeepChain; /// A chain identifier -pub type ChainId = u64; +pub type ChainId = Id; pub type BatchId = Epoch; #[derive(Debug, Copy, Clone, IntoStaticStr)] @@ -127,14 +126,9 @@ pub enum ChainSyncingState { } impl SyncingChain { - pub fn id(target_root: &Hash256, target_slot: &Slot) -> u64 { - let mut hasher = std::collections::hash_map::DefaultHasher::new(); - (target_root, target_slot).hash(&mut hasher); - hasher.finish() - } - #[allow(clippy::too_many_arguments)] pub fn new( + id: Id, start_epoch: Epoch, target_head_slot: Slot, target_head_root: Hash256, @@ -145,8 +139,6 @@ impl SyncingChain { let mut peers = FnvHashMap::default(); peers.insert(peer_id, Default::default()); - let id = SyncingChain::::id(&target_head_root, &target_head_slot); - SyncingChain { id, chain_type, @@ -165,6 +157,11 @@ impl SyncingChain { } } + /// Returns true if this chain has the same target + pub fn has_same_target(&self, target_head_slot: Slot, target_head_root: Hash256) -> bool { + self.target_head_slot == target_head_slot && self.target_head_root == target_head_root + } + /// Check if the chain has peers from which to process batches. pub fn available_peers(&self) -> usize { self.peers.len() @@ -1258,7 +1255,7 @@ impl slog::KV for SyncingChain { serializer: &mut dyn slog::Serializer, ) -> slog::Result { use slog::Value; - serializer.emit_u64("id", self.id)?; + serializer.emit_u32("id", self.id)?; Value::serialize(&self.start_epoch, record, "from", serializer)?; Value::serialize( &self.target_head_slot.epoch(T::EthSpec::slots_per_epoch()), diff --git a/beacon_node/network/src/sync/range_sync/chain_collection.rs b/beacon_node/network/src/sync/range_sync/chain_collection.rs index 16dadb3660..15bdf85e20 100644 --- a/beacon_node/network/src/sync/range_sync/chain_collection.rs +++ b/beacon_node/network/src/sync/range_sync/chain_collection.rs @@ -9,6 +9,7 @@ use crate::metrics; use crate::sync::network_context::SyncNetworkContext; use beacon_chain::{BeaconChain, BeaconChainTypes}; use fnv::FnvHashMap; +use lighthouse_network::service::api_types::Id; use lighthouse_network::PeerId; use lighthouse_network::SyncInfo; use slog::{crit, debug, error}; @@ -29,9 +30,9 @@ const MIN_FINALIZED_CHAIN_PROCESSED_EPOCHS: u64 = 10; #[derive(Clone)] pub enum RangeSyncState { /// A finalized chain is being synced. - Finalized(u64), + Finalized(Id), /// There are no finalized chains and we are syncing one more head chains. - Head(SmallVec<[u64; PARALLEL_HEAD_CHAINS]>), + Head(SmallVec<[Id; PARALLEL_HEAD_CHAINS]>), /// There are no head or finalized chains and no long range sync is in progress. Idle, } @@ -74,7 +75,7 @@ impl ChainCollection { if syncing_id == id { // the finalized chain that was syncing was removed debug_assert!(was_syncing && sync_type == RangeSyncType::Finalized); - let syncing_head_ids: SmallVec<[u64; PARALLEL_HEAD_CHAINS]> = self + let syncing_head_ids: SmallVec<[Id; PARALLEL_HEAD_CHAINS]> = self .head_chains .iter() .filter(|(_id, chain)| chain.is_syncing()) @@ -355,7 +356,7 @@ impl ChainCollection { .collect::>(); preferred_ids.sort_unstable(); - let mut syncing_chains = SmallVec::<[u64; PARALLEL_HEAD_CHAINS]>::new(); + let mut syncing_chains = SmallVec::<[Id; PARALLEL_HEAD_CHAINS]>::new(); for (_, _, id) in preferred_ids { let chain = self.head_chains.get_mut(&id).expect("known chain"); if syncing_chains.len() < PARALLEL_HEAD_CHAINS { @@ -465,15 +466,17 @@ impl ChainCollection { sync_type: RangeSyncType, network: &mut SyncNetworkContext, ) { - let id = SyncingChain::::id(&target_head_root, &target_head_slot); let collection = if let RangeSyncType::Finalized = sync_type { &mut self.finalized_chains } else { &mut self.head_chains }; - match collection.entry(id) { - Entry::Occupied(mut entry) => { - let chain = entry.get_mut(); + + match collection + .iter_mut() + .find(|(_, chain)| chain.has_same_target(target_head_slot, target_head_root)) + { + Some((&id, chain)) => { debug!(self.log, "Adding peer to known chain"; "peer_id" => %peer, "sync_type" => ?sync_type, &chain); debug_assert_eq!(chain.target_head_root, target_head_root); debug_assert_eq!(chain.target_head_slot, target_head_slot); @@ -483,13 +486,16 @@ impl ChainCollection { } else { error!(self.log, "Chain removed after adding peer"; "chain" => id, "reason" => ?remove_reason); } - let chain = entry.remove(); - self.on_chain_removed(&id, chain.is_syncing(), sync_type); + let is_syncing = chain.is_syncing(); + collection.remove(&id); + self.on_chain_removed(&id, is_syncing, sync_type); } } - Entry::Vacant(entry) => { + None => { let peer_rpr = peer.to_string(); + let id = network.next_id(); let new_chain = SyncingChain::new( + id, start_epoch, target_head_slot, target_head_root, @@ -497,9 +503,8 @@ impl ChainCollection { sync_type.into(), &self.log, ); - debug_assert_eq!(new_chain.get_id(), id); debug!(self.log, "New chain added to sync"; "peer_id" => peer_rpr, "sync_type" => ?sync_type, &new_chain); - entry.insert(new_chain); + collection.insert(id, new_chain); metrics::inc_counter_vec(&metrics::SYNCING_CHAINS_ADDED, &[sync_type.as_str()]); self.update_metrics(); } From 70194dfc6a3f4d10c9059610f889ff5a4e863a6a Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Thu, 30 Jan 2025 18:01:34 +1100 Subject: [PATCH 103/254] Implement PeerDAS Fulu fork activation (#6795) Addresses #6706 This PR activates PeerDAS at the Fulu fork epoch instead of `EIP_7594_FORK_EPOCH`. This means we no longer support testing PeerDAS with Deneb / Electrs, as it's now part of a hard fork. --- beacon_node/beacon_chain/src/beacon_chain.rs | 51 +++ .../src/data_column_verification.rs | 2 +- beacon_node/beacon_chain/src/errors.rs | 4 + .../beacon_chain/src/fulu_readiness.rs | 11 +- beacon_node/beacon_chain/src/kzg_utils.rs | 2 +- beacon_node/beacon_chain/src/test_utils.rs | 221 +++++++++++-- .../fixtures/test_data_column_sidecars.ssz | Bin 0 -> 320512 bytes .../tests/attestation_production.rs | 15 +- .../beacon_chain/tests/block_verification.rs | 292 +++++++++++------- beacon_node/beacon_chain/tests/store_tests.rs | 63 +++- beacon_node/beacon_processor/src/lib.rs | 2 +- .../execution_layer/src/engine_api/http.rs | 30 +- .../src/test_utils/handle_rpc.rs | 43 ++- beacon_node/http_api/src/test_utils.rs | 21 +- .../tests/broadcast_validation_tests.rs | 126 ++++---- .../lighthouse_network/src/discovery/enr.rs | 8 +- .../lighthouse_network/src/rpc/codec.rs | 40 +-- .../lighthouse_network/src/rpc/protocol.rs | 15 +- .../lighthouse_network/src/types/globals.rs | 4 +- .../lighthouse_network/src/types/pubsub.rs | 22 +- .../src/network_beacon_processor/mod.rs | 5 + .../src/network_beacon_processor/tests.rs | 25 +- beacon_node/network/src/service.rs | 53 ++-- .../src/sync/block_sidecar_coupling.rs | 4 +- .../network/src/sync/network_context.rs | 2 + beacon_node/network/src/sync/tests/lookups.rs | 64 ++-- beacon_node/network/src/sync/tests/range.rs | 243 ++++++++++++--- beacon_node/store/src/hot_cold_store.rs | 61 +++- beacon_node/store/src/metadata.rs | 4 +- .../mainnet/config.yaml | 2 - consensus/fork_choice/tests/tests.rs | 2 +- consensus/types/presets/gnosis/deneb.yaml | 2 - consensus/types/presets/gnosis/eip7594.yaml | 10 - consensus/types/presets/gnosis/fulu.yaml | 9 +- consensus/types/presets/mainnet/eip7594.yaml | 10 - consensus/types/presets/mainnet/fulu.yaml | 9 +- consensus/types/presets/minimal/eip7594.yaml | 10 - consensus/types/presets/minimal/fulu.yaml | 9 +- consensus/types/src/chain_spec.rs | 51 +-- consensus/types/src/data_column_sidecar.rs | 14 - consensus/types/src/fork_name.rs | 7 + consensus/types/src/preset.rs | 20 +- crypto/kzg/src/lib.rs | 3 + scripts/local_testnet/network_params_das.yaml | 8 +- testing/ef_tests/check_all_files_accessed.py | 7 +- testing/ef_tests/src/cases.rs | 3 + .../compute_columns_for_custody_groups.rs | 8 +- .../ef_tests/src/cases/get_custody_groups.rs | 8 +- .../cases/kzg_compute_cells_and_kzg_proofs.rs | 8 +- .../cases/kzg_recover_cells_and_kzg_proofs.rs | 8 +- .../cases/kzg_verify_cell_kzg_proof_batch.rs | 8 +- testing/ef_tests/src/handler.rs | 31 +- testing/ef_tests/src/type_name.rs | 11 + testing/ef_tests/tests/tests.rs | 75 +++-- 54 files changed, 1126 insertions(+), 640 deletions(-) create mode 100644 beacon_node/beacon_chain/src/test_utils/fixtures/test_data_column_sidecars.ssz delete mode 100644 consensus/types/presets/gnosis/eip7594.yaml delete mode 100644 consensus/types/presets/mainnet/eip7594.yaml delete mode 100644 consensus/types/presets/minimal/eip7594.yaml diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index d0c294b44f..ca21b519f1 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -34,6 +34,7 @@ use crate::execution_payload::{get_execution_payload, NotifyExecutionLayer, Prep use crate::fork_choice_signal::{ForkChoiceSignalRx, ForkChoiceSignalTx, ForkChoiceWaitResult}; use crate::graffiti_calculator::GraffitiCalculator; use crate::head_tracker::{HeadTracker, HeadTrackerReader, SszHeadTracker}; +use crate::kzg_utils::reconstruct_blobs; use crate::light_client_finality_update_verification::{ Error as LightClientFinalityUpdateError, VerifiedLightClientFinalityUpdate, }; @@ -1249,6 +1250,55 @@ impl BeaconChain { self.store.get_blobs(block_root).map_err(Error::from) } + /// 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> { + self.store.get_data_columns(block_root).map_err(Error::from) + } + + /// Returns the blobs at the given root, if any. + /// + /// Uses the `block.epoch()` to determine whether to retrieve blobs or columns from the store. + /// + /// If at least 50% of columns are retrieved, blobs will be reconstructed and returned, + /// otherwise an error `InsufficientColumnsToReconstructBlobs` is returned. + /// + /// ## Errors + /// May return a database error. + pub fn get_or_reconstruct_blobs( + &self, + block_root: &Hash256, + ) -> Result>, Error> { + let Some(block) = self.store.get_blinded_block(block_root)? else { + return Ok(None); + }; + + if self.spec.is_peer_das_enabled_for_epoch(block.epoch()) { + if let Some(columns) = self.store.get_data_columns(block_root)? { + let num_required_columns = self.spec.number_of_columns / 2; + let reconstruction_possible = columns.len() >= num_required_columns as usize; + if reconstruction_possible { + reconstruct_blobs(&self.kzg, &columns, None, &block, &self.spec) + .map(Some) + .map_err(Error::FailedToReconstructBlobs) + } else { + Err(Error::InsufficientColumnsToReconstructBlobs { + columns_found: columns.len(), + }) + } + } else { + Ok(None) + } + } else { + self.get_blobs(block_root).map(|b| b.blobs()) + } + } + /// Returns the data columns at the given root, if any. /// /// ## Errors @@ -5850,6 +5900,7 @@ impl BeaconChain { let kzg = self.kzg.as_ref(); + // TODO(fulu): we no longer need blob proofs from PeerDAS and could avoid computing. kzg_utils::validate_blobs::( kzg, expected_kzg_commitments, diff --git a/beacon_node/beacon_chain/src/data_column_verification.rs b/beacon_node/beacon_chain/src/data_column_verification.rs index 1bd17485ab..565e76704e 100644 --- a/beacon_node/beacon_chain/src/data_column_verification.rs +++ b/beacon_node/beacon_chain/src/data_column_verification.rs @@ -699,7 +699,7 @@ mod test { #[tokio::test] async fn empty_data_column_sidecars_fails_validation() { - let spec = ForkName::latest().make_genesis_spec(E::default_spec()); + let spec = ForkName::Fulu.make_genesis_spec(E::default_spec()); let harness = BeaconChainHarness::builder(E::default()) .spec(spec.into()) .deterministic_keypairs(64) diff --git a/beacon_node/beacon_chain/src/errors.rs b/beacon_node/beacon_chain/src/errors.rs index 2a8fd4cd01..2e13ab4090 100644 --- a/beacon_node/beacon_chain/src/errors.rs +++ b/beacon_node/beacon_chain/src/errors.rs @@ -226,6 +226,10 @@ pub enum BeaconChainError { EmptyRpcCustodyColumns, AttestationError(AttestationError), AttestationCommitteeIndexNotSet, + InsufficientColumnsToReconstructBlobs { + columns_found: usize, + }, + FailedToReconstructBlobs(String), } easy_from_to!(SlotProcessingError, BeaconChainError); diff --git a/beacon_node/beacon_chain/src/fulu_readiness.rs b/beacon_node/beacon_chain/src/fulu_readiness.rs index 71494623f8..872fe58f2b 100644 --- a/beacon_node/beacon_chain/src/fulu_readiness.rs +++ b/beacon_node/beacon_chain/src/fulu_readiness.rs @@ -1,7 +1,7 @@ //! Provides tools for checking if a node is ready for the Fulu upgrade. use crate::{BeaconChain, BeaconChainTypes}; -use execution_layer::http::{ENGINE_GET_PAYLOAD_V5, ENGINE_NEW_PAYLOAD_V5}; +use execution_layer::http::{ENGINE_GET_PAYLOAD_V4, ENGINE_NEW_PAYLOAD_V4}; use serde::{Deserialize, Serialize}; use std::fmt; use std::time::Duration; @@ -87,14 +87,15 @@ impl BeaconChain { Ok(capabilities) => { let mut missing_methods = String::from("Required Methods Unsupported:"); let mut all_good = true; - if !capabilities.get_payload_v5 { + // TODO(fulu) switch to v5 when the EL is ready + if !capabilities.get_payload_v4 { missing_methods.push(' '); - missing_methods.push_str(ENGINE_GET_PAYLOAD_V5); + missing_methods.push_str(ENGINE_GET_PAYLOAD_V4); all_good = false; } - if !capabilities.new_payload_v5 { + if !capabilities.new_payload_v4 { missing_methods.push(' '); - missing_methods.push_str(ENGINE_NEW_PAYLOAD_V5); + missing_methods.push_str(ENGINE_NEW_PAYLOAD_V4); all_good = false; } diff --git a/beacon_node/beacon_chain/src/kzg_utils.rs b/beacon_node/beacon_chain/src/kzg_utils.rs index dcb3864f78..06cce14144 100644 --- a/beacon_node/beacon_chain/src/kzg_utils.rs +++ b/beacon_node/beacon_chain/src/kzg_utils.rs @@ -186,7 +186,7 @@ pub fn blobs_to_data_column_sidecars( .map_err(DataColumnSidecarError::BuildSidecarFailed) } -fn build_data_column_sidecars( +pub(crate) fn build_data_column_sidecars( kzg_commitments: KzgCommitments, kzg_commitments_inclusion_proof: FixedVector, signed_block_header: SignedBeaconBlockHeader, diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index ba0a2159da..e88ce71a7b 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -1,8 +1,9 @@ +use crate::blob_verification::GossipVerifiedBlob; use crate::block_verification_types::{AsBlock, RpcBlock}; -use crate::kzg_utils::blobs_to_data_column_sidecars; +use crate::data_column_verification::CustodyDataColumn; +use crate::kzg_utils::build_data_column_sidecars; use crate::observed_operations::ObservationOutcome; pub use crate::persisted_beacon_chain::PersistedBeaconChain; -use crate::BeaconBlockResponseWrapper; pub use crate::{ beacon_chain::{BEACON_CHAIN_DB_KEY, ETH1_CACHE_DB_KEY, FORK_CHOICE_DB_KEY, OP_POOL_DB_KEY}, migrate::MigratorConfig, @@ -16,6 +17,7 @@ use crate::{ BeaconChain, BeaconChainTypes, BlockError, ChainConfig, ServerSentEventHandler, StateSkipConfig, }; +use crate::{get_block_root, BeaconBlockResponseWrapper}; use bls::get_withdrawal_credentials; use eth2::types::SignedBlockContentsTuple; use execution_layer::test_utils::generate_genesis_header; @@ -74,6 +76,11 @@ pub const FORK_NAME_ENV_VAR: &str = "FORK_NAME"; // Environment variable to read if `ci_logger` feature is enabled. pub const CI_LOGGER_DIR_ENV_VAR: &str = "CI_LOGGER_DIR"; +// Pre-computed data column sidecar using a single static blob from: +// `beacon_node/execution_layer/src/test_utils/fixtures/mainnet/test_blobs_bundle.ssz` +const TEST_DATA_COLUMN_SIDECARS_SSZ: &[u8] = + include_bytes!("test_utils/fixtures/test_data_column_sidecars.ssz"); + // Default target aggregators to set during testing, this ensures an aggregator at each slot. // // You should mutate the `ChainSpec` prior to initialising the harness if you would like to use @@ -105,7 +112,7 @@ static KZG_NO_PRECOMP: LazyLock> = LazyLock::new(|| { }); pub fn get_kzg(spec: &ChainSpec) -> Arc { - if spec.eip7594_fork_epoch.is_some() { + if spec.fulu_fork_epoch.is_some() { KZG_PEERDAS.clone() } else if spec.deneb_fork_epoch.is_some() { KZG.clone() @@ -224,6 +231,7 @@ pub struct Builder { mock_execution_layer: Option>, testing_slot_clock: Option, validator_monitor_config: Option, + import_all_data_columns: bool, runtime: TestRuntime, log: Logger, } @@ -366,6 +374,7 @@ where mock_execution_layer: None, testing_slot_clock: None, validator_monitor_config: None, + import_all_data_columns: false, runtime, log, } @@ -458,6 +467,11 @@ where self } + pub fn import_all_data_columns(mut self, import_all_data_columns: bool) -> Self { + self.import_all_data_columns = import_all_data_columns; + self + } + pub fn execution_layer_from_url(mut self, url: &str) -> Self { assert!( self.execution_layer.is_none(), @@ -575,6 +589,7 @@ where .expect("should build dummy backend") .shutdown_sender(shutdown_tx) .chain_config(chain_config) + .import_all_data_columns(self.import_all_data_columns) .event_handler(Some(ServerSentEventHandler::new_with_capacity( log.clone(), 5, @@ -762,15 +777,13 @@ where pub fn get_head_block(&self) -> RpcBlock { let block = self.chain.head_beacon_block(); let block_root = block.canonical_root(); - let blobs = self.chain.get_blobs(&block_root).unwrap().blobs(); - RpcBlock::new(Some(block_root), block, blobs).unwrap() + self.build_rpc_block_from_store_blobs(Some(block_root), block) } pub fn get_full_block(&self, block_root: &Hash256) -> RpcBlock { let block = self.chain.get_blinded_block(block_root).unwrap().unwrap(); let full_block = self.chain.store.make_full_block(block_root, block).unwrap(); - let blobs = self.chain.get_blobs(block_root).unwrap().blobs(); - RpcBlock::new(Some(*block_root), Arc::new(full_block), blobs).unwrap() + self.build_rpc_block_from_store_blobs(Some(*block_root), Arc::new(full_block)) } pub fn get_all_validators(&self) -> Vec { @@ -2271,22 +2284,19 @@ where self.set_current_slot(slot); let (block, blob_items) = block_contents; - let sidecars = blob_items - .map(|(proofs, blobs)| BlobSidecar::build_sidecars(blobs, &block, proofs, &self.spec)) - .transpose() - .unwrap(); + let rpc_block = self.build_rpc_block_from_blobs(block_root, block, blob_items)?; let block_hash: SignedBeaconBlockHash = self .chain .process_block( block_root, - RpcBlock::new(Some(block_root), block, sidecars).unwrap(), + rpc_block, NotifyExecutionLayer::Yes, BlockImportSource::RangeSync, || Ok(()), ) .await? .try_into() - .unwrap(); + .expect("block blobs are available"); self.chain.recompute_head_at_current_slot().await; Ok(block_hash) } @@ -2297,16 +2307,13 @@ where ) -> Result { let (block, blob_items) = block_contents; - let sidecars = blob_items - .map(|(proofs, blobs)| BlobSidecar::build_sidecars(blobs, &block, proofs, &self.spec)) - .transpose() - .unwrap(); let block_root = block.canonical_root(); + let rpc_block = self.build_rpc_block_from_blobs(block_root, block, blob_items)?; let block_hash: SignedBeaconBlockHash = self .chain .process_block( block_root, - RpcBlock::new(Some(block_root), block, sidecars).unwrap(), + rpc_block, NotifyExecutionLayer::Yes, BlockImportSource::RangeSync, || Ok(()), @@ -2318,6 +2325,75 @@ where Ok(block_hash) } + /// Builds an `Rpc` block from a `SignedBeaconBlock` and blobs or data columns retrieved from + /// the database. + pub fn build_rpc_block_from_store_blobs( + &self, + block_root: Option, + block: Arc>, + ) -> RpcBlock { + let block_root = block_root.unwrap_or_else(|| get_block_root(&block)); + let has_blobs = block + .message() + .body() + .blob_kzg_commitments() + .is_ok_and(|c| !c.is_empty()); + if !has_blobs { + return RpcBlock::new_without_blobs(Some(block_root), block); + } + + // Blobs are stored as data columns from Fulu (PeerDAS) + if self.spec.is_peer_das_enabled_for_epoch(block.epoch()) { + let columns = self.chain.get_data_columns(&block_root).unwrap().unwrap(); + let custody_columns = columns + .into_iter() + .map(CustodyDataColumn::from_asserted_custody) + .collect::>(); + RpcBlock::new_with_custody_columns(Some(block_root), block, custody_columns, &self.spec) + .unwrap() + } else { + let blobs = self.chain.get_blobs(&block_root).unwrap().blobs(); + RpcBlock::new(Some(block_root), block, blobs).unwrap() + } + } + + /// Builds an `RpcBlock` from a `SignedBeaconBlock` and `BlobsList`. + fn build_rpc_block_from_blobs( + &self, + block_root: Hash256, + block: Arc>>, + blob_items: Option<(KzgProofs, BlobsList)>, + ) -> Result, BlockError> { + Ok(if self.spec.is_peer_das_enabled_for_epoch(block.epoch()) { + let sampling_column_count = self + .chain + .data_availability_checker + .get_sampling_column_count(); + + if blob_items.is_some_and(|(_, blobs)| !blobs.is_empty()) { + // Note: this method ignores the actual custody columns and just take the first + // `sampling_column_count` for testing purpose only, because the chain does not + // currently have any knowledge of the columns being custodied. + let columns = generate_data_column_sidecars_from_block(&block, &self.spec) + .into_iter() + .take(sampling_column_count) + .map(CustodyDataColumn::from_asserted_custody) + .collect::>(); + RpcBlock::new_with_custody_columns(Some(block_root), block, columns, &self.spec)? + } else { + RpcBlock::new_without_blobs(Some(block_root), block) + } + } else { + let blobs = blob_items + .map(|(proofs, blobs)| { + BlobSidecar::build_sidecars(blobs, &block, proofs, &self.spec) + }) + .transpose() + .unwrap(); + RpcBlock::new(Some(block_root), block, blobs)? + }) + } + pub fn process_attestations(&self, attestations: HarnessAttestations) { let num_validators = self.validator_keypairs.len(); let mut unaggregated = Vec::with_capacity(num_validators); @@ -2991,6 +3067,56 @@ where Ok(()) } + + /// Simulate some of the blobs / data columns being seen on gossip. + /// Converts the blobs to data columns if the slot is Fulu or later. + pub async fn process_gossip_blobs_or_columns<'a>( + &self, + block: &SignedBeaconBlock, + blobs: impl Iterator>, + proofs: impl Iterator, + custody_columns_opt: Option>, + ) { + let is_peerdas_enabled = self.chain.spec.is_peer_das_enabled_for_epoch(block.epoch()); + if is_peerdas_enabled { + let custody_columns = custody_columns_opt.unwrap_or_else(|| { + let sampling_column_count = self + .chain + .data_availability_checker + .get_sampling_column_count() as u64; + (0..sampling_column_count).collect() + }); + + let verified_columns = generate_data_column_sidecars_from_block(block, &self.spec) + .into_iter() + .filter(|c| custody_columns.contains(&c.index)) + .map(|sidecar| { + let column_index = sidecar.index; + self.chain + .verify_data_column_sidecar_for_gossip(sidecar, column_index) + }) + .collect::, _>>() + .unwrap(); + + if !verified_columns.is_empty() { + self.chain + .process_gossip_data_columns(verified_columns, || Ok(())) + .await + .unwrap(); + } + } else { + for (i, (kzg_proof, blob)) in proofs.into_iter().zip(blobs).enumerate() { + let sidecar = + Arc::new(BlobSidecar::new(i, blob.clone(), block, *kzg_proof).unwrap()); + let gossip_blob = GossipVerifiedBlob::new(sidecar, i as u64, &self.chain) + .expect("should obtain gossip verified blob"); + self.chain + .process_gossip_blob(gossip_blob) + .await + .expect("should import valid gossip verified blob"); + } + } + } } // Junk `Debug` impl to satistfy certain trait bounds during testing. @@ -3176,10 +3302,59 @@ pub fn generate_rand_block_and_data_columns( SignedBeaconBlock>, DataColumnSidecarList, ) { - let kzg = get_kzg(spec); - let (block, blobs) = generate_rand_block_and_blobs(fork_name, num_blobs, rng, spec); - let blob_refs = blobs.iter().map(|b| &b.blob).collect::>(); - let data_columns = blobs_to_data_column_sidecars(&blob_refs, &block, &kzg, spec).unwrap(); - + let (block, _blobs) = generate_rand_block_and_blobs(fork_name, num_blobs, rng, spec); + let data_columns = generate_data_column_sidecars_from_block(&block, spec); (block, data_columns) } + +/// Generate data column sidecars from pre-computed cells and proofs. +fn generate_data_column_sidecars_from_block( + block: &SignedBeaconBlock, + spec: &ChainSpec, +) -> DataColumnSidecarList { + let kzg_commitments = block.message().body().blob_kzg_commitments().unwrap(); + if kzg_commitments.is_empty() { + return vec![]; + } + + let kzg_commitments_inclusion_proof = block + .message() + .body() + .kzg_commitments_merkle_proof() + .unwrap(); + let signed_block_header = block.signed_block_header(); + + // load the precomputed column sidecar to avoid computing them for every block in the tests. + let template_data_columns = RuntimeVariableList::>::from_ssz_bytes( + TEST_DATA_COLUMN_SIDECARS_SSZ, + spec.number_of_columns as usize, + ) + .unwrap(); + + let (cells, proofs) = template_data_columns + .into_iter() + .map(|sidecar| { + let DataColumnSidecar { + column, kzg_proofs, .. + } = sidecar; + // There's only one cell per column for a single blob + let cell_bytes: Vec = column.into_iter().next().unwrap().into(); + let kzg_cell = cell_bytes.try_into().unwrap(); + let kzg_proof = kzg_proofs.into_iter().next().unwrap(); + (kzg_cell, kzg_proof) + }) + .collect::<(Vec<_>, Vec<_>)>(); + + // Repeat the cells and proofs for every blob + let blob_cells_and_proofs_vec = + vec![(cells.try_into().unwrap(), proofs.try_into().unwrap()); kzg_commitments.len()]; + + build_data_column_sidecars( + kzg_commitments.clone(), + kzg_commitments_inclusion_proof, + signed_block_header, + blob_cells_and_proofs_vec, + spec, + ) + .unwrap() +} diff --git a/beacon_node/beacon_chain/src/test_utils/fixtures/test_data_column_sidecars.ssz b/beacon_node/beacon_chain/src/test_utils/fixtures/test_data_column_sidecars.ssz new file mode 100644 index 0000000000000000000000000000000000000000..112dd43b0474b1d1263d951429f7865f24bf1033 GIT binary patch literal 320512 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