From 2c76ee5b6b03cdcd43563e89d1befa7f07f4cc75 Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Wed, 20 May 2026 06:56:49 -0600 Subject: [PATCH 01/18] Gloas lookup sync boilerplate (#9322) Implements the boring boilerplate to send envelopes by root requests and process them. Pre-step to - https://github.com/sigp/lighthouse/pull/9155 Co-Authored-By: dapplion <35266934+dapplion@users.noreply.github.com> --- beacon_node/beacon_chain/src/beacon_chain.rs | 6 + beacon_node/beacon_processor/src/lib.rs | 10 ++ .../src/scheduler/work_queue.rs | 6 + .../src/service/api_types.rs | 2 + .../src/network_beacon_processor/mod.rs | 19 +++ .../network_beacon_processor/sync_methods.rs | 57 +++++++ beacon_node/network/src/router.rs | 35 ++++- .../network/src/sync/block_lookups/mod.rs | 2 + beacon_node/network/src/sync/manager.rs | 66 +++++++- .../network/src/sync/network_context.rs | 145 +++++++++++++++++- .../src/sync/network_context/requests.rs | 4 + .../requests/payload_envelopes_by_root.rs | 54 +++++++ 12 files changed, 398 insertions(+), 8 deletions(-) create mode 100644 beacon_node/network/src/sync/network_context/requests/payload_envelopes_by_root.rs diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index af8cd477d6..f3f6cd299e 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -6339,6 +6339,12 @@ impl BeaconChain { .contains_block(root) } + pub fn envelope_is_known_to_fork_choice(&self, root: &Hash256) -> bool { + self.canonical_head + .fork_choice_read_lock() + .is_payload_received(root) + } + /// Determines the beacon proposer for the next slot. If that proposer is registered in the /// `execution_layer`, provide the `execution_layer` with the necessary information to produce /// `PayloadAttributes` for future calls to fork choice. diff --git a/beacon_node/beacon_processor/src/lib.rs b/beacon_node/beacon_processor/src/lib.rs index 25944bcf8a..ce3851ea54 100644 --- a/beacon_node/beacon_processor/src/lib.rs +++ b/beacon_node/beacon_processor/src/lib.rs @@ -418,6 +418,7 @@ pub enum Work { process_fn: AsyncFn, }, RpcCustodyColumn(AsyncFn), + RpcEnvelope(AsyncFn), ColumnReconstruction(AsyncFn), IgnoredRpcBlock { process_fn: BlockingFn, @@ -485,6 +486,7 @@ pub enum WorkType { RpcBlock, RpcBlobs, RpcCustodyColumn, + RpcEnvelope, ColumnReconstruction, IgnoredRpcBlock, ChainSegment, @@ -548,6 +550,7 @@ impl Work { Work::RpcBlock { .. } => WorkType::RpcBlock, Work::RpcBlobs { .. } => WorkType::RpcBlobs, Work::RpcCustodyColumn { .. } => WorkType::RpcCustodyColumn, + Work::RpcEnvelope(_) => WorkType::RpcEnvelope, Work::ColumnReconstruction(_) => WorkType::ColumnReconstruction, Work::IgnoredRpcBlock { .. } => WorkType::IgnoredRpcBlock, Work::ChainSegment { .. } => WorkType::ChainSegment, @@ -825,6 +828,8 @@ impl BeaconProcessor { Some(item) } else if let Some(item) = work_queues.rpc_custody_column_queue.pop() { Some(item) + } else if let Some(item) = work_queues.rpc_envelope_queue.pop() { + Some(item) // Check delayed blocks before gossip blocks, the gossip blocks might rely // on the delayed ones. } else if let Some(item) = work_queues.delayed_block_queue.pop() { @@ -1192,6 +1197,9 @@ impl BeaconProcessor { work_queues.rpc_block_queue.push(work, work_id) } Work::RpcBlobs { .. } => work_queues.rpc_blob_queue.push(work, work_id), + Work::RpcEnvelope(_) => { + work_queues.rpc_envelope_queue.push(work, work_id) + } Work::RpcCustodyColumn { .. } => { work_queues.rpc_custody_column_queue.push(work, work_id) } @@ -1330,6 +1338,7 @@ impl BeaconProcessor { WorkType::RpcBlobs | WorkType::IgnoredRpcBlock => { work_queues.rpc_blob_queue.len() } + WorkType::RpcEnvelope => work_queues.rpc_envelope_queue.len(), WorkType::RpcCustodyColumn => work_queues.rpc_custody_column_queue.len(), WorkType::ColumnReconstruction => { work_queues.column_reconstruction_queue.len() @@ -1523,6 +1532,7 @@ impl BeaconProcessor { } | Work::RpcBlobs { process_fn } | Work::RpcCustodyColumn(process_fn) + | Work::RpcEnvelope(process_fn) | Work::ColumnReconstruction(process_fn) => task_spawner.spawn_async(process_fn), Work::IgnoredRpcBlock { process_fn } => task_spawner.spawn_blocking(process_fn), Work::GossipBlock(work) diff --git a/beacon_node/beacon_processor/src/scheduler/work_queue.rs b/beacon_node/beacon_processor/src/scheduler/work_queue.rs index eb57b97df2..2fdc15182c 100644 --- a/beacon_node/beacon_processor/src/scheduler/work_queue.rs +++ b/beacon_node/beacon_processor/src/scheduler/work_queue.rs @@ -120,6 +120,7 @@ pub struct BeaconProcessorQueueLengths { rpc_block_queue: usize, rpc_blob_queue: usize, rpc_custody_column_queue: usize, + rpc_envelope_queue: usize, column_reconstruction_queue: usize, chain_segment_queue: usize, backfill_chain_segment: usize, @@ -195,6 +196,8 @@ impl BeaconProcessorQueueLengths { // We don't request more than `PARENT_DEPTH_TOLERANCE` (32) lookups, so we can limit // this queue size. With 48 max blobs per block, each column sidecar list could be up to 12MB. rpc_custody_column_queue: 64, + // Bounded by `PARENT_DEPTH_TOLERANCE`; one envelope per Gloas block. + rpc_envelope_queue: 1024, column_reconstruction_queue: 1, chain_segment_queue: 64, backfill_chain_segment: 64, @@ -253,6 +256,7 @@ pub struct WorkQueues { pub rpc_block_queue: FifoQueue>, pub rpc_blob_queue: FifoQueue>, pub rpc_custody_column_queue: FifoQueue>, + pub rpc_envelope_queue: FifoQueue>, pub column_reconstruction_queue: LifoQueue>, pub chain_segment_queue: FifoQueue>, pub backfill_chain_segment: FifoQueue>, @@ -323,6 +327,7 @@ impl WorkQueues { let rpc_block_queue = FifoQueue::new(queue_lengths.rpc_block_queue); let rpc_blob_queue = FifoQueue::new(queue_lengths.rpc_blob_queue); let rpc_custody_column_queue = FifoQueue::new(queue_lengths.rpc_custody_column_queue); + let rpc_envelope_queue = FifoQueue::new(queue_lengths.rpc_envelope_queue); let column_reconstruction_queue = LifoQueue::new(queue_lengths.column_reconstruction_queue); let chain_segment_queue = FifoQueue::new(queue_lengths.chain_segment_queue); let backfill_chain_segment = FifoQueue::new(queue_lengths.backfill_chain_segment); @@ -391,6 +396,7 @@ impl WorkQueues { rpc_block_queue, rpc_blob_queue, rpc_custody_column_queue, + rpc_envelope_queue, chain_segment_queue, column_reconstruction_queue, backfill_chain_segment, diff --git a/beacon_node/lighthouse_network/src/service/api_types.rs b/beacon_node/lighthouse_network/src/service/api_types.rs index f598f59aee..2429b813e9 100644 --- a/beacon_node/lighthouse_network/src/service/api_types.rs +++ b/beacon_node/lighthouse_network/src/service/api_types.rs @@ -23,6 +23,8 @@ pub enum SyncRequestId { SingleBlock { id: SingleLookupReqId }, /// Request searching for a set of blobs given a hash. SingleBlob { id: SingleLookupReqId }, + /// Request searching for a payload envelope given a hash. + SinglePayloadEnvelope { id: SingleLookupReqId }, /// Request searching for a set of data columns given a hash and list of column indices. DataColumnsByRoot(DataColumnsByRootRequestId), /// Blocks by range request diff --git a/beacon_node/network/src/network_beacon_processor/mod.rs b/beacon_node/network/src/network_beacon_processor/mod.rs index 7bf969db10..7817feb0bd 100644 --- a/beacon_node/network/src/network_beacon_processor/mod.rs +++ b/beacon_node/network/src/network_beacon_processor/mod.rs @@ -588,6 +588,25 @@ impl NetworkBeaconProcessor { }) } + /// Create a new `Work` event for an RPC-fetched payload envelope. `process_lookup_envelope` + /// reports the result back to sync. + pub fn send_lookup_envelope( + self: &Arc, + block_root: Hash256, + envelope: Arc>, + seen_timestamp: Duration, + process_type: BlockProcessType, + ) -> Result<(), Error> { + let s = self.clone(); + self.try_send(BeaconWorkEvent { + drop_during_sync: false, + work: Work::RpcEnvelope(Box::pin(async move { + s.process_lookup_envelope(block_root, envelope, seen_timestamp, process_type) + .await; + })), + }) + } + /// Create a new `Work` event for some custody columns. `process_rpc_custody_columns` reports /// the result back to sync. pub fn send_rpc_custody_columns( diff --git a/beacon_node/network/src/network_beacon_processor/sync_methods.rs b/beacon_node/network/src/network_beacon_processor/sync_methods.rs index 988a68c9dd..e3ba6fb3c4 100644 --- a/beacon_node/network/src/network_beacon_processor/sync_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/sync_methods.rs @@ -426,6 +426,63 @@ impl NetworkBeaconProcessor { }); } + /// Attempt to verify and import an execution payload envelope received via RPC. + #[instrument( + name = "lh_process_lookup_envelope", + parent = None, + level = "debug", + skip_all, + fields(?block_root), + )] + pub async fn process_lookup_envelope( + self: Arc>, + block_root: Hash256, + envelope: Arc>, + _seen_timestamp: Duration, + process_type: BlockProcessType, + ) { + debug!( + ?block_root, + slot = %envelope.slot(), + ?process_type, + "Processing RPC payload envelope" + ); + + // Gossip verification runs the same signature / slot / builder-index / block-hash checks + // independently of gossip propagation, so we can reuse it for RPC-fetched envelopes. + #[allow(clippy::result_large_err)] + let result = match self + .chain + .clone() + .verify_envelope_for_gossip(envelope.clone()) + .await + { + Ok(verified) => { + self.chain + .process_execution_payload_envelope( + block_root, + verified, + NotifyExecutionLayer::Yes, + BlockImportSource::Lookup, + || Ok(()), + ) + .await + } + Err(e) => Err(e), + }; + + // TODO(gloas): structured penalty classification arrives with the envelope lookup state + // machine; for now, fold the EnvelopeError into BlockError::InternalError so it flows + // through the existing `BlockProcessingResult::Err` path. + let result: Result = + result.map_err(|e| BlockError::InternalError(format!("envelope: {e}"))); + + self.send_sync_message(SyncMessage::BlockComponentProcessed { + process_type, + result: result.into(), + }); + } + pub fn process_historic_data_columns( &self, batch_id: CustodyBackfillBatchId, diff --git a/beacon_node/network/src/router.rs b/beacon_node/network/src/router.rs index a718997e0a..35939c6f39 100644 --- a/beacon_node/network/src/router.rs +++ b/beacon_node/network/src/router.rs @@ -26,6 +26,7 @@ use tokio_stream::wrappers::UnboundedReceiverStream; use tracing::{debug, error, trace, warn}; use types::{ BlobSidecar, DataColumnSidecar, EthSpec, ForkContext, PartialDataColumn, SignedBeaconBlock, + SignedExecutionPayloadEnvelope, }; /// Handles messages from the network and routes them to the appropriate service to be handled. @@ -348,10 +349,13 @@ impl Router { Response::DataColumnsByRange(data_column) => { self.on_data_columns_by_range_response(peer_id, app_request_id, data_column); } - // TODO(EIP-7732): implement outgoing payload envelopes by range and root - // responses once sync manager requests them. - Response::PayloadEnvelopesByRoot(_) | Response::PayloadEnvelopesByRange(_) => { - debug!("Requesting envelopes by root and by range not supported yet"); + Response::PayloadEnvelopesByRoot(envelope) => { + self.on_payload_envelopes_by_root_response(peer_id, app_request_id, envelope); + } + // TODO(EIP-7732): implement outgoing payload envelopes by range responses + // once sync manager requests them. + Response::PayloadEnvelopesByRange(_) => { + debug!("Requesting envelopes by range not supported yet"); } // Lighthouse currently only serves BlocksByHead and does not issue it as a client, // so receiving a response is unexpected. Drop it without crashing. @@ -821,6 +825,29 @@ impl Router { } } + /// Handle a `PayloadEnvelopesByRoot` response from the peer. + pub fn on_payload_envelopes_by_root_response( + &mut self, + peer_id: PeerId, + app_request_id: AppRequestId, + envelope: Option>>, + ) { + let sync_request_id = match app_request_id { + AppRequestId::Sync(id @ SyncRequestId::SinglePayloadEnvelope { .. }) => id, + other => { + crit!(request = ?other, %peer_id, "PayloadEnvelopesByRoot response on incorrect request"); + return; + } + }; + + self.send_to_sync(SyncMessage::RpcPayloadEnvelope { + sync_request_id, + peer_id, + envelope, + seen_timestamp: self.chain.slot_clock.now_duration().unwrap_or_default(), + }); + } + fn handle_beacon_processor_send_result( &mut self, result: Result<(), crate::network_beacon_processor::Error>, diff --git a/beacon_node/network/src/sync/block_lookups/mod.rs b/beacon_node/network/src/sync/block_lookups/mod.rs index 3929f74aa0..f10610c751 100644 --- a/beacon_node/network/src/sync/block_lookups/mod.rs +++ b/beacon_node/network/src/sync/block_lookups/mod.rs @@ -559,6 +559,8 @@ impl BlockLookups { BlockProcessType::SingleCustodyColumn(id) => { self.on_processing_result_inner::>(id, result, cx) } + // TODO(gloas): route into the payload envelope lookup state machine. + BlockProcessType::SinglePayloadEnvelope(_) => Ok(LookupResult::Pending), }; self.on_lookup_result(process_type.id(), lookup_result, "processing_result", cx); } diff --git a/beacon_node/network/src/sync/manager.rs b/beacon_node/network/src/sync/manager.rs index 347b018a93..14a38f0e72 100644 --- a/beacon_node/network/src/sync/manager.rs +++ b/beacon_node/network/src/sync/manager.rs @@ -73,7 +73,8 @@ use strum::IntoStaticStr; use tokio::sync::mpsc; use tracing::{debug, error, info, trace}; use types::{ - BlobSidecar, DataColumnSidecar, EthSpec, ForkContext, Hash256, SignedBeaconBlock, Slot, + BlobSidecar, DataColumnSidecar, EthSpec, ForkContext, Hash256, SignedBeaconBlock, + SignedExecutionPayloadEnvelope, Slot, }; /// The number of slots ahead of us that is allowed before requesting a long-range (batch) Sync @@ -132,6 +133,14 @@ pub enum SyncMessage { seen_timestamp: Duration, }, + /// A payload envelope has been received from the RPC. + RpcPayloadEnvelope { + sync_request_id: SyncRequestId, + peer_id: PeerId, + envelope: Option>>, + seen_timestamp: Duration, + }, + /// A block with an unknown parent has been received. UnknownParentBlock(PeerId, Arc>, Hash256), @@ -193,6 +202,7 @@ pub enum BlockProcessType { SingleBlock { id: Id }, SingleBlob { id: Id }, SingleCustodyColumn(Id), + SinglePayloadEnvelope(Id), } impl BlockProcessType { @@ -200,7 +210,8 @@ impl BlockProcessType { match self { BlockProcessType::SingleBlock { id } | BlockProcessType::SingleBlob { id } - | BlockProcessType::SingleCustodyColumn(id) => *id, + | BlockProcessType::SingleCustodyColumn(id) + | BlockProcessType::SinglePayloadEnvelope(id) => *id, } } } @@ -502,6 +513,9 @@ impl SyncManager { SyncRequestId::SingleBlob { id } => { self.on_single_blob_response(id, peer_id, RpcEvent::RPCError(error)) } + SyncRequestId::SinglePayloadEnvelope { id } => { + self.on_single_payload_envelope_response(id, peer_id, RpcEvent::RPCError(error)) + } SyncRequestId::DataColumnsByRoot(req_id) => { self.on_data_columns_by_root_response(req_id, peer_id, RpcEvent::RPCError(error)) } @@ -848,6 +862,17 @@ impl SyncManager { } => { self.rpc_data_column_received(sync_request_id, peer_id, data_column, seen_timestamp) } + SyncMessage::RpcPayloadEnvelope { + sync_request_id, + peer_id, + envelope, + seen_timestamp, + } => self.rpc_payload_envelope_received( + sync_request_id, + peer_id, + envelope, + seen_timestamp, + ), SyncMessage::UnknownParentBlock(peer_id, block, block_root) => { let block_slot = block.slot(); let parent_root = block.parent_root(); @@ -1209,6 +1234,27 @@ impl SyncManager { } } + // TODO(gloas): dispatch into block_lookups once the envelope lookup state machine lands. + fn rpc_payload_envelope_received( + &mut self, + sync_request_id: SyncRequestId, + peer_id: PeerId, + envelope: Option>>, + seen_timestamp: Duration, + ) { + match sync_request_id { + SyncRequestId::SinglePayloadEnvelope { id } => self + .on_single_payload_envelope_response( + id, + peer_id, + RpcEvent::from_chunk(envelope, seen_timestamp), + ), + _ => { + crit!(%peer_id, "bad request id for payload envelope"); + } + } + } + fn rpc_data_column_received( &mut self, sync_request_id: SyncRequestId, @@ -1237,6 +1283,22 @@ impl SyncManager { } } + fn on_single_payload_envelope_response( + &mut self, + id: SingleLookupReqId, + peer_id: PeerId, + envelope: RpcEvent>>, + ) { + if let Some(_resp) = self + .network + .on_single_payload_envelope_response(id, peer_id, envelope) + { + // TODO(gloas): dispatch into + // `block_lookups.on_download_response::>(...)` once + // the envelope lookup state machine lands. + } + } + fn on_single_blob_response( &mut self, id: SingleLookupReqId, diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index 465e23998b..9d5ac40c0a 100644 --- a/beacon_node/network/src/sync/network_context.rs +++ b/beacon_node/network/src/sync/network_context.rs @@ -2,7 +2,10 @@ //! channel and stores a global RPC ID to perform requests. use self::custody::{ActiveCustodyRequest, Error as CustodyRequestError}; -pub use self::requests::{BlocksByRootSingleRequest, DataColumnsByRootSingleBlockRequest}; +pub use self::requests::{ + BlocksByRootSingleRequest, DataColumnsByRootSingleBlockRequest, + PayloadEnvelopesByRootSingleRequest, +}; use super::SyncMessage; use super::block_sidecar_coupling::RangeBlockComponentsRequest; use super::manager::BlockProcessType; @@ -37,6 +40,7 @@ pub use requests::LookupVerifyError; use requests::{ ActiveRequests, BlobsByRangeRequestItems, BlobsByRootRequestItems, BlocksByRangeRequestItems, BlocksByRootRequestItems, DataColumnsByRangeRequestItems, DataColumnsByRootRequestItems, + PayloadEnvelopesByRootRequestItems, }; #[cfg(test)] use slot_clock::SlotClock; @@ -52,7 +56,7 @@ use tracing::{Span, debug, debug_span, error, warn}; use types::data::FixedBlobSidecarList; use types::{ BlobSidecar, BlockImportSource, ColumnIndex, DataColumnSidecar, DataColumnSidecarList, EthSpec, - ForkContext, Hash256, SignedBeaconBlock, Slot, + ForkContext, Hash256, SignedBeaconBlock, SignedExecutionPayloadEnvelope, Slot, }; pub mod custody; @@ -201,6 +205,9 @@ pub struct SyncNetworkContext { ActiveRequests>, /// A mapping of active BlobsByRoot requests, including both current slot and parent lookups. blobs_by_root_requests: ActiveRequests>, + /// A mapping of active PayloadEnvelopesByRoot requests + payload_envelopes_by_root_requests: + ActiveRequests>, /// A mapping of active DataColumnsByRoot requests data_columns_by_root_requests: ActiveRequests>, @@ -294,6 +301,7 @@ impl SyncNetworkContext { request_id: 1, blocks_by_root_requests: ActiveRequests::new("blocks_by_root"), blobs_by_root_requests: ActiveRequests::new("blobs_by_root"), + payload_envelopes_by_root_requests: ActiveRequests::new("payload_envelopes_by_root"), data_columns_by_root_requests: ActiveRequests::new("data_columns_by_root"), blocks_by_range_requests: ActiveRequests::new("blocks_by_range"), blobs_by_range_requests: ActiveRequests::new("blobs_by_range"), @@ -322,6 +330,7 @@ impl SyncNetworkContext { request_id: _, blocks_by_root_requests, blobs_by_root_requests, + payload_envelopes_by_root_requests, data_columns_by_root_requests, blocks_by_range_requests, blobs_by_range_requests, @@ -345,6 +354,10 @@ impl SyncNetworkContext { .active_requests_of_peer(peer_id) .into_iter() .map(|id| SyncRequestId::SingleBlob { id: *id }); + let payload_envelopes_by_root_ids = payload_envelopes_by_root_requests + .active_requests_of_peer(peer_id) + .into_iter() + .map(|id| SyncRequestId::SinglePayloadEnvelope { id: *id }); let data_column_by_root_ids = data_columns_by_root_requests .active_requests_of_peer(peer_id) .into_iter() @@ -363,6 +376,7 @@ impl SyncNetworkContext { .map(|req_id| SyncRequestId::DataColumnsByRange(*req_id)); blocks_by_root_ids .chain(blobs_by_root_ids) + .chain(payload_envelopes_by_root_ids) .chain(data_column_by_root_ids) .chain(blocks_by_range_ids) .chain(blobs_by_range_ids) @@ -419,6 +433,7 @@ impl SyncNetworkContext { request_id: _, blocks_by_root_requests, blobs_by_root_requests, + payload_envelopes_by_root_requests, data_columns_by_root_requests, blocks_by_range_requests, blobs_by_range_requests, @@ -441,6 +456,7 @@ impl SyncNetworkContext { for peer_id in blocks_by_root_requests .iter_request_peers() .chain(blobs_by_root_requests.iter_request_peers()) + .chain(payload_envelopes_by_root_requests.iter_request_peers()) .chain(data_columns_by_root_requests.iter_request_peers()) .chain(blocks_by_range_requests.iter_request_peers()) .chain(blobs_by_range_requests.iter_request_peers()) @@ -927,6 +943,81 @@ impl SyncNetworkContext { Ok(LookupRequestResult::RequestSent(id.req_id)) } + /// Request a payload envelope for a block root via PayloadEnvelopesByRoot RPC. + #[allow(dead_code)] + pub fn payload_lookup_request( + &mut self, + lookup_id: SingleLookupId, + lookup_peers: Arc>>, + block_root: Hash256, + ) -> Result { + // Skip the download if fork-choice already saw this envelope (e.g. imported via gossip + // before the lookup got here). + if self.chain.envelope_is_known_to_fork_choice(&block_root) { + return Ok(LookupRequestResult::NoRequestNeeded( + "envelope already known to fork-choice", + )); + } + + let active_request_count_by_peer = self.active_request_count_by_peer(); + let Some(peer_id) = lookup_peers + .read() + .iter() + .map(|peer| { + ( + active_request_count_by_peer.get(peer).copied().unwrap_or(0), + rand::random::(), + peer, + ) + }) + .min() + .map(|(_, _, peer)| *peer) + else { + return Ok(LookupRequestResult::Pending("no peers")); + }; + + let id = SingleLookupReqId { + lookup_id, + req_id: self.next_id(), + }; + + let request = PayloadEnvelopesByRootSingleRequest { block_root }; + + let network_request = RequestType::PayloadEnvelopesByRoot( + request + .clone() + .into_request(&self.fork_context) + .map_err(RpcRequestSendError::InternalError)?, + ); + self.network_send + .send(NetworkMessage::SendRequest { + peer_id, + request: network_request, + app_request_id: AppRequestId::Sync(SyncRequestId::SinglePayloadEnvelope { id }), + }) + .map_err(|_| RpcRequestSendError::InternalError("network send error".to_owned()))?; + + debug!( + method = "PayloadEnvelopesByRoot", + ?block_root, + peer = %peer_id, + %id, + "Sync RPC request sent" + ); + + self.payload_envelopes_by_root_requests.insert( + id, + peer_id, + // true = enforce that the peer returns a response. We only request a single envelope + // and the peer must have it. + true, + PayloadEnvelopesByRootRequestItems::new(request), + Span::none(), + ); + + Ok(LookupRequestResult::RequestSent(id.req_id)) + } + /// Request necessary blobs for `block_root`. Requests only the necessary blobs by checking: /// - If we have a downloaded but not yet processed block /// - If the da_checker has a pending block @@ -1476,6 +1567,27 @@ impl SyncNetworkContext { self.on_rpc_response_result(resp, peer_id) } + pub(crate) fn on_single_payload_envelope_response( + &mut self, + id: SingleLookupReqId, + peer_id: PeerId, + rpc_event: RpcEvent>>, + ) -> Option>>> { + let resp = self + .payload_envelopes_by_root_requests + .on_response(id, rpc_event); + let resp = resp.map(|res| { + res.and_then(|(mut envelopes, seen_timestamp)| { + match envelopes.pop() { + Some(envelope) => Ok((envelope, seen_timestamp)), + // Should never happen, we enforce at least 1 chunk. + None => Err(LookupVerifyError::NotEnoughResponsesReturned { actual: 0 }.into()), + } + }) + }); + self.on_rpc_response_result(resp, peer_id) + } + #[allow(clippy::type_complexity)] pub(crate) fn on_data_columns_by_root_response( &mut self, @@ -1652,6 +1764,35 @@ impl SyncNetworkContext { }) } + #[allow(dead_code)] + pub fn send_payload_for_processing( + &self, + block_root: Hash256, + envelope: Arc>, + seen_timestamp: Duration, + process_type: BlockProcessType, + ) -> Result<(), SendErrorProcessor> { + let beacon_processor = self + .beacon_processor_if_enabled() + .ok_or(SendErrorProcessor::ProcessorNotAvailable)?; + + debug!( + ?block_root, + ?process_type, + "Sending payload envelope for processing" + ); + + beacon_processor + .send_lookup_envelope(block_root, envelope, seen_timestamp, process_type) + .map_err(|e| { + error!( + error = ?e, + "Failed to send sync payload envelope to processor" + ); + SendErrorProcessor::SendError + }) + } + pub fn send_custody_columns_for_processing( &self, _id: Id, diff --git a/beacon_node/network/src/sync/network_context/requests.rs b/beacon_node/network/src/sync/network_context/requests.rs index ad60dffb45..8c091eca80 100644 --- a/beacon_node/network/src/sync/network_context/requests.rs +++ b/beacon_node/network/src/sync/network_context/requests.rs @@ -16,6 +16,9 @@ pub use data_columns_by_range::DataColumnsByRangeRequestItems; pub use data_columns_by_root::{ DataColumnsByRootRequestItems, DataColumnsByRootSingleBlockRequest, }; +pub use payload_envelopes_by_root::{ + PayloadEnvelopesByRootRequestItems, PayloadEnvelopesByRootSingleRequest, +}; use crate::metrics; @@ -27,6 +30,7 @@ mod blocks_by_range; mod blocks_by_root; mod data_columns_by_range; mod data_columns_by_root; +mod payload_envelopes_by_root; #[derive(Debug, PartialEq, Eq, IntoStaticStr)] pub enum LookupVerifyError { diff --git a/beacon_node/network/src/sync/network_context/requests/payload_envelopes_by_root.rs b/beacon_node/network/src/sync/network_context/requests/payload_envelopes_by_root.rs new file mode 100644 index 0000000000..a142d86e90 --- /dev/null +++ b/beacon_node/network/src/sync/network_context/requests/payload_envelopes_by_root.rs @@ -0,0 +1,54 @@ +use lighthouse_network::rpc::methods::PayloadEnvelopesByRootRequest; +use std::sync::Arc; +use types::{EthSpec, ForkContext, Hash256, SignedExecutionPayloadEnvelope}; + +use super::{ActiveRequestItems, LookupVerifyError}; + +#[derive(Debug, Clone)] +pub struct PayloadEnvelopesByRootSingleRequest { + pub block_root: Hash256, +} + +impl PayloadEnvelopesByRootSingleRequest { + pub fn into_request( + self, + fork_context: &ForkContext, + ) -> Result { + PayloadEnvelopesByRootRequest::new(vec![self.block_root], fork_context) + } +} + +pub struct PayloadEnvelopesByRootRequestItems { + request: PayloadEnvelopesByRootSingleRequest, + items: Vec>>, +} + +impl PayloadEnvelopesByRootRequestItems { + pub fn new(request: PayloadEnvelopesByRootSingleRequest) -> Self { + Self { + request, + items: vec![], + } + } +} + +impl ActiveRequestItems for PayloadEnvelopesByRootRequestItems { + type Item = Arc>; + + /// Append a response to the single chunk request. We expect exactly one envelope per + /// block root. Returns `true` when the single expected item has been received. + fn add(&mut self, envelope: Self::Item) -> Result { + let block_root = envelope.message.beacon_block_root; + if self.request.block_root != block_root { + return Err(LookupVerifyError::UnrequestedBlockRoot(block_root)); + } + + self.items.push(envelope); + // Always returns true, we expect a single envelope per block root + Ok(true) + } + + fn consume(&mut self) -> Vec { + std::mem::take(&mut self.items) + } +} From a9637c16502abb0215b9a76b7067207b6bc70d8c Mon Sep 17 00:00:00 2001 From: Daniel Knopik <107140945+dknopik@users.noreply.github.com> Date: Thu, 21 May 2026 05:25:02 +0200 Subject: [PATCH 02/18] Partial columns cleanup (#9321) #8314 left a few ugly potentially panicking location behind - all of them believed to be unreachable, but this PR fixes them regardless for good hygiene. Update to `ethereum_ssz 0.10.4` for two new helpers: `not_inplace` and `clone_zeroed`. Remove remaining `expect` and `todo!` in favour of these helpers and one new fallible (but practically infallible) method. Co-Authored-By: Daniel Knopik --- Cargo.lock | 4 ++-- .../src/data_column_verification.rs | 11 ++++++++- beacon_node/http_api/src/publish_blocks.rs | 12 ++++++---- .../lighthouse_network/src/types/partial.rs | 18 +++++--------- .../gossip_methods.rs | 24 +++++++++++-------- .../types/src/data/data_column_sidecar.rs | 17 ++++++------- 6 files changed, 47 insertions(+), 39 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 078f699f3c..d42bcd8fc1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3282,9 +3282,9 @@ dependencies = [ [[package]] name = "ethereum_ssz" -version = "0.10.3" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368a4a4e4273b0135111fe9464e35465067766a8f664615b5a86338b73864407" +checksum = "e462875ad8693755ea8913d6e905715c76ea4836e2254e18c9cf0f7a8f8c2a13" dependencies = [ "alloy-primitives", "arbitrary", diff --git a/beacon_node/beacon_chain/src/data_column_verification.rs b/beacon_node/beacon_chain/src/data_column_verification.rs index 71562b376b..45cd687b36 100644 --- a/beacon_node/beacon_chain/src/data_column_verification.rs +++ b/beacon_node/beacon_chain/src/data_column_verification.rs @@ -1220,7 +1220,16 @@ pub fn validate_partial_data_column_sidecar_for_gossip( header, }; } - Err(MissingCellsError::UnexpectedError(e)) => todo!("handle unexpected error {:?}", e), + Err(MissingCellsError::UnexpectedError(e)) => { + return PartialColumnVerificationResult::ErrWithValidHeader { + err: GossipDataColumnError::InternalError(format!( + "An unexpected error occurred while validating partial data columns: {:?}", + e + )) + .into(), + header, + }; + } }; // We do not have to check block related data here, as we create the verifiable column from diff --git a/beacon_node/http_api/src/publish_blocks.rs b/beacon_node/http_api/src/publish_blocks.rs index e96c86b17f..ca4ab85524 100644 --- a/beacon_node/http_api/src/publish_blocks.rs +++ b/beacon_node/http_api/src/publish_blocks.rs @@ -524,11 +524,15 @@ pub(crate) fn publish_column_sidecars( if chain.config.enable_partial_columns && let DataColumnSidecar::Fulu(fulu_data_col) = data_col.as_ref() { - let mut partial = fulu_data_col.to_partial(); - if let Some(header) = partial.sidecar.header.take() { - partial_header = Some(header); + match fulu_data_col.to_partial() { + Ok(mut partial) => { + if let Some(header) = partial.sidecar.header.take() { + partial_header = Some(header); + } + partial_columns.push(Arc::new(partial)); + } + Err(err) => crit!(?err, "Could not convert from full to partial"), } - partial_columns.push(Arc::new(partial)); } let subnet = DataColumnSubnetId::from_column_index(*data_col.index(), &chain.spec); diff --git a/beacon_node/lighthouse_network/src/types/partial.rs b/beacon_node/lighthouse_network/src/types/partial.rs index f25ce9ec36..26705b7106 100644 --- a/beacon_node/lighthouse_network/src/types/partial.rs +++ b/beacon_node/lighthouse_network/src/types/partial.rs @@ -9,7 +9,7 @@ use std::sync::Arc; use tracing::{debug, error}; use types::core::{EthSpec, Hash256}; use types::data::{ - CellBitmap, PartialDataColumn, PartialDataColumnHeader, PartialDataColumnPartsMetadata, + PartialDataColumn, PartialDataColumnHeader, PartialDataColumnPartsMetadata, PartialDataColumnSidecar, PartialDataColumnSidecarRef, }; @@ -32,12 +32,8 @@ impl OutgoingPartialColumn { header_sent_set: HeaderSentSet, ) -> Self { // For now, always request all cells - let mut requests = partial_column.sidecar.cells_present_bitmap.clone(); - for idx in 0..requests.len() { - requests - .set(idx, true) - .expect("Bound asserted via `len` above"); - } + let mut requests = partial_column.sidecar.cells_present_bitmap.clone_zeroed(); + requests.not_inplace(); let metadata = PartialDataColumnPartsMetadata:: { available: partial_column.sidecar.cells_present_bitmap.clone(), requests, @@ -45,10 +41,7 @@ impl OutgoingPartialColumn { .into(); let header_message = PartialDataColumnSidecarRef { - cells_present_bitmap: CellBitmap::::with_capacity( - partial_column.sidecar.cells_present_bitmap.len(), - ) - .expect("Taking length from bitmap with same bound"), + cells_present_bitmap: partial_column.sidecar.cells_present_bitmap.clone_zeroed(), column: vec![], kzg_proofs: vec![], header: Some(header).into(), @@ -210,7 +203,7 @@ impl Partial for OutgoingPartialColumn { let send = self .partial_column .sidecar - .filter(|idx| want.get(idx).expect("Bound checked above")) + .filter(|idx| want.get(idx).unwrap_or(false)) .map_err(|err| { error!(?err, "Unexpected error filtering sidecar"); PartialError::InvalidFormat @@ -262,6 +255,7 @@ mod tests { use fixed_bytes::FixedBytesExtended; use libp2p::identity::Keypair; use ssz_types::FixedVector; + use types::CellBitmap; use types::block::{BeaconBlockHeader, SignedBeaconBlockHeader}; use types::core::{MinimalEthSpec, Slot}; use types::data::PartialDataColumnHeader; 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 d34668b138..7a902649cb 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -1381,16 +1381,20 @@ impl NetworkBeaconProcessor { &[&data_column_index.to_string()], ); - let mut column = col.to_partial(); - let header = column.sidecar.header.take(); - if let Some(header) = header { - self.send_network_message(NetworkMessage::PublishPartialColumns { - columns: vec![Arc::new(column)], - header: Arc::new(header), - }); - } else { - crit!("Converting from full to partial yielded headerless partial") - }; + match col.to_partial() { + Ok(mut column) => { + let header = column.sidecar.header.take(); + if let Some(header) = header { + self.send_network_message(NetworkMessage::PublishPartialColumns { + columns: vec![Arc::new(column)], + header: Arc::new(header), + }); + } else { + crit!("Converting from full to partial yielded headerless partial") + }; + } + Err(err) => crit!(?err, "Could not convert from full to partial"), + } } let result = self diff --git a/consensus/types/src/data/data_column_sidecar.rs b/consensus/types/src/data/data_column_sidecar.rs index 170aa99666..d15651730f 100644 --- a/consensus/types/src/data/data_column_sidecar.rs +++ b/consensus/types/src/data/data_column_sidecar.rs @@ -250,19 +250,16 @@ impl DataColumnSidecarFulu { } /// Convert this full data column into a verifiable partial data column. - pub fn to_partial(&self) -> PartialDataColumn { + /// Note: This is not expected to ever fail. + pub fn to_partial(&self) -> Result, PartialDataColumnSidecarError> { let cell_count = self.column.len(); - let mut bitmap = - CellBitmap::::with_capacity(cell_count).expect("our column has the same bound"); - for idx in 0..cell_count { - bitmap - .set(idx, true) - .expect("The correct size is initialized right above"); - } + let mut bitmap = CellBitmap::::with_capacity(cell_count) + .map_err(|_| PartialDataColumnSidecarError::UnexpectedBounds)?; + bitmap.not_inplace(); let block_root = self.block_root(); - PartialDataColumn { + Ok(PartialDataColumn { block_root, index: self.index, sidecar: PartialDataColumnSidecar { @@ -276,7 +273,7 @@ impl DataColumnSidecarFulu { }) .into(), }, - } + }) } } From 1caaa10fa86cfe9ad47cffc03f7de81b3e6642e6 Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Thu, 21 May 2026 02:35:35 -0600 Subject: [PATCH 03/18] Drop unused EthSpec generic from Stores (#9281) Co-Authored-By: dapplion <35266934+dapplion@users.noreply.github.com> --- beacon_node/beacon_chain/src/beacon_chain.rs | 4 +- .../src/beacon_fork_choice_store.rs | 12 ++-- beacon_node/beacon_chain/src/builder.rs | 19 +++---- .../overflow_lru_cache.rs | 10 ++-- beacon_node/beacon_chain/src/migrate.rs | 4 +- .../payload_attestation_verification/tests.rs | 2 +- .../beacon_chain/src/persisted_custody.rs | 4 +- beacon_node/beacon_chain/src/test_utils.rs | 18 +++--- .../beacon_chain/tests/op_verification.rs | 2 +- .../beacon_chain/tests/prepare_payload.rs | 8 +-- .../beacon_chain/tests/schema_stability.rs | 2 +- beacon_node/beacon_chain/tests/store_tests.rs | 14 ++--- beacon_node/client/src/builder.rs | 15 +++-- beacon_node/http_api/src/test_utils.rs | 2 +- .../src/network_beacon_processor/mod.rs | 3 +- beacon_node/network/src/persisted_dht.rs | 13 ++--- .../network/src/subnet_service/tests/mod.rs | 7 +-- beacon_node/network/src/sync/tests/mod.rs | 2 +- beacon_node/src/lib.rs | 2 +- beacon_node/store/src/database/interface.rs | 13 ++--- .../store/src/database/leveldb_impl.rs | 13 ++--- beacon_node/store/src/database/redb_impl.rs | 15 ++--- beacon_node/store/src/forwards_iter.rs | 18 +++--- beacon_node/store/src/hot_cold_store.rs | 16 +++--- beacon_node/store/src/invariants.rs | 2 +- beacon_node/store/src/iter.rs | 56 ++++++++----------- beacon_node/store/src/lib.rs | 8 +-- beacon_node/store/src/memory_store.rs | 12 ++-- beacon_node/store/src/reconstruct.rs | 4 +- consensus/fork_choice/tests/tests.rs | 2 +- database_manager/src/lib.rs | 22 ++++---- 31 files changed, 141 insertions(+), 183 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index f3f6cd299e..2259e1d809 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -325,8 +325,8 @@ pub enum StateSkipConfig { } pub trait BeaconChainTypes: Send + Sync + 'static { - type HotStore: store::ItemStore; - type ColdStore: store::ItemStore; + type HotStore: store::ItemStore; + type ColdStore: store::ItemStore; type SlotClock: slot_clock::SlotClock; type EthSpec: types::EthSpec; } diff --git a/beacon_node/beacon_chain/src/beacon_fork_choice_store.rs b/beacon_node/beacon_chain/src/beacon_fork_choice_store.rs index 95fde28f5b..133eaa2fc6 100644 --- a/beacon_node/beacon_chain/src/beacon_fork_choice_store.rs +++ b/beacon_node/beacon_chain/src/beacon_fork_choice_store.rs @@ -129,8 +129,8 @@ impl BalancesCache { /// Implements `fork_choice::ForkChoiceStore` in order to provide a persistent backing to the /// `fork_choice::ForkChoice` struct. #[derive(Debug, Educe)] -#[educe(PartialEq(bound(E: EthSpec, Hot: ItemStore, Cold: ItemStore)))] -pub struct BeaconForkChoiceStore, Cold: ItemStore> { +#[educe(PartialEq(bound(E: EthSpec, Hot: ItemStore, Cold: ItemStore)))] +pub struct BeaconForkChoiceStore { #[educe(PartialEq(ignore))] store: Arc>, balances_cache: BalancesCache, @@ -150,8 +150,8 @@ pub struct BeaconForkChoiceStore, Cold: ItemStore< impl BeaconForkChoiceStore where E: EthSpec, - Hot: ItemStore, - Cold: ItemStore, + Hot: ItemStore, + Cold: ItemStore, { /// Initialize `Self` from some `anchor` checkpoint which may or may not be the genesis state. /// @@ -267,8 +267,8 @@ where impl ForkChoiceStore for BeaconForkChoiceStore where E: EthSpec, - Hot: ItemStore, - Cold: ItemStore, + Hot: ItemStore, + Cold: ItemStore, { type Error = Error; diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index e668bef7c0..61c026e0a9 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -60,8 +60,8 @@ pub struct Witness( impl BeaconChainTypes for Witness where - THotStore: ItemStore + 'static, - TColdStore: ItemStore + 'static, + THotStore: ItemStore + 'static, + TColdStore: ItemStore + 'static, TSlotClock: SlotClock + 'static, E: EthSpec + 'static, { @@ -115,8 +115,8 @@ pub struct BeaconChainBuilder { impl BeaconChainBuilder> where - THotStore: ItemStore + 'static, - TColdStore: ItemStore + 'static, + THotStore: ItemStore + 'static, + TColdStore: ItemStore + 'static, TSlotClock: SlotClock + 'static, E: EthSpec + 'static, { @@ -1162,8 +1162,8 @@ where impl BeaconChainBuilder> where - THotStore: ItemStore + 'static, - TColdStore: ItemStore + 'static, + THotStore: ItemStore + 'static, + TColdStore: ItemStore + 'static, E: EthSpec + 'static, { /// Sets the `BeaconChain` slot clock to `TestingSlotClock`. @@ -1301,11 +1301,8 @@ mod test { let validator_count = 1; let genesis_time = 13_371_337; - let store: HotColdDB< - MinimalEthSpec, - MemoryStore, - MemoryStore, - > = HotColdDB::open_ephemeral(StoreConfig::default(), ChainSpec::minimal().into()).unwrap(); + let store: HotColdDB = + HotColdDB::open_ephemeral(StoreConfig::default(), ChainSpec::minimal().into()).unwrap(); let spec = MinimalEthSpec::default_spec(); let genesis_state = interop_genesis_state( 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 3034e196b9..8a80f835ab 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 @@ -802,7 +802,7 @@ mod test { fn get_store_with_spec( db_path: &TempDir, spec: Arc, - ) -> Arc, BeaconNodeBackend>> { + ) -> Arc> { 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"); @@ -860,8 +860,8 @@ mod test { ) where E: EthSpec, - Hot: ItemStore, - Cold: ItemStore, + Hot: ItemStore, + Cold: ItemStore, { let chain = &harness.chain; let head = chain.head_snapshot(); @@ -946,8 +946,8 @@ mod test { where E: EthSpec, T: BeaconChainTypes< - HotStore = BeaconNodeBackend, - ColdStore = BeaconNodeBackend, + HotStore = BeaconNodeBackend, + ColdStore = BeaconNodeBackend, EthSpec = E, >, { diff --git a/beacon_node/beacon_chain/src/migrate.rs b/beacon_node/beacon_chain/src/migrate.rs index 3c17c1ebba..9c70bcafa2 100644 --- a/beacon_node/beacon_chain/src/migrate.rs +++ b/beacon_node/beacon_chain/src/migrate.rs @@ -30,7 +30,7 @@ pub const DEFAULT_EPOCHS_PER_MIGRATION: u64 = 1; /// The background migrator runs a thread to perform pruning and migrate state from the hot /// to the cold database. -pub struct BackgroundMigrator, Cold: ItemStore> { +pub struct BackgroundMigrator { db: Arc>, /// Record of when the last migration ran, for enforcing `epochs_per_migration`. prev_migration: Arc>, @@ -135,7 +135,7 @@ pub struct FinalizationNotification { pub prev_migration: Arc>, } -impl, Cold: ItemStore> BackgroundMigrator { +impl BackgroundMigrator { /// Create a new `BackgroundMigrator` and spawn its thread if necessary. pub fn new(db: Arc>, config: MigratorConfig) -> Self { // Estimate last migration run from DB split slot. diff --git a/beacon_node/beacon_chain/src/payload_attestation_verification/tests.rs b/beacon_node/beacon_chain/src/payload_attestation_verification/tests.rs index 7faad98e55..c45df51ac8 100644 --- a/beacon_node/beacon_chain/src/payload_attestation_verification/tests.rs +++ b/beacon_node/beacon_chain/src/payload_attestation_verification/tests.rs @@ -43,7 +43,7 @@ struct TestContext { keypairs: Vec, spec: ChainSpec, genesis_block_root: Hash256, - store: Arc, store::MemoryStore>>, + store: Arc>, } impl TestContext { diff --git a/beacon_node/beacon_chain/src/persisted_custody.rs b/beacon_node/beacon_chain/src/persisted_custody.rs index ba221c67b5..cc7219fa90 100644 --- a/beacon_node/beacon_chain/src/persisted_custody.rs +++ b/beacon_node/beacon_chain/src/persisted_custody.rs @@ -9,7 +9,7 @@ pub const CUSTODY_DB_KEY: Hash256 = Hash256::ZERO; pub struct PersistedCustody(pub CustodyContextSsz); -pub fn load_custody_context, Cold: ItemStore>( +pub fn load_custody_context( store: Arc>, ) -> Option { let res: Result, _> = @@ -22,7 +22,7 @@ pub fn load_custody_context, Cold: ItemStore>( } /// Attempt to persist the custody context object to `self.store`. -pub fn persist_custody_context, Cold: ItemStore>( +pub fn persist_custody_context( store: Arc>, custody_context: CustodyContextSsz, ) -> Result<(), store::Error> { diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index 8e9cc61208..c2ccad7d8c 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -124,8 +124,8 @@ pub fn get_kzg(spec: &ChainSpec) -> Arc { pub type BaseHarnessType = Witness; -pub type DiskHarnessType = BaseHarnessType, BeaconNodeBackend>; -pub type EphemeralHarnessType = BaseHarnessType, MemoryStore>; +pub type DiskHarnessType = BaseHarnessType; +pub type EphemeralHarnessType = BaseHarnessType; pub type BoxedMutator = Box< dyn FnOnce( @@ -334,7 +334,7 @@ impl Builder> { /// Manually restore from a given `MemoryStore`. pub fn resumed_ephemeral_store( mut self, - store: Arc, MemoryStore>>, + store: Arc>, ) -> Self { let mutator = move |builder: BeaconChainBuilder<_>| { builder @@ -350,7 +350,7 @@ impl Builder> { /// Disk store, start from genesis. pub fn fresh_disk_store( mut self, - store: Arc, BeaconNodeBackend>>, + store: Arc>, ) -> Self { let validator_keypairs = self .validator_keypairs @@ -384,7 +384,7 @@ impl Builder> { /// Disk store, resume. pub fn resumed_disk_store( mut self, - store: Arc, BeaconNodeBackend>>, + store: Arc>, ) -> Self { let mutator = move |builder: BeaconChainBuilder<_>| { builder @@ -399,8 +399,8 @@ impl Builder> { impl Builder> where E: EthSpec, - Hot: ItemStore, - Cold: ItemStore, + Hot: ItemStore, + Cold: ItemStore, { pub fn new(eth_spec_instance: E) -> Self { let runtime = TestRuntime::default(); @@ -760,8 +760,8 @@ pub type HarnessSyncContributions = Vec<( impl BeaconChainHarness> where E: EthSpec, - Hot: ItemStore, - Cold: ItemStore, + Hot: ItemStore, + Cold: ItemStore, { pub fn builder(eth_spec_instance: E) -> Builder> { create_test_tracing_subscriber(); diff --git a/beacon_node/beacon_chain/tests/op_verification.rs b/beacon_node/beacon_chain/tests/op_verification.rs index 2f97f10745..adc14541a9 100644 --- a/beacon_node/beacon_chain/tests/op_verification.rs +++ b/beacon_node/beacon_chain/tests/op_verification.rs @@ -27,7 +27,7 @@ static KEYPAIRS: LazyLock> = type E = MinimalEthSpec; type TestHarness = BeaconChainHarness>; -type HotColdDB = store::HotColdDB, BeaconNodeBackend>; +type HotColdDB = store::HotColdDB; fn get_store(db_path: &TempDir) -> Arc { let spec = Arc::new(test_spec::()); diff --git a/beacon_node/beacon_chain/tests/prepare_payload.rs b/beacon_node/beacon_chain/tests/prepare_payload.rs index 47dd1ef517..de8bfb3865 100644 --- a/beacon_node/beacon_chain/tests/prepare_payload.rs +++ b/beacon_node/beacon_chain/tests/prepare_payload.rs @@ -34,7 +34,7 @@ type TestHarness = BeaconChainHarness>; fn get_store( db_path: &TempDir, spec: Arc, -) -> Arc, BeaconNodeBackend>> { +) -> Arc> { let store_config = StoreConfig { prune_payloads: false, ..StoreConfig::default() @@ -46,7 +46,7 @@ fn get_store_generic( db_path: &TempDir, config: StoreConfig, spec: Arc, -) -> Arc, BeaconNodeBackend>> { +) -> Arc> { create_test_tracing_subscriber(); let hot_path = db_path.path().join("chain_db"); let cold_path = db_path.path().join("freezer_db"); @@ -64,7 +64,7 @@ fn get_store_generic( } fn get_harness( - store: Arc, BeaconNodeBackend>>, + store: Arc>, validator_count: usize, ) -> TestHarness { // Most tests expect to retain historic states, so we use this as the default. @@ -81,7 +81,7 @@ fn get_harness( } fn get_harness_generic( - store: Arc, BeaconNodeBackend>>, + store: Arc>, validator_count: usize, chain_config: ChainConfig, node_custody_type: NodeCustodyType, diff --git a/beacon_node/beacon_chain/tests/schema_stability.rs b/beacon_node/beacon_chain/tests/schema_stability.rs index 8200748ae6..899a40511d 100644 --- a/beacon_node/beacon_chain/tests/schema_stability.rs +++ b/beacon_node/beacon_chain/tests/schema_stability.rs @@ -20,7 +20,7 @@ use tempfile::{TempDir, tempdir}; use types::{ChainSpec, Hash256, MainnetEthSpec, Slot}; type E = MainnetEthSpec; -type Store = Arc, BeaconNodeBackend>>; +type Store = Arc>; type TestHarness = BeaconChainHarness>; const VALIDATOR_COUNT: usize = 32; diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index 0ff9f6841d..7e50f4e5ac 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -106,7 +106,7 @@ fn get_or_reconstruct_blobs( } } -fn get_store(db_path: &TempDir) -> Arc, BeaconNodeBackend>> { +fn get_store(db_path: &TempDir) -> Arc> { let store_config = StoreConfig { prune_payloads: false, ..StoreConfig::default() @@ -118,7 +118,7 @@ fn get_store_generic( db_path: &TempDir, config: StoreConfig, spec: ChainSpec, -) -> Arc, BeaconNodeBackend>> { +) -> Arc> { create_test_tracing_subscriber(); let hot_path = db_path.path().join("chain_db"); let cold_path = db_path.path().join("freezer_db"); @@ -136,7 +136,7 @@ fn get_store_generic( } fn get_harness( - store: Arc, BeaconNodeBackend>>, + store: Arc>, validator_count: usize, ) -> TestHarness { // Most tests expect to retain historic states, so we use this as the default. @@ -153,7 +153,7 @@ fn get_harness( } fn get_harness_import_all_data_columns( - store: Arc, BeaconNodeBackend>>, + store: Arc>, validator_count: usize, ) -> TestHarness { // Most tests expect to retain historic states, so we use this as the default. @@ -171,7 +171,7 @@ fn get_harness_import_all_data_columns( } fn get_harness_generic( - store: Arc, BeaconNodeBackend>>, + store: Arc>, validator_count: usize, chain_config: ChainConfig, node_custody_type: NodeCustodyType, @@ -205,7 +205,7 @@ fn check_db_invariants(harness: &TestHarness) { } fn get_states_descendant_of_block( - store: &HotColdDB, BeaconNodeBackend>, + store: &HotColdDB, block_root: Hash256, ) -> Vec<(Hash256, Slot)> { let summaries = store.load_hot_state_summaries().unwrap(); @@ -5859,7 +5859,7 @@ async fn test_gloas_hot_state_hierarchy() { /// 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, BeaconNodeBackend>>, + store: Arc>, ) { let split_slot = store.get_split_slot(); assert_eq!( diff --git a/beacon_node/client/src/builder.rs b/beacon_node/client/src/builder.rs index f532ef716e..0a3c414632 100644 --- a/beacon_node/client/src/builder.rs +++ b/beacon_node/client/src/builder.rs @@ -98,8 +98,8 @@ impl where TSlotClock: SlotClock + Clone + 'static, E: EthSpec + 'static, - THotStore: ItemStore + 'static, - TColdStore: ItemStore + 'static, + THotStore: ItemStore + 'static, + TColdStore: ItemStore + 'static, { /// Instantiates a new, empty builder. /// @@ -815,8 +815,8 @@ impl where TSlotClock: SlotClock + Clone + 'static, E: EthSpec + 'static, - THotStore: ItemStore + 'static, - TColdStore: ItemStore + 'static, + THotStore: ItemStore + 'static, + TColdStore: ItemStore + 'static, { /// Consumes the internal `BeaconChainBuilder`, attaching the resulting `BeaconChain` to self. #[instrument(skip_all)] @@ -847,8 +847,7 @@ where } } -impl - ClientBuilder, BeaconNodeBackend>> +impl ClientBuilder> where TSlotClock: SlotClock + 'static, E: EthSpec + 'static, @@ -889,8 +888,8 @@ where impl ClientBuilder> where E: EthSpec + 'static, - THotStore: ItemStore + 'static, - TColdStore: ItemStore + 'static, + THotStore: ItemStore + 'static, + TColdStore: ItemStore + 'static, { /// Specifies that the slot clock should read the time from the computers system clock. pub fn system_time_slot_clock(mut self) -> Result { diff --git a/beacon_node/http_api/src/test_utils.rs b/beacon_node/http_api/src/test_utils.rs index f27a04d17a..467a5216b1 100644 --- a/beacon_node/http_api/src/test_utils.rs +++ b/beacon_node/http_api/src/test_utils.rs @@ -57,7 +57,7 @@ pub struct ApiServer> { type HarnessBuilder = Builder>; type Initializer = Box) -> HarnessBuilder>; -type Mutator = BoxedMutator, MemoryStore>; +type Mutator = BoxedMutator; impl InteractiveTester { pub async fn new(spec: Option, validator_count: usize) -> Self { diff --git a/beacon_node/network/src/network_beacon_processor/mod.rs b/beacon_node/network/src/network_beacon_processor/mod.rs index 7817feb0bd..434f7ecc8b 100644 --- a/beacon_node/network/src/network_beacon_processor/mod.rs +++ b/beacon_node/network/src/network_beacon_processor/mod.rs @@ -1267,8 +1267,7 @@ use { }; #[cfg(test)] -pub(crate) type TestBeaconChainType = - Witness, MemoryStore>; +pub(crate) type TestBeaconChainType = Witness; #[cfg(test)] impl NetworkBeaconProcessor> { diff --git a/beacon_node/network/src/persisted_dht.rs b/beacon_node/network/src/persisted_dht.rs index 113b3cdd32..3672f97113 100644 --- a/beacon_node/network/src/persisted_dht.rs +++ b/beacon_node/network/src/persisted_dht.rs @@ -6,7 +6,7 @@ use types::{EthSpec, Hash256}; /// 32-byte key for accessing the `DhtEnrs`. All zero because `DhtEnrs` has its own column. pub const DHT_DB_KEY: Hash256 = Hash256::ZERO; -pub fn load_dht, Cold: ItemStore>( +pub fn load_dht( store: Arc>, ) -> Vec { // Load DHT from store @@ -20,7 +20,7 @@ pub fn load_dht, Cold: ItemStore>( } /// Attempt to persist the ENR's in the DHT to `self.store`. -pub fn persist_dht, Cold: ItemStore>( +pub fn persist_dht( store: Arc>, enrs: Vec, ) -> Result<(), store::Error> { @@ -28,7 +28,7 @@ pub fn persist_dht, Cold: ItemStore>( } /// Attempts to clear any DHT entries. -pub fn clear_dht, Cold: ItemStore>( +pub fn clear_dht( store: Arc>, ) -> Result<(), store::Error> { store.hot_db.delete::(&DHT_DB_KEY) @@ -75,11 +75,8 @@ mod tests { use types::{ChainSpec, MinimalEthSpec}; #[test] fn test_persisted_dht() { - let store: HotColdDB< - MinimalEthSpec, - MemoryStore, - MemoryStore, - > = HotColdDB::open_ephemeral(StoreConfig::default(), ChainSpec::minimal().into()).unwrap(); + let store: HotColdDB = + HotColdDB::open_ephemeral(StoreConfig::default(), ChainSpec::minimal().into()).unwrap(); let enrs = vec![Enr::from_str("enr:-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnh4MHcBFZntXNFrdvJjX04jRzjzCBOonrkTfj499SZuOh8R33Ls8RRcy5wBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQPKY0yuDUmstAHYpMa2_oxVtw0RW_QAdpzBQA8yWM0xOIN1ZHCCdl8").unwrap()]; store .put_item(&DHT_DB_KEY, &PersistedDht { enrs: enrs.clone() }) diff --git a/beacon_node/network/src/subnet_service/tests/mod.rs b/beacon_node/network/src/subnet_service/tests/mod.rs index 619154d738..745934053a 100644 --- a/beacon_node/network/src/subnet_service/tests/mod.rs +++ b/beacon_node/network/src/subnet_service/tests/mod.rs @@ -25,12 +25,7 @@ const SLOT_DURATION_MILLIS: u64 = 400; const TEST_LOG_LEVEL: Option<&str> = None; -type TestBeaconChainType = Witness< - SystemTimeSlotClock, - MainnetEthSpec, - MemoryStore, - MemoryStore, ->; +type TestBeaconChainType = Witness; pub struct TestBeaconChain { chain: Arc>, diff --git a/beacon_node/network/src/sync/tests/mod.rs b/beacon_node/network/src/sync/tests/mod.rs index dd8c3ae432..4e185cc081 100644 --- a/beacon_node/network/src/sync/tests/mod.rs +++ b/beacon_node/network/src/sync/tests/mod.rs @@ -26,7 +26,7 @@ use types::{ForkName, Hash256, MinimalEthSpec as E, Slot}; mod lookups; mod range; -type T = Witness, MemoryStore>; +type T = Witness; /// This test utility enables integration testing of Lighthouse sync components. /// diff --git a/beacon_node/src/lib.rs b/beacon_node/src/lib.rs index e33da17e26..6400427f8c 100644 --- a/beacon_node/src/lib.rs +++ b/beacon_node/src/lib.rs @@ -20,7 +20,7 @@ use types::{ChainSpec, Epoch, EthSpec, ForkName}; /// A type-alias to the tighten the definition of a production-intended `Client`. pub type ProductionClient = - Client, BeaconNodeBackend>>; + Client>; /// The beacon node `Client` that is used in production. /// diff --git a/beacon_node/store/src/database/interface.rs b/beacon_node/store/src/database/interface.rs index 5646f1179c..7e0a09a3e9 100644 --- a/beacon_node/store/src/database/interface.rs +++ b/beacon_node/store/src/database/interface.rs @@ -6,18 +6,17 @@ use crate::{ColumnIter, ColumnKeyIter, DBColumn, Error, ItemStore, Key, KeyValue use crate::{KeyValueStoreOp, StoreConfig, config::DatabaseBackend}; use std::collections::HashSet; use std::path::Path; -use types::EthSpec; -pub enum BeaconNodeBackend { +pub enum BeaconNodeBackend { #[cfg(feature = "leveldb")] - LevelDb(leveldb_impl::LevelDB), + LevelDb(leveldb_impl::LevelDB), #[cfg(feature = "redb")] - Redb(redb_impl::Redb), + Redb(redb_impl::Redb), } -impl ItemStore for BeaconNodeBackend {} +impl ItemStore for BeaconNodeBackend {} -impl KeyValueStore for BeaconNodeBackend { +impl KeyValueStore for BeaconNodeBackend { fn get_bytes(&self, column: DBColumn, key: &[u8]) -> Result>, Error> { match self { #[cfg(feature = "leveldb")] @@ -183,7 +182,7 @@ impl KeyValueStore for BeaconNodeBackend { } } -impl BeaconNodeBackend { +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 { diff --git a/beacon_node/store/src/database/leveldb_impl.rs b/beacon_node/store/src/database/leveldb_impl.rs index 6e01648263..0531eb900e 100644 --- a/beacon_node/store/src/database/leveldb_impl.rs +++ b/beacon_node/store/src/database/leveldb_impl.rs @@ -15,15 +15,13 @@ use leveldb::{ options::{Options, ReadOptions}, }; use std::collections::HashSet; -use std::marker::PhantomData; use std::path::Path; -use types::{EthSpec, Hash256}; +use types::Hash256; use super::interface::WriteOptions; -pub struct LevelDB { +pub struct LevelDB { db: Database, - _phantom: PhantomData, } impl From for leveldb::options::WriteOptions { @@ -34,7 +32,7 @@ impl From for leveldb::options::WriteOptions { } } -impl LevelDB { +impl LevelDB { pub fn open(path: &Path) -> Result { let mut options = Options::new(); @@ -42,10 +40,7 @@ impl LevelDB { let db = Database::open(path, options)?; - Ok(Self { - db, - _phantom: PhantomData, - }) + Ok(Self { db }) } pub fn read_options(&self) -> ReadOptions<'_, BytesKey> { diff --git a/beacon_node/store/src/database/redb_impl.rs b/beacon_node/store/src/database/redb_impl.rs index 4077326eca..dc39f22114 100644 --- a/beacon_node/store/src/database/redb_impl.rs +++ b/beacon_node/store/src/database/redb_impl.rs @@ -3,17 +3,15 @@ use crate::{DBColumn, Error, KeyValueStoreOp}; use parking_lot::RwLock; use redb::TableDefinition; use std::collections::HashSet; -use std::{borrow::BorrowMut, marker::PhantomData, path::Path}; +use std::{borrow::BorrowMut, path::Path}; use strum::IntoEnumIterator; -use types::EthSpec; use super::interface::WriteOptions; pub const DB_FILE_NAME: &str = "database.redb"; -pub struct Redb { +pub struct Redb { db: RwLock, - _phantom: PhantomData, } impl From for redb::Durability { @@ -26,19 +24,16 @@ impl From for redb::Durability { } } -impl Redb { +impl Redb { pub fn open(path: &Path) -> Result { let db_file = path.join(DB_FILE_NAME); let db = redb::Database::create(db_file)?; for column in DBColumn::iter() { - Redb::::create_table(&db, column.into())?; + Self::create_table(&db, column.into())?; } - Ok(Self { - db: db.into(), - _phantom: PhantomData, - }) + Ok(Self { db: db.into() }) } fn create_table(db: &redb::Database, table_name: &str) -> Result<(), Error> { diff --git a/beacon_node/store/src/forwards_iter.rs b/beacon_node/store/src/forwards_iter.rs index 255b7d8eac..ef4312f506 100644 --- a/beacon_node/store/src/forwards_iter.rs +++ b/beacon_node/store/src/forwards_iter.rs @@ -9,7 +9,7 @@ pub type HybridForwardsBlockRootsIterator<'a, E, Hot, Cold> = pub type HybridForwardsStateRootsIterator<'a, E, Hot, Cold> = HybridForwardsIterator<'a, E, Hot, Cold>; -impl, Cold: ItemStore> HotColdDB { +impl HotColdDB { fn simple_forwards_iterator( &self, column: DBColumn, @@ -116,7 +116,7 @@ impl, Cold: ItemStore> HotColdDB } /// 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> { +pub struct FrozenForwardsIterator<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> { inner: ColumnIter<'a, Vec>, column: DBColumn, next_slot: Slot, @@ -124,9 +124,7 @@ pub struct FrozenForwardsIterator<'a, E: EthSpec, Hot: ItemStore, Cold: ItemS _phantom: PhantomData<(E, Hot, Cold)>, } -impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> - FrozenForwardsIterator<'a, E, Hot, Cold> -{ +impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> FrozenForwardsIterator<'a, E, Hot, Cold> { /// `end_slot` is EXCLUSIVE here. pub fn new( store: &'a HotColdDB, @@ -148,7 +146,7 @@ impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> } } -impl, Cold: ItemStore> Iterator +impl Iterator for FrozenForwardsIterator<'_, E, Hot, Cold> { type Item = Result<(Hash256, Slot)>; @@ -199,7 +197,7 @@ impl Iterator for SimpleForwardsIterator { } /// Fusion of the above two approaches to forwards iteration. Fast and efficient. -pub enum HybridForwardsIterator<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> { +pub enum HybridForwardsIterator<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> { PreFinalization { iter: Box>, store: &'a HotColdDB, @@ -220,9 +218,7 @@ pub enum HybridForwardsIterator<'a, E: EthSpec, Hot: ItemStore, Cold: ItemSto Finished, } -impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> - HybridForwardsIterator<'a, E, Hot, Cold> -{ +impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> HybridForwardsIterator<'a, E, Hot, Cold> { /// Construct a new hybrid iterator. /// /// The `get_state` closure should return a beacon state and final block/state root to backtrack @@ -349,7 +345,7 @@ impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> } } -impl, Cold: ItemStore> Iterator +impl Iterator for HybridForwardsIterator<'_, E, Hot, Cold> { type Item = Result<(Hash256, Slot)>; diff --git a/beacon_node/store/src/hot_cold_store.rs b/beacon_node/store/src/hot_cold_store.rs index e9b9de76e6..a625a97004 100644 --- a/beacon_node/store/src/hot_cold_store.rs +++ b/beacon_node/store/src/hot_cold_store.rs @@ -49,7 +49,7 @@ use zstd::{Decoder, Encoder}; /// Stores vector fields like the `block_roots` and `state_roots` separately, and only stores /// intermittent "restore point" states pre-finalization. #[derive(Debug)] -pub struct HotColdDB, Cold: ItemStore> { +pub struct HotColdDB { /// The slot and state root at the point where the database is split between hot and cold. /// /// States with slots less than `split.slot` are in the cold DB, while states with slots @@ -217,11 +217,11 @@ pub enum HotColdDBError { Rollback, } -impl HotColdDB, MemoryStore> { +impl HotColdDB { pub fn open_ephemeral( config: StoreConfig, spec: Arc, - ) -> Result, MemoryStore>, Error> { + ) -> Result, Error> { config.verify::()?; let hierarchy = config.hierarchy_config.to_moduli()?; @@ -258,7 +258,7 @@ impl HotColdDB, MemoryStore> { } } -impl HotColdDB, BeaconNodeBackend> { +impl HotColdDB { /// 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 @@ -451,7 +451,7 @@ impl HotColdDB, BeaconNodeBackend> { } } -impl, Cold: ItemStore> HotColdDB { +impl HotColdDB { fn cold_storage_strategy(&self, slot: Slot) -> Result { // The start slot for the freezer HDiff is always 0 Ok(self.hierarchy.storage_strategy(slot, Slot::new(0))?) @@ -3575,7 +3575,7 @@ impl, Cold: ItemStore> HotColdDB /// This function previously did a combination of freezer migration alongside pruning. Now it is /// *just* responsible for copying relevant data to the freezer, while pruning is implemented /// in `prune_hot_db`. -pub fn migrate_database, Cold: ItemStore>( +pub fn migrate_database( store: Arc>, finalized_state_root: Hash256, finalized_block_root: Hash256, @@ -3786,7 +3786,7 @@ pub enum StateSummaryIteratorError { /// Return the ancestor state root of a state beyond SlotsPerHistoricalRoot using the roots iterator /// and the store -pub fn get_ancestor_state_root<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore>( +pub fn get_ancestor_state_root<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore>( store: &'a HotColdDB, from_state: &'a BeaconState, target_slot: Slot, @@ -3993,7 +3993,7 @@ impl StoreItem for HotStateSummary { impl HotStateSummary { /// Construct a new summary of the given state. - pub fn new, Cold: ItemStore>( + pub fn new( store: &HotColdDB, state_root: Hash256, state: &BeaconState, diff --git a/beacon_node/store/src/invariants.rs b/beacon_node/store/src/invariants.rs index d251fb8800..4ec72b82bd 100644 --- a/beacon_node/store/src/invariants.rs +++ b/beacon_node/store/src/invariants.rs @@ -242,7 +242,7 @@ pub enum InvariantViolation { ColdStateBaseSummaryMissing { slot: Slot, base_slot: Slot }, } -impl, Cold: ItemStore> HotColdDB { +impl HotColdDB { /// Run all database invariant checks. /// /// The `ctx` parameter provides data from the beacon chain layer (fork choice, state cache, diff --git a/beacon_node/store/src/iter.rs b/beacon_node/store/src/iter.rs index 0cb803d1ed..cf1ab86ffe 100644 --- a/beacon_node/store/src/iter.rs +++ b/beacon_node/store/src/iter.rs @@ -13,12 +13,12 @@ use types::{ /// /// It is assumed that all ancestors for this object are stored in the database. If this is not the /// case, the iterator will start returning `None` prior to genesis. -pub trait AncestorIter<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore, I: Iterator> { +pub trait AncestorIter<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore, I: Iterator> { /// Returns an iterator over the roots of the ancestors of `self`. fn try_iter_ancestor_roots(&self, store: &'a HotColdDB) -> Option; } -impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> +impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> AncestorIter<'a, E, Hot, Cold, BlockRootsIterator<'a, E, Hot, Cold>> for SignedBeaconBlock { /// Iterates across all available prior block roots of `self`, starting at the most recent and ending @@ -37,7 +37,7 @@ impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> } } -impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> +impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> AncestorIter<'a, E, Hot, Cold, StateRootsIterator<'a, E, Hot, Cold>> for BeaconState { /// Iterates across all available prior state roots of `self`, starting at the most recent and ending @@ -51,13 +51,11 @@ impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> } } -pub struct StateRootsIterator<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> { +pub struct StateRootsIterator<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> { inner: RootsIterator<'a, E, Hot, Cold>, } -impl, Cold: ItemStore> Clone - for StateRootsIterator<'_, E, Hot, Cold> -{ +impl Clone for StateRootsIterator<'_, E, Hot, Cold> { fn clone(&self) -> Self { Self { inner: self.inner.clone(), @@ -65,7 +63,7 @@ impl, Cold: ItemStore> Clone } } -impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> StateRootsIterator<'a, E, Hot, Cold> { +impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> StateRootsIterator<'a, E, Hot, Cold> { pub fn new(store: &'a HotColdDB, beacon_state: &'a BeaconState) -> Self { Self { inner: RootsIterator::new(store, beacon_state), @@ -79,7 +77,7 @@ impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> StateRootsIterator<' } } -impl, Cold: ItemStore> Iterator +impl Iterator for StateRootsIterator<'_, E, Hot, Cold> { type Item = Result<(Hash256, Slot), Error>; @@ -99,13 +97,11 @@ impl, Cold: ItemStore> Iterator /// exhausted. /// /// Returns `None` for roots prior to genesis or when there is an error reading from `Store`. -pub struct BlockRootsIterator<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> { +pub struct BlockRootsIterator<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> { inner: RootsIterator<'a, E, Hot, Cold>, } -impl, Cold: ItemStore> Clone - for BlockRootsIterator<'_, E, Hot, Cold> -{ +impl Clone for BlockRootsIterator<'_, E, Hot, Cold> { fn clone(&self) -> Self { Self { inner: self.inner.clone(), @@ -113,7 +109,7 @@ impl, Cold: ItemStore> Clone } } -impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> BlockRootsIterator<'a, E, Hot, Cold> { +impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> BlockRootsIterator<'a, E, Hot, Cold> { /// Create a new iterator over all block roots in the given `beacon_state` and prior states. pub fn new(store: &'a HotColdDB, beacon_state: &'a BeaconState) -> Self { Self { @@ -138,7 +134,7 @@ impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> BlockRootsIterator<' } } -impl, Cold: ItemStore> Iterator +impl Iterator for BlockRootsIterator<'_, E, Hot, Cold> { type Item = Result<(Hash256, Slot), Error>; @@ -151,13 +147,13 @@ impl, Cold: ItemStore> Iterator } /// Iterator over state and block roots that backtracks using the vectors from a `BeaconState`. -pub struct RootsIterator<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> { +pub struct RootsIterator<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> { store: &'a HotColdDB, beacon_state: Cow<'a, BeaconState>, slot: Slot, } -impl, Cold: ItemStore> Clone for RootsIterator<'_, E, Hot, Cold> { +impl Clone for RootsIterator<'_, E, Hot, Cold> { fn clone(&self) -> Self { Self { store: self.store, @@ -167,7 +163,7 @@ impl, Cold: ItemStore> Clone for RootsIterator< } } -impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> RootsIterator<'a, E, Hot, Cold> { +impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> RootsIterator<'a, E, Hot, Cold> { pub fn new(store: &'a HotColdDB, beacon_state: &'a BeaconState) -> Self { Self { store, @@ -234,9 +230,7 @@ impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> RootsIterator<'a, E, } } -impl, Cold: ItemStore> Iterator - for RootsIterator<'_, E, Hot, Cold> -{ +impl Iterator for RootsIterator<'_, E, Hot, Cold> { /// (block_root, state_root, slot) type Item = Result<(Hash256, Hash256, Slot), Error>; @@ -246,15 +240,13 @@ impl, Cold: ItemStore> Iterator } /// Block iterator that uses the `parent_root` of each block to backtrack. -pub struct ParentRootBlockIterator<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> { +pub struct ParentRootBlockIterator<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> { store: &'a HotColdDB, next_block_root: Hash256, _phantom: PhantomData, } -impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> - ParentRootBlockIterator<'a, E, Hot, Cold> -{ +impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> ParentRootBlockIterator<'a, E, Hot, Cold> { pub fn new(store: &'a HotColdDB, start_block_root: Hash256) -> Self { Self { store, @@ -283,7 +275,7 @@ impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> } } -impl, Cold: ItemStore> Iterator +impl Iterator for ParentRootBlockIterator<'_, E, Hot, Cold> { type Item = Result<(Hash256, SignedBeaconBlock>), Error>; @@ -295,11 +287,11 @@ impl, Cold: ItemStore> Iterator #[derive(Clone)] /// Extends `BlockRootsIterator`, returning `SignedBeaconBlock` instances, instead of their roots. -pub struct BlockIterator<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> { +pub struct BlockIterator<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> { roots: BlockRootsIterator<'a, E, Hot, Cold>, } -impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> BlockIterator<'a, E, Hot, Cold> { +impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> BlockIterator<'a, E, Hot, Cold> { /// Create a new iterator over all blocks in the given `beacon_state` and prior states. pub fn new(store: &'a HotColdDB, beacon_state: &'a BeaconState) -> Self { Self { @@ -324,9 +316,7 @@ impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> BlockIterator<'a, E, } } -impl, Cold: ItemStore> Iterator - for BlockIterator<'_, E, Hot, Cold> -{ +impl Iterator for BlockIterator<'_, E, Hot, Cold> { type Item = Result>, Error>; fn next(&mut self) -> Option { @@ -338,7 +328,7 @@ impl, Cold: ItemStore> Iterator /// /// Return `Err(HistoryUnavailable)` in the case where no more backtrack states are available /// due to weak subjectivity sync. -fn next_historical_root_backtrack_state, Cold: ItemStore>( +fn next_historical_root_backtrack_state( store: &HotColdDB, current_state: &BeaconState, ) -> Result, Error> { @@ -386,7 +376,7 @@ mod test { harness.get_current_state() } - fn get_store() -> HotColdDB, MemoryStore> { + fn get_store() -> HotColdDB { let store = HotColdDB::open_ephemeral(Config::default(), Arc::new(E::default_spec())).unwrap(); // Init achor info so anchor slot is set. Use a random block as it is only used for the diff --git a/beacon_node/store/src/lib.rs b/beacon_node/store/src/lib.rs index bd8caa3ad5..56cdd18fbe 100644 --- a/beacon_node/store/src/lib.rs +++ b/beacon_node/store/src/lib.rs @@ -46,7 +46,7 @@ pub type ColumnKeyIter<'a, K> = Box> + 'a>; pub type RawEntryIter<'a> = Result, Vec), Error>> + 'a>, Error>; -pub trait KeyValueStore: Sync + Send + Sized + 'static { +pub trait KeyValueStore: Sync + Send + Sized + 'static { /// Retrieve some bytes in `column` with `key`. fn get_bytes(&self, column: DBColumn, key: &[u8]) -> Result>, Error>; @@ -177,7 +177,7 @@ pub enum KeyValueStoreOp { DeleteKey(DBColumn, Vec), } -pub trait ItemStore: KeyValueStore + Sync + Send + Sized + 'static { +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(); @@ -493,7 +493,7 @@ mod tests { } } - fn test_impl(store: impl ItemStore) { + fn test_impl(store: impl ItemStore) { let key = Hash256::random(); let item = StorableThing { a: 1, b: 42 }; @@ -531,7 +531,7 @@ mod tests { #[test] fn exists() { - let store = MemoryStore::::open(); + let store = MemoryStore::open(); let key = Hash256::random(); let item = StorableThing { a: 1, b: 42 }; diff --git a/beacon_node/store/src/memory_store.rs b/beacon_node/store/src/memory_store.rs index 6baef61c9d..8be9278d90 100644 --- a/beacon_node/store/src/memory_store.rs +++ b/beacon_node/store/src/memory_store.rs @@ -4,28 +4,24 @@ use crate::{ }; use parking_lot::RwLock; use std::collections::{BTreeMap, HashSet}; -use std::marker::PhantomData; -use types::*; type DBMap = BTreeMap>; /// A thread-safe `BTreeMap` wrapper. -pub struct MemoryStore { +pub struct MemoryStore { db: RwLock, - _phantom: PhantomData, } -impl MemoryStore { +impl MemoryStore { /// Create a new, empty database. pub fn open() -> Self { Self { db: RwLock::new(BTreeMap::new()), - _phantom: PhantomData, } } } -impl KeyValueStore for 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: DBColumn, key: &[u8]) -> Result>, Error> { let column_key = BytesKey::from_vec(get_key_for_col(col, key)); @@ -148,4 +144,4 @@ impl KeyValueStore for MemoryStore { } } -impl ItemStore for MemoryStore {} +impl ItemStore for MemoryStore {} diff --git a/beacon_node/store/src/reconstruct.rs b/beacon_node/store/src/reconstruct.rs index 04a519af02..2fb40daa0d 100644 --- a/beacon_node/store/src/reconstruct.rs +++ b/beacon_node/store/src/reconstruct.rs @@ -15,8 +15,8 @@ use types::{EthSpec, Slot}; impl HotColdDB where E: EthSpec, - Hot: ItemStore, - Cold: ItemStore, + Hot: ItemStore, + Cold: ItemStore, { pub fn reconstruct_historic_states( self: &Arc, diff --git a/consensus/fork_choice/tests/tests.rs b/consensus/fork_choice/tests/tests.rs index 353893026b..848834b4d8 100644 --- a/consensus/fork_choice/tests/tests.rs +++ b/consensus/fork_choice/tests/tests.rs @@ -79,7 +79,7 @@ impl ForkChoiceTest { /// Get a value from the `ForkChoice` instantiation. fn get(&self, func: T) -> U where - T: Fn(&BeaconForkChoiceStore, MemoryStore>) -> U, + T: Fn(&BeaconForkChoiceStore) -> U, { func( self.harness diff --git a/database_manager/src/lib.rs b/database_manager/src/lib.rs index 608400fa7e..2509b500e0 100644 --- a/database_manager/src/lib.rs +++ b/database_manager/src/lib.rs @@ -55,7 +55,7 @@ pub fn display_db_version( let blobs_path = client_config.get_blobs_db_path(); let mut version = CURRENT_SCHEMA_VERSION; - HotColdDB::, BeaconNodeBackend>::open( + HotColdDB::::open( &hot_path, &cold_path, &blobs_path, @@ -143,13 +143,13 @@ pub fn inspect_db( let mut num_keys = 0; let sub_db = if inspect_config.freezer { - BeaconNodeBackend::::open(&client_config.store, &cold_path) + BeaconNodeBackend::open(&client_config.store, &cold_path) .map_err(|e| format!("Unable to open freezer DB: {e:?}"))? } else if inspect_config.blobs_db { - BeaconNodeBackend::::open(&client_config.store, &blobs_path) + BeaconNodeBackend::open(&client_config.store, &blobs_path) .map_err(|e| format!("Unable to open blobs DB: {e:?}"))? } else { - BeaconNodeBackend::::open(&client_config.store, &hot_path) + BeaconNodeBackend::open(&client_config.store, &hot_path) .map_err(|e| format!("Unable to open hot DB: {e:?}"))? }; @@ -264,17 +264,17 @@ pub fn compact_db( let (sub_db, db_name) = if compact_config.freezer { ( - BeaconNodeBackend::::open(&client_config.store, &cold_path)?, + BeaconNodeBackend::open(&client_config.store, &cold_path)?, "freezer_db", ) } else if compact_config.blobs_db { ( - BeaconNodeBackend::::open(&client_config.store, &blobs_path)?, + BeaconNodeBackend::open(&client_config.store, &blobs_path)?, "blobs_db", ) } else { ( - BeaconNodeBackend::::open(&client_config.store, &hot_path)?, + BeaconNodeBackend::open(&client_config.store, &hot_path)?, "hot_db", ) }; @@ -309,7 +309,7 @@ pub fn migrate_db( let mut from = CURRENT_SCHEMA_VERSION; let to = migrate_config.to; - let db = HotColdDB::, BeaconNodeBackend>::open( + let db = HotColdDB::::open( &hot_path, &cold_path, &blobs_path, @@ -339,7 +339,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::, BeaconNodeBackend>::open( + let db = HotColdDB::::open( &hot_path, &cold_path, &blobs_path, @@ -363,7 +363,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::, BeaconNodeBackend>::open( + let db = HotColdDB::::open( &hot_path, &cold_path, &blobs_path, @@ -398,7 +398,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::, BeaconNodeBackend>::open( + let db = HotColdDB::::open( &hot_path, &cold_path, &blobs_path, From b5d5644eebcb889b025c13b084c30ac1025adb59 Mon Sep 17 00:00:00 2001 From: Daniel Knopik <107140945+dknopik@users.noreply.github.com> Date: Thu, 21 May 2026 22:00:16 +0200 Subject: [PATCH 04/18] Add getBlobsV3 to `LIGHTHOUSE_CAPABILITIES` (#9330) Forgot to add `ENGINE_GET_BLOBS_V3` to `LIGHTHOUSE_CAPABILITIES`. Add `ENGINE_GET_BLOBS_V3` to `LIGHTHOUSE_CAPABILITIES`. Co-Authored-By: Daniel Knopik --- beacon_node/execution_layer/src/engine_api/http.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/beacon_node/execution_layer/src/engine_api/http.rs b/beacon_node/execution_layer/src/engine_api/http.rs index 110e155c77..7c63f78a22 100644 --- a/beacon_node/execution_layer/src/engine_api/http.rs +++ b/beacon_node/execution_layer/src/engine_api/http.rs @@ -94,6 +94,7 @@ pub static LIGHTHOUSE_CAPABILITIES: &[&str] = &[ ENGINE_GET_CLIENT_VERSION_V1, ENGINE_GET_BLOBS_V1, ENGINE_GET_BLOBS_V2, + ENGINE_GET_BLOBS_V3, ]; /// We opt to initialize the JsonClientVersionV1 rather than the ClientVersionV1 From 60abd4b5b985f5ef47baa799c43c085521e3e596 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Thu, 21 May 2026 23:21:20 -0700 Subject: [PATCH 05/18] Gloas alpha spec 8 (#9315) https://github.com/ethereum/consensus-specs/releases/tag/v1.7.0-alpha.8 Co-Authored-By: Eitan Seri-Levi Co-Authored-By: Michael Sproul --- beacon_node/beacon_chain/src/beacon_chain.rs | 30 ++- .../src/block_production/gloas.rs | 64 +++-- .../beacon_chain/src/execution_payload.rs | 20 +- .../gossip_verified_bid.rs | 98 +++++++- .../src/payload_bid_verification/mod.rs | 2 +- .../src/payload_bid_verification/tests.rs | 15 +- .../gossip_verified_proposer_preferences.rs | 47 +++- .../proposer_preferences_verification/mod.rs | 2 +- .../proposer_preference_cache.rs | 2 +- .../tests.rs | 6 +- .../tests/payload_invalidation.rs | 1 + beacon_node/execution_layer/src/engine_api.rs | 43 ++-- .../src/engine_api/json_structures.rs | 5 + beacon_node/execution_layer/src/lib.rs | 9 +- .../src/test_utils/mock_builder.rs | 7 +- .../src/test_utils/mock_execution_layer.rs | 7 +- beacon_node/http_api/tests/tests.rs | 2 +- .../gossip_methods.rs | 2 +- .../mainnet/config.yaml | 4 +- consensus/fork_choice/src/fork_choice.rs | 96 +++++--- consensus/proto_array/src/error.rs | 5 + .../src/fork_choice_test_definition.rs | 6 +- consensus/proto_array/src/proto_array.rs | 99 ++++++-- .../src/proto_array_fork_choice.rs | 30 +++ .../process_operations.rs | 44 ++-- .../state_processing/src/upgrade/gloas.rs | 94 ++++--- consensus/types/configs/mainnet.yaml | 4 +- .../types/src/builder/proposer_preferences.rs | 2 +- consensus/types/src/core/chain_spec.rs | 92 ++++++- consensus/types/src/state/beacon_state.rs | 4 +- testing/ef_tests/Makefile | 2 +- testing/ef_tests/src/cases/fork_choice.rs | 233 +++++++++++++++++- testing/ef_tests/src/handler.rs | 13 +- testing/ef_tests/tests/tests.rs | 6 + .../src/test_rig.rs | 8 +- .../src/proposer_preferences_service.rs | 2 +- 36 files changed, 863 insertions(+), 243 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 2259e1d809..db8f55a18a 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -96,8 +96,8 @@ use eth2::types::{ SseExtendedPayloadAttributes, SseHead, }; use execution_layer::{ - BlockProposalContents, BlockProposalContentsType, BuilderParams, ChainHealth, ExecutionLayer, - FailedCondition, PayloadAttributes, PayloadStatus, + BlockProposalContents, BlockProposalContentsType, BuilderParams, ChainHealth, + DEFAULT_GAS_LIMIT, ExecutionLayer, FailedCondition, PayloadAttributes, PayloadStatus, }; use fixed_bytes::FixedBytesExtended; use fork_choice::{ @@ -2185,12 +2185,20 @@ impl BeaconChain { // TODO(gloas) do we want to use a dedicated envelope cache instead? // Maybe the new gloas DA cache? (Or should the gloas DA cache use - // the envelopes_times_cache internally?) + // the envelopes_times_cache internally? + // The payload is considered present only if it was observed before + // the payload due deadline (PAYLOAD_DUE_BPS into the slot). + let payload_due = self.spec.get_payload_due(); let payload_present = self .envelope_times_cache .read() .cache - .contains_key(&beacon_block_root); + .get(&beacon_block_root) + .and_then(|entry| entry.timestamps.observed) + .is_some_and(|observed| { + let slot_start = self.slot_clock.start_of(request_slot); + slot_start.is_some_and(|start| observed.saturating_sub(start) < payload_due) + }); // TODO(EIP-7732): Check blob data availability. For now, default to true. let blob_data_available = true; @@ -6476,6 +6484,19 @@ impl BeaconChain { None }; + let target_gas_limit = if prepare_slot_fork.gloas_enabled() { + let proposer_gas_limit = execution_layer.get_proposer_gas_limit(proposer).await; + if proposer_gas_limit.is_none() { + warn!( + %proposer, + "No proposer gas limit configured, falling back to parent gas limit" + ); + } + proposer_gas_limit.or(Some(DEFAULT_GAS_LIMIT)) + } else { + None + }; + let payload_attributes = PayloadAttributes::new( self.slot_clock .start_of(prepare_slot) @@ -6486,6 +6507,7 @@ impl BeaconChain { withdrawals.map(Into::into), parent_beacon_block_root, slot_number, + target_gas_limit, ); execution_layer diff --git a/beacon_node/beacon_chain/src/block_production/gloas.rs b/beacon_node/beacon_chain/src/block_production/gloas.rs index 6510c20ba7..82dad6f6ad 100644 --- a/beacon_node/beacon_chain/src/block_production/gloas.rs +++ b/beacon_node/beacon_chain/src/block_production/gloas.rs @@ -2,11 +2,13 @@ use std::collections::{HashMap, HashSet}; use std::marker::PhantomData; use std::sync::Arc; +use proto_array::PayloadStatus; + use bls::{PublicKeyBytes, Signature}; use execution_layer::{ - BlockProposalContentsGloas, BuilderParams, PayloadAttributes, PayloadParameters, + BlockProposalContentsGloas, BuilderParams, DEFAULT_GAS_LIMIT, PayloadAttributes, + PayloadParameters, }; -use fork_choice::PayloadStatus; use operation_pool::CompactAttestationRef; use ssz::Encode; use state_processing::common::{get_attesting_indices_from_state, get_indexed_payload_attestation}; @@ -150,8 +152,24 @@ impl BeaconChain { verification: ProduceBlockVerification, builder_boost_factor: Option, ) -> Result, BlockProductionError> { - // Extract the parent's execution requests from the envelope (if parent was full). - let parent_execution_requests = if parent_payload_status == PayloadStatus::Full { + let parent_root = if state.slot() > 0 { + *state + .get_block_root(state.slot() - 1) + .map_err(|_| BlockProductionError::UnableToGetBlockRootFromState)? + } else { + state.latest_block_header().canonical_root() + }; + + let should_build_on_full = self + .canonical_head + .fork_choice_read_lock() + .should_build_on_full(&parent_root, parent_payload_status) + .map_err(|e| { + BlockProductionError::BeaconChain(Box::new(BeaconChainError::ForkChoiceError(e))) + })?; + + // Extract the parent's execution requests from the envelope (if building on full). + let parent_execution_requests = if should_build_on_full { parent_envelope .as_ref() .map(|env| env.message.execution_requests.clone()) @@ -197,7 +215,7 @@ impl BeaconChain { .clone() .produce_execution_payload_bid( state, - parent_payload_status, + should_build_on_full, parent_envelope, produce_at_slot, BID_VALUE_SELF_BUILD, @@ -700,12 +718,12 @@ impl BeaconChain { /// data needed to construct the `ExecutionPayloadEnvelope` after the beacon block is /// created, plus the EL block value and `should_override_builder` flag used by the /// caller to compare against any cached p2p builder bid. - #[allow(clippy::type_complexity)] + #[allow(clippy::type_complexity, clippy::too_many_arguments)] #[instrument(level = "debug", skip_all)] pub async fn produce_execution_payload_bid( self: Arc, state: BeaconState, - parent_payload_status: PayloadStatus, + should_build_on_full: bool, parent_envelope: Option>>, produce_at_slot: Slot, bid_value: u64, @@ -751,20 +769,18 @@ impl BeaconChain { let parent_bid = state.latest_execution_payload_bid()?; - // TODO(gloas): need should_extend_payload check here as well let parent_block_slot = state.latest_block_header().slot; let parent_is_pre_gloas = !self .spec .fork_name_at_slot::(parent_block_slot) .gloas_enabled(); - let parent_block_hash = - if parent_payload_status == PayloadStatus::Full || parent_is_pre_gloas { - // Build on parent bid's payload. - parent_bid.block_hash - } else { - // Skip parent bid's payload. For genesis this is the EL genesis hash. - parent_bid.parent_block_hash - }; + let parent_block_hash = if should_build_on_full || parent_is_pre_gloas { + // Build on parent bid's payload. + parent_bid.block_hash + } else { + // Skip parent bid's payload. For genesis this is the EL genesis hash. + parent_bid.parent_block_hash + }; // TODO(gloas) this should be BlockProductionVersion::V4 // V3 is okay for now as long as we're not connected to a builder @@ -953,10 +969,7 @@ fn get_execution_payload_gloas( compute_timestamp_at_slot(state, state.slot(), spec).map_err(BeaconStateError::from)?; let random = *state.get_randao_mix(current_epoch)?; - // TODO(gloas): this gas limit calc is not necessarily right let parent_bid = state.latest_execution_payload_bid()?; - let latest_gas_limit = parent_bid.gas_limit; - let is_parent_block_full = parent_block_hash == parent_bid.block_hash; let withdrawals = if is_parent_block_full { @@ -992,7 +1005,6 @@ fn get_execution_payload_gloas( random, proposer_index, parent_block_hash, - latest_gas_limit, builder_params, withdrawals, parent_beacon_block_root, @@ -1020,7 +1032,6 @@ async fn prepare_execution_payload( random: Hash256, proposer_index: u64, parent_block_hash: ExecutionBlockHash, - parent_gas_limit: u64, builder_params: BuilderParams, withdrawals: Vec, parent_beacon_block_root: Hash256, @@ -1058,6 +1069,10 @@ where .get_suggested_fee_recipient(proposer_index) .await; let slot_number = Some(builder_params.slot.as_u64()); + let target_gas_limit = execution_layer + .get_proposer_gas_limit(proposer_index) + .await + .unwrap_or(DEFAULT_GAS_LIMIT); let payload_attributes = PayloadAttributes::new( timestamp, @@ -1066,13 +1081,12 @@ where Some(withdrawals), Some(parent_beacon_block_root), slot_number, + Some(target_gas_limit), ); - - let target_gas_limit = execution_layer.get_proposer_gas_limit(proposer_index).await; let payload_parameters = PayloadParameters { parent_hash: parent_block_hash, - parent_gas_limit, - proposer_gas_limit: target_gas_limit, + parent_gas_limit: None, + proposer_gas_limit: Some(target_gas_limit), payload_attributes: &payload_attributes, forkchoice_update_params: &forkchoice_update_params, current_fork: fork, diff --git a/beacon_node/beacon_chain/src/execution_payload.rs b/beacon_node/beacon_chain/src/execution_payload.rs index 16542eea2d..c8976fc6a8 100644 --- a/beacon_node/beacon_chain/src/execution_payload.rs +++ b/beacon_node/beacon_chain/src/execution_payload.rs @@ -342,7 +342,7 @@ pub fn get_execution_payload( Ok(join_handle) } -/// Prepares an execution payload for inclusion in a block. +/// Prepares an execution payload (pre-gloas) for inclusion in a block. /// /// ## Errors /// @@ -373,6 +373,13 @@ where { let spec = &chain.spec; let fork = spec.fork_name_at_slot::(builder_params.slot); + + if fork.gloas_enabled() { + return Err(BlockProductionError::InvalidBlockVariant( + "Called pre-gloas prepare_execution_payload on a gloas block".to_string(), + )); + } + let execution_layer = chain .execution_layer .as_ref() @@ -403,25 +410,20 @@ where .get_suggested_fee_recipient(proposer_index) .await; - let slot_number = if fork.gloas_enabled() { - Some(builder_params.slot.as_u64()) - } else { - None - }; - let payload_attributes = PayloadAttributes::new( timestamp, random, suggested_fee_recipient, withdrawals, parent_beacon_block_root, - slot_number, + None, + None, ); 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, + parent_gas_limit: Some(latest_execution_payload_header_gas_limit), proposer_gas_limit: target_gas_limit, payload_attributes: &payload_attributes, forkchoice_update_params: &forkchoice_update_params, diff --git a/beacon_node/beacon_chain/src/payload_bid_verification/gossip_verified_bid.rs b/beacon_node/beacon_chain/src/payload_bid_verification/gossip_verified_bid.rs index 1f3f074598..354705b92c 100644 --- a/beacon_node/beacon_chain/src/payload_bid_verification/gossip_verified_bid.rs +++ b/beacon_node/beacon_chain/src/payload_bid_verification/gossip_verified_bid.rs @@ -43,9 +43,6 @@ pub(crate) fn verify_bid_consistency( if bid.fee_recipient != proposer_preferences.message.fee_recipient { return Err(PayloadBidError::InvalidFeeRecipient); } - if bid.gas_limit != proposer_preferences.message.gas_limit { - return Err(PayloadBidError::InvalidGasLimit); - } let max_blobs_per_block = spec.max_blobs_per_block(bid_slot.epoch(E::slots_per_epoch())) as usize; @@ -161,7 +158,23 @@ impl GossipVerifiedPayloadBid { }); } - // TODO(gloas) [IGNORE] bid.parent_block_hash is the block hash of a known execution payload in fork choice. + // TODO(gloas): [IGNORE] bid.parent_block_hash is the block hash of a known execution + // payload in fork choice. + + // TODO(gloas): This uses head state's bid gas_limit as parent_gas_limit, which is only + // correct when the bid's parent is the head. If the parent is an ancestor further back + // this check may be inaccurate. Fixing this requires storing + // gas_limit in fork choice or looking it up from the store by parent_block_hash. Taking the above + // TODO into consideration maybe should persist parent block hash and gas limit in fork choice? + if let Ok(parent_bid) = head_state.latest_execution_payload_bid() + && !is_gas_limit_target_compatible( + parent_bid.gas_limit, + signed_bid.message.gas_limit, + proposer_preferences.message.target_gas_limit, + )? + { + return Err(PayloadBidError::InvalidGasLimit); + } drop(fork_choice); @@ -263,8 +276,36 @@ impl BeaconChain { } } +/// Check if `gas_limit` is compatible with `target_gas_limit` under the +/// EIP-1559 transition rule from `parent_gas_limit`. +pub fn is_gas_limit_target_compatible( + parent_gas_limit: u64, + gas_limit: u64, + target_gas_limit: u64, +) -> Result { + let max_gas_limit_difference = (parent_gas_limit / 1024) + .max(1) + .checked_sub(1) + .ok_or(PayloadBidError::InvalidGasLimit)?; + let min_gas_limit = parent_gas_limit + .checked_sub(max_gas_limit_difference) + .ok_or(PayloadBidError::InvalidGasLimit)?; + let max_gas_limit = parent_gas_limit + .checked_add(max_gas_limit_difference) + .ok_or(PayloadBidError::InvalidGasLimit)?; + + if target_gas_limit >= min_gas_limit && target_gas_limit <= max_gas_limit { + Ok(gas_limit == target_gas_limit) + } else if target_gas_limit > max_gas_limit { + Ok(gas_limit == max_gas_limit) + } else { + Ok(gas_limit == min_gas_limit) + } +} + #[cfg(test)] mod tests { + use super::is_gas_limit_target_compatible; use bls::Signature; use kzg::KzgCommitment; use ssz_types::VariableList; @@ -288,11 +329,14 @@ mod tests { } } - fn make_preferences(fee_recipient: Address, gas_limit: u64) -> SignedProposerPreferences { + fn make_preferences( + fee_recipient: Address, + target_gas_limit: u64, + ) -> SignedProposerPreferences { SignedProposerPreferences { message: ProposerPreferences { fee_recipient, - gas_limit, + target_gas_limit, ..ProposerPreferences::default() }, signature: Signature::empty(), @@ -382,13 +426,41 @@ mod tests { } #[test] - fn test_gas_limit_mismatch() { - let (state, spec) = state_and_spec(); - let current_slot = Slot::new(10); - let bid = make_bid(current_slot, Address::ZERO, 30_000_000); - let prefs = make_preferences(Address::ZERO, 50_000_000); + fn test_is_gas_limit_target_compatible_increase_within_limit() { + assert!(is_gas_limit_target_compatible(60_000_000, 60_000_100, 60_000_100).unwrap()); + } - let result = verify_bid_consistency::(&bid, current_slot, &prefs, &state, &spec); - assert!(matches!(result, Err(PayloadBidError::InvalidGasLimit))); + #[test] + fn test_is_gas_limit_target_compatible_increase_exceeding_limit() { + // max_diff = 60_000_000 / 1024 - 1 = 58_592 + // max_gas_limit = 60_000_000 + 58_592 = 60_058_592 + assert!(is_gas_limit_target_compatible(60_000_000, 60_058_592, 100_000_000).unwrap()); + } + + #[test] + fn test_is_gas_limit_target_compatible_increase_exceeding_off_by_one() { + assert!(!is_gas_limit_target_compatible(60_000_000, 60_058_593, 100_000_000).unwrap()); + } + + #[test] + fn test_is_gas_limit_target_compatible_decrease_within_limit() { + assert!(is_gas_limit_target_compatible(60_000_000, 59_999_990, 59_999_990).unwrap()); + } + + #[test] + fn test_is_gas_limit_target_compatible_decrease_exceeding_limit() { + // min_gas_limit = 60_000_000 - 58_592 = 59_941_408 + assert!(is_gas_limit_target_compatible(60_000_000, 59_941_408, 30_000_000).unwrap()); + } + + #[test] + fn test_is_gas_limit_target_compatible_target_equals_parent() { + assert!(is_gas_limit_target_compatible(60_000_000, 60_000_000, 60_000_000).unwrap()); + } + + #[test] + fn test_is_gas_limit_target_compatible_parent_underflows() { + // parent=1023: max(1023/1024, 1) - 1 = max(0, 1) - 1 = 0, no change allowed + assert!(is_gas_limit_target_compatible(1023, 1023, 60_000_000).unwrap()); } } diff --git a/beacon_node/beacon_chain/src/payload_bid_verification/mod.rs b/beacon_node/beacon_chain/src/payload_bid_verification/mod.rs index 514695f5c0..a40fd14872 100644 --- a/beacon_node/beacon_chain/src/payload_bid_verification/mod.rs +++ b/beacon_node/beacon_chain/src/payload_bid_verification/mod.rs @@ -48,7 +48,7 @@ pub enum PayloadBidError { }, /// The bids fee recipient doesn't match the proposer preferences fee recipient. InvalidFeeRecipient, - /// The bids gas limit doesn't match the proposer preferences gas limit. + /// The bid's gas limit is not compatible with the proposer's target gas limit. InvalidGasLimit, /// The bids execution payment is non-zero ExecutionPaymentNonZero { execution_payment: u64 }, diff --git a/beacon_node/beacon_chain/src/payload_bid_verification/tests.rs b/beacon_node/beacon_chain/src/payload_bid_verification/tests.rs index c68e6d9d32..ccdf64d41d 100644 --- a/beacon_node/beacon_chain/src/payload_bid_verification/tests.rs +++ b/beacon_node/beacon_chain/src/payload_bid_verification/tests.rs @@ -101,6 +101,17 @@ impl TestContext { root: Hash256::ZERO, }; + // Set a non-zero gas_limit on latest_execution_payload_bid so the gas limit + // compatibility check doesn't reject all bids at genesis. + if let Ok(bid) = state.latest_execution_payload_bid_mut() { + bid.gas_limit = 30_000_000; + } + // Update body_root to reflect the modified bid (genesis block embeds it). + let genesis_body_root = genesis_block(&state, &spec) + .expect("should build genesis block") + .body_root(); + state.latest_block_header_mut().body_root = genesis_body_root; + let inactive_keypair = &keypairs[NUM_BUILDERS]; let inactive_creds = builder_withdrawal_credentials(&inactive_keypair.pk, &spec); let inactive_builder_index = state @@ -248,7 +259,7 @@ fn make_signed_preferences( proposal_slot: Slot, validator_index: u64, fee_recipient: Address, - gas_limit: u64, + target_gas_limit: u64, ) -> Arc { Arc::new(SignedProposerPreferences { message: ProposerPreferences { @@ -256,7 +267,7 @@ fn make_signed_preferences( proposal_slot, validator_index, fee_recipient, - gas_limit, + target_gas_limit, }, signature: Signature::empty(), }) diff --git a/beacon_node/beacon_chain/src/proposer_preferences_verification/gossip_verified_proposer_preferences.rs b/beacon_node/beacon_chain/src/proposer_preferences_verification/gossip_verified_proposer_preferences.rs index 4ba33fde72..586721d8c1 100644 --- a/beacon_node/beacon_chain/src/proposer_preferences_verification/gossip_verified_proposer_preferences.rs +++ b/beacon_node/beacon_chain/src/proposer_preferences_verification/gossip_verified_proposer_preferences.rs @@ -18,13 +18,16 @@ pub(crate) fn verify_preferences_consistency( preferences: &ProposerPreferences, current_slot: Slot, head_state: &BeaconState, + spec: &ChainSpec, ) -> Result<(), ProposerPreferencesError> { let proposal_slot = preferences.proposal_slot; let validator_index = preferences.validator_index; let current_epoch = current_slot.epoch(E::slots_per_epoch()); let proposal_epoch = proposal_slot.epoch(E::slots_per_epoch()); - if proposal_epoch < current_epoch || proposal_epoch > current_epoch.saturating_add(1u64) { + if proposal_epoch < current_epoch + || proposal_epoch > current_epoch.saturating_add(spec.min_seed_lookahead) + { return Err(ProposerPreferencesError::InvalidProposalEpoch { proposal_epoch }); } @@ -35,7 +38,7 @@ pub(crate) fn verify_preferences_consistency( }); } - if !head_state.is_valid_proposal_slot(preferences)? { + if !head_state.is_valid_proposal_slot(preferences, spec)? { return Err(ProposerPreferencesError::InvalidProposalSlot { validator_index, proposal_slot, @@ -83,7 +86,12 @@ impl GossipVerifiedProposerPreferences { }); } - verify_preferences_consistency(&signed_preferences.message, current_slot, head_state)?; + verify_preferences_consistency( + &signed_preferences.message, + current_slot, + head_state, + ctx.spec, + )?; // Verify signature proposer_preferences_signature_set( @@ -155,11 +163,13 @@ impl BeaconChain { #[cfg(test)] mod tests { use types::{ - Address, BeaconState, EthSpec, Hash256, MinimalEthSpec, ProposerPreferences, Slot, + Address, BeaconState, ChainSpec, EthSpec, Hash256, MinimalEthSpec, ProposerPreferences, + Slot, }; use super::verify_preferences_consistency; use crate::proposer_preferences_verification::ProposerPreferencesError; + use crate::test_utils::{fork_name_from_env, test_spec}; type E = MinimalEthSpec; @@ -169,20 +179,28 @@ mod tests { proposal_slot, validator_index, fee_recipient: Address::ZERO, - gas_limit: 30_000_000, + target_gas_limit: 30_000_000, } } fn state() -> BeaconState { - BeaconState::new(0, <_>::default(), &E::default_spec()) + let spec = spec(); + BeaconState::new(0, <_>::default(), &spec) + } + + fn spec() -> ChainSpec { + test_spec::() } #[test] fn test_invalid_epoch_too_old() { + if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } let current_slot = Slot::new(2 * E::slots_per_epoch()); let prefs = make_preferences(Slot::new(3), 0); - let result = verify_preferences_consistency::(&prefs, current_slot, &state()); + let result = verify_preferences_consistency::(&prefs, current_slot, &state(), &spec()); assert!(matches!( result, Err(ProposerPreferencesError::InvalidProposalEpoch { .. }) @@ -191,10 +209,13 @@ mod tests { #[test] fn test_invalid_epoch_too_far_ahead() { + if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } let current_slot = Slot::new(E::slots_per_epoch()); let prefs = make_preferences(Slot::new(3 * E::slots_per_epoch() + 1), 0); - let result = verify_preferences_consistency::(&prefs, current_slot, &state()); + let result = verify_preferences_consistency::(&prefs, current_slot, &state(), &spec()); assert!(matches!( result, Err(ProposerPreferencesError::InvalidProposalEpoch { .. }) @@ -203,10 +224,13 @@ mod tests { #[test] fn test_proposal_slot_already_passed() { + if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } let current_slot = Slot::new(10); let prefs = make_preferences(Slot::new(9), 0); - let result = verify_preferences_consistency::(&prefs, current_slot, &state()); + let result = verify_preferences_consistency::(&prefs, current_slot, &state(), &spec()); assert!(matches!( result, Err(ProposerPreferencesError::ProposalSlotAlreadyPassed { .. }) @@ -215,10 +239,13 @@ mod tests { #[test] fn test_proposal_slot_equal_to_current() { + if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } let current_slot = Slot::new(10); let prefs = make_preferences(Slot::new(10), 0); - let result = verify_preferences_consistency::(&prefs, current_slot, &state()); + let result = verify_preferences_consistency::(&prefs, current_slot, &state(), &spec()); assert!(matches!( result, Err(ProposerPreferencesError::ProposalSlotAlreadyPassed { .. }) diff --git a/beacon_node/beacon_chain/src/proposer_preferences_verification/mod.rs b/beacon_node/beacon_chain/src/proposer_preferences_verification/mod.rs index a2e96dfce1..6c79e56733 100644 --- a/beacon_node/beacon_chain/src/proposer_preferences_verification/mod.rs +++ b/beacon_node/beacon_chain/src/proposer_preferences_verification/mod.rs @@ -24,7 +24,7 @@ mod tests; #[derive(Debug)] pub enum ProposerPreferencesError { - /// The proposal slot is not in the current or next epoch. + /// The proposal slot is not within the proposer lookahead. InvalidProposalEpoch { proposal_epoch: Epoch }, /// The proposal slot has already passed. ProposalSlotAlreadyPassed { diff --git a/beacon_node/beacon_chain/src/proposer_preferences_verification/proposer_preference_cache.rs b/beacon_node/beacon_chain/src/proposer_preferences_verification/proposer_preference_cache.rs index 7bbdf34888..c423418fbc 100644 --- a/beacon_node/beacon_chain/src/proposer_preferences_verification/proposer_preference_cache.rs +++ b/beacon_node/beacon_chain/src/proposer_preferences_verification/proposer_preference_cache.rs @@ -87,7 +87,7 @@ mod tests { proposal_slot: slot, validator_index, fee_recipient: Address::ZERO, - gas_limit: 30_000_000, + target_gas_limit: 30_000_000, }, signature: Signature::empty(), }), diff --git a/beacon_node/beacon_chain/src/proposer_preferences_verification/tests.rs b/beacon_node/beacon_chain/src/proposer_preferences_verification/tests.rs index 468e08ff3b..53c1c4ded3 100644 --- a/beacon_node/beacon_chain/src/proposer_preferences_verification/tests.rs +++ b/beacon_node/beacon_chain/src/proposer_preferences_verification/tests.rs @@ -112,7 +112,7 @@ impl TestContext { let slot_in_epoch = slot.as_usize() % E::slots_per_epoch() as usize; let epoch = slot.epoch(E::slots_per_epoch()); let current_epoch = state.slot().epoch(E::slots_per_epoch()); - let index = if epoch == current_epoch.saturating_add(1u64) { + let index = if epoch == current_epoch.saturating_add(self.spec.min_seed_lookahead) { E::slots_per_epoch() as usize + slot_in_epoch } else { slot_in_epoch @@ -131,7 +131,7 @@ fn make_signed_preferences( proposal_slot, validator_index, fee_recipient: Address::ZERO, - gas_limit: 30_000_000, + target_gas_limit: 30_000_000, }, signature: Signature::empty(), }) @@ -271,7 +271,7 @@ fn same_validator_different_dependent_root_not_deduplicated() { validator_index: 42, dependent_root: Hash256::repeat_byte(0xaa), fee_recipient: Address::ZERO, - gas_limit: 30_000_000, + target_gas_limit: 30_000_000, }, signature: Signature::empty(), }), diff --git a/beacon_node/beacon_chain/tests/payload_invalidation.rs b/beacon_node/beacon_chain/tests/payload_invalidation.rs index be85fc2245..abf1fe48a6 100644 --- a/beacon_node/beacon_chain/tests/payload_invalidation.rs +++ b/beacon_node/beacon_chain/tests/payload_invalidation.rs @@ -1035,6 +1035,7 @@ async fn payload_preparation() { None, None, None, + None, ); assert_eq!(rig.previous_payload_attributes(), payload_attributes); } diff --git a/beacon_node/execution_layer/src/engine_api.rs b/beacon_node/execution_layer/src/engine_api.rs index acf5f2778b..7337a29c8f 100644 --- a/beacon_node/execution_layer/src/engine_api.rs +++ b/beacon_node/execution_layer/src/engine_api.rs @@ -178,6 +178,8 @@ pub struct PayloadAttributes { pub parent_beacon_block_root: Hash256, #[superstruct(only(V4), partial_getter(copy))] pub slot_number: u64, + #[superstruct(only(V4), partial_getter(copy))] + pub target_gas_limit: u64, } impl PayloadAttributes { @@ -188,19 +190,29 @@ impl PayloadAttributes { withdrawals: Option>, parent_beacon_block_root: Option, slot_number: Option, + target_gas_limit: Option, ) -> Self { - match (withdrawals, parent_beacon_block_root, slot_number) { - (Some(withdrawals), Some(parent_beacon_block_root), Some(slot_number)) => { - PayloadAttributes::V4(PayloadAttributesV4 { - timestamp, - prev_randao, - suggested_fee_recipient, - withdrawals, - parent_beacon_block_root, - slot_number, - }) - } - (Some(withdrawals), Some(parent_beacon_block_root), None) => { + match ( + withdrawals, + parent_beacon_block_root, + slot_number, + target_gas_limit, + ) { + ( + Some(withdrawals), + Some(parent_beacon_block_root), + Some(slot_number), + Some(target_gas_limit), + ) => PayloadAttributes::V4(PayloadAttributesV4 { + timestamp, + prev_randao, + suggested_fee_recipient, + withdrawals, + parent_beacon_block_root, + slot_number, + target_gas_limit, + }), + (Some(withdrawals), Some(parent_beacon_block_root), _, _) => { PayloadAttributes::V3(PayloadAttributesV3 { timestamp, prev_randao, @@ -209,13 +221,13 @@ impl PayloadAttributes { parent_beacon_block_root, }) } - (Some(withdrawals), None, _) => PayloadAttributes::V2(PayloadAttributesV2 { + (Some(withdrawals), None, _, _) => PayloadAttributes::V2(PayloadAttributesV2 { timestamp, prev_randao, suggested_fee_recipient, withdrawals, }), - (None, _, _) => PayloadAttributes::V1(PayloadAttributesV1 { + (None, _, _, _) => PayloadAttributes::V1(PayloadAttributesV1 { timestamp, prev_randao, suggested_fee_recipient, @@ -260,7 +272,7 @@ impl From for SsePayloadAttributes { withdrawals, parent_beacon_block_root, }), - // V4 maps to V3 for SSE (slot_number is not part of the SSE spec) + // V4 maps to V3 for SSE (slot_number/target_gas_limit are not part of the SSE spec) PayloadAttributes::V4(PayloadAttributesV4 { timestamp, prev_randao, @@ -268,6 +280,7 @@ impl From for SsePayloadAttributes { withdrawals, parent_beacon_block_root, slot_number: _, + target_gas_limit: _, }) => Self::V3(SsePayloadAttributesV3 { timestamp, prev_randao, 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 9d9391a1e1..fb516e3e16 100644 --- a/beacon_node/execution_layer/src/engine_api/json_structures.rs +++ b/beacon_node/execution_layer/src/engine_api/json_structures.rs @@ -777,6 +777,9 @@ pub struct JsonPayloadAttributes { #[superstruct(only(V4))] #[serde(with = "serde_utils::u64_hex_be")] pub slot_number: u64, + #[superstruct(only(V4))] + #[serde(with = "serde_utils::u64_hex_be")] + pub target_gas_limit: u64, } impl From for JsonPayloadAttributes { @@ -807,6 +810,7 @@ impl From for JsonPayloadAttributes { withdrawals: pa.withdrawals.into_iter().map(Into::into).collect(), parent_beacon_block_root: pa.parent_beacon_block_root, slot_number: pa.slot_number, + target_gas_limit: pa.target_gas_limit, }), } } @@ -840,6 +844,7 @@ impl From for PayloadAttributes { withdrawals: jpa.withdrawals.into_iter().map(Into::into).collect(), parent_beacon_block_root: jpa.parent_beacon_block_root, slot_number: jpa.slot_number, + target_gas_limit: jpa.target_gas_limit, }), } } diff --git a/beacon_node/execution_layer/src/lib.rs b/beacon_node/execution_layer/src/lib.rs index b2dabb7c01..b1b8b0deaa 100644 --- a/beacon_node/execution_layer/src/lib.rs +++ b/beacon_node/execution_layer/src/lib.rs @@ -73,6 +73,8 @@ pub const DEFAULT_EXECUTION_ENDPOINT: &str = "http://localhost:8551/"; /// Name for the default file used for the jwt secret. pub const DEFAULT_JWT_FILE: &str = "jwt.hex"; +pub const DEFAULT_GAS_LIMIT: u64 = 60_000_000; + /// A fee recipient address for use during block production. Only used as a very last resort if /// there is no address provided by the user. /// @@ -358,7 +360,10 @@ impl> BlockProposalContents { pub parent_hash: ExecutionBlockHash, - pub parent_gas_limit: u64, + // NOTE: The `parent_gas_limit` is a bit scuffed. We made it optional for Gloas because it + // isn't currently required, but it should possibly be made non-optional again if needed. + // Or we should superstruct this type. + pub parent_gas_limit: Option, pub proposer_gas_limit: Option, pub payload_attributes: &'a PayloadAttributes, pub forkchoice_update_params: &'a ForkchoiceUpdateParameters, @@ -2082,7 +2087,7 @@ fn verify_builder_bid( 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)); + .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 { 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 d6243a7c4d..d456c9adc1 100644 --- a/beacon_node/execution_layer/src/test_utils/mock_builder.rs +++ b/beacon_node/execution_layer/src/test_utils/mock_builder.rs @@ -282,7 +282,7 @@ impl BidStuff for BuilderBid { #[derive(Clone)] pub struct PayloadParametersCloned { pub parent_hash: ExecutionBlockHash, - pub parent_gas_limit: u64, + pub parent_gas_limit: Option, pub proposer_gas_limit: Option, pub payload_attributes: PayloadAttributes, pub forkchoice_update_params: ForkchoiceUpdateParameters, @@ -903,6 +903,7 @@ impl MockBuilder { expected_withdrawals, None, None, + None, ), ForkName::Deneb | ForkName::Electra | ForkName::Fulu => PayloadAttributes::new( timestamp, @@ -911,6 +912,7 @@ impl MockBuilder { expected_withdrawals, Some(head_block_root), None, + None, ), ForkName::Gloas => PayloadAttributes::new( timestamp, @@ -919,6 +921,7 @@ impl MockBuilder { expected_withdrawals, Some(head_block_root), Some(slot.as_u64()), + None, // TODO(gloas): pass target_gas_limit ), ForkName::Base | ForkName::Altair => { return Err("invalid fork".to_string()); @@ -969,7 +972,7 @@ impl MockBuilder { let payload_parameters = PayloadParametersCloned { parent_hash: head_execution_hash, - parent_gas_limit: head_gas_limit, + parent_gas_limit: Some(head_gas_limit), proposer_gas_limit: Some(proposer_gas_limit), payload_attributes, forkchoice_update_params, 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 5b721bcab2..583808281f 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 @@ -105,6 +105,7 @@ impl MockExecutionLayer { None, None, None, + None, ); // Insert a proposer to ensure the fork choice updated command works. @@ -146,11 +147,12 @@ impl MockExecutionLayer { None, None, None, + None, ); let payload_parameters = PayloadParameters { parent_hash, - parent_gas_limit, + parent_gas_limit: Some(parent_gas_limit), proposer_gas_limit: None, payload_attributes: &payload_attributes, forkchoice_update_params: &forkchoice_update_params, @@ -199,11 +201,12 @@ impl MockExecutionLayer { None, None, None, + None, ); let payload_parameters = PayloadParameters { parent_hash, - parent_gas_limit, + parent_gas_limit: Some(parent_gas_limit), proposer_gas_limit: None, payload_attributes: &payload_attributes, forkchoice_update_params: &forkchoice_update_params, diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index a7fe34593a..3da0841a4e 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -2929,7 +2929,7 @@ impl ApiTester { proposal_slot, validator_index: validator_index as u64, fee_recipient: Address::repeat_byte(0xaa), - gas_limit: 30_000_000, + target_gas_limit: 30_000_000, }; let epoch = proposal_slot.epoch(E::slots_per_epoch()); 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 7a902649cb..3e8845f017 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -4058,7 +4058,6 @@ impl NetworkBeaconProcessor { PayloadBidError::BadSignature | PayloadBidError::InvalidBuilder { .. } | PayloadBidError::InvalidFeeRecipient - | PayloadBidError::InvalidGasLimit | PayloadBidError::ExecutionPaymentNonZero { .. } | PayloadBidError::InvalidBlobKzgCommitments { .. }, ) => { @@ -4076,6 +4075,7 @@ impl NetworkBeaconProcessor { | PayloadBidError::ParentBlockRootUnknown { .. } | PayloadBidError::ParentBlockRootNotCanonical { .. } | PayloadBidError::BuilderCantCoverBid { .. } + | PayloadBidError::InvalidGasLimit | PayloadBidError::BeaconStateError(_) | PayloadBidError::InternalError(_) | PayloadBidError::InvalidBidSlot { .. } 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 02bf37cb55..ced9679142 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 @@ -93,8 +93,8 @@ SYNC_MESSAGE_DUE_BPS: 3333 CONTRIBUTION_DUE_BPS: 6667 # Gloas -# 2**6 (= 64) epochs -MIN_BUILDER_WITHDRAWABILITY_DELAY: 64 +# 2**13 (= 8192) epochs +MIN_BUILDER_WITHDRAWABILITY_DELAY: 8192 # 2500 basis points, 25% of SLOT_DURATION_MS ATTESTATION_DUE_BPS_GLOAS: 2500 # 5000 basis points, 50% of SLOT_DURATION_MS diff --git a/consensus/fork_choice/src/fork_choice.rs b/consensus/fork_choice/src/fork_choice.rs index a60859585c..2de8ce7d81 100644 --- a/consensus/fork_choice/src/fork_choice.rs +++ b/consensus/fork_choice/src/fork_choice.rs @@ -1207,7 +1207,6 @@ where fn validate_on_payload_attestation( &self, indexed_payload_attestation: &IndexedPayloadAttestation, - is_from_block: AttestationFromBlock, ) -> Result<(), InvalidPayloadAttestation> { // This check is from `is_valid_indexed_payload_attestation`, but we do it immediately to // avoid wasting time on junk attestations. @@ -1233,25 +1232,6 @@ where }); } - // PTC votes can only change the vote for their assigned beacon block, return early otherwise - if block.slot != indexed_payload_attestation.data.slot { - return Ok(()); - } - - // Gossip payload attestations must be for the current slot. - // NOTE: signature is assumed to have been verified by caller. - // https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/fork-choice.md - if matches!(is_from_block, AttestationFromBlock::False) - && indexed_payload_attestation.data.slot != self.fc_store.get_current_slot() - { - return Err( - InvalidPayloadAttestation::PayloadAttestationNotCurrentSlot { - attestation_slot: indexed_payload_attestation.data.slot, - current_slot: self.fc_store.get_current_slot(), - }, - ); - } - Ok(()) } @@ -1339,34 +1319,69 @@ where pub fn on_payload_attestation( &mut self, system_time_current_slot: Slot, - attestation: &IndexedPayloadAttestation, + payload_attestation: &IndexedPayloadAttestation, is_from_block: AttestationFromBlock, ptc: &[usize], ) -> Result<(), Error> { self.update_time(system_time_current_slot)?; - if attestation.data.beacon_block_root.is_zero() { + if payload_attestation.data.beacon_block_root.is_zero() { return Ok(()); } // TODO(gloas): Should ignore wrong-slot payload attestations at the caller, they could // have been processed at the correct slot when received on gossip, but then have the // wrong-slot by the time they make it to here (TOCTOU). - self.validate_on_payload_attestation(attestation, is_from_block)?; + // TODO(gloas): Consider inlining validate_on_payload_attestation here to look more like the spec. + self.validate_on_payload_attestation(payload_attestation)?; - // Resolve validator indices to PTC committee positions. - let ptc_indices: Vec = attestation - .attesting_indices - .iter() - .filter_map(|validator_index| ptc.iter().position(|&p| p == *validator_index as usize)) - .collect(); + // PTC votes can only change the vote for their assigned beacon block, return early otherwise. + let block = self + .proto_array + .get_block(&payload_attestation.data.beacon_block_root) + .ok_or(InvalidPayloadAttestation::UnknownHeadBlock { + beacon_block_root: payload_attestation.data.beacon_block_root, + })?; + if block.slot != payload_attestation.data.slot { + return Ok(()); + } + + // Gossip payload attestations must be for the current slot. + if matches!(is_from_block, AttestationFromBlock::False) + && payload_attestation.data.slot != self.fc_store.get_current_slot() + { + return Err( + InvalidPayloadAttestation::PayloadAttestationNotCurrentSlot { + attestation_slot: payload_attestation.data.slot, + current_slot: self.fc_store.get_current_slot(), + } + .into(), + ); + } + + // Resolve validator indices to all PTC committee positions. A validator may + // appear multiple times in the PTC committee. + let mut ptc_indices = vec![]; + let mut validators_found = 0; + for validator_index in payload_attestation.attesting_indices.iter() { + let mut found = false; + for (ptc_index, &ptc_validator_index) in ptc.iter().enumerate() { + if ptc_validator_index == *validator_index as usize { + ptc_indices.push(ptc_index); + found = true; + } + } + if found { + validators_found += 1; + } + } // Check that all the attesters are in the PTC - if ptc_indices.len() != attestation.attesting_indices.len() { + if validators_found != payload_attestation.attesting_indices.len() { return Err( InvalidPayloadAttestation::PayloadAttestationAttestersNotInPtc { - attesting_indices_len: attestation.attesting_indices.len(), - attesting_indices_in_ptc: ptc_indices.len(), + attesting_indices_len: payload_attestation.attesting_indices.len(), + attesting_indices_in_ptc: validators_found, } .into(), ); @@ -1374,10 +1389,10 @@ where for &ptc_index in &ptc_indices { self.proto_array.process_payload_attestation( - attestation.data.beacon_block_root, + payload_attestation.data.beacon_block_root, ptc_index, - attestation.data.payload_present, - attestation.data.blob_data_available, + payload_attestation.data.payload_present, + payload_attestation.data.blob_data_available, )?; } @@ -1522,6 +1537,17 @@ where && self.is_finalized_checkpoint_or_descendant(*block_root) } + /// Called by the proposer to decide whether to build on the full or empty parent. + pub fn should_build_on_full( + &self, + block_root: &Hash256, + parent_payload_status: PayloadStatus, + ) -> Result> { + self.proto_array + .should_build_on_full::(block_root, parent_payload_status) + .map_err(Error::ProtoArrayStringError) + } + /// Returns whether the proposer should extend the execution payload chain of the given block. pub fn should_extend_payload(&self, block_root: &Hash256) -> Result> { let proposer_boost_root = self.fc_store.proposer_boost_root(); diff --git a/consensus/proto_array/src/error.rs b/consensus/proto_array/src/error.rs index bb47af97d9..d185ed371c 100644 --- a/consensus/proto_array/src/error.rs +++ b/consensus/proto_array/src/error.rs @@ -1,3 +1,4 @@ +use crate::PayloadStatus; use safe_arith::ArithError; use types::{Checkpoint, Epoch, ExecutionBlockHash, Hash256, Slot}; @@ -62,6 +63,10 @@ pub enum Error { }, NoViableChildren, OnBlockRequiresProposerIndex, + InvalidPayloadStatus { + block_root: Hash256, + payload_status: PayloadStatus, + }, } impl From for Error { diff --git a/consensus/proto_array/src/fork_choice_test_definition.rs b/consensus/proto_array/src/fork_choice_test_definition.rs index d537f16bb2..43b76ec7cb 100644 --- a/consensus/proto_array/src/fork_choice_test_definition.rs +++ b/consensus/proto_array/src/fork_choice_test_definition.rs @@ -556,7 +556,11 @@ impl ForkChoiceTestDefinition { node_v29.payload_data_availability_votes = BitVector::from_bytes(smallvec::smallvec![fill; 64]) .expect("valid 512-bit bitvector"); - // Per spec, is_payload_timely/is_payload_data_available require + // Mark all PTC members as having participated. + node_v29.ptc_participation = + BitVector::from_bytes(smallvec::smallvec![0xFF; 64]) + .expect("valid 512-bit bitvector"); + // Per spec, payload_timeliness/payload_data_availability require // the payload to be in payload_states (payload_received). node_v29.payload_received = is_timely || is_data_available; } diff --git a/consensus/proto_array/src/proto_array.rs b/consensus/proto_array/src/proto_array.rs index 78f5026689..8ac8354f06 100644 --- a/consensus/proto_array/src/proto_array.rs +++ b/consensus/proto_array/src/proto_array.rs @@ -155,6 +155,10 @@ pub struct ProtoNode { /// Tiebreak derived as: `num_set_bits() > ptc_size / 2`. #[superstruct(only(V29))] pub payload_data_availability_votes: BitVector, + /// Tracks which PTC members have cast a vote. + /// Bit i set means PTC member i has submitted a payload attestation. + #[superstruct(only(V29))] + pub ptc_participation: BitVector, /// Whether the execution payload for this block has been received and validated locally. /// Maps to `root in store.payload_states` in the spec. #[superstruct(only(V29), partial_getter(copy))] @@ -193,31 +197,60 @@ impl ProtoNode { } } - pub fn is_payload_timely(&self) -> bool { + /// Checks if `timely` matches our view of payload timeliness. + /// Returns whether the execution payload for the node is considered `timely` + /// (or not `timely` when `timely` is `false`), taking into consideration local + /// availability and PTC votes. + pub fn payload_timeliness(&self, timely: bool) -> Result { let Ok(node) = self.as_v29() else { - return false; + return Err(Error::InvalidNodeVariant { + block_root: self.root(), + }); }; - // Equivalent to `if root not in store.payload_states` in the spec. + // Equivalent to `if not is_payload_verified(store, root)` in the spec. if !node.payload_received { - return false; + return Ok(!timely); } - node.payload_timeliness_votes.num_set_bits() > E::payload_timely_threshold() + let matching_votes = if timely { + node.payload_timeliness_votes.num_set_bits() + } else { + // We take into consideration only participating ptc votes. An unset bit + // in `payload_timeliness_votes` could be an absent vote or a no vote. + node.ptc_participation + .num_set_bits() + .saturating_sub(node.payload_timeliness_votes.num_set_bits()) + }; + Ok(matching_votes > E::payload_timely_threshold()) } - pub fn is_payload_data_available(&self) -> bool { + /// Checks if `available` matches our view of payload data availability. + /// Return whether the blob data for the node is considered `available` + /// (or not, when `available` is `False`), taking into consideration local + /// availability and PTC votes. + pub fn payload_data_availability(&self, available: bool) -> Result { let Ok(node) = self.as_v29() else { - return false; + return Err(Error::InvalidNodeVariant { + block_root: self.root(), + }); }; - // Equivalent to `if root not in store.payload_states` in the spec. + // Equivalent to `if not is_payload_verified(store, root)` in the spec. if !node.payload_received { - return false; + return Ok(!available); } - node.payload_data_availability_votes.num_set_bits() - > E::data_availability_timely_threshold() + let matching_votes = if available { + node.payload_data_availability_votes.num_set_bits() + } else { + // We take into consideration only participating ptc votes. An unset bit + // in `payload_data_availability_votes` could be an absent vote or a no vote. + node.ptc_participation + .num_set_bits() + .saturating_sub(node.payload_data_availability_votes.num_set_bits()) + }; + Ok(matching_votes > E::data_availability_timely_threshold()) } } @@ -605,6 +638,7 @@ impl ProtoArray { execution_payload_parent_hash, payload_timeliness_votes: BitVector::default(), payload_data_availability_votes: BitVector::default(), + ptc_participation: BitVector::default(), payload_received: false, proposer_index, // Spec: `record_block_timeliness` + `get_forkchoice_store`. @@ -1501,12 +1535,46 @@ impl ProtoArray { } } + /// Called by the proposer to decide whether to build on the full or empty + /// parent pending node. Returns false if the PTC has voted the data as unavailable. + pub fn should_build_on_full( + &self, + fc_node: &IndexedForkChoiceNode, + proto_node: &ProtoNode, + ) -> Result { + if fc_node.payload_status == PayloadStatus::Pending { + return Err(Error::InvalidPayloadStatus { + block_root: proto_node.root(), + payload_status: fc_node.payload_status, + }); + } + + if fc_node.payload_status == PayloadStatus::Empty { + return Ok(false); + } + // Check that false votes have not achieved an absolute majority. This allows the payload to be + // considered available when either a majority have voted true or not enough votes have + // been cast either way. + Ok(!proto_node.payload_data_availability::(false)?) + } + pub fn should_extend_payload( &self, fc_node: &IndexedForkChoiceNode, proto_node: &ProtoNode, proposer_boost_root: Hash256, ) -> Result { + let Ok(node) = proto_node.as_v29() else { + return Err(Error::InvalidNodeVariant { + block_root: fc_node.root, + }); + }; + + // Spec equivalent to `if not is_payload_verified(store, root): return False` + if !node.payload_received { + return Ok(false); + } + // Per spec: `proposer_root == Root()` is one of the `or` conditions that // makes `should_extend_payload` return True. if proposer_boost_root.is_zero() { @@ -1531,11 +1599,10 @@ impl ProtoArray { .ok_or(Error::InvalidNodeIndex(parent_index))? .root(); - Ok( - (proto_node.is_payload_timely::() && proto_node.is_payload_data_available::()) - || proposer_boost_parent_root != fc_node.root - || proposer_boost_node.is_parent_node_full(), - ) + Ok((proto_node.payload_timeliness::(true)? + && proto_node.payload_data_availability::(true)?) + || proposer_boost_parent_root != fc_node.root + || proposer_boost_node.is_parent_node_full()) } /// Update the tree with new finalization information. The tree is only actually pruned if both diff --git a/consensus/proto_array/src/proto_array_fork_choice.rs b/consensus/proto_array/src/proto_array_fork_choice.rs index 7abba8a1f6..96d2302266 100644 --- a/consensus/proto_array/src/proto_array_fork_choice.rs +++ b/consensus/proto_array/src/proto_array_fork_choice.rs @@ -640,6 +640,9 @@ impl ProtoArrayForkChoice { .map_err(|e| { format!("process_payload_attestation: data availability set failed: {e:?}") })?; + v29.ptc_participation + .set(ptc_index, true) + .map_err(|e| format!("process_payload_attestation: participation set failed: {e:?}"))?; Ok(()) } @@ -1006,6 +1009,33 @@ impl ProtoArrayForkChoice { }) } + /// Called by the proposer to decide whether to build on the full or empty + /// parent. Returns false if the PTC has voted the data as unavailable. + pub fn should_build_on_full( + &self, + block_root: &Hash256, + parent_payload_status: PayloadStatus, + ) -> Result { + let block_index = self + .proto_array + .indices + .get(block_root) + .ok_or_else(|| format!("Unknown block root: {block_root:?}"))?; + let proto_node = self + .proto_array + .nodes + .get(*block_index) + .ok_or_else(|| format!("Missing node at index: {block_index}"))?; + let fc_node = IndexedForkChoiceNode { + root: proto_node.root(), + proto_node_index: *block_index, + payload_status: parent_payload_status, + }; + self.proto_array + .should_build_on_full::(&fc_node, proto_node) + .map_err(|e| format!("{e:?}")) + } + /// Returns whether the proposer should extend the parent's execution payload chain. /// /// This checks timeliness, data availability, and proposer boost conditions per the spec. 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 422e0afe06..f88a325d4e 100644 --- a/consensus/state_processing/src/per_block_processing/process_operations.rs +++ b/consensus/state_processing/src/per_block_processing/process_operations.rs @@ -916,25 +916,24 @@ pub fn process_deposit_requests_post_gloas( /// Check if there is a pending deposit for a new validator with the given pubkey. // TODO(gloas): cache the deposit signature validation or remove this loop entirely if possible, // it is `O(n * m)` where `n` is max 8192 and `m` is max 128M. -fn is_pending_validator( - state: &BeaconState, +pub fn is_pending_validator<'a>( + pending_deposits: impl IntoIterator, pubkey: &PublicKeyBytes, spec: &ChainSpec, -) -> Result { - for deposit in state.pending_deposits()?.iter() { - if deposit.pubkey == *pubkey { - let deposit_data = DepositData { - pubkey: deposit.pubkey, - withdrawal_credentials: deposit.withdrawal_credentials, - amount: deposit.amount, - signature: deposit.signature.clone(), - }; - if is_valid_deposit_signature(&deposit_data, spec).is_ok() { - return Ok(true); - } - } - } - Ok(false) +) -> bool { + pending_deposits.into_iter().any(|deposit| { + deposit.pubkey == *pubkey + && is_valid_deposit_signature( + &DepositData { + pubkey: deposit.pubkey, + withdrawal_credentials: deposit.withdrawal_credentials, + amount: deposit.amount, + signature: deposit.signature.clone(), + }, + spec, + ) + .is_ok() + }) } pub fn process_deposit_request_post_gloas( @@ -964,7 +963,7 @@ pub fn process_deposit_request_post_gloas( if is_builder || (has_builder_prefix && !is_validator - && !is_pending_validator(state, &deposit_request.pubkey, spec)?) + && !is_pending_validator(state.pending_deposits()?, &deposit_request.pubkey, spec)) { // Apply builder deposits immediately apply_deposit_for_builder( @@ -1003,7 +1002,7 @@ pub fn apply_deposit_for_builder( signature: SignatureBytes, slot: Slot, spec: &ChainSpec, -) -> Result<(), BeaconStateError> { +) -> Result, BeaconStateError> { match builder_index_opt { None => { // Verify the deposit signature (proof of possession) which is not checked by the deposit contract @@ -1014,13 +1013,16 @@ pub fn apply_deposit_for_builder( signature, }; if is_valid_deposit_signature(&deposit_data, spec).is_ok() { - state.add_builder_to_registry( + let builder_index = state.add_builder_to_registry( pubkey, withdrawal_credentials, amount, slot, spec, )?; + Ok(Some(builder_index)) + } else { + Ok(None) } } Some(builder_index) => { @@ -1030,9 +1032,9 @@ pub fn apply_deposit_for_builder( .ok_or(BeaconStateError::UnknownBuilder(builder_index))? .balance .safe_add_assign(amount)?; + Ok(Some(builder_index)) } } - Ok(()) } // Make sure to build the pubkey cache before calling this function diff --git a/consensus/state_processing/src/upgrade/gloas.rs b/consensus/state_processing/src/upgrade/gloas.rs index 84cdbf22c2..c26547e304 100644 --- a/consensus/state_processing/src/upgrade/gloas.rs +++ b/consensus/state_processing/src/upgrade/gloas.rs @@ -1,18 +1,16 @@ -use crate::per_block_processing::{ - is_valid_deposit_signature, process_operations::apply_deposit_for_builder, -}; +use crate::per_block_processing::process_operations::apply_deposit_for_builder; +use crate::per_block_processing::process_operations::is_pending_validator; use milhouse::{List, Vector}; use safe_arith::SafeArith; use ssz_types::BitVector; use ssz_types::FixedVector; -use std::collections::HashSet; +use std::collections::HashMap; use std::mem; use tree_hash::TreeHash; use typenum::Unsigned; use types::{ BeaconState, BeaconStateError as Error, BeaconStateGloas, BuilderPendingPayment, ChainSpec, - DepositData, EthSpec, ExecutionPayloadBid, ExecutionRequests, Fork, - is_builder_withdrawal_credential, + EthSpec, ExecutionPayloadBid, ExecutionRequests, Fork, is_builder_withdrawal_credential, }; /// Transform a `Fulu` state into a `Gloas` state. @@ -80,6 +78,7 @@ pub fn upgrade_state_to_gloas( // Execution Bid latest_execution_payload_bid: ExecutionPayloadBid { block_hash: pre.latest_execution_payload_header.block_hash, + gas_limit: pre.latest_execution_payload_header.gas_limit, execution_requests_root: ExecutionRequests::::default().tree_hash_root(), ..Default::default() }, @@ -167,66 +166,57 @@ fn onboard_builders_from_pending_deposits( state: &mut BeaconState, spec: &ChainSpec, ) -> Result<(), Error> { - // Rather than tracking all `validator_pubkeys` in one place as the spec does, we keep a - // hashset for *just* the new validator pubkeys, and use the state's efficient - // `get_validator_index` function instead of an O(n) iteration over the full validator list. - let mut new_validator_pubkeys = HashSet::new(); - // Clone pending deposits to avoid borrow conflicts when mutating state. let current_pending_deposits = state.pending_deposits()?.clone(); let mut pending_deposits = List::empty(); + // TODO(gloas): introduce a global builder pubkey cache, see: + // https://github.com/sigp/lighthouse/issues/8783 + let mut builder_pubkey_to_index = state + .builders()? + .iter() + .enumerate() + .map(|(i, b)| (b.pubkey, i as u64)) + .collect::>(); + for deposit in ¤t_pending_deposits { // Deposits for existing validators stay in the pending queue. - if new_validator_pubkeys.contains(&deposit.pubkey) - || state.get_validator_index(&deposit.pubkey)?.is_some() - { + if state.get_validator_index(&deposit.pubkey)?.is_some() { pending_deposits.push(deposit.clone())?; continue; } - // Re-scan builder list each iteration because `apply_deposit_for_builder` may add - // new builders to the registry. - // TODO(gloas): this linear scan could be optimized, see: - // https://github.com/sigp/lighthouse/issues/8783 - let builder_index = state - .builders()? - .iter() - .position(|b| b.pubkey == deposit.pubkey); + if !builder_pubkey_to_index.contains_key(&deposit.pubkey) { + // Deposits without builder withdrawal credentials are for new validators. + if !is_builder_withdrawal_credential(deposit.withdrawal_credentials, spec) { + pending_deposits.push(deposit.clone())?; + continue; + } - let has_builder_credentials = - is_builder_withdrawal_credential(deposit.withdrawal_credentials, spec); - - if builder_index.is_some() || has_builder_credentials { - let builder_index_opt = builder_index.map(|i| i as u64); - apply_deposit_for_builder( - state, - builder_index_opt, - deposit.pubkey, - deposit.withdrawal_credentials, - deposit.amount, - deposit.signature.clone(), - deposit.slot, - spec, - )?; - continue; + // If there is a valid pending deposit for a new validator with this pubkey, + // keep this deposit in the pending queue to be applied to that validator later. + if is_pending_validator(&pending_deposits, &deposit.pubkey, spec) { + pending_deposits.push(deposit.clone())?; + continue; + } } - // If there is a pending deposit for a new validator that has a valid signature, - // track the pubkey so that subsequent builder deposits for the same pubkey stay - // in pending (applied to the validator later) rather than creating a builder. - // Deposits with invalid signatures are dropped since they would fail in - // apply_pending_deposit anyway. - let deposit_data = DepositData { - pubkey: deposit.pubkey, - withdrawal_credentials: deposit.withdrawal_credentials, - amount: deposit.amount, - signature: deposit.signature.clone(), - }; - if is_valid_deposit_signature(&deposit_data, spec).is_ok() { - new_validator_pubkeys.insert(deposit.pubkey); - pending_deposits.push(deposit.clone())?; + let builder_index = builder_pubkey_to_index.get(&deposit.pubkey).copied(); + + if let Some(new_builder_index) = apply_deposit_for_builder( + state, + builder_index, + deposit.pubkey, + deposit.withdrawal_credentials, + deposit.amount, + deposit.signature.clone(), + deposit.slot, + spec, + )? { + builder_pubkey_to_index + .entry(deposit.pubkey) + .or_insert(new_builder_index); } } diff --git a/consensus/types/configs/mainnet.yaml b/consensus/types/configs/mainnet.yaml index 25bf872a7a..743384bcc9 100644 --- a/consensus/types/configs/mainnet.yaml +++ b/consensus/types/configs/mainnet.yaml @@ -91,8 +91,8 @@ SYNC_MESSAGE_DUE_BPS: 3333 CONTRIBUTION_DUE_BPS: 6667 # Gloas -# 2**6 (= 64) epochs -MIN_BUILDER_WITHDRAWABILITY_DELAY: 64 +# 2**13 (= 8192) epochs +MIN_BUILDER_WITHDRAWABILITY_DELAY: 8192 # 2500 basis points, 25% of SLOT_DURATION_MS ATTESTATION_DUE_BPS_GLOAS: 2500 # 5000 basis points, 50% of SLOT_DURATION_MS diff --git a/consensus/types/src/builder/proposer_preferences.rs b/consensus/types/src/builder/proposer_preferences.rs index e3773e333d..4f27020105 100644 --- a/consensus/types/src/builder/proposer_preferences.rs +++ b/consensus/types/src/builder/proposer_preferences.rs @@ -16,7 +16,7 @@ pub struct ProposerPreferences { pub proposal_slot: Slot, pub validator_index: u64, pub fee_recipient: Address, - pub gas_limit: u64, + pub target_gas_limit: u64, } impl SignedRoot for ProposerPreferences {} diff --git a/consensus/types/src/core/chain_spec.rs b/consensus/types/src/core/chain_spec.rs index c54d032891..c42bb4b5b9 100644 --- a/consensus/types/src/core/chain_spec.rs +++ b/consensus/types/src/core/chain_spec.rs @@ -108,6 +108,7 @@ pub struct ChainSpec { pub proposer_reorg_cutoff_bps: u64, pub attestation_due_bps: u64, pub attestation_due_bps_gloas: u64, + pub payload_due_bps: u64, pub payload_attestation_due_bps: u64, pub aggregate_due_bps: u64, pub sync_message_due_bps: u64, @@ -118,6 +119,7 @@ pub struct ChainSpec { */ pub unaggregated_attestation_due: Duration, pub unaggregated_attestation_due_gloas: Duration, + pub payload_due: Duration, pub payload_attestation_due: Duration, pub aggregate_attestation_due: Duration, pub sync_message_due: Duration, @@ -894,6 +896,11 @@ impl ChainSpec { } } + /// Spec: `get_payload_due_ms`. + pub fn get_payload_due(&self) -> Duration { + self.payload_due + } + /// Spec: `get_payload_attestation_due_ms`. pub fn get_payload_attestation_due(&self) -> Duration { self.payload_attestation_due @@ -974,6 +981,9 @@ impl ChainSpec { self.unaggregated_attestation_due_gloas = self .compute_slot_component_duration(self.attestation_due_bps_gloas) .expect("invalid chain spec: cannot compute unaggregated_attestation_due_gloas"); + self.payload_due = self + .compute_slot_component_duration(self.payload_due_bps) + .expect("invalid chain spec: cannot compute payload_due"); self.payload_attestation_due = self .compute_slot_component_duration(self.payload_attestation_due_bps) .expect("invalid chain spec: cannot compute payload_attestation_due"); @@ -1108,6 +1118,7 @@ impl ChainSpec { proposer_reorg_cutoff_bps: 1667, attestation_due_bps: 3333, attestation_due_bps_gloas: 2500, + payload_due_bps: 7500, payload_attestation_due_bps: 7500, aggregate_due_bps: 6667, sync_message_due_bps: 3333, @@ -1118,6 +1129,7 @@ impl ChainSpec { */ unaggregated_attestation_due: Duration::from_millis(3999), unaggregated_attestation_due_gloas: Duration::from_millis(3000), + payload_due: Duration::from_millis(9000), payload_attestation_due: Duration::from_millis(9000), aggregate_attestation_due: Duration::from_millis(8000), sync_message_due: Duration::from_millis(3999), @@ -1270,7 +1282,7 @@ impl ChainSpec { gloas_fork_epoch: None, builder_payment_threshold_numerator: 6, builder_payment_threshold_denominator: 10, - min_builder_withdrawability_delay: Epoch::new(64), + min_builder_withdrawability_delay: Epoch::new(8192), churn_limit_quotient_gloas: option_wrapper(|| u64::checked_pow(2, 15)) .expect("calculation does not overflow"), consolidation_churn_limit_quotient: option_wrapper(|| u64::checked_pow(2, 16)) @@ -1440,6 +1452,7 @@ impl ChainSpec { */ unaggregated_attestation_due: Duration::from_millis(1999), unaggregated_attestation_due_gloas: Duration::from_millis(1500), + payload_due: Duration::from_millis(4500), payload_attestation_due: Duration::from_millis(4500), aggregate_attestation_due: Duration::from_millis(4000), sync_message_due: Duration::from_millis(1999), @@ -1531,6 +1544,7 @@ impl ChainSpec { proposer_reorg_cutoff_bps: 1667, attestation_due_bps: 3333, attestation_due_bps_gloas: 2500, + payload_due_bps: 7500, payload_attestation_due_bps: 7500, aggregate_due_bps: 6667, @@ -1540,6 +1554,7 @@ impl ChainSpec { */ unaggregated_attestation_due: Duration::from_millis(1666), unaggregated_attestation_due_gloas: Duration::from_millis(1250), + payload_due: Duration::from_millis(3750), payload_attestation_due: Duration::from_millis(3750), aggregate_attestation_due: Duration::from_millis(3333), sync_message_due: Duration::from_millis(1666), @@ -1693,7 +1708,7 @@ impl ChainSpec { gloas_fork_epoch: None, builder_payment_threshold_numerator: 6, builder_payment_threshold_denominator: 10, - min_builder_withdrawability_delay: Epoch::new(64), + min_builder_withdrawability_delay: Epoch::new(8192), churn_limit_quotient_gloas: option_wrapper(|| u64::checked_pow(2, 15)) .expect("calculation does not overflow"), consolidation_churn_limit_quotient: option_wrapper(|| u64::checked_pow(2, 16)) @@ -2136,6 +2151,9 @@ pub struct Config { #[serde(default = "default_attestation_due_bps_gloas")] #[serde(with = "serde_utils::quoted_u64")] attestation_due_bps_gloas: u64, + #[serde(default = "default_payload_due_bps")] + #[serde(with = "serde_utils::quoted_u64")] + payload_due_bps: u64, #[serde(default = "default_payload_attestation_due_bps")] #[serde(with = "serde_utils::quoted_u64")] payload_attestation_due_bps: u64, @@ -2379,6 +2397,10 @@ const fn default_attestation_due_bps_gloas() -> u64 { 2500 } +const fn default_payload_due_bps() -> u64 { + 7500 +} + const fn default_payload_attestation_due_bps() -> u64 { 7500 } @@ -2396,7 +2418,7 @@ const fn default_contribution_due_bps() -> u64 { } const fn default_min_builder_withdrawability_delay() -> u64 { - 64 + 8192 } const fn default_churn_limit_quotient_gloas() -> u64 { @@ -2656,6 +2678,7 @@ impl Config { proposer_reorg_cutoff_bps: spec.proposer_reorg_cutoff_bps, attestation_due_bps: spec.attestation_due_bps, attestation_due_bps_gloas: spec.attestation_due_bps_gloas, + payload_due_bps: spec.payload_due_bps, payload_attestation_due_bps: spec.payload_attestation_due_bps, aggregate_due_bps: spec.aggregate_due_bps, sync_message_due_bps: spec.sync_message_due_bps, @@ -2759,6 +2782,7 @@ impl Config { proposer_reorg_cutoff_bps, attestation_due_bps, attestation_due_bps_gloas, + payload_due_bps, payload_attestation_due_bps, aggregate_due_bps, sync_message_due_bps, @@ -2867,6 +2891,7 @@ impl Config { proposer_reorg_cutoff_bps, attestation_due_bps, attestation_due_bps_gloas, + payload_due_bps, payload_attestation_due_bps, aggregate_due_bps, sync_message_due_bps, @@ -3694,6 +3719,30 @@ mod yaml_tests { let custom_spec = custom_spec.compute_derived_values::(); let tiny_due = custom_spec.get_unaggregated_attestation_due(); assert_eq!(tiny_due, Duration::from_millis(1)); // 12000 * 1 / 10000 = 1.2 -> 1 + + // Test payload due (7500 bps = 75% of 12s = 9s) + let spec = ChainSpec::mainnet().compute_derived_values::(); + let payload_due = spec.get_payload_due(); + assert_eq!(payload_due, Duration::from_millis(9000)); // 12000 * 7500 / 10000 + + // Test payload attestation due (7500 bps = 75% of 12s = 9s) + let payload_att_due = spec.get_payload_attestation_due(); + assert_eq!(payload_att_due, Duration::from_millis(9000)); // 12000 * 7500 / 10000 + + // Test gloas attestation due (2500 bps = 25% of 12s = 3s) + assert_eq!( + spec.unaggregated_attestation_due_gloas, + Duration::from_millis(3000) + ); // 12000 * 2500 / 10000 + + // Test gloas with custom bps + let mut custom_spec = spec; + custom_spec.attestation_due_bps_gloas = 5000; + let custom_spec = custom_spec.compute_derived_values::(); + assert_eq!( + custom_spec.unaggregated_attestation_due_gloas, + Duration::from_millis(6000) + ); // 12000 * 5000 / 10000 } #[test] @@ -3715,6 +3764,19 @@ mod yaml_tests { Duration::from_millis(8000) ); + // Mainnet payload due: 12000ms slots, 7500 bps = 9000ms + assert_eq!(mainnet.get_payload_due(), Duration::from_millis(9000)); + assert_eq!( + mainnet.get_payload_attestation_due(), + Duration::from_millis(9000) + ); + + // Mainnet gloas: 12000ms slots, 2500 bps = 3000ms + assert_eq!( + mainnet.unaggregated_attestation_due_gloas, + Duration::from_millis(3000) + ); + // Minimal spec: 6000ms slots, 3333 bps = 1999ms, 6667 bps = 4000ms let minimal = ChainSpec::minimal(); assert_eq!( @@ -3730,6 +3792,18 @@ mod yaml_tests { minimal.get_contribution_message_due(), Duration::from_millis(4000) ); + // Minimal payload due: 6000ms slots, 7500 bps = 4500ms + assert_eq!(minimal.get_payload_due(), Duration::from_millis(4500)); + assert_eq!( + minimal.get_payload_attestation_due(), + Duration::from_millis(4500) + ); + + // Minimal gloas: 6000ms slots, 2500 bps = 1500ms + assert_eq!( + minimal.unaggregated_attestation_due_gloas, + Duration::from_millis(1500) + ); // Gnosis spec: 5000ms slots, 3333 bps = 1666ms, 6667 bps = 3333ms let gnosis = ChainSpec::gnosis(); @@ -3746,6 +3820,18 @@ mod yaml_tests { gnosis.get_contribution_message_due(), Duration::from_millis(3333) ); + // Gnosis payload due: 5000ms slots, 7500 bps = 3750ms + assert_eq!(gnosis.get_payload_due(), Duration::from_millis(3750)); + assert_eq!( + gnosis.get_payload_attestation_due(), + Duration::from_millis(3750) + ); + + // Gnosis gloas: 5000ms slots, 2500 bps = 1250ms + assert_eq!( + gnosis.unaggregated_attestation_due_gloas, + Duration::from_millis(1250) + ); } #[test] diff --git a/consensus/types/src/state/beacon_state.rs b/consensus/types/src/state/beacon_state.rs index 4d2c7533ca..027acfab7f 100644 --- a/consensus/types/src/state/beacon_state.rs +++ b/consensus/types/src/state/beacon_state.rs @@ -1333,6 +1333,7 @@ impl BeaconState { pub fn is_valid_proposal_slot( &self, preferences: &ProposerPreferences, + spec: &ChainSpec, ) -> Result { let current_epoch = self.current_epoch(); let proposal_epoch = preferences.proposal_slot.epoch(E::slots_per_epoch()); @@ -1341,8 +1342,7 @@ impl BeaconState { return Ok(false); } - let next_epoch = current_epoch.saturating_add(1u64); - if proposal_epoch > next_epoch { + if proposal_epoch > current_epoch.saturating_add(spec.min_seed_lookahead) { return Ok(false); } diff --git a/testing/ef_tests/Makefile b/testing/ef_tests/Makefile index 36f6684685..f566a89ded 100644 --- a/testing/ef_tests/Makefile +++ b/testing/ef_tests/Makefile @@ -1,6 +1,6 @@ # To download/extract nightly tests, run: # CONSENSUS_SPECS_TEST_VERSION=nightly make -CONSENSUS_SPECS_TEST_VERSION ?= v1.7.0-alpha.7 +CONSENSUS_SPECS_TEST_VERSION ?= v1.7.0-alpha.8 REPO_NAME := consensus-spec-tests OUTPUT_DIR := ./$(REPO_NAME) diff --git a/testing/ef_tests/src/cases/fork_choice.rs b/testing/ef_tests/src/cases/fork_choice.rs index 8b0b74d256..69fce09505 100644 --- a/testing/ef_tests/src/cases/fork_choice.rs +++ b/testing/ef_tests/src/cases/fork_choice.rs @@ -1,6 +1,6 @@ use super::*; use crate::decode::{ssz_decode_file, ssz_decode_file_with, ssz_decode_state, yaml_decode_file}; -use ::fork_choice::{PayloadVerificationStatus, ProposerHeadError}; +use ::fork_choice::{AttestationFromBlock, PayloadVerificationStatus, ProposerHeadError}; use beacon_chain::beacon_proposer_cache::compute_proposer_duties_from_head; use beacon_chain::blob_verification::GossipBlobError; use beacon_chain::block_verification_types::LookupBlock; @@ -19,13 +19,16 @@ use beacon_chain::{ custody_context::NodeCustodyType, test_utils::{BeaconChainHarness, EphemeralHarnessType}, }; +use bls::AggregateSignature; use execution_layer::{ PayloadStatusV1, PayloadStatusV1Status, json_structures::JsonPayloadStatusV1Status, }; use serde::Deserialize; use ssz_derive::Decode; +use ssz_types::VariableList; use state_processing::VerifySignatures; use state_processing::envelope_processing::verify_execution_payload_envelope; +use state_processing::per_block_processing::is_valid_indexed_payload_attestation; use state_processing::state_advance::complete_state_advance; use std::future::Future; use std::sync::Arc; @@ -34,8 +37,8 @@ use types::{ Attestation, AttestationRef, AttesterSlashing, AttesterSlashingRef, BeaconBlock, BeaconState, BlobSidecar, BlobsList, BlockImportSource, Checkpoint, DataColumnSidecar, DataColumnSidecarList, DataColumnSubnetId, ExecutionBlockHash, Hash256, IndexedAttestation, - KzgProof, ProposerPreparationData, SignedBeaconBlock, SignedExecutionPayloadEnvelope, Slot, - Uint256, + IndexedPayloadAttestation, KzgProof, PayloadAttestationMessage, ProposerPreparationData, + SignedBeaconBlock, SignedExecutionPayloadEnvelope, Slot, Uint256, }; // When set to true, cache any states fetched from the db. @@ -63,6 +66,13 @@ pub struct ShouldOverrideFcu { result: bool, } +#[derive(Debug, Clone, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct PayloadVoteCheck { + block_root: Hash256, + votes: Vec>, +} + #[derive(Debug, Clone, Deserialize)] #[serde(deny_unknown_fields)] pub struct Checks { @@ -78,6 +88,8 @@ pub struct Checks { get_proposer_head: Option, should_override_forkchoice_update: Option, head_payload_status: Option, + payload_timeliness_vote: Option, + payload_data_availability_vote: Option, } #[derive(Debug, Clone, Deserialize)] @@ -108,6 +120,7 @@ pub enum Step< TAttesterSlashing, TPowBlock, TExecutionPayload = String, + TPayloadAttestationMessage = String, > { Tick { tick: u64, @@ -146,6 +159,15 @@ pub enum Step< execution_payload: TExecutionPayload, valid: bool, }, + PayloadAttestationMessage { + payload_attestation_message: TPayloadAttestationMessage, + #[serde(default = "default_true")] + valid: bool, + }, +} + +fn default_true() -> bool { + true } #[derive(Debug, Clone, Deserialize)] @@ -170,6 +192,7 @@ pub struct ForkChoiceTest { AttesterSlashing, PowBlock, SignedExecutionPayloadEnvelope, + PayloadAttestationMessage, >, >, } @@ -184,8 +207,12 @@ impl LoadCase for ForkChoiceTest { .expect("path must be valid OsStr") .to_string(); let spec = &testing_spec::(fork_name); - let steps: Vec, String, String, String>> = - yaml_decode_file(&path.join("steps.yaml"))?; + + #[allow(clippy::type_complexity)] + let steps: Vec< + Step, String, String, String, String, String>, + > = yaml_decode_file(&path.join("steps.yaml"))?; + // Resolve the object names in `steps.yaml` into actual decoded block/attestation objects. let steps = steps .into_iter() @@ -301,6 +328,18 @@ impl LoadCase for ForkChoiceTest { valid, }) } + Step::PayloadAttestationMessage { + payload_attestation_message, + valid, + } => { + let msg: PayloadAttestationMessage = ssz_decode_file( + &path.join(format!("{payload_attestation_message}.ssz_snappy")), + )?; + Ok(Step::PayloadAttestationMessage { + payload_attestation_message: msg, + valid, + }) + } }) .collect::>()?; let anchor_state = ssz_decode_state(&path.join("anchor_state.ssz_snappy"), spec)?; @@ -381,6 +420,8 @@ impl Case for ForkChoiceTest { get_proposer_head, should_override_forkchoice_update: should_override_fcu, head_payload_status, + payload_timeliness_vote, + payload_data_availability_vote, } = checks.as_ref(); if let Some(expected_head) = head { @@ -431,6 +472,14 @@ impl Case for ForkChoiceTest { if let Some(expected_status) = head_payload_status { tester.check_head_payload_status(*expected_status)?; } + + if let Some(expected) = payload_timeliness_vote { + tester.check_payload_timeliness_vote(expected)?; + } + + if let Some(expected) = payload_data_availability_vote { + tester.check_payload_data_availability_vote(expected)?; + } } Step::MaybeValidBlockAndColumns { @@ -446,6 +495,13 @@ impl Case for ForkChoiceTest { } => { tester.process_execution_payload(execution_payload, *valid)?; } + Step::PayloadAttestationMessage { + payload_attestation_message, + valid, + } => { + tester + .process_payload_attestation_message(payload_attestation_message, *valid)?; + } } } @@ -1149,6 +1205,173 @@ impl Tester { expected_should_override_fcu.result, ) } + + pub fn process_payload_attestation_message( + &self, + msg: &PayloadAttestationMessage, + valid: bool, + ) -> Result<(), Error> { + let slot = msg.data.slot; + let block_root = msg.data.beacon_block_root; + + // Get the state at the block to compute the PTC and verify signature. + let store = &self.harness.chain.store; + let block = store + .get_blinded_block(&block_root) + .map_err(|e| Error::InternalError(format!("Failed to load block: {e:?}")))?; + + let state_opt = block.and_then(|block| { + store + .get_hot_state(&block.state_root(), CACHE_STATE_IN_TESTS) + .ok()? + }); + + // Build IndexedPayloadAttestation from the message. + let indexed = IndexedPayloadAttestation:: { + attesting_indices: VariableList::new(vec![msg.validator_index]).unwrap(), + data: msg.data.clone(), + signature: AggregateSignature::from(&msg.signature), + }; + + let result = if let Some(ref state) = state_opt { + is_valid_indexed_payload_attestation( + state, + &indexed, + VerifySignatures::True, + &self.spec, + ) + .map_err(|e| { + Error::InternalError(format!( + "payload attestation signature verification failed for validator {}: {:?}", + msg.validator_index, e + )) + }) + .and_then(|_| { + let ptc = state.get_ptc(slot, &self.spec).map_err(|e| { + Error::InternalError(format!( + "Could not compute PTC for block root {block_root:?} at slot {slot:?}: {e:?}" + )) + })?; + + self.harness + .chain + .canonical_head + .fork_choice_write_lock() + .on_payload_attestation( + self.harness.chain.slot().unwrap(), + &indexed, + AttestationFromBlock::False, + &ptc.0, + ) + .map_err(|e| { + Error::InternalError(format!( + "on_payload_attestation for validator {} failed: {:?}", + msg.validator_index, e + )) + }) + }) + } else { + Err(Error::InternalError(format!( + "Could not get state for block root {block_root:?} at slot {slot:?}" + ))) + }; + + if valid { + result?; + } else if result.is_ok() { + return Err(Error::DidntFail(format!( + "payload_attestation_message for validator {} should have failed", + msg.validator_index + ))); + } + + Ok(()) + } + + pub fn check_payload_timeliness_vote(&self, expected: &PayloadVoteCheck) -> Result<(), Error> { + let fc = self.harness.chain.canonical_head.fork_choice_read_lock(); + let proto_array = fc.proto_array().core_proto_array(); + + let node_index = proto_array + .indices + .get(&expected.block_root) + .ok_or_else(|| { + Error::InternalError(format!( + "Block root {:?} not found in proto array", + expected.block_root + )) + })?; + let node = proto_array + .nodes + .get(*node_index) + .ok_or_else(|| Error::InternalError(format!("Node index {} not found", node_index)))?; + let v29 = node + .as_v29() + .map_err(|_| Error::InternalError("Node is not V29".to_string()))?; + + let timeliness_votes = &v29.payload_timeliness_votes; + let participation = &v29.ptc_participation; + + for (i, expected_vote) in expected.votes.iter().enumerate() { + let actual = if !participation.get(i).unwrap() { + None // not yet voted + } else { + Some(timeliness_votes.get(i).unwrap()) + }; + if actual != *expected_vote { + return Err(Error::NotEqual(format!( + "payload_timeliness_vote[{}]: Got {:?} | Expected {:?}", + i, actual, expected_vote + ))); + } + } + + Ok(()) + } + + pub fn check_payload_data_availability_vote( + &self, + expected: &PayloadVoteCheck, + ) -> Result<(), Error> { + let fc = self.harness.chain.canonical_head.fork_choice_read_lock(); + let proto_array = fc.proto_array().core_proto_array(); + + let node_index = proto_array + .indices + .get(&expected.block_root) + .ok_or_else(|| { + Error::InternalError(format!( + "Block root {:?} not found in proto array", + expected.block_root + )) + })?; + let node = proto_array + .nodes + .get(*node_index) + .ok_or_else(|| Error::InternalError(format!("Node index {} not found", node_index)))?; + let v29 = node + .as_v29() + .map_err(|_| Error::InternalError("Node is not V29".to_string()))?; + + let availability_votes = &v29.payload_data_availability_votes; + let participation = &v29.ptc_participation; + + for (i, expected_vote) in expected.votes.iter().enumerate() { + let actual = if !participation.get(i).unwrap() { + None // not yet voted + } else { + Some(availability_votes.get(i).unwrap()) + }; + if actual != *expected_vote { + return Err(Error::NotEqual(format!( + "payload_data_availability_vote[{}]: Got {:?} | Expected {:?}", + i, actual, expected_vote + ))); + } + } + + Ok(()) + } } /// Checks that the `head` checkpoint from the beacon chain head matches the `fc` checkpoint gleaned diff --git a/testing/ef_tests/src/handler.rs b/testing/ef_tests/src/handler.rs index e380f51c0a..52cc5d57ae 100644 --- a/testing/ef_tests/src/handler.rs +++ b/testing/ef_tests/src/handler.rs @@ -715,10 +715,8 @@ impl Handler for ForkChoiceHandler { return false; } - // Deposit tests exist only for Electra and Fulu (not Gloas). - if self.handler_name == "deposit_with_reorg" - && (!fork_name.electra_enabled() || fork_name.gloas_enabled()) - { + // Deposit tests exist only for Electra and later. + if self.handler_name == "deposit_with_reorg" && !fork_name.electra_enabled() { return false; } @@ -727,10 +725,11 @@ impl Handler for ForkChoiceHandler { return false; } - // on_execution_payload_envelope and get_parent_payload_status tests exist only for - // Gloas and later. + // on_execution_payload_envelope, get_parent_payload_status, and + // on_payload_attestation_message tests exist only for Gloas and later. if (self.handler_name == "on_execution_payload_envelope" - || self.handler_name == "get_parent_payload_status") + || self.handler_name == "get_parent_payload_status" + || self.handler_name == "on_payload_attestation_message") && !fork_name.gloas_enabled() { return false; diff --git a/testing/ef_tests/tests/tests.rs b/testing/ef_tests/tests/tests.rs index ca383efdb0..0ff854bd21 100644 --- a/testing/ef_tests/tests/tests.rs +++ b/testing/ef_tests/tests/tests.rs @@ -1079,6 +1079,12 @@ fn fork_choice_get_parent_payload_status() { ForkChoiceHandler::::new("get_parent_payload_status").run(); } +#[test] +fn fork_choice_on_payload_attestation_message() { + ForkChoiceHandler::::new("on_payload_attestation_message").run(); + ForkChoiceHandler::::new("on_payload_attestation_message").run(); +} + #[test] fn optimistic_sync() { OptimisticSyncHandler::::default().run(); diff --git a/testing/execution_engine_integration/src/test_rig.rs b/testing/execution_engine_integration/src/test_rig.rs index ed6b5787b5..61f25e63a1 100644 --- a/testing/execution_engine_integration/src/test_rig.rs +++ b/testing/execution_engine_integration/src/test_rig.rs @@ -320,6 +320,7 @@ impl TestRig { Some(vec![]), None, None, + None, ), ) .await; @@ -366,11 +367,12 @@ impl TestRig { Some(vec![]), None, None, + None, ); let payload_parameters = PayloadParameters { parent_hash, - parent_gas_limit, + parent_gas_limit: Some(parent_gas_limit), proposer_gas_limit: None, payload_attributes: &payload_attributes, forkchoice_update_params: &forkchoice_update_params, @@ -527,11 +529,12 @@ impl TestRig { Some(vec![]), None, None, + None, ); let payload_parameters = PayloadParameters { parent_hash, - parent_gas_limit, + parent_gas_limit: Some(parent_gas_limit), proposer_gas_limit: None, payload_attributes: &payload_attributes, forkchoice_update_params: &forkchoice_update_params, @@ -588,6 +591,7 @@ impl TestRig { Some(vec![]), None, None, + None, ); let slot = Slot::new(42); let head_block_root = Hash256::repeat_byte(100); diff --git a/validator_client/validator_services/src/proposer_preferences_service.rs b/validator_client/validator_services/src/proposer_preferences_service.rs index fbefdf5d96..fc17a1bce6 100644 --- a/validator_client/validator_services/src/proposer_preferences_service.rs +++ b/validator_client/validator_services/src/proposer_preferences_service.rs @@ -136,7 +136,7 @@ impl ProposerPreferencesSer proposal_slot: duty.slot, validator_index: duty.validator_index, fee_recipient, - gas_limit: proposal_data.gas_limit, + target_gas_limit: proposal_data.gas_limit, }, )); } From 5693d860029571651ed1b497a01f56dc9fe6b6d9 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Fri, 22 May 2026 10:50:45 -0700 Subject: [PATCH 06/18] Ensure we use the right fork when calculating payload attestation sig domain (#9342) Using `state.fork` is a bit sketchy at the fork boundary. It's safer to just use the payload attestations slot Co-Authored-By: Eitan Seri-Levi --- .../src/per_block_processing/signature_sets.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 0686c4d605..ef7109dd94 100644 --- a/consensus/state_processing/src/per_block_processing/signature_sets.rs +++ b/consensus/state_processing/src/per_block_processing/signature_sets.rs @@ -378,10 +378,11 @@ where .data .slot .epoch(E::slots_per_epoch()); + let fork = spec.fork_at_epoch(epoch); let domain = spec.get_domain( epoch, Domain::PTCAttester, - &state.fork(), + &fork, state.genesis_validators_root(), ); From 5045e8dd85cdb4fe50e65f9160a72edefaba074d Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Fri, 22 May 2026 10:50:50 -0700 Subject: [PATCH 07/18] Custody backfill sync only penalize peers once per batch (#9340) During custody backfill sync if a peer fails to serve columns for a batch don't penalize them more than once per batch Co-Authored-By: Eitan Seri-Levi --- .../network/src/sync/custody_backfill_sync/mod.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/beacon_node/network/src/sync/custody_backfill_sync/mod.rs b/beacon_node/network/src/sync/custody_backfill_sync/mod.rs index fe4c7dfe4c..c85610613c 100644 --- a/beacon_node/network/src/sync/custody_backfill_sync/mod.rs +++ b/beacon_node/network/src/sync/custody_backfill_sync/mod.rs @@ -593,7 +593,7 @@ impl CustodyBackFillSync { Err(err) => { debug!(batch_epoch = %batch_id, error = ?err, "Batch download failed"); - // If there are any coupling errors, penalize the appropriate peers + // If there are any coupling errors, penalize the appropriate peers. if let RpcResponseError::BlockComponentCouplingError(coupling_error) = err && let CouplingError::DataColumnPeerFailure { error, @@ -601,15 +601,19 @@ impl CustodyBackFillSync { exceeded_retries: _, } = coupling_error { + let mut failed_peers = HashSet::new(); for (column_index, faulty_peer) in faulty_peers { debug!( ?error, ?column_index, ?faulty_peer, - "Custody backfill sync penalizing peer" + "Custody backfill sync: peer failed to serve column" ); + failed_peers.insert(faulty_peer); + } + for peer in failed_peers { network.report_peer( - faulty_peer, + peer, PeerAction::LowToleranceError, "Peer failed to serve column", ); From 0565a01633fbafdb4a261058e7216b5ff6bd35af Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Sun, 24 May 2026 17:21:17 -0700 Subject: [PATCH 08/18] Gloas dont enforce peer column custody on block import (#9341) Peers that advertise that they have imported a block may not have the columns for that slot available post-Gloas. Ensure that we dont penalize them. Co-Authored-By: Eitan Seri-Levi --- beacon_node/network/src/sync/network_context/custody.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/beacon_node/network/src/sync/network_context/custody.rs b/beacon_node/network/src/sync/network_context/custody.rs index 620962b40b..2b96800e37 100644 --- a/beacon_node/network/src/sync/network_context/custody.rs +++ b/beacon_node/network/src/sync/network_context/custody.rs @@ -305,7 +305,12 @@ impl ActiveCustodyRequest { // must have its columns in custody. In that case, set `true = enforce max_requests` // and downscore if data_columns_by_root does not return the expected custody // columns. For the rest of peers, don't downscore if columns are missing. - lookup_peers.contains(&peer_id), + // + // Post-Gloas, blocks and payload envelopes are decoupled. A peer may + // have the block but not yet imported the envelope and data columns. + // Don't enforce max_responses in this case. + lookup_peers.contains(&peer_id) + && !cx.fork_context.current_fork_name().gloas_enabled(), ) .map_err(Error::SendFailed)?; From b9a68ad2c65f2a81e66afeadcaee9d4f9bbae4c2 Mon Sep 17 00:00:00 2001 From: Mac L Date: Mon, 25 May 2026 05:21:26 +0400 Subject: [PATCH 09/18] Add support for jemalloc memory profiling (#9326) Add a new feature flag to `lighthouse` which adds jemalloc profiling support. We could manually add this during memory profiling but it is a nice QoL to have this built-in imo Co-Authored-By: Mac L --- lighthouse/Cargo.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lighthouse/Cargo.toml b/lighthouse/Cargo.toml index 3595cf04e7..09fd6d4afe 100644 --- a/lighthouse/Cargo.toml +++ b/lighthouse/Cargo.toml @@ -37,6 +37,8 @@ beacon-node-redb = ["store/redb"] console-subscriber = ["console-subscriber/default"] # Force the use of the system memory allocator rather than jemalloc. sysmalloc = ["malloc_utils/sysmalloc"] +# Enable jemalloc heap profiling support. +jemalloc-profiling = ["malloc_utils/jemalloc-profiling"] [dependencies] account_manager = { "path" = "../account_manager" } From 89ee020330be75cc32bd8cd6752a43ada7d9b22d Mon Sep 17 00:00:00 2001 From: Mac L Date: Mon, 25 May 2026 05:29:34 +0400 Subject: [PATCH 10/18] Add macro to simplify `into_full_block` implementations (#9294) Use a macro to remove the repetitive fork variant boilerplate in `signed_beacon_block.rs` when implementing `into_full_block` for the various `SignedBeaconBlock` variants Co-Authored-By: Mac L --- consensus/types/Cargo.toml | 2 +- .../types/src/block/signed_beacon_block.rs | 350 ++++-------------- 2 files changed, 76 insertions(+), 276 deletions(-) diff --git a/consensus/types/Cargo.toml b/consensus/types/Cargo.toml index 9ee827c7b9..8d991163d2 100644 --- a/consensus/types/Cargo.toml +++ b/consensus/types/Cargo.toml @@ -44,6 +44,7 @@ merkle_proof = { workspace = true } metastruct = "0.1.0" milhouse = { workspace = true } parking_lot = { workspace = true } +paste = { workspace = true } rand = { workspace = true } rand_xorshift = { workspace = true } rayon = { workspace = true } @@ -67,7 +68,6 @@ yaml_serde = { workspace = true } [dev-dependencies] beacon_chain = { workspace = true } criterion = { workspace = true } -paste = { workspace = true } state_processing = { workspace = true } tokio = { workspace = true } types = { path = ".", features = ["arbitrary"] } diff --git a/consensus/types/src/block/signed_beacon_block.rs b/consensus/types/src/block/signed_beacon_block.rs index 11ac17dece..1a87a519d0 100644 --- a/consensus/types/src/block/signed_beacon_block.rs +++ b/consensus/types/src/block/signed_beacon_block.rs @@ -433,285 +433,85 @@ impl From>> } // Post-Bellatrix blocks can be "unblinded" by adding the full payload. -// NOTE: It might be nice to come up with a `superstruct` pattern to abstract over this before -// the first fork after Bellatrix. -impl SignedBeaconBlockBellatrix> { - pub fn into_full_block( - self, - execution_payload: ExecutionPayloadBellatrix, - ) -> SignedBeaconBlockBellatrix> { - let SignedBeaconBlockBellatrix { - message: - BeaconBlockBellatrix { - slot, - proposer_index, - parent_root, - state_root, - body: - BeaconBlockBodyBellatrix { - randao_reveal, - eth1_data, - graffiti, - proposer_slashings, - attester_slashings, - attestations, - deposits, - voluntary_exits, - sync_aggregate, - execution_payload: BlindedPayloadBellatrix { .. }, +macro_rules! impl_into_full_block { + ($fork:ident, [ $($extra_field:ident),* $(,)? ]) => { + paste::paste! { + impl []> { + pub fn into_full_block( + self, + execution_payload: [], + ) -> []> { + let [] { + message: + [] { + slot, + proposer_index, + parent_root, + state_root, + body: + [] { + randao_reveal, + eth1_data, + graffiti, + proposer_slashings, + attester_slashings, + attestations, + deposits, + voluntary_exits, + sync_aggregate, + execution_payload: [] { .. }, + $($extra_field,)* + }, + }, + signature, + } = self; + [] { + message: [] { + slot, + proposer_index, + parent_root, + state_root, + body: [] { + randao_reveal, + eth1_data, + graffiti, + proposer_slashings, + attester_slashings, + attestations, + deposits, + voluntary_exits, + sync_aggregate, + execution_payload: [] { execution_payload }, + $($extra_field,)* + }, }, - }, - signature, - } = self; - SignedBeaconBlockBellatrix { - message: BeaconBlockBellatrix { - slot, - proposer_index, - parent_root, - state_root, - body: BeaconBlockBodyBellatrix { - randao_reveal, - eth1_data, - graffiti, - proposer_slashings, - attester_slashings, - attestations, - deposits, - voluntary_exits, - sync_aggregate, - execution_payload: FullPayloadBellatrix { execution_payload }, - }, - }, - signature, + signature, + } + } + } } - } + }; } -impl SignedBeaconBlockCapella> { - pub fn into_full_block( - self, - execution_payload: ExecutionPayloadCapella, - ) -> SignedBeaconBlockCapella> { - let SignedBeaconBlockCapella { - message: - BeaconBlockCapella { - slot, - proposer_index, - parent_root, - state_root, - body: - BeaconBlockBodyCapella { - randao_reveal, - eth1_data, - graffiti, - proposer_slashings, - attester_slashings, - attestations, - deposits, - voluntary_exits, - sync_aggregate, - execution_payload: BlindedPayloadCapella { .. }, - bls_to_execution_changes, - }, - }, - signature, - } = self; - SignedBeaconBlockCapella { - message: BeaconBlockCapella { - slot, - proposer_index, - parent_root, - state_root, - body: BeaconBlockBodyCapella { - randao_reveal, - eth1_data, - graffiti, - proposer_slashings, - attester_slashings, - attestations, - deposits, - voluntary_exits, - sync_aggregate, - execution_payload: FullPayloadCapella { execution_payload }, - bls_to_execution_changes, - }, - }, - signature, - } - } -} - -impl SignedBeaconBlockDeneb> { - pub fn into_full_block( - self, - execution_payload: ExecutionPayloadDeneb, - ) -> SignedBeaconBlockDeneb> { - let SignedBeaconBlockDeneb { - message: - BeaconBlockDeneb { - slot, - proposer_index, - parent_root, - state_root, - body: - BeaconBlockBodyDeneb { - randao_reveal, - eth1_data, - graffiti, - proposer_slashings, - attester_slashings, - attestations, - deposits, - voluntary_exits, - sync_aggregate, - execution_payload: BlindedPayloadDeneb { .. }, - bls_to_execution_changes, - blob_kzg_commitments, - }, - }, - signature, - } = self; - SignedBeaconBlockDeneb { - message: BeaconBlockDeneb { - slot, - proposer_index, - parent_root, - state_root, - body: BeaconBlockBodyDeneb { - randao_reveal, - eth1_data, - graffiti, - proposer_slashings, - attester_slashings, - attestations, - deposits, - voluntary_exits, - sync_aggregate, - execution_payload: FullPayloadDeneb { execution_payload }, - bls_to_execution_changes, - blob_kzg_commitments, - }, - }, - signature, - } - } -} - -impl SignedBeaconBlockElectra> { - pub fn into_full_block( - self, - execution_payload: ExecutionPayloadElectra, - ) -> SignedBeaconBlockElectra> { - let SignedBeaconBlockElectra { - message: - BeaconBlockElectra { - slot, - proposer_index, - parent_root, - state_root, - body: - BeaconBlockBodyElectra { - randao_reveal, - eth1_data, - graffiti, - proposer_slashings, - attester_slashings, - attestations, - deposits, - voluntary_exits, - sync_aggregate, - execution_payload: BlindedPayloadElectra { .. }, - bls_to_execution_changes, - blob_kzg_commitments, - execution_requests, - }, - }, - signature, - } = self; - SignedBeaconBlockElectra { - message: BeaconBlockElectra { - slot, - proposer_index, - parent_root, - state_root, - body: BeaconBlockBodyElectra { - randao_reveal, - eth1_data, - graffiti, - proposer_slashings, - attester_slashings, - attestations, - deposits, - voluntary_exits, - sync_aggregate, - execution_payload: FullPayloadElectra { execution_payload }, - bls_to_execution_changes, - blob_kzg_commitments, - execution_requests, - }, - }, - signature, - } - } -} - -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_into_full_block!(Bellatrix, []); +impl_into_full_block!(Capella, [bls_to_execution_changes]); +impl_into_full_block!(Deneb, [bls_to_execution_changes, blob_kzg_commitments]); +impl_into_full_block!( + Electra, + [ + bls_to_execution_changes, + blob_kzg_commitments, + execution_requests + ] +); +impl_into_full_block!( + Fulu, + [ + bls_to_execution_changes, + blob_kzg_commitments, + execution_requests + ] +); // We can convert gloas blocks without payloads into blocks "with" payloads. // TODO(EIP-7732) Look into whether we can remove this in the future since no blinded blocks post-gloas From b5d44bff36b844337725ce05cdd2d6afdc102e4b Mon Sep 17 00:00:00 2001 From: Daniel Knopik <107140945+dknopik@users.noreply.github.com> Date: Mon, 25 May 2026 03:44:43 +0200 Subject: [PATCH 11/18] Enable partial data columns by default on Hoodi and Sepolia (#9343) Enable partial data columns by default on Hoodi and Sepolia. Co-Authored-By: Daniel Knopik --- beacon_node/src/cli.rs | 13 +++++++- beacon_node/src/config.rs | 11 +++++- book/src/help_bn.md | 6 +++- lighthouse/tests/beacon_node.rs | 59 ++++++++++++++++++++++++++++++++- 4 files changed, 85 insertions(+), 4 deletions(-) diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index 51cda0fac3..9de2edb3de 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -674,11 +674,22 @@ pub fn cli_app() -> Command { Arg::new("enable-partial-columns") .long("enable-partial-columns") .help("Enable partial messages for data columns. This can reduce the amount of \ - data sent over the network.") + data sent over the network. Enabled by default on Hoodi and Sepolia; use \ + --disable-partial-columns to opt out.") .action(ArgAction::SetTrue) .help_heading(FLAG_HEADER) .display_order(0) ) + .arg( + Arg::new("disable-partial-columns") + .long("disable-partial-columns") + .help("Disable partial messages for data columns. Use this on Hoodi or Sepolia \ + to opt out of the default-enabled behavior.") + .action(ArgAction::SetTrue) + .conflicts_with("enable-partial-columns") + .help_heading(FLAG_HEADER) + .display_order(0) + ) /* * Monitoring metrics */ diff --git a/beacon_node/src/config.rs b/beacon_node/src/config.rs index f10f9e3b45..1388611c3e 100644 --- a/beacon_node/src/config.rs +++ b/beacon_node/src/config.rs @@ -110,7 +110,16 @@ pub fn get_config( set_network_config(&mut client_config.network, cli_args, &data_dir_ref)?; - if parse_flag(cli_args, "enable-partial-columns") { + let default_partial_columns_enabled = spec + .config_name + .as_ref() + .is_some_and(|name| matches!(name.as_str(), "hoodi" | "sepolia")); + let user_disable_partial_columns = parse_flag(cli_args, "disable-partial-columns"); + let user_enable_partial_columns = parse_flag(cli_args, "enable-partial-columns"); + let enable_partial_columns = !user_disable_partial_columns + && (user_enable_partial_columns || default_partial_columns_enabled); + + if enable_partial_columns { // Partial messages assume that each subnet maps to exactly one column. // Check this here to avoid weird issues on networks where this is not the case. if spec.data_column_sidecar_subnet_count == E::number_of_columns() as u64 { diff --git a/book/src/help_bn.md b/book/src/help_bn.md index b580bcae52..7e771a2b4a 100644 --- a/book/src/help_bn.md +++ b/book/src/help_bn.md @@ -482,6 +482,9 @@ Flags: --disable-packet-filter Disables the discovery packet filter. Useful for testing in smaller networks + --disable-partial-columns + Disable partial messages for data columns. Use this on Hoodi or + Sepolia to opt out of the default-enabled behavior. --disable-proposer-reorgs Do not attempt to reorg late blocks from other validators when proposing. @@ -499,7 +502,8 @@ Flags: --listen-address and the UDP port will be --discovery-port. --enable-partial-columns Enable partial messages for data columns. This can reduce the amount - of data sent over the network. + of data sent over the network. Enabled by default on Hoodi and + Sepolia; use --disable-partial-columns to opt out. --enable-private-discovery Lighthouse by default does not discover private IP addresses. Set this flag to enable connection attempts to local addresses. diff --git a/lighthouse/tests/beacon_node.rs b/lighthouse/tests/beacon_node.rs index 0c5d9a5933..623ca1f403 100644 --- a/lighthouse/tests/beacon_node.rs +++ b/lighthouse/tests/beacon_node.rs @@ -2874,7 +2874,7 @@ fn partial_columns() { assert!(config.network.enable_partial_columns); assert!(config.chain.enable_partial_columns); }); - // And disabled by default: + // And disabled by default on mainnet: CommandLineTest::new() .run_with_zero_port() .with_config(|config| { @@ -2882,3 +2882,60 @@ fn partial_columns() { assert!(!config.chain.enable_partial_columns); }) } + +#[test] +fn partial_columns_default_hoodi() { + CommandLineTest::new() + .flag("network", Some("hoodi")) + .run_with_zero_port() + .with_config(|config| { + assert!(config.network.enable_partial_columns); + assert!(config.chain.enable_partial_columns); + }); +} + +#[test] +fn partial_columns_default_sepolia() { + CommandLineTest::new() + .flag("network", Some("sepolia")) + .run_with_zero_port() + .with_config(|config| { + assert!(config.network.enable_partial_columns); + assert!(config.chain.enable_partial_columns); + }); +} + +#[test] +fn partial_columns_disable_overrides_hoodi_default() { + CommandLineTest::new() + .flag("network", Some("hoodi")) + .flag("disable-partial-columns", None) + .run_with_zero_port() + .with_config(|config| { + assert!(!config.network.enable_partial_columns); + assert!(!config.chain.enable_partial_columns); + }); +} + +#[test] +fn partial_columns_disable_on_mainnet_no_op() { + CommandLineTest::new() + .flag("disable-partial-columns", None) + .run_with_zero_port() + .with_config(|config| { + assert!(!config.network.enable_partial_columns); + assert!(!config.chain.enable_partial_columns); + }); +} + +#[test] +fn partial_columns_enable_disable_conflict() { + let mut cmd = base_cmd(); + cmd.arg("--enable-partial-columns") + .arg("--disable-partial-columns"); + let output = cmd.output().expect("should run command"); + assert!( + !output.status.success(), + "expected clap to reject --enable-partial-columns and --disable-partial-columns together", + ); +} From 9b961960c49bb109d0d7363b109be13029b33d32 Mon Sep 17 00:00:00 2001 From: chonghe <44791194+chong-he@users.noreply.github.com> Date: Mon, 25 May 2026 10:11:27 +0800 Subject: [PATCH 12/18] Deprecate some `reorg`-related CLI flags and read from spec (#9177) - #9123 Co-Authored-By: Tan Chee Keong Co-Authored-By: chonghe <44791194+chong-he@users.noreply.github.com> --- Cargo.lock | 1 + beacon_node/beacon_chain/src/beacon_chain.rs | 26 ++++--- .../beacon_chain/src/block_production/mod.rs | 20 ++++-- beacon_node/beacon_chain/src/builder.rs | 21 +----- beacon_node/beacon_chain/src/chain_config.rs | 33 ++------- .../http_api/tests/interactive_tests.rs | 24 ++----- beacon_node/src/cli.rs | 14 ++-- beacon_node/src/config.rs | 68 +++++++++--------- book/src/advanced_re-orgs.md | 8 --- book/src/help_bn.md | 14 ++-- consensus/proto_array/src/proto_array.rs | 8 +-- consensus/types/src/core/chain_spec.rs | 66 +++++++++-------- lighthouse/tests/beacon_node.rs | 71 +++---------------- lighthouse/tests/exec.rs | 1 - testing/ef_tests/Cargo.toml | 1 + testing/ef_tests/src/cases/fork_choice.rs | 18 +++-- 16 files changed, 141 insertions(+), 253 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d42bcd8fc1..129be32fcd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2857,6 +2857,7 @@ dependencies = [ "kzg", "logging", "milhouse", + "proto_array", "rayon", "serde", "serde_json", diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index db8f55a18a..d78e279936 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -113,7 +113,7 @@ use operation_pool::{ CompactAttestationRef, OperationPool, PersistedOperationPool, ReceivedPreCapella, }; use parking_lot::{Mutex, RwLock, RwLockWriteGuard}; -use proto_array::{DoNotReOrg, ProposerHeadError}; +use proto_array::{DoNotReOrg, ProposerHeadError, ReOrgThreshold}; use rand::RngCore; use safe_arith::SafeArith; use slasher::Slasher; @@ -5239,15 +5239,14 @@ impl BeaconChain { let _timer = metrics::start_timer(&metrics::FORK_CHOICE_OVERRIDE_FCU_TIMES); // Never override if proposer re-orgs are disabled. - let re_org_head_threshold = self - .config - .re_org_head_threshold - .ok_or(Box::new(DoNotReOrg::ReOrgsDisabled.into()))?; + if self.config.disable_proposer_reorg { + return Err(Box::new(DoNotReOrg::ReOrgsDisabled.into())); + }; - let re_org_parent_threshold = self - .config - .re_org_parent_threshold - .ok_or(Box::new(DoNotReOrg::ReOrgsDisabled.into()))?; + let re_org_head_threshold = ReOrgThreshold(self.spec.reorg_head_weight_threshold); + let re_org_parent_threshold = ReOrgThreshold(self.spec.reorg_parent_weight_threshold); + let re_org_max_epochs_since_finalization = + Epoch::new(self.spec.reorg_max_epochs_since_finalization); let head_block_root = canonical_forkchoice_params.head_root; @@ -5260,7 +5259,7 @@ impl BeaconChain { re_org_head_threshold, re_org_parent_threshold, &self.config.re_org_disallowed_offsets, - self.config.re_org_max_epochs_since_finalization, + re_org_max_epochs_since_finalization, ) .map_err(|e| e.map_inner_error(Error::ProposerHeadForkChoiceError))?; @@ -5281,7 +5280,12 @@ impl BeaconChain { .and_then(|slot_start| { let now = self.slot_clock.now_duration()?; let slot_delay = now.saturating_sub(slot_start); - Some(slot_delay <= self.config.re_org_cutoff(self.spec.get_slot_duration())) + let re_org_cutoff_duration = self + .spec + .compute_slot_component_duration(self.spec.proposer_reorg_cutoff_bps) + .ok()?; + + Some(slot_delay <= re_org_cutoff_duration) }) .unwrap_or(false) } else { diff --git a/beacon_node/beacon_chain/src/block_production/mod.rs b/beacon_node/beacon_chain/src/block_production/mod.rs index fd5e381023..a94bc697b9 100644 --- a/beacon_node/beacon_chain/src/block_production/mod.rs +++ b/beacon_node/beacon_chain/src/block_production/mod.rs @@ -1,10 +1,10 @@ use std::{sync::Arc, time::Duration}; use fork_choice::PayloadStatus; -use proto_array::ProposerHeadError; +use proto_array::{ProposerHeadError, ReOrgThreshold}; use slot_clock::SlotClock; use tracing::{debug, error, info, instrument, warn}; -use types::{BeaconState, Hash256, SignedExecutionPayloadEnvelope, Slot}; +use types::{BeaconState, Epoch, Hash256, SignedExecutionPayloadEnvelope, Slot}; use crate::{ BeaconChain, BeaconChainTypes, BlockProductionError, StateSkipConfig, @@ -174,8 +174,10 @@ impl BeaconChain { head_slot: Slot, canonical_head: Hash256, ) -> Option<(BeaconState, Hash256)> { - let re_org_head_threshold = self.config.re_org_head_threshold?; - let re_org_parent_threshold = self.config.re_org_parent_threshold?; + let re_org_head_threshold = ReOrgThreshold(self.spec.reorg_head_weight_threshold); + let re_org_parent_threshold = ReOrgThreshold(self.spec.reorg_parent_weight_threshold); + let re_org_max_epochs_since_finalization = + Epoch::new(self.spec.reorg_max_epochs_since_finalization); if self.spec.proposer_score_boost.is_none() { warn!( @@ -198,8 +200,12 @@ impl BeaconChain { // 1. It seems we have time to propagate and still receive the proposer boost. // 2. The current head block was seen late. // 3. The `get_proposer_head` conditions from fork choice pass. - let proposing_on_time = - slot_delay < self.config.re_org_cutoff(self.spec.get_slot_duration()); + let re_org_cutoff_duration = self + .spec + .compute_slot_component_duration(self.spec.proposer_reorg_cutoff_bps) + .ok()?; + + let proposing_on_time = slot_delay < re_org_cutoff_duration; if !proposing_on_time { debug!(reason = "not proposing on time", "Not attempting re-org"); return None; @@ -223,7 +229,7 @@ impl BeaconChain { re_org_head_threshold, re_org_parent_threshold, &self.config.re_org_disallowed_offsets, - self.config.re_org_max_epochs_since_finalization, + re_org_max_epochs_since_finalization, ) .map_err(|e| match e { ProposerHeadError::DoNotReOrg(reason) => { diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index 61c026e0a9..b8da2bcded 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -30,7 +30,7 @@ use kzg::Kzg; use logging::crit; use operation_pool::{OperationPool, PersistedOperationPool}; use parking_lot::{Mutex, RwLock}; -use proto_array::{DisallowedReOrgOffsets, ReOrgThreshold}; +use proto_array::DisallowedReOrgOffsets; use rand::RngCore; use rayon::prelude::*; use slasher::Slasher; @@ -47,8 +47,8 @@ use tracing::{debug, error, info, warn}; use tree_hash::TreeHash; use types::data::CustodyIndex; use types::{ - BeaconState, BlobSidecarList, ChainSpec, ColumnIndex, DataColumnSidecarList, Epoch, EthSpec, - Hash256, SignedBeaconBlock, Slot, + BeaconState, BlobSidecarList, ChainSpec, ColumnIndex, DataColumnSidecarList, EthSpec, Hash256, + SignedBeaconBlock, Slot, }; /// An empty struct used to "witness" all the `BeaconChainTypes` traits. It has no user-facing @@ -176,21 +176,6 @@ where self } - /// Sets the proposer re-org threshold. - pub fn proposer_re_org_head_threshold(mut self, threshold: Option) -> Self { - self.chain_config.re_org_head_threshold = threshold; - self - } - - /// Sets the proposer re-org max epochs since finalization. - pub fn proposer_re_org_max_epochs_since_finalization( - mut self, - epochs_since_finalization: Epoch, - ) -> Self { - self.chain_config.re_org_max_epochs_since_finalization = epochs_since_finalization; - self - } - /// Sets the proposer re-org disallowed offsets list. pub fn proposer_re_org_disallowed_offsets( mut self, diff --git a/beacon_node/beacon_chain/src/chain_config.rs b/beacon_node/beacon_chain/src/chain_config.rs index b2c017a469..dde09bf105 100644 --- a/beacon_node/beacon_chain/src/chain_config.rs +++ b/beacon_node/beacon_chain/src/chain_config.rs @@ -1,15 +1,10 @@ use crate::custody_context::NodeCustodyType; -pub use proto_array::{DisallowedReOrgOffsets, ReOrgThreshold}; +pub use proto_array::DisallowedReOrgOffsets; use serde::{Deserialize, Serialize}; use std::str::FromStr; use std::{collections::HashSet, sync::LazyLock, time::Duration}; -use types::{Checkpoint, Epoch, Hash256}; +use types::{Checkpoint, Hash256}; -pub const DEFAULT_RE_ORG_HEAD_THRESHOLD: ReOrgThreshold = ReOrgThreshold(20); -pub const DEFAULT_RE_ORG_PARENT_THRESHOLD: ReOrgThreshold = ReOrgThreshold(160); -pub const DEFAULT_RE_ORG_MAX_EPOCHS_SINCE_FINALIZATION: Epoch = Epoch::new(2); -/// Default to 1/12th of the slot, which is 1 second on mainnet. -pub const DEFAULT_RE_ORG_CUTOFF_DENOMINATOR: u32 = 12; pub const DEFAULT_FORK_CHOICE_BEFORE_PROPOSAL_TIMEOUT: u64 = 250; /// Default fraction of a slot lookahead for payload preparation (12/3 = 4 seconds on mainnet). @@ -41,14 +36,6 @@ pub struct ChainConfig { pub archive: bool, /// The max size of a message that can be sent over the network. pub max_network_size: usize, - /// Maximum percentage of the head committee weight at which to attempt re-orging the canonical head. - pub re_org_head_threshold: Option, - /// Minimum percentage of the parent committee weight at which to attempt re-orging the canonical head. - pub re_org_parent_threshold: Option, - /// Maximum number of epochs since finalization for attempting a proposer re-org. - pub re_org_max_epochs_since_finalization: Epoch, - /// Maximum delay after the start of the slot at which to propose a reorging block. - pub re_org_cutoff_millis: Option, /// Additional epoch offsets at which re-orging block proposals are not permitted. /// /// By default this list is empty, but it can be useful for reacting to network conditions, e.g. @@ -125,6 +112,8 @@ pub struct ChainConfig { pub enable_partial_columns: bool, /// The node's custody type, determining how many data columns to custody and sample. pub node_custody_type: NodeCustodyType, + /// Disable proposer re-org + pub disable_proposer_reorg: bool, } impl Default for ChainConfig { @@ -134,10 +123,6 @@ impl Default for ChainConfig { weak_subjectivity_checkpoint: None, archive: false, max_network_size: 10 * 1_048_576, // 10M - re_org_head_threshold: Some(DEFAULT_RE_ORG_HEAD_THRESHOLD), - re_org_parent_threshold: Some(DEFAULT_RE_ORG_PARENT_THRESHOLD), - re_org_max_epochs_since_finalization: DEFAULT_RE_ORG_MAX_EPOCHS_SINCE_FINALIZATION, - re_org_cutoff_millis: None, re_org_disallowed_offsets: DisallowedReOrgOffsets::default(), fork_choice_before_proposal_timeout_ms: DEFAULT_FORK_CHOICE_BEFORE_PROPOSAL_TIMEOUT, // Builder fallback configs that are set in `clap` will override these. @@ -168,15 +153,7 @@ impl Default for ChainConfig { disable_get_blobs: false, enable_partial_columns: false, node_custody_type: NodeCustodyType::Fullnode, + disable_proposer_reorg: false, } } } - -impl ChainConfig { - /// The latest delay from the start of the slot at which to attempt a 1-slot re-org. - pub fn re_org_cutoff(&self, slot_duration: Duration) -> Duration { - self.re_org_cutoff_millis - .map(Duration::from_millis) - .unwrap_or_else(|| slot_duration / DEFAULT_RE_ORG_CUTOFF_DENOMINATOR) - } -} diff --git a/beacon_node/http_api/tests/interactive_tests.rs b/beacon_node/http_api/tests/interactive_tests.rs index b47f8e946a..7b5fb02714 100644 --- a/beacon_node/http_api/tests/interactive_tests.rs +++ b/beacon_node/http_api/tests/interactive_tests.rs @@ -2,7 +2,7 @@ use beacon_chain::custody_context::NodeCustodyType; use beacon_chain::{ ChainConfig, - chain_config::{DisallowedReOrgOffsets, ReOrgThreshold}, + chain_config::DisallowedReOrgOffsets, test_utils::{ AttestationStrategy, BlockStrategy, LightClientStrategy, SyncCommitteeStrategy, test_spec, }, @@ -23,7 +23,7 @@ use std::sync::Arc; use std::time::Duration; use types::{ Address, Epoch, EthSpec, ExecPayload, ExecutionBlockHash, ForkName, Hash256, MainnetEthSpec, - MinimalEthSpec, ProposerPreparationData, Slot, Uint256, + MinimalEthSpec, ProposerPreparationData, Slot, }; type E = MainnetEthSpec; @@ -181,8 +181,6 @@ pub struct ReOrgTest { parent_distance: u64, /// Number of slots between head block and block proposal slot. head_distance: u64, - re_org_threshold: u64, - max_epochs_since_finalization: u64, percent_parent_votes: usize, percent_empty_votes: usize, percent_head_votes: usize, @@ -201,8 +199,6 @@ impl Default for ReOrgTest { head_slot: Slot::new(E::slots_per_epoch() - 2), parent_distance: 1, head_distance: 1, - re_org_threshold: 20, - max_epochs_since_finalization: 2, percent_parent_votes: 100, percent_empty_votes: 100, percent_head_votes: 0, @@ -388,8 +384,6 @@ pub async fn proposer_boost_re_org_test( head_slot, parent_distance, head_distance, - re_org_threshold, - max_epochs_since_finalization, percent_parent_votes, percent_empty_votes, percent_head_votes, @@ -403,8 +397,7 @@ pub async fn proposer_boost_re_org_test( // TODO(EIP-7732): extend test for Gloas — `get_validator_blocks_v3` is missing the // `Eth-Execution-Payload-Blinded` header for Gloas block production responses. - let mut spec = ForkName::Fulu.make_genesis_spec(E::default_spec()); - spec.terminal_total_difficulty = Uint256::from(1); + let spec = ForkName::Fulu.make_genesis_spec(E::default_spec()); // Ensure there are enough validators to have `attesters_per_slot`. let attesters_per_slot = 10; @@ -427,14 +420,9 @@ pub async fn proposer_boost_re_org_test( validator_count, None, Some(Box::new(move |builder| { - builder - .proposer_re_org_head_threshold(Some(ReOrgThreshold(re_org_threshold))) - .proposer_re_org_max_epochs_since_finalization(Epoch::new( - max_epochs_since_finalization, - )) - .proposer_re_org_disallowed_offsets( - DisallowedReOrgOffsets::new::(disallowed_offsets).unwrap(), - ) + builder.proposer_re_org_disallowed_offsets( + DisallowedReOrgOffsets::new::(disallowed_offsets).unwrap(), + ) })), Default::default(), false, diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index 9de2edb3de..647b5858cb 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -1331,8 +1331,7 @@ pub fn cli_app() -> Command { .long("proposer-reorg-threshold") .action(ArgAction::Set) .value_name("PERCENT") - .help("Percentage of head vote weight below which to attempt a proposer reorg. \ - Default: 20%") + .help("DEPRECATED. This flag has no effect.") .conflicts_with("disable-proposer-reorgs") .display_order(0) ) @@ -1340,8 +1339,7 @@ pub fn cli_app() -> Command { Arg::new("proposer-reorg-parent-threshold") .long("proposer-reorg-parent-threshold") .value_name("PERCENT") - .help("Percentage of parent vote weight above which to attempt a proposer reorg. \ - Default: 160%") + .help("DEPRECATED. This flag has no effect.") .conflicts_with("disable-proposer-reorgs") .action(ArgAction::Set) .display_order(0) @@ -1351,8 +1349,7 @@ pub fn cli_app() -> Command { .long("proposer-reorg-epochs-since-finalization") .action(ArgAction::Set) .value_name("EPOCHS") - .help("Maximum number of epochs since finalization at which proposer reorgs are \ - allowed. Default: 2") + .help("DEPRECATED. This flag has no effect.") .conflicts_with("disable-proposer-reorgs") .display_order(0) ) @@ -1361,10 +1358,7 @@ pub fn cli_app() -> Command { .long("proposer-reorg-cutoff") .value_name("MILLISECONDS") .action(ArgAction::Set) - .help("Maximum delay after the start of the slot at which to propose a reorging \ - block. Lower values can prevent failed reorgs by ensuring the block has \ - ample time to propagate and be processed by the network. The default is \ - 1/12th of a slot (1 second on mainnet)") + .help("DEPRECATED. This flag has no effect.") .conflicts_with("disable-proposer-reorgs") .display_order(0) ) diff --git a/beacon_node/src/config.rs b/beacon_node/src/config.rs index 1388611c3e..045b432dc9 100644 --- a/beacon_node/src/config.rs +++ b/beacon_node/src/config.rs @@ -1,8 +1,6 @@ use account_utils::{STDIN_INPUTS_FLAG, read_input_from_user}; use beacon_chain::chain_config::{ - DEFAULT_PREPARE_PAYLOAD_LOOKAHEAD_FACTOR, DEFAULT_RE_ORG_HEAD_THRESHOLD, - DEFAULT_RE_ORG_MAX_EPOCHS_SINCE_FINALIZATION, DEFAULT_RE_ORG_PARENT_THRESHOLD, - DisallowedReOrgOffsets, INVALID_HOLESKY_BLOCK_ROOT, ReOrgThreshold, + DEFAULT_PREPARE_PAYLOAD_LOOKAHEAD_FACTOR, DisallowedReOrgOffsets, INVALID_HOLESKY_BLOCK_ROOT, }; use beacon_chain::custody_context::NodeCustodyType; use beacon_chain::graffiti_calculator::GraffitiOrigin; @@ -753,41 +751,39 @@ pub fn get_config( .individual_tracking_threshold = count; } - if cli_args.get_flag("disable-proposer-reorgs") { - client_config.chain.re_org_head_threshold = None; - client_config.chain.re_org_parent_threshold = None; - } else { - client_config.chain.re_org_head_threshold = Some( - clap_utils::parse_optional(cli_args, "proposer-reorg-threshold")? - .map(ReOrgThreshold) - .unwrap_or(DEFAULT_RE_ORG_HEAD_THRESHOLD), - ); - client_config.chain.re_org_max_epochs_since_finalization = - clap_utils::parse_optional(cli_args, "proposer-reorg-epochs-since-finalization")? - .unwrap_or(DEFAULT_RE_ORG_MAX_EPOCHS_SINCE_FINALIZATION); - client_config.chain.re_org_cutoff_millis = - clap_utils::parse_optional(cli_args, "proposer-reorg-cutoff")?; + client_config.chain.disable_proposer_reorg = cli_args.get_flag("disable-proposer-reorgs"); - client_config.chain.re_org_parent_threshold = Some( - clap_utils::parse_optional(cli_args, "proposer-reorg-parent-threshold")? - .map(ReOrgThreshold) - .unwrap_or(DEFAULT_RE_ORG_PARENT_THRESHOLD), - ); + if clap_utils::parse_optional::(cli_args, "proposer-reorg-threshold")?.is_some() { + warn!("The proposer-reorg-threshold flag is deprecated"); + } - if let Some(disallowed_offsets_str) = - clap_utils::parse_optional::(cli_args, "proposer-reorg-disallowed-offsets")? - { - let disallowed_offsets = disallowed_offsets_str - .split(',') - .map(|s| { - s.parse() - .map_err(|e| format!("invalid disallowed-offsets: {e:?}")) - }) - .collect::, _>>()?; - client_config.chain.re_org_disallowed_offsets = - DisallowedReOrgOffsets::new::(disallowed_offsets) - .map_err(|e| format!("invalid disallowed-offsets: {e:?}"))?; - } + if clap_utils::parse_optional::(cli_args, "proposer-reorg-epochs-since-finalization")? + .is_some() + { + warn!("The proposer-reorg-epochs-since-finalization flag is deprecated"); + } + + if clap_utils::parse_optional::(cli_args, "proposer-reorg-cutoff")?.is_some() { + warn!("The proposer-reorg-cutoff flag is deprecated"); + } + + if clap_utils::parse_optional::(cli_args, "proposer-reorg-parent-threshold")?.is_some() { + warn!("The proposer-reorg-parent-threshold flag is deprecated"); + } + + if let Some(disallowed_offsets_str) = + clap_utils::parse_optional::(cli_args, "proposer-reorg-disallowed-offsets")? + { + let disallowed_offsets = disallowed_offsets_str + .split(',') + .map(|s| { + s.parse() + .map_err(|e| format!("invalid disallowed-offsets: {e:?}")) + }) + .collect::, _>>()?; + client_config.chain.re_org_disallowed_offsets = + DisallowedReOrgOffsets::new::(disallowed_offsets) + .map_err(|e| format!("invalid disallowed-offsets: {e:?}"))?; } client_config.chain.prepare_payload_lookahead = diff --git a/book/src/advanced_re-orgs.md b/book/src/advanced_re-orgs.md index 3a31778786..71751f354f 100644 --- a/book/src/advanced_re-orgs.md +++ b/book/src/advanced_re-orgs.md @@ -14,14 +14,6 @@ attestations and transactions that can be included. There are three flags which control the re-orging behaviour: * `--disable-proposer-reorgs`: turn re-orging off (it's on by default). -* `--proposer-reorg-threshold N`: attempt to orphan blocks with less than N% of the committee vote. If this parameter isn't set then N defaults to 20% when the feature is enabled. -* `--proposer-reorg-epochs-since-finalization N`: only attempt to re-org late blocks when the number of epochs since finalization is less than or equal to N. The default is 2 epochs, - meaning re-orgs will only be attempted when the chain is finalizing optimally. -* `--proposer-reorg-cutoff T`: only attempt to re-org late blocks when the proposal is being made - before T milliseconds into the slot. Delays between the validator client and the beacon node can - cause some blocks to be requested later than the start of the slot, which makes them more likely - to fail. The default cutoff is 1000ms on mainnet, which gives blocks 3000ms to be signed and - propagated before the attestation deadline at 4000ms. * `--proposer-reorg-disallowed-offsets N1,N2,N3...`: Prohibit Lighthouse from attempting to reorg at specific offsets in each epoch. A disallowed offset `N` prevents reorging blocks from being proposed at any `slot` such that `slot % SLOTS_PER_EPOCH == N`. The value to this flag is a diff --git a/book/src/help_bn.md b/book/src/help_bn.md index 7e771a2b4a..30163f1f0c 100644 --- a/book/src/help_bn.md +++ b/book/src/help_bn.md @@ -306,10 +306,7 @@ Options: values are useful for ensuring the EL is given ample notice. Default: 1/3 of a slot. --proposer-reorg-cutoff - Maximum delay after the start of the slot at which to propose a - reorging block. Lower values can prevent failed reorgs by ensuring the - block has ample time to propagate and be processed by the network. The - default is 1/12th of a slot (1 second on mainnet) + DEPRECATED. This flag has no effect. --proposer-reorg-disallowed-offsets Comma-separated list of integer offsets which can be used to avoid proposing reorging blocks at certain slots. An offset of N means that @@ -318,14 +315,11 @@ Options: avoided. Any offsets supplied with this flag will impose additional restrictions. --proposer-reorg-epochs-since-finalization - Maximum number of epochs since finalization at which proposer reorgs - are allowed. Default: 2 + DEPRECATED. This flag has no effect. --proposer-reorg-parent-threshold - Percentage of parent vote weight above which to attempt a proposer - reorg. Default: 160% + DEPRECATED. This flag has no effect. --proposer-reorg-threshold - Percentage of head vote weight below which to attempt a proposer - reorg. Default: 20% + DEPRECATED. This flag has no effect. --prune-blobs Prune blobs from Lighthouse's database when they are older than the data data availability boundary relative to the current epoch. diff --git a/consensus/proto_array/src/proto_array.rs b/consensus/proto_array/src/proto_array.rs index 8ac8354f06..6ff5eabb04 100644 --- a/consensus/proto_array/src/proto_array.rs +++ b/consensus/proto_array/src/proto_array.rs @@ -701,11 +701,9 @@ impl ProtoArray { justified_balances: &JustifiedBalances, spec: &ChainSpec, ) -> bool { - let reorg_threshold = calculate_committee_fraction::( - justified_balances, - spec.reorg_head_weight_threshold.unwrap_or(20), - ) - .unwrap_or(0); + let reorg_threshold = + calculate_committee_fraction::(justified_balances, spec.reorg_head_weight_threshold) + .unwrap_or(0); let head_weight = head_node .attestation_score(PayloadStatus::Pending) diff --git a/consensus/types/src/core/chain_spec.rs b/consensus/types/src/core/chain_spec.rs index c42bb4b5b9..25dcb4ba06 100644 --- a/consensus/types/src/core/chain_spec.rs +++ b/consensus/types/src/core/chain_spec.rs @@ -152,9 +152,9 @@ pub struct ChainSpec { * Fork choice */ pub proposer_score_boost: Option, - pub reorg_head_weight_threshold: Option, - pub reorg_parent_weight_threshold: Option, - pub reorg_max_epochs_since_finalization: Option, + pub reorg_head_weight_threshold: u64, + pub reorg_parent_weight_threshold: u64, + pub reorg_max_epochs_since_finalization: u64, /* * Eth1 @@ -925,7 +925,7 @@ impl ChainSpec { } /// Calculate the duration into a slot for a given slot component - fn compute_slot_component_duration( + pub fn compute_slot_component_duration( &self, component_basis_points: u64, ) -> Result { @@ -1163,9 +1163,9 @@ impl ChainSpec { * Fork choice */ proposer_score_boost: Some(40), - reorg_head_weight_threshold: Some(20), - reorg_parent_weight_threshold: Some(160), - reorg_max_epochs_since_finalization: Some(2), + reorg_head_weight_threshold: 20, + reorg_parent_weight_threshold: 160, + reorg_max_epochs_since_finalization: 2, /* * Eth1 @@ -1588,9 +1588,9 @@ impl ChainSpec { * Fork choice */ proposer_score_boost: Some(40), - reorg_head_weight_threshold: Some(20), - reorg_parent_weight_threshold: Some(160), - reorg_max_epochs_since_finalization: Some(2), + reorg_head_weight_threshold: 20, + reorg_parent_weight_threshold: 160, + reorg_max_epochs_since_finalization: 2, /* * Eth1 @@ -2028,12 +2028,15 @@ pub struct Config { #[serde(skip_serializing_if = "Option::is_none")] proposer_score_boost: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - reorg_head_weight_threshold: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - reorg_parent_weight_threshold: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - reorg_max_epochs_since_finalization: Option>, + #[serde(default = "default_reorg_head_weight_threshold")] + #[serde(with = "serde_utils::quoted_u64")] + reorg_head_weight_threshold: u64, + #[serde(default = "default_reorg_parent_weight_threshold")] + #[serde(with = "serde_utils::quoted_u64")] + reorg_parent_weight_threshold: u64, + #[serde(default = "default_reorg_max_epochs_since_finalization")] + #[serde(with = "serde_utils::quoted_u64")] + reorg_max_epochs_since_finalization: u64, #[serde(with = "serde_utils::quoted_u64")] deposit_chain_id: u64, @@ -2433,6 +2436,18 @@ const fn default_max_per_epoch_activation_churn_limit_gloas() -> u64 { 256_000_000_000 } +const fn default_reorg_head_weight_threshold() -> u64 { + 20 +} + +const fn default_reorg_parent_weight_threshold() -> u64 { + 160 +} + +const fn default_reorg_max_epochs_since_finalization() -> u64 { + 2 +} + fn max_blocks_by_root_request_common(max_request_blocks: u64) -> usize { let max_request_blocks = max_request_blocks as usize; RuntimeVariableList::::new( @@ -2626,15 +2641,9 @@ impl Config { max_per_epoch_activation_churn_limit: spec.max_per_epoch_activation_churn_limit, proposer_score_boost: spec.proposer_score_boost.map(|value| MaybeQuoted { value }), - reorg_head_weight_threshold: spec - .reorg_head_weight_threshold - .map(|value| MaybeQuoted { value }), - reorg_parent_weight_threshold: spec - .reorg_parent_weight_threshold - .map(|value| MaybeQuoted { value }), - reorg_max_epochs_since_finalization: spec - .reorg_max_epochs_since_finalization - .map(|value| MaybeQuoted { value }), + reorg_head_weight_threshold: spec.reorg_head_weight_threshold, + reorg_parent_weight_threshold: spec.reorg_parent_weight_threshold, + reorg_max_epochs_since_finalization: spec.reorg_max_epochs_since_finalization, deposit_chain_id: spec.deposit_chain_id, deposit_network_id: spec.deposit_network_id, @@ -2846,10 +2855,9 @@ impl Config { max_per_epoch_activation_churn_limit, churn_limit_quotient, proposer_score_boost: proposer_score_boost.map(|q| q.value), - reorg_head_weight_threshold: reorg_head_weight_threshold.map(|q| q.value), - reorg_parent_weight_threshold: reorg_parent_weight_threshold.map(|q| q.value), - reorg_max_epochs_since_finalization: reorg_max_epochs_since_finalization - .map(|q| q.value), + reorg_head_weight_threshold, + reorg_parent_weight_threshold, + reorg_max_epochs_since_finalization, deposit_chain_id, deposit_network_id, deposit_contract_address, diff --git a/lighthouse/tests/beacon_node.rs b/lighthouse/tests/beacon_node.rs index 623ca1f403..38d4275a02 100644 --- a/lighthouse/tests/beacon_node.rs +++ b/lighthouse/tests/beacon_node.rs @@ -1,8 +1,6 @@ use crate::exec::{CommandLineTestExec, CompletedTest}; use beacon_node::beacon_chain::chain_config::{ - DEFAULT_RE_ORG_CUTOFF_DENOMINATOR, DEFAULT_RE_ORG_HEAD_THRESHOLD, - DEFAULT_RE_ORG_MAX_EPOCHS_SINCE_FINALIZATION, DEFAULT_SYNC_TOLERANCE_EPOCHS, - DisallowedReOrgOffsets, + DEFAULT_SYNC_TOLERANCE_EPOCHS, DisallowedReOrgOffsets, }; use beacon_node::beacon_chain::custody_context::NodeCustodyType; use beacon_node::{ @@ -2344,19 +2342,12 @@ fn ensure_panic_on_failed_launch() { fn enable_proposer_re_orgs_default() { CommandLineTest::new() .run_with_zero_port() - .with_config(|config| { - assert_eq!( - config.chain.re_org_head_threshold, - Some(DEFAULT_RE_ORG_HEAD_THRESHOLD) - ); - assert_eq!( - config.chain.re_org_max_epochs_since_finalization, - DEFAULT_RE_ORG_MAX_EPOCHS_SINCE_FINALIZATION, - ); - assert_eq!( - config.chain.re_org_cutoff(Duration::from_secs(12)), - Duration::from_secs(12) / DEFAULT_RE_ORG_CUTOFF_DENOMINATOR - ); + .with_config_and_spec::(|config, spec| { + assert!(!config.chain.disable_proposer_reorg); + assert_eq!(spec.reorg_head_weight_threshold, 20); + assert_eq!(spec.reorg_parent_weight_threshold, 160); + assert_eq!(spec.reorg_max_epochs_since_finalization, 2); + assert_eq!(spec.proposer_reorg_cutoff_bps, 1667); }); } @@ -2365,52 +2356,8 @@ fn disable_proposer_re_orgs() { CommandLineTest::new() .flag("disable-proposer-reorgs", None) .run_with_zero_port() - .with_config(|config| { - assert_eq!(config.chain.re_org_head_threshold, None); - assert_eq!(config.chain.re_org_parent_threshold, None) - }); -} - -#[test] -fn proposer_re_org_parent_threshold() { - CommandLineTest::new() - .flag("proposer-reorg-parent-threshold", Some("90")) - .run_with_zero_port() - .with_config(|config| assert_eq!(config.chain.re_org_parent_threshold.unwrap().0, 90)); -} - -#[test] -fn proposer_re_org_head_threshold() { - CommandLineTest::new() - .flag("proposer-reorg-threshold", Some("90")) - .run_with_zero_port() - .with_config(|config| assert_eq!(config.chain.re_org_head_threshold.unwrap().0, 90)); -} - -#[test] -fn proposer_re_org_max_epochs_since_finalization() { - CommandLineTest::new() - .flag("proposer-reorg-epochs-since-finalization", Some("8")) - .run_with_zero_port() - .with_config(|config| { - assert_eq!( - config.chain.re_org_max_epochs_since_finalization.as_u64(), - 8 - ) - }); -} - -#[test] -fn proposer_re_org_cutoff() { - CommandLineTest::new() - .flag("proposer-reorg-cutoff", Some("500")) - .run_with_zero_port() - .with_config(|config| { - assert_eq!( - config.chain.re_org_cutoff(Duration::from_secs(12)), - Duration::from_millis(500) - ) - }); + // When --disable-proposer-reorg is used, the field in ChainConfig should become true + .with_config(|config| assert!(config.chain.disable_proposer_reorg)); } #[test] diff --git a/lighthouse/tests/exec.rs b/lighthouse/tests/exec.rs index a25558bc2f..696cf2f40a 100644 --- a/lighthouse/tests/exec.rs +++ b/lighthouse/tests/exec.rs @@ -144,7 +144,6 @@ impl CompletedTest { func(&self.config, &self.dir); } - #[allow(dead_code)] pub fn with_config_and_spec(self, func: F) { let spec = ChainSpec::from_config::(&self.chain_config).unwrap(); func(&self.config, spec); diff --git a/testing/ef_tests/Cargo.toml b/testing/ef_tests/Cargo.toml index 9d09c3dfe6..ac51e827ad 100644 --- a/testing/ef_tests/Cargo.toml +++ b/testing/ef_tests/Cargo.toml @@ -28,6 +28,7 @@ hex = { workspace = true } kzg = { workspace = true } logging = { workspace = true } milhouse = { workspace = true } +proto_array = { workspace = true } rayon = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/testing/ef_tests/src/cases/fork_choice.rs b/testing/ef_tests/src/cases/fork_choice.rs index 69fce09505..2954ee7eb4 100644 --- a/testing/ef_tests/src/cases/fork_choice.rs +++ b/testing/ef_tests/src/cases/fork_choice.rs @@ -4,10 +4,7 @@ use ::fork_choice::{AttestationFromBlock, PayloadVerificationStatus, ProposerHea use beacon_chain::beacon_proposer_cache::compute_proposer_duties_from_head; use beacon_chain::blob_verification::GossipBlobError; use beacon_chain::block_verification_types::LookupBlock; -use beacon_chain::chain_config::{ - DEFAULT_RE_ORG_HEAD_THRESHOLD, DEFAULT_RE_ORG_MAX_EPOCHS_SINCE_FINALIZATION, - DEFAULT_RE_ORG_PARENT_THRESHOLD, DisallowedReOrgOffsets, -}; +use beacon_chain::chain_config::DisallowedReOrgOffsets; use beacon_chain::data_column_verification::GossipVerifiedDataColumn; use beacon_chain::slot_clock::SlotClock; use beacon_chain::{ @@ -23,6 +20,7 @@ use bls::AggregateSignature; use execution_layer::{ PayloadStatusV1, PayloadStatusV1Status, json_structures::JsonPayloadStatusV1Status, }; +use proto_array::ReOrgThreshold; use serde::Deserialize; use ssz_derive::Decode; use ssz_types::VariableList; @@ -36,9 +34,9 @@ use std::time::Duration; use types::{ Attestation, AttestationRef, AttesterSlashing, AttesterSlashingRef, BeaconBlock, BeaconState, BlobSidecar, BlobsList, BlockImportSource, Checkpoint, DataColumnSidecar, - DataColumnSidecarList, DataColumnSubnetId, ExecutionBlockHash, Hash256, IndexedAttestation, - IndexedPayloadAttestation, KzgProof, PayloadAttestationMessage, ProposerPreparationData, - SignedBeaconBlock, SignedExecutionPayloadEnvelope, Slot, Uint256, + DataColumnSidecarList, DataColumnSubnetId, Epoch, ExecutionBlockHash, Hash256, + IndexedAttestation, IndexedPayloadAttestation, KzgProof, PayloadAttestationMessage, + ProposerPreparationData, SignedBeaconBlock, SignedExecutionPayloadEnvelope, Slot, Uint256, }; // When set to true, cache any states fetched from the db. @@ -1027,10 +1025,10 @@ impl Tester { let proposer_head_result = fc.get_proposer_head( slot, canonical_head, - DEFAULT_RE_ORG_HEAD_THRESHOLD, - DEFAULT_RE_ORG_PARENT_THRESHOLD, + ReOrgThreshold(self.spec.reorg_head_weight_threshold), + ReOrgThreshold(self.spec.reorg_parent_weight_threshold), &DisallowedReOrgOffsets::default(), - DEFAULT_RE_ORG_MAX_EPOCHS_SINCE_FINALIZATION, + Epoch::new(self.spec.reorg_max_epochs_since_finalization), ); let proposer_head = match proposer_head_result { Ok(head) => head.parent_node.root(), From 4903fff43052b048f9e28ae149f65c3faafaed69 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Mon, 25 May 2026 15:06:27 +1000 Subject: [PATCH 13/18] Fix non-canonical payload attestation processing (#9305) Breakout from: - https://github.com/sigp/lighthouse/pull/9295 We currently do not handle the verification of payload attestations on non-canonical side chains, we always attempt to use the head. The included regression test demonstrates this, and there is _also_ a fork choice compliance test in #9295 that triggers it. This PR is a bit opinionated, but I'll explain my judgements: - We need a way to get the PTC for an arbitrary slot from an arbitrary state. This involves potential state advances, database lookups, etc. There is some fiddly logic required to check that states are in range/etc. - We _already have_ a cache with the exact same lifecycle as the PTCs, namely the attester shuffling cache. Therefore, we can de-duplicate a lot of the complexity by storing the PTCs for a given epoch (and decision block) in this cache. The other opinionated change is in the tests. The previous tests were set up kind of nicely to avoid instantiating a `BeaconChainHarness`. However they were not using mocking, which made testing the non-canonical chain case kind of infeasible. To remedy this, I've changed them to just use a beacon chain harness and create two chains using its relatively easy to use methods for doing this. The running time of the tests goes from something like 2.6s for 8 tests to 3.3s for 9 tests, which is only an increase of 0.04s/test. Negligible. Another plus to using the `BeaconChainHarness` is that it avoids a bunch of the cruft to create synthetic non-mocked beacon chain bits. At the same time, I've made some attempt to improve modularity (and fit with the `GossipVerificationContext`) by pulling out the guts of `with_committee_cache` into a new function (`with_cached_shuffling`) that clearly shows its dependency surface. Co-Authored-By: Michael Sproul Co-Authored-By: dapplion <35266934+dapplion@users.noreply.github.com> --- .../src/attestation_verification.rs | 6 +- beacon_node/beacon_chain/src/beacon_chain.rs | 186 ++------ beacon_node/beacon_chain/src/errors.rs | 7 + .../gossip_verified_payload_attestation.rs | 73 +-- .../payload_attestation_verification/mod.rs | 14 +- .../payload_attestation_verification/tests.rs | 316 +++++++++---- .../beacon_chain/src/shuffling_cache.rs | 426 +++++++++++++++--- .../beacon_chain/src/state_advance_timer.rs | 39 +- beacon_node/beacon_chain/tests/store_tests.rs | 6 +- .../gossip_methods.rs | 3 +- .../per_block_processing/signature_sets.rs | 27 +- 11 files changed, 687 insertions(+), 416 deletions(-) diff --git a/beacon_node/beacon_chain/src/attestation_verification.rs b/beacon_node/beacon_chain/src/attestation_verification.rs index f35de59e1f..635ca3a2ae 100644 --- a/beacon_node/beacon_chain/src/attestation_verification.rs +++ b/beacon_node/beacon_chain/src/attestation_verification.rs @@ -1023,7 +1023,8 @@ impl<'a, T: BeaconChainTypes> VerifiedUnaggregatedAttestation<'a, T> { let (committee_opt, committees_per_slot) = chain.with_committee_cache( attestation.data.target.root, attestation.data.slot.epoch(T::EthSpec::slots_per_epoch()), - |committee_cache, _| { + |cached_shuffling, _| { + let committee_cache = cached_shuffling.committee_cache.as_ref(); let committee_opt = committee_cache .get_beacon_committee(attestation.data.slot, attestation.committee_index) .map(|beacon_committee| beacon_committee.committee.to_vec()); @@ -1574,7 +1575,8 @@ where return Err(Error::UnknownTargetRoot(target.root)); } - chain.with_committee_cache(target.root, attestation_epoch, |committee_cache, _| { + chain.with_committee_cache(target.root, attestation_epoch, |cached_shuffling, _| { + let committee_cache = cached_shuffling.committee_cache.as_ref(); let committees_per_slot = committee_cache.committees_per_slot(); Ok(committee_cache diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index d78e279936..b3d258a2fb 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -77,7 +77,7 @@ use crate::persisted_custody::persist_custody_context; use crate::persisted_fork_choice::PersistedForkChoice; use crate::pre_finalization_cache::PreFinalizationBlockCache; use crate::proposer_preferences_verification::proposer_preference_cache::GossipVerifiedProposerPreferenceCache; -use crate::shuffling_cache::{BlockShufflingIds, ShufflingCache}; +use crate::shuffling_cache::{CachedPTCs, CachedShuffling, ShufflingCache, with_cached_shuffling}; use crate::sync_committee_verification::{ Error as SyncCommitteeError, VerifiedSyncCommitteeMessage, VerifiedSyncContribution, }; @@ -472,7 +472,7 @@ pub struct BeaconChain { /// HTTP server is enabled. pub event_handler: Option>, /// Caches the attester shuffling for a given epoch and shuffling key root. - pub shuffling_cache: RwLock, + pub shuffling_cache: RwLock>, /// Caches the beacon block proposer shuffling for a given epoch and shuffling key root. pub beacon_proposer_cache: Arc>, /// Caches a map of `validator_index -> validator_pubkey`. @@ -1696,7 +1696,8 @@ impl BeaconChain { let (duties, dependent_root) = self.with_committee_cache( head_block_root, epoch, - |committee_cache, dependent_root| { + |cached_shuffling, dependent_root| { + let committee_cache = cached_shuffling.committee_cache.as_ref(); let duties = validator_indices .iter() .map(|validator_index| { @@ -4914,15 +4915,20 @@ impl BeaconChain { ) -> Result<(), BlockError> { for relative_epoch in [RelativeEpoch::Current, RelativeEpoch::Next] { let shuffling_id = AttestationShufflingId::new(block_root, state, relative_epoch)?; + let shuffling_epoch = relative_epoch.into_epoch(state.current_epoch()); - let shuffling_is_cached = self.shuffling_cache.read().contains(&shuffling_id); + if self.shuffling_cache.read().contains(&shuffling_id) { + continue; + } - if !shuffling_is_cached { - state.build_committee_cache(relative_epoch, &self.spec)?; - let committee_cache = state.committee_cache(relative_epoch)?; - self.shuffling_cache - .write() - .insert_committee_cache(shuffling_id, committee_cache); + state.build_committee_cache(relative_epoch, &self.spec)?; + let committee_cache = state.committee_cache(relative_epoch)?.clone(); + + if let Some(ptcs) = CachedPTCs::try_from_state(state, shuffling_epoch, &self.spec)? { + self.shuffling_cache.write().insert_committee_cache( + shuffling_id, + CachedShuffling::new(committee_cache, ptcs), + ); } } Ok(()) @@ -7013,11 +7019,11 @@ impl BeaconChain { ) } - /// Runs the `map_fn` with the committee cache for `shuffling_epoch` from the chain with head + /// Runs the `map_fn` with the cached shuffling for `shuffling_epoch` from the chain with head /// `head_block_root`. The `map_fn` will be supplied two values: /// - /// - `&CommitteeCache`: the committee cache that serves the given parameters. - /// - `Hash256`: the "shuffling decision root" which uniquely identifies the `CommitteeCache`. + /// - `&CachedShuffling`: the committee cache and optional PTCs that serve the given parameters. + /// - `Hash256`: the "shuffling decision root" which uniquely identifies the cached shuffling. /// /// It's not necessary that `head_block_root` matches our current view of the chain, it can be /// any block that is: @@ -7034,12 +7040,12 @@ impl BeaconChain { /// /// ## Notes /// - /// This function exists in this odd "map" pattern because efficiently obtaining a committee + /// This function exists in this odd "map" pattern because efficiently obtaining a shuffling /// can be complex. It might involve reading straight from the `beacon_chain.shuffling_cache` /// or it might involve reading it from a state from the DB. Due to the complexities of /// `RwLock`s on the shuffling cache, a simple `Cow` isn't suitable here. /// - /// If the committee for `(head_block_root, shuffling_epoch)` isn't found in the + /// If the shuffling for `(head_block_root, shuffling_epoch)` isn't found in the /// `shuffling_cache`, we will read a state from disk and then update the `shuffling_cache`. pub fn with_committee_cache( &self, @@ -7048,149 +7054,17 @@ impl BeaconChain { map_fn: F, ) -> Result where - F: Fn(&CommitteeCache, Hash256) -> Result, + F: Fn(&CachedShuffling, Hash256) -> Result, { - let head_block = self - .canonical_head - .fork_choice_read_lock() - .get_block(&head_block_root) - .ok_or(Error::MissingBeaconBlock(head_block_root))?; - - let shuffling_id = BlockShufflingIds { - current: head_block.current_epoch_shuffling_id.clone(), - next: head_block.next_epoch_shuffling_id.clone(), - previous: None, - block_root: head_block.root, - } - .id_for_epoch(shuffling_epoch) - .ok_or_else(|| Error::InvalidShufflingId { + with_cached_shuffling( + &self.canonical_head, + &self.shuffling_cache, + &self.store, + &self.spec, + head_block_root, shuffling_epoch, - head_block_epoch: head_block.slot.epoch(T::EthSpec::slots_per_epoch()), - })?; - - // Obtain the shuffling cache, timing how long we wait. - let mut shuffling_cache = { - let _ = - metrics::start_timer(&metrics::ATTESTATION_PROCESSING_SHUFFLING_CACHE_WAIT_TIMES); - self.shuffling_cache.write() - }; - - if let Some(cache_item) = shuffling_cache.get(&shuffling_id) { - // The shuffling cache is no longer required, drop the write-lock to allow concurrent - // access. - drop(shuffling_cache); - - let committee_cache = cache_item.wait()?; - map_fn(&committee_cache, shuffling_id.shuffling_decision_block) - } else { - // Create an entry in the cache that "promises" this value will eventually be computed. - // This avoids the case where multiple threads attempt to produce the same value at the - // same time. - // - // Creating the promise whilst we hold the `shuffling_cache` lock will prevent the same - // promise from being created twice. - let sender = shuffling_cache.create_promise(shuffling_id.clone())?; - - // Drop the shuffling cache to avoid holding the lock for any longer than - // required. - drop(shuffling_cache); - - debug!( - shuffling_id = ?shuffling_epoch, - head_block_root = head_block_root.to_string(), - "Committee cache miss" - ); - - // If the block's state will be so far ahead of `shuffling_epoch` that even its - // previous epoch committee cache will be too new, then error. Callers of this function - // shouldn't be requesting such old shufflings for this `head_block_root`. - let head_block_epoch = head_block.slot.epoch(T::EthSpec::slots_per_epoch()); - if head_block_epoch > shuffling_epoch + 1 { - return Err(Error::InvalidStateForShuffling { - state_epoch: head_block_epoch, - shuffling_epoch, - }); - } - - let state_read_timer = - metrics::start_timer(&metrics::ATTESTATION_PROCESSING_STATE_READ_TIMES); - - // If the head of the chain can serve this request, use it. - // - // This code is a little awkward because we need to ensure that the head we read and - // the head we copy is identical. Taking one lock to read the head values and another - // to copy the head is liable to race-conditions. - let head_state_opt = self.with_head(|head| { - if head.beacon_block_root == head_block_root { - Ok(Some((head.beacon_state.clone(), head.beacon_state_root()))) - } else { - Ok::<_, Error>(None) - } - })?; - - // Compute the `target_slot` to advance the block's state to. - // - // Since there's a one-epoch look-ahead on the attester shuffling, it suffices to - // only advance into the first slot of the epoch prior to `shuffling_epoch`. - // - // If the `head_block` is already ahead of that slot, then we should load the state - // at that slot, as we've determined above that the `shuffling_epoch` cache will - // not be too far in the past. - let target_slot = std::cmp::max( - shuffling_epoch - .saturating_sub(1_u64) - .start_slot(T::EthSpec::slots_per_epoch()), - head_block.slot, - ); - - // If the head state is useful for this request, use it. Otherwise, read a state from - // disk that is advanced as close as possible to `target_slot`. - let (mut state, state_root) = if let Some((state, state_root)) = head_state_opt { - (state, state_root) - } else { - // We assume that the `Pending` state has the same shufflings as a `Full` state - // for the same block. Analysis: https://hackmd.io/@dapplion/gloas_dependant_root - let (state_root, state) = self - .store - .get_advanced_hot_state(head_block_root, target_slot, head_block.state_root)? - .ok_or(Error::MissingBeaconState(head_block.state_root))?; - (state, state_root) - }; - - metrics::stop_timer(state_read_timer); - let state_skip_timer = - metrics::start_timer(&metrics::ATTESTATION_PROCESSING_STATE_SKIP_TIMES); - - // If the state is still in an earlier epoch, advance it to the `target_slot` so - // that its next epoch committee cache matches the `shuffling_epoch`. - if state.current_epoch() + 1 < shuffling_epoch { - // Advance the state into the required slot, using the "partial" method since the - // state roots are not relevant for the shuffling. - partial_state_advance(&mut state, Some(state_root), target_slot, &self.spec)?; - } - metrics::stop_timer(state_skip_timer); - - let committee_building_timer = - metrics::start_timer(&metrics::ATTESTATION_PROCESSING_COMMITTEE_BUILDING_TIMES); - - let relative_epoch = RelativeEpoch::from_epoch(state.current_epoch(), shuffling_epoch) - .map_err(Error::IncorrectStateForAttestation)?; - - state.build_committee_cache(relative_epoch, &self.spec)?; - - let committee_cache = state.committee_cache(relative_epoch)?.clone(); - let shuffling_decision_block = shuffling_id.shuffling_decision_block; - - self.shuffling_cache - .write() - .insert_committee_cache(shuffling_id, &committee_cache); - - metrics::stop_timer(committee_building_timer); - - sender.send(committee_cache.clone()); - - map_fn(&committee_cache, shuffling_decision_block) - } + map_fn, + ) } /// Dumps the entire canonical chain, from the head to genesis to a vector for analysis. diff --git a/beacon_node/beacon_chain/src/errors.rs b/beacon_node/beacon_chain/src/errors.rs index 9802f091e0..5efe9a3c23 100644 --- a/beacon_node/beacon_chain/src/errors.rs +++ b/beacon_node/beacon_chain/src/errors.rs @@ -251,6 +251,13 @@ pub enum BeaconChainError { request_epoch: Epoch, cache_epoch: Epoch, }, + AttesterCachePtcOutOfBounds { + slot: Slot, + epoch: Epoch, + }, + AttesterCacheNoPtcPreGloas { + slot: Slot, + }, SkipProposerPreparation, FailedColumnCustodyInfoUpdate, } diff --git a/beacon_node/beacon_chain/src/payload_attestation_verification/gossip_verified_payload_attestation.rs b/beacon_node/beacon_chain/src/payload_attestation_verification/gossip_verified_payload_attestation.rs index c36c73b344..3e9f9e4b60 100644 --- a/beacon_node/beacon_chain/src/payload_attestation_verification/gossip_verified_payload_attestation.rs +++ b/beacon_node/beacon_chain/src/payload_attestation_verification/gossip_verified_payload_attestation.rs @@ -2,26 +2,29 @@ use super::Error; use crate::beacon_chain::BeaconStore; use crate::canonical_head::CanonicalHead; use crate::observed_attesters::ObservedPayloadAttesters; +use crate::shuffling_cache::{ShufflingCache, with_cached_shuffling}; use crate::validator_pubkey_cache::ValidatorPubkeyCache; use crate::{BeaconChain, BeaconChainError, BeaconChainTypes, metrics}; use bls::AggregateSignature; use educe::Educe; use eth2::types::{EventKind, ForkVersionedResponse}; use parking_lot::RwLock; -use safe_arith::SafeArith; use slot_clock::SlotClock; -use state_processing::per_block_processing::signature_sets::indexed_payload_attestation_signature_set; -use state_processing::state_advance::partial_state_advance; +use state_processing::per_block_processing::signature_sets::indexed_payload_attestation_signature_set_from_pubkeys; use std::borrow::Cow; -use types::{ChainSpec, EthSpec, IndexedPayloadAttestation, PTC, PayloadAttestationMessage, Slot}; +use types::{ + ChainSpec, EthSpec, Hash256, IndexedPayloadAttestation, PTC, PayloadAttestationMessage, Slot, +}; pub struct GossipVerificationContext<'a, T: BeaconChainTypes> { pub slot_clock: &'a T::SlotClock, pub spec: &'a ChainSpec, pub observed_payload_attesters: &'a RwLock>, pub canonical_head: &'a CanonicalHead, + pub shuffling_cache: &'a RwLock>, pub validator_pubkey_cache: &'a RwLock>, pub store: &'a BeaconStore, + pub genesis_validators_root: Hash256, } /// A `PayloadAttestationMessage` that has been verified for propagation on the gossip network. @@ -76,56 +79,18 @@ impl VerifiedPayloadAttestationMessage { return Err(Error::UnknownHeadBlock { beacon_block_root }); } - // Get head state for PTC computation. If the cached head state is too stale - // (e.g. during liveness failures with many skipped slots), fall back to loading - // a more recent state from the store and advancing it if necessary. - let head = ctx.canonical_head.cached_head(); - let head_state = &head.snapshot.beacon_state; - let message_epoch = slot.epoch(T::EthSpec::slots_per_epoch()); - let state_epoch = head_state.current_epoch(); - - // get_ptc can serve epochs in [state_epoch - 1, state_epoch + min_seed_lookahead]. - // If the message epoch is beyond that range, the head state is stale. - let advanced_state = if message_epoch - > state_epoch - .safe_add(ctx.spec.min_seed_lookahead) - .map_err(BeaconChainError::from)? - { - let head_block_root = head.head_block_root(); - let target_slot = message_epoch.start_slot(T::EthSpec::slots_per_epoch()); - - let (state_root, mut state) = ctx - .store - .get_advanced_hot_state( - head_block_root, - target_slot, - head.snapshot.beacon_state_root(), - ) - .map_err(BeaconChainError::from)? - .ok_or(BeaconChainError::MissingBeaconState( - head.snapshot.beacon_state_root(), - ))?; - - if state - .current_epoch() - .safe_add(ctx.spec.min_seed_lookahead) - .map_err(BeaconChainError::from)? - < message_epoch - { - partial_state_advance(&mut state, Some(state_root), target_slot, ctx.spec) - .map_err(BeaconChainError::from)?; - } - - Some(state) - } else { - None - }; - - let state = advanced_state.as_ref().unwrap_or(head_state); + let ptc = with_cached_shuffling( + ctx.canonical_head, + ctx.shuffling_cache, + ctx.store, + ctx.spec, + beacon_block_root, + message_epoch, + |cached_shuffling, _| cached_shuffling.ptc_for_slot(slot), + )?; // [REJECT] `validator_index` is within `get_ptc(state, data.slot)`. - let ptc = state.get_ptc(slot, ctx.spec)?; if !ptc.0.contains(&(validator_index as usize)) { return Err(Error::NotInPTC { validator_index, @@ -145,11 +110,11 @@ impl VerifiedPayloadAttestationMessage { { // [REJECT] The signature is valid with respect to the `validator_index`. let pubkey_cache = ctx.validator_pubkey_cache.read(); - let signature_set = indexed_payload_attestation_signature_set( - state, + let signature_set = indexed_payload_attestation_signature_set_from_pubkeys( |validator_index| pubkey_cache.get(validator_index).map(Cow::Borrowed), &indexed_payload_attestation.signature, &indexed_payload_attestation, + ctx.genesis_validators_root, ctx.spec, ) .map_err(|_| Error::UnknownValidatorIndex(validator_index))?; @@ -204,8 +169,10 @@ impl BeaconChain { spec: &self.spec, observed_payload_attesters: &self.observed_payload_attesters, canonical_head: &self.canonical_head, + shuffling_cache: &self.shuffling_cache, validator_pubkey_cache: &self.validator_pubkey_cache, store: &self.store, + genesis_validators_root: self.genesis_validators_root, } } diff --git a/beacon_node/beacon_chain/src/payload_attestation_verification/mod.rs b/beacon_node/beacon_chain/src/payload_attestation_verification/mod.rs index 477527c0aa..89ae1bbbdd 100644 --- a/beacon_node/beacon_chain/src/payload_attestation_verification/mod.rs +++ b/beacon_node/beacon_chain/src/payload_attestation_verification/mod.rs @@ -9,7 +9,7 @@ use crate::BeaconChainError; use strum::AsRefStr; -use types::{BeaconStateError, Hash256, Slot}; +use types::{Hash256, Slot}; pub mod gossip_verified_payload_attestation; @@ -86,12 +86,6 @@ pub enum Error { /// We were unable to process this message due to an internal error. It's unclear if the /// message is valid. BeaconChainError(Box), - /// An error reading beacon state. - /// - /// ## Peer scoring - /// - /// We were unable to process this message due to an internal error. - BeaconStateError(BeaconStateError), } impl From for Error { @@ -100,11 +94,5 @@ impl From for Error { } } -impl From for Error { - fn from(e: BeaconStateError) -> Self { - Error::BeaconStateError(e) - } -} - #[cfg(test)] mod tests; diff --git a/beacon_node/beacon_chain/src/payload_attestation_verification/tests.rs b/beacon_node/beacon_chain/src/payload_attestation_verification/tests.rs index c45df51ac8..d4b82c41fc 100644 --- a/beacon_node/beacon_chain/src/payload_attestation_verification/tests.rs +++ b/beacon_node/beacon_chain/src/payload_attestation_verification/tests.rs @@ -1,25 +1,15 @@ use std::sync::Arc; use std::time::Duration; -use bls::{Keypair, Signature}; -use fork_choice::ForkChoice; -use genesis::{generate_deterministic_keypairs, interop_genesis_state}; -use parking_lot::RwLock; -use proto_array::PayloadStatus; +use bls::Signature; use slot_clock::{SlotClock, TestingSlotClock}; use state_processing::AllCaches; -use state_processing::genesis::genesis_block; -use store::{HotColdDB, StoreConfig}; use types::{ - ChainSpec, Checkpoint, Domain, Epoch, EthSpec, Hash256, MinimalEthSpec, PayloadAttestationData, - PayloadAttestationMessage, SignedBeaconBlock, SignedRoot, Slot, + Domain, Epoch, EthSpec, ForkName, Hash256, MinimalEthSpec, PayloadAttestationData, + PayloadAttestationMessage, SignedRoot, Slot, }; use crate::{ - beacon_fork_choice_store::BeaconForkChoiceStore, - beacon_snapshot::BeaconSnapshot, - canonical_head::CanonicalHead, - observed_attesters::ObservedPayloadAttesters, payload_attestation_verification::{ Error as PayloadAttestationError, gossip_verified_payload_attestation::{ @@ -27,7 +17,6 @@ use crate::{ }, }, test_utils::{BeaconChainHarness, EphemeralHarnessType, fork_name_from_env, test_spec}, - validator_pubkey_cache::ValidatorPubkeyCache, }; type E = MinimalEthSpec; @@ -36,96 +25,48 @@ type T = EphemeralHarnessType; const NUM_VALIDATORS: usize = 64; struct TestContext { - canonical_head: CanonicalHead, - observed_payload_attesters: RwLock>, - validator_pubkey_cache: RwLock>, - slot_clock: TestingSlotClock, - keypairs: Vec, - spec: ChainSpec, + harness: BeaconChainHarness, genesis_block_root: Hash256, - store: Arc>, } impl TestContext { fn new() -> Self { - let spec = test_spec::(); - let store = Arc::new( - HotColdDB::open_ephemeral(StoreConfig::default(), Arc::new(spec.clone())) - .expect("should open ephemeral store"), - ); - - let keypairs = generate_deterministic_keypairs(NUM_VALIDATORS); - - let mut state = - interop_genesis_state::(&keypairs, 0, Hash256::repeat_byte(0x42), None, &spec) - .expect("should build genesis state"); - - *state.finalized_checkpoint_mut() = Checkpoint { - epoch: Epoch::new(1), - root: Hash256::ZERO, - }; - - let mut block = genesis_block(&state, &spec).expect("should build genesis block"); - *block.state_root_mut() = state - .update_tree_hash_cache() - .expect("should hash genesis state"); - let signed_block = SignedBeaconBlock::from_block(block, Signature::empty()); - let block_root = signed_block.canonical_root(); - - let snapshot = BeaconSnapshot::new( - Arc::new(signed_block.clone()), - None, - block_root, - state.clone(), - ); - - let fc_store = BeaconForkChoiceStore::get_forkchoice_store(store.clone(), snapshot.clone()) - .expect("should create fork choice store"); - let fork_choice = - ForkChoice::from_anchor(fc_store, block_root, &signed_block, &state, None, &spec) - .expect("should create fork choice"); - - let canonical_head = - CanonicalHead::new(fork_choice, Arc::new(snapshot), PayloadStatus::Pending); - + let spec = Arc::new(test_spec::()); let slot_clock = TestingSlotClock::new( Slot::new(0), Duration::from_secs(0), spec.get_slot_duration(), ); - // Advance past genesis so `now_with_past_tolerance` doesn't underflow. - slot_clock.set_current_time(spec.get_slot_duration()); + let harness = BeaconChainHarness::builder(E::default()) + .spec(spec) + .deterministic_keypairs(NUM_VALIDATORS) + .fresh_ephemeral_store() + .testing_slot_clock(slot_clock) + .build(); - let validator_pubkey_cache = - ValidatorPubkeyCache::new(&state, store.clone()).expect("should create pubkey cache"); + // Advance past genesis so `now_with_past_tolerance` doesn't underflow. + harness + .chain + .slot_clock + .set_current_time(harness.spec.get_slot_duration()); + let genesis_block_root = harness.chain.genesis_block_root; Self { - canonical_head, - observed_payload_attesters: RwLock::new(ObservedPayloadAttesters::default()), - validator_pubkey_cache: RwLock::new(validator_pubkey_cache), - slot_clock, - keypairs, - spec, - genesis_block_root: block_root, - store, + harness, + genesis_block_root, } } fn gossip_ctx(&self) -> GossipVerificationContext<'_, T> { - GossipVerificationContext { - slot_clock: &self.slot_clock, - spec: &self.spec, - observed_payload_attesters: &self.observed_payload_attesters, - canonical_head: &self.canonical_head, - validator_pubkey_cache: &self.validator_pubkey_cache, - store: &self.store, - } + self.harness.chain.payload_attestation_gossip_context() } fn ptc_members(&self, slot: Slot) -> Vec { - let head = self.canonical_head.cached_head(); + let head = self.harness.chain.canonical_head.cached_head(); let state = &head.snapshot.beacon_state; - let ptc = state.get_ptc(slot, &self.spec).expect("should get PTC"); + let ptc = state + .get_ptc(slot, &self.harness.spec) + .expect("should get PTC"); ptc.0.to_vec() } @@ -134,16 +75,18 @@ impl TestContext { data: PayloadAttestationData, validator_index: u64, ) -> PayloadAttestationMessage { - let head = self.canonical_head.cached_head(); + let head = self.harness.chain.canonical_head.cached_head(); let state = &head.snapshot.beacon_state; - let domain = self.spec.get_domain( + let domain = self.harness.spec.get_domain( data.slot.epoch(E::slots_per_epoch()), Domain::PTCAttester, &state.fork(), state.genesis_validators_root(), ); let message = data.signing_root(domain); - let signature = self.keypairs[validator_index as usize].sk.sign(message); + let signature = self.harness.validator_keypairs[validator_index as usize] + .sk + .sign(message); PayloadAttestationMessage { validator_index, data, @@ -192,7 +135,7 @@ fn past_slot() { return; } let ctx = TestContext::new(); - ctx.slot_clock.set_slot(5); + ctx.harness.chain.slot_clock.set_slot(5); let gossip = ctx.gossip_ctx(); let msg = make_payload_attestation(Slot::new(0), 0, ctx.genesis_block_root); @@ -328,20 +271,95 @@ fn duplicate_after_valid() { )); } -/// Exercises the `partial_state_advance` fallback in gossip verification when -/// the head state is too stale to compute PTC membership (e.g., during a -/// network liveness failure with many missed slots). #[tokio::test] -async fn stale_head_with_partial_advance() { +async fn ptc_cache_is_primed_at_gloas_fork_boundary() { + // Only run this test once, when FORK_NAME=gloas exactly. + let mut spec = test_spec::(); + if spec.fork_name_at_epoch(Epoch::new(0)) != ForkName::Gloas { + return; + } + + let gloas_fork_epoch = Epoch::new(2); + spec.gloas_fork_epoch = Some(gloas_fork_epoch); + assert_eq!( + spec.fork_name_at_epoch(gloas_fork_epoch - 1), + ForkName::Fulu + ); + assert_eq!(spec.fork_name_at_epoch(gloas_fork_epoch), ForkName::Gloas); + + let slots_per_epoch = E::slots_per_epoch(); + let fork_boundary_slot = gloas_fork_epoch.start_slot(slots_per_epoch); + let test_slots = (fork_boundary_slot.as_u64() + ..fork_boundary_slot.as_u64() + slots_per_epoch * 2) + .map(Slot::new); + + let harness = BeaconChainHarness::builder(E::default()) + .spec(Arc::new(spec)) + .deterministic_keypairs(NUM_VALIDATORS) + .fresh_ephemeral_store() + .mock_execution_layer() + .build(); + + harness.extend_to_slot(fork_boundary_slot).await; + + for slot in test_slots { + harness.chain.slot_clock.set_slot(slot.as_u64()); + assert!( + harness + .chain + .shuffling_cache + .read() + .check_gloas_ptcs_invariant(&harness.spec), + "shuffling cache should satisfy the Gloas PTC invariant" + ); + + let head = harness.chain.canonical_head.cached_head(); + let state = &head.snapshot.beacon_state; + let ptc = state.get_ptc(slot, &harness.spec).expect("should get PTC"); + let validator_index = *ptc.0.first().expect("PTC should have a member") as u64; + let data = PayloadAttestationData { + beacon_block_root: head.head_block_root(), + slot, + payload_present: true, + blob_data_available: true, + }; + let domain = harness.spec.get_domain( + data.slot.epoch(slots_per_epoch), + Domain::PTCAttester, + &state.fork(), + state.genesis_validators_root(), + ); + let signature = harness.validator_keypairs[validator_index as usize] + .sk + .sign(data.signing_root(domain)); + let msg = PayloadAttestationMessage { + validator_index, + data, + signature, + }; + + let result = harness + .chain + .verify_payload_attestation_message_for_gossip(msg); + assert!( + result.is_ok(), + "expected PTC payload attestation to verify at slot {}, got: {:?}", + slot, + result.unwrap_err() + ); + } +} + +/// Exercises payload attestation gossip verification when the message epoch is ahead of the +/// canonical head due to many missed slots. +#[tokio::test] +async fn stale_head_payload_attestation() { if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { return; } let slots_per_epoch = E::slots_per_epoch(); - // Head at epoch 1, message at epoch 5 — 4 epochs of missed slots. - // This exceeds min_seed_lookahead (1), triggering the fallback path: - // get_advanced_hot_state loads the stored state, then partial_state_advance - // advances it through epoch boundaries to populate ptc_window. + // Head at epoch 1, message at epoch 5: 4 epochs of missed slots. let head_slot = Slot::new(slots_per_epoch); let missed_epochs = 4; let target_slot = Slot::new(slots_per_epoch * (1 + missed_epochs)); @@ -360,7 +378,7 @@ async fn stale_head_with_partial_advance() { let head_epoch = head.snapshot.beacon_state.current_epoch(); assert!( target_epoch > head_epoch + harness.spec.min_seed_lookahead, - "precondition: message epoch must exceed head + min_seed_lookahead to trigger fallback" + "precondition: message epoch must exceed head + min_seed_lookahead" ); // GIVEN a slot clock advanced to epoch 5 without producing blocks @@ -385,7 +403,9 @@ async fn stale_head_with_partial_advance() { .expect("should get PTC from reference state"); let validator_index = *ptc.0.first().expect("PTC should have at least one member") as u64; - // WHEN a properly-signed payload attestation from a PTC member is verified. + // WHEN a properly-signed payload attestation from a PTC member is verified. The signature + // domain should come from the spec fork schedule and genesis validators root, not a loaded + // state in the verifier. let domain = harness.spec.get_domain( target_epoch, Domain::PTCAttester, @@ -420,3 +440,105 @@ async fn stale_head_with_partial_advance() { result.unwrap_err() ); } + +/// Exercises payload attestation gossip verification for a non-canonical block whose PTC differs +/// from the canonical chain's PTC for the same slot. +#[tokio::test] +async fn side_chain_payload_attestation_uses_side_chain_ptc() { + if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } + + let slots_per_epoch = E::slots_per_epoch(); + let fork_slot = Slot::new(slots_per_epoch); + let target_slot = Slot::new(slots_per_epoch * 4); + let target_epoch = target_slot.epoch(slots_per_epoch); + + let harness = BeaconChainHarness::builder(E::default()) + .default_spec() + .deterministic_keypairs(NUM_VALIDATORS) + .fresh_ephemeral_store() + .mock_execution_layer() + .build(); + + // Build a common prefix through epoch 1. + harness.extend_to_slot(fork_slot).await; + let fork_state = harness.chain.head_snapshot().beacon_state.clone(); + + // Build two branches for several epochs. The side chain skips its first slot, giving it + // different RANDAO mixes and therefore a different PTC by the target slot. The canonical chain + // is processed second and receives sub-finality attestations, so it remains the head without + // finalizing past the side-chain fork point. + let side_slots: Vec<_> = ((fork_slot + 2).as_u64()..=target_slot.as_u64()) + .map(Slot::new) + .collect(); + let canonical_slots: Vec<_> = ((fork_slot + 1).as_u64()..=target_slot.as_u64()) + .map(Slot::new) + .collect(); + let canonical_validators = (0..NUM_VALIDATORS / 2).collect::>(); + + let results = harness + .add_blocks_on_multiple_chains(vec![ + (fork_state.clone(), side_slots, vec![]), + (fork_state, canonical_slots, canonical_validators), + ]) + .await; + + let side_head_root: Hash256 = results[0].2.into(); + let side_head_state = &results[0].3; + let canonical_head_root: Hash256 = results[1].2.into(); + let canonical_head_state = &results[1].3; + + assert_ne!(side_head_root, canonical_head_root); + assert_eq!( + harness.chain.head_snapshot().beacon_block_root, + canonical_head_root + ); + + let side_ptc = side_head_state + .get_ptc(target_slot, &harness.spec) + .expect("should get side-chain PTC"); + let canonical_ptc = canonical_head_state + .get_ptc(target_slot, &harness.spec) + .expect("should get canonical PTC"); + assert_ne!( + side_ptc, canonical_ptc, + "precondition: side-chain PTC should differ from canonical PTC" + ); + + let validator_index = side_ptc + .0 + .iter() + .copied() + .find(|validator_index| !canonical_ptc.0.contains(validator_index)) + .expect("should find a validator in the side-chain PTC only") + as u64; + + let domain = harness.spec.get_domain( + target_epoch, + Domain::PTCAttester, + &side_head_state.fork(), + side_head_state.genesis_validators_root(), + ); + let data = PayloadAttestationData { + beacon_block_root: side_head_root, + slot: target_slot, + payload_present: true, + blob_data_available: true, + }; + let message = data.signing_root(domain); + let signature = harness.validator_keypairs[validator_index as usize] + .sk + .sign(message); + let msg = PayloadAttestationMessage { + validator_index, + data, + signature, + }; + + let verified = harness + .chain + .verify_payload_attestation_message_for_gossip(msg) + .expect("side-chain payload attestation should verify"); + assert_eq!(verified.ptc(), &side_ptc); +} diff --git a/beacon_node/beacon_chain/src/shuffling_cache.rs b/beacon_node/beacon_chain/src/shuffling_cache.rs index 0377b553e3..daaede6ed1 100644 --- a/beacon_node/beacon_chain/src/shuffling_cache.rs +++ b/beacon_node/beacon_chain/src/shuffling_cache.rs @@ -3,23 +3,28 @@ use std::sync::Arc; use itertools::Itertools; use oneshot_broadcast::{Receiver, Sender, oneshot}; +use parking_lot::RwLock; +use state_processing::state_advance::partial_state_advance; use tracing::debug; use types::{ - AttestationShufflingId, BeaconState, Epoch, EthSpec, Hash256, RelativeEpoch, - state::CommitteeCache, + AttestationShufflingId, BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec, Hash256, PTC, + RelativeEpoch, Slot, state::CommitteeCache, }; -use crate::{BeaconChainError, metrics}; +use crate::{ + BeaconChainError, BeaconChainTypes, BeaconStore, canonical_head::CanonicalHead, metrics, +}; -/// The size of the cache that stores committee caches for quicker verification. +/// The size of the cache that stores shufflings for quicker verification. /// -/// Each entry should be `8 + 800,000 = 800,008` bytes in size with 100k validators. (8-byte hash + -/// 100k indices). Therefore, this cache should be approx `16 * 800,008 = 12.8 MB`. (Note: this -/// ignores a few extra bytes in the caches that should be insignificant compared to the indices). +/// Each entry should be around `8 * 2M + 128KB ~= 16 MB` in size with 2M validators +/// and 32 512-validator PTCs. Therefore, this cache should be approx +/// `16 * 16 MB ~= 256 MB`. (Note: this ignores a few extra bytes in the +/// caches that should be insignificant compared to the indices). pub const DEFAULT_CACHE_SIZE: usize = 16; -/// The maximum number of concurrent committee cache "promises" that can be issued. In effect, this -/// limits the number of concurrent states that can be loaded into memory for the committee cache. +/// The maximum number of concurrent shuffling "promises" that can be issued. In effect, this +/// limits the number of concurrent states that can be loaded into memory for the shuffling. /// This prevents excessive memory usage at the cost of rejecting some attestations. /// /// We set this value to 2 since states can be quite large and have a significant impact on memory @@ -30,19 +35,82 @@ pub const DEFAULT_CACHE_SIZE: usize = 16; const MAX_CONCURRENT_PROMISES: usize = 2; #[derive(Clone)] -pub enum CacheItem { - /// A committee. - Committee(Arc), - /// A promise for a future committee. - Promise(Receiver>), +pub struct CachedShuffling { + pub committee_cache: Arc, + pub ptcs: CachedPTCs, } -impl CacheItem { +#[derive(Clone)] +pub enum CachedPTCs { + PreGloas, + PostGloas(Vec>, Epoch), +} + +impl CachedPTCs { + /// Returns `None` at the Gloas fork boundary (pre-Gloas state, Gloas shuffling epoch); the + /// on-demand miss path in `with_cached_shuffling` handles those. + pub fn try_from_state( + state: &BeaconState, + epoch: Epoch, + spec: &ChainSpec, + ) -> Result, BeaconChainError> { + if shuffling_requires_ptcs(epoch, spec) { + if !state.fork_name_unchecked().gloas_enabled() { + return Ok(None); + } + let ptcs = epoch + .slot_iter(E::slots_per_epoch()) + .map(|slot| state.get_ptc(slot, spec)) + .collect::, _>>()?; + Ok(Some(Self::PostGloas(ptcs, epoch))) + } else { + Ok(Some(Self::PreGloas)) + } + } +} + +impl CachedShuffling { + pub fn new(committee_cache: Arc, ptcs: CachedPTCs) -> Self { + Self { + committee_cache, + ptcs, + } + } + + pub fn ptc_for_slot(&self, slot: Slot) -> Result, BeaconChainError> { + match &self.ptcs { + CachedPTCs::PreGloas => Err(BeaconChainError::AttesterCacheNoPtcPreGloas { slot }), + &CachedPTCs::PostGloas(ref ptcs, epoch) => { + if slot.epoch(E::slots_per_epoch()) != epoch { + Err(BeaconChainError::AttesterCachePtcOutOfBounds { slot, epoch }) + } else { + ptcs.get(slot.as_usize() % E::slots_per_epoch() as usize) + .cloned() + .ok_or(BeaconChainError::AttesterCachePtcOutOfBounds { slot, epoch }) + } + } + } + } +} + +fn shuffling_requires_ptcs(shuffling_epoch: Epoch, spec: &ChainSpec) -> bool { + spec.fork_name_at_epoch(shuffling_epoch).gloas_enabled() +} + +#[derive(Clone)] +pub enum CacheItem { + /// A cached shuffling. + Committee(CachedShuffling), + /// A promise for a future cached shuffling. + Promise(Receiver>), +} + +impl CacheItem { pub fn is_promise(&self) -> bool { matches!(self, CacheItem::Promise(_)) } - pub fn wait(self) -> Result, BeaconChainError> { + pub fn wait(self) -> Result, BeaconChainError> { match self { CacheItem::Committee(cache) => Ok(cache), CacheItem::Promise(receiver) => receiver @@ -52,17 +120,17 @@ impl CacheItem { } } -/// Provides a cache for `CommitteeCache`. +/// Provides a cache for `CommitteeCache` and the associated optional PTCs. /// /// It has been named `ShufflingCache` because `CommitteeCacheCache` is a bit weird and looks like /// a find/replace error. -pub struct ShufflingCache { - cache: HashMap, +pub struct ShufflingCache { + cache: HashMap>, cache_size: usize, head_shuffling_ids: BlockShufflingIds, } -impl ShufflingCache { +impl ShufflingCache { pub fn new(cache_size: usize, head_shuffling_ids: BlockShufflingIds) -> Self { Self { cache: HashMap::new(), @@ -71,22 +139,22 @@ impl ShufflingCache { } } - pub fn get(&mut self, key: &AttestationShufflingId) -> Option { + pub fn get(&mut self, key: &AttestationShufflingId) -> Option> { match self.cache.get(key) { - // The cache contained the committee cache, return it. + // The cache contained the shuffling, return it. item @ Some(CacheItem::Committee(_)) => { metrics::inc_counter(&metrics::SHUFFLING_CACHE_HITS); item.cloned() } - // The cache contains a promise for the committee cache. Check to see if the promise has + // The cache contains a promise for the shuffling. Check to see if the promise has // already been resolved, without waiting for it. item @ Some(CacheItem::Promise(receiver)) => match receiver.try_recv() { // The promise has already been resolved. Replace the entry in the cache with a - // `Committee` entry and then return the committee. - Ok(Some(committee)) => { + // `Committee` entry and then return the cached shuffling. + Ok(Some(cached_shuffling)) => { metrics::inc_counter(&metrics::SHUFFLING_CACHE_PROMISE_HITS); metrics::inc_counter(&metrics::SHUFFLING_CACHE_HITS); - let ready = CacheItem::Committee(committee); + let ready = CacheItem::Committee(cached_shuffling); self.insert_cache_item(key.clone(), ready.clone()); Some(ready) } @@ -97,8 +165,8 @@ impl ShufflingCache { metrics::inc_counter(&metrics::SHUFFLING_CACHE_HITS); item.cloned() } - // The sender has been dropped without sending a committee. There was most likely an - // error computing the committee cache. Drop the key from the cache and return + // The sender has been dropped without sending a shuffling. There was most likely an + // error computing the shuffling. Drop the key from the cache and return // `None` so the caller can recompute the committee. // // It's worth noting that this is the only place where we removed unresolved @@ -113,7 +181,7 @@ impl ShufflingCache { None } }, - // The cache does not have this committee and it's not already promised to be computed. + // The cache does not have this shuffling and it's not already promised to be computed. None => { metrics::inc_counter(&metrics::SHUFFLING_CACHE_MISSES); None @@ -125,27 +193,41 @@ impl ShufflingCache { self.cache.contains_key(key) } - pub fn insert_committee_cache( + /// Check that all entries for Gloas epochs have PTCs. + #[cfg(test)] + pub fn check_gloas_ptcs_invariant(&self, spec: &ChainSpec) -> bool { + self.cache.iter().all(|(key, item)| { + if shuffling_requires_ptcs(key.shuffling_epoch, spec) { + match item { + CacheItem::Committee(cached_shuffling) => { + matches!(cached_shuffling.ptcs, CachedPTCs::PostGloas(..)) + } + CacheItem::Promise(_) => true, + } + } else { + true + } + }) + } + + pub fn insert_committee_cache( &mut self, key: AttestationShufflingId, - committee_cache: &C, + cached_shuffling: CachedShuffling, ) { - if self - .cache - .get(&key) - // Replace the committee if it's not present or if it's a promise. A bird in the hand is - // worth two in the promise-bush! - .is_none_or(CacheItem::is_promise) - { - self.insert_cache_item( - key, - CacheItem::Committee(committee_cache.to_arc_committee_cache()), - ); + match self.cache.get(&key) { + Some(CacheItem::Committee(_)) => { + // Calculation is deterministic, so no need to replace the existing entry. + } + // A bird in the hand is worth two in the promise-bush! + Some(CacheItem::Promise(_)) | None => { + self.insert_cache_item(key, CacheItem::Committee(cached_shuffling)); + } } } /// Prunes the cache first before inserting a new cache item. - fn insert_cache_item(&mut self, key: AttestationShufflingId, cache_item: CacheItem) { + fn insert_cache_item(&mut self, key: AttestationShufflingId, cache_item: CacheItem) { self.prune_cache(); self.cache.insert(key, cache_item); } @@ -188,7 +270,7 @@ impl ShufflingCache { pub fn create_promise( &mut self, key: AttestationShufflingId, - ) -> Result>, BeaconChainError> { + ) -> Result>, BeaconChainError> { let num_active_promises = self .cache .iter() @@ -212,20 +294,170 @@ impl ShufflingCache { } } -/// A helper trait to allow lazy-cloning of the committee cache when inserting into the cache. -pub trait ToArcCommitteeCache { - fn to_arc_committee_cache(&self) -> Arc; -} +pub fn with_cached_shuffling( + canonical_head: &CanonicalHead, + shuffling_cache_lock: &RwLock>, + store: &BeaconStore, + spec: &ChainSpec, + head_block_root: Hash256, + shuffling_epoch: Epoch, + map_fn: F, +) -> Result +where + T: BeaconChainTypes, + F: Fn(&CachedShuffling, Hash256) -> Result, + Error: From, +{ + let head_block = canonical_head + .fork_choice_read_lock() + .get_block(&head_block_root) + .ok_or(BeaconChainError::MissingBeaconBlock(head_block_root))?; -impl ToArcCommitteeCache for CommitteeCache { - fn to_arc_committee_cache(&self) -> Arc { - Arc::new(self.clone()) + let shuffling_id = BlockShufflingIds { + current: head_block.current_epoch_shuffling_id.clone(), + next: head_block.next_epoch_shuffling_id.clone(), + previous: None, + block_root: head_block.root, } -} + .id_for_epoch(shuffling_epoch) + .ok_or_else(|| BeaconChainError::InvalidShufflingId { + shuffling_epoch, + head_block_epoch: head_block.slot.epoch(T::EthSpec::slots_per_epoch()), + })?; -impl ToArcCommitteeCache for Arc { - fn to_arc_committee_cache(&self) -> Arc { - self.clone() + let mut shuffling_cache = { + let _ = metrics::start_timer(&metrics::ATTESTATION_PROCESSING_SHUFFLING_CACHE_WAIT_TIMES); + shuffling_cache_lock.write() + }; + + if let Some(cache_item) = shuffling_cache.get(&shuffling_id) { + drop(shuffling_cache); + + let cached_shuffling = cache_item.wait()?; + map_fn(&cached_shuffling, shuffling_id.shuffling_decision_block) + } else { + // Create an entry in the cache that "promises" this value will eventually be computed. + // This avoids the case where multiple threads attempt to produce the same value at the + // same time. + // + // Creating the promise whilst we hold the `shuffling_cache` lock will prevent the same + // promise from being created twice. + let sender = shuffling_cache.create_promise(shuffling_id.clone())?; + + // Drop the shuffling cache to avoid holding the lock for any longer than required. + drop(shuffling_cache); + + debug!( + shuffling_id = ?shuffling_epoch, + head_block_root = head_block_root.to_string(), + "Committee cache miss" + ); + + // If the block's state will be so far ahead of `shuffling_epoch` that even its previous + // epoch committee cache will be too new, then error. Callers of this function shouldn't be + // requesting such old shufflings for this `head_block_root`. + let head_block_epoch = head_block.slot.epoch(T::EthSpec::slots_per_epoch()); + if head_block_epoch > shuffling_epoch + 1 { + return Err(BeaconChainError::InvalidStateForShuffling { + state_epoch: head_block_epoch, + shuffling_epoch, + } + .into()); + } + + let state_read_timer = + metrics::start_timer(&metrics::ATTESTATION_PROCESSING_STATE_READ_TIMES); + + let cached_head = canonical_head.cached_head(); + let head_state_opt = if cached_head.head_block_root() == head_block_root { + Some(( + cached_head.snapshot.beacon_state.clone(), + cached_head.head_state_root(), + )) + } else { + None + }; + + // Compute the `target_slot` to advance the block's state to. + // + // Since there's a one-epoch look-ahead on the attester shuffling, it suffices to only + // advance into the first slot of the epoch prior to `shuffling_epoch`. + // + // If the `head_block` is already ahead of that slot, then we should load the state at that + // slot, as we've determined above that the `shuffling_epoch` cache will not be too far in + // the past. + let mut target_slot = std::cmp::max( + shuffling_epoch + .saturating_sub(1_u64) + .start_slot(T::EthSpec::slots_per_epoch()), + head_block.slot, + ); + if spec.gloas_fork_epoch == Some(shuffling_epoch) { + target_slot = std::cmp::max( + target_slot, + shuffling_epoch.start_slot(T::EthSpec::slots_per_epoch()), + ); + } + + // If the head state is useful for this request, use it. Otherwise, read a state from disk + // that is advanced as close as possible to `target_slot`. + let (mut state, state_root) = if let Some((state, state_root)) = head_state_opt { + (state, state_root) + } else { + let (state_root, state) = store + .get_advanced_hot_state(head_block_root, target_slot, head_block.state_root) + .map_err(BeaconChainError::DBError)? + .ok_or(BeaconChainError::MissingBeaconState(head_block.state_root))?; + (state, state_root) + }; + + metrics::stop_timer(state_read_timer); + let state_skip_timer = + metrics::start_timer(&metrics::ATTESTATION_PROCESSING_STATE_SKIP_TIMES); + + // If the state is still in an earlier epoch, advance it to the `target_slot` so that its + // next epoch committee cache matches the `shuffling_epoch`. + let advance_to_gloas_fork = spec.gloas_fork_epoch == Some(shuffling_epoch) + && state.current_epoch() < shuffling_epoch; + if state.current_epoch() + 1 < shuffling_epoch || advance_to_gloas_fork { + // Advance the state into the required slot, using the "partial" method since the state + // roots are not relevant for the shuffling. + partial_state_advance(&mut state, Some(state_root), target_slot, spec) + .map_err(BeaconChainError::from)?; + } + metrics::stop_timer(state_skip_timer); + + let committee_building_timer = + metrics::start_timer(&metrics::ATTESTATION_PROCESSING_COMMITTEE_BUILDING_TIMES); + + let relative_epoch = RelativeEpoch::from_epoch(state.current_epoch(), shuffling_epoch) + .map_err(BeaconChainError::IncorrectStateForAttestation)?; + + state + .build_committee_cache(relative_epoch, spec) + .map_err(BeaconChainError::from)?; + + let committee_cache = state + .committee_cache(relative_epoch) + .map_err(BeaconChainError::from)? + .clone(); + // The state has been advanced through the upgrade if needed, so `try_from_state` + // cannot return None here. + let ptcs = CachedPTCs::try_from_state(&state, shuffling_epoch, spec)?.ok_or( + BeaconChainError::BeaconStateError(BeaconStateError::IncorrectStateVariant), + )?; + let shuffling_decision_block = shuffling_id.shuffling_decision_block; + let cached_shuffling = CachedShuffling::new(committee_cache, ptcs); + + shuffling_cache_lock + .write() + .insert_committee_cache(shuffling_id, cached_shuffling.clone()); + + metrics::stop_timer(committee_building_timer); + + sender.send(cached_shuffling.clone()); + + map_fn(&cached_shuffling, shuffling_decision_block) } } @@ -304,7 +536,7 @@ mod test { const TEST_CACHE_SIZE: usize = 5; // Creates a new shuffling cache for testing - fn new_shuffling_cache() -> ShufflingCache { + fn new_shuffling_cache() -> ShufflingCache { create_test_tracing_subscriber(); let current_epoch = 8; @@ -318,6 +550,10 @@ mod test { ShufflingCache::new(TEST_CACHE_SIZE, head_shuffling_ids) } + fn cached_shuffling(committee_cache: Arc) -> CachedShuffling { + CachedShuffling::new(committee_cache, CachedPTCs::PreGloas) + } + /// Returns two different committee caches for testing. fn committee_caches() -> (Arc, Arc) { let harness = BeaconChainHarness::builder(MinimalEthSpec) @@ -366,12 +602,12 @@ mod test { ); // Resolve the promise. - sender.send(committee_a.clone()); + sender.send(cached_shuffling(committee_a.clone())); // Ensure the promise has been resolved. let item = cache.get(&id_a).unwrap(); assert!( - matches!(item, CacheItem::Committee(committee) if committee == committee_a), + matches!(item, CacheItem::Committee(cached_shuffling) if cached_shuffling.committee_cache == committee_a), "the promise should be resolved" ); assert_eq!(cache.cache.len(), 1, "the cache should have one entry"); @@ -428,30 +664,30 @@ mod test { ); // Resolve promise A. - sender_a.send(committee_a.clone()); + sender_a.send(cached_shuffling(committee_a.clone())); // Ensure promise A has been resolved. let item = cache.get(&id_a).unwrap(); assert!( - matches!(item, CacheItem::Committee(committee) if committee == committee_a), + matches!(item, CacheItem::Committee(cached_shuffling) if cached_shuffling.committee_cache == committee_a), "promise A should be resolved" ); // Resolve promise B. - sender_b.send(committee_b.clone()); + sender_b.send(cached_shuffling(committee_b.clone())); // Ensure promise B has been resolved. let item = cache.get(&id_b).unwrap(); assert!( - matches!(item, CacheItem::Committee(committee) if committee == committee_b), + matches!(item, CacheItem::Committee(cached_shuffling) if cached_shuffling.committee_cache == committee_b), "promise B should be resolved" ); // Check both entries again. assert!( - matches!(cache.get(&id_a).unwrap(), CacheItem::Committee(committee) if committee == committee_a), + matches!(cache.get(&id_a).unwrap(), CacheItem::Committee(cached_shuffling) if cached_shuffling.committee_cache == committee_a), "promise A should remain resolved" ); assert!( - matches!(cache.get(&id_b).unwrap(), CacheItem::Committee(committee) if committee == committee_b), + matches!(cache.get(&id_b).unwrap(), CacheItem::Committee(cached_shuffling) if cached_shuffling.committee_cache == committee_b), "promise B should remain resolved" ); assert_eq!(cache.cache.len(), 2, "the cache should have two entries"); @@ -485,9 +721,9 @@ mod test { let mut cache = new_shuffling_cache(); let id_a = shuffling_id(1); let committee_cache_a = Arc::new(CommitteeCache::default()); - cache.insert_committee_cache(id_a.clone(), &committee_cache_a); + cache.insert_committee_cache(id_a.clone(), cached_shuffling(committee_cache_a.clone())); assert!( - matches!(cache.get(&id_a).unwrap(), CacheItem::Committee(committee_cache) if committee_cache == committee_cache_a), + matches!(cache.get(&id_a).unwrap(), CacheItem::Committee(cached_shuffling) if cached_shuffling.committee_cache == committee_cache_a), "should insert committee cache" ); } @@ -500,7 +736,10 @@ mod test { .collect::>(); for (shuffling_id, committee_cache) in shuffling_id_and_committee_caches.iter() { - cache.insert_committee_cache(shuffling_id.clone(), committee_cache); + cache.insert_committee_cache( + shuffling_id.clone(), + cached_shuffling(committee_cache.clone()), + ); } for i in 1..(TEST_CACHE_SIZE + 1) { @@ -533,7 +772,7 @@ mod test { shuffling_epoch: (current_epoch + 1).into(), shuffling_decision_block: Hash256::from_low_u64_be(current_epoch + i as u64), }; - cache.insert_committee_cache(shuffling_id, &committee_cache); + cache.insert_committee_cache(shuffling_id, cached_shuffling(committee_cache.clone())); } // Now, update the head shuffling ids @@ -546,11 +785,17 @@ mod test { cache.update_head_shuffling_ids(head_shuffling_ids.clone()); // Insert head state shuffling ids. Should not be overridden by other shuffling ids. - cache.insert_committee_cache(head_shuffling_ids.current.clone(), &committee_cache); - cache.insert_committee_cache(head_shuffling_ids.next.clone(), &committee_cache); + cache.insert_committee_cache( + head_shuffling_ids.current.clone(), + cached_shuffling(committee_cache.clone()), + ); + cache.insert_committee_cache( + head_shuffling_ids.next.clone(), + cached_shuffling(committee_cache.clone()), + ); cache.insert_committee_cache( head_shuffling_ids.previous.clone().unwrap(), - &committee_cache, + cached_shuffling(committee_cache.clone()), ); // Insert a few entries for older epochs. @@ -559,7 +804,7 @@ mod test { shuffling_epoch: Epoch::from(i), shuffling_decision_block: Hash256::from_low_u64_be(i as u64), }; - cache.insert_committee_cache(shuffling_id, &committee_cache); + cache.insert_committee_cache(shuffling_id, cached_shuffling(committee_cache.clone())); } assert!( @@ -580,4 +825,41 @@ mod test { "should limit cache size" ); } + + /// Pre-Gloas state across the Gloas fork: epoch G-1 returns `Some(PreGloas)`, epoch G and + /// G+1 return `None` (the boundary skip). + #[test] + fn try_from_state_skips_at_gloas_boundary() { + create_test_tracing_subscriber(); + + let mut spec = ForkName::Fulu.make_genesis_spec(E::default_spec()); + let gloas_fork_epoch = Epoch::new(2); + spec.gloas_fork_epoch = Some(gloas_fork_epoch); + + let harness = BeaconChainHarness::builder(MinimalEthSpec) + .spec(Arc::new(spec.clone())) + .deterministic_keypairs(8) + .fresh_ephemeral_store() + .build(); + let state = harness.get_current_state(); + assert!(!state.fork_name_unchecked().gloas_enabled()); + + for (epoch, expect_pre_gloas) in [ + (gloas_fork_epoch - 1, true), + (gloas_fork_epoch, false), + (gloas_fork_epoch + 1, false), + ] { + let result = CachedPTCs::::try_from_state(&state, epoch, &spec) + .expect("must not error at the boundary"); + if expect_pre_gloas { + assert!( + matches!(result, Some(CachedPTCs::PreGloas)), + "epoch {}: expected Some(PreGloas)", + epoch + ); + } else { + assert!(result.is_none(), "epoch {}: expected None", epoch); + } + } + } } diff --git a/beacon_node/beacon_chain/src/state_advance_timer.rs b/beacon_node/beacon_chain/src/state_advance_timer.rs index cb916cb514..6408f861f8 100644 --- a/beacon_node/beacon_chain/src/state_advance_timer.rs +++ b/beacon_node/beacon_chain/src/state_advance_timer.rs @@ -15,7 +15,9 @@ //! 2. There's a possibility that the head block is never built upon, causing wasted CPU cycles. use crate::validator_monitor::HISTORIC_EPOCHS as VALIDATOR_MONITOR_HISTORIC_EPOCHS; use crate::{ - BeaconChain, BeaconChainError, BeaconChainTypes, chain_config::FORK_CHOICE_LOOKAHEAD_FACTOR, + BeaconChain, BeaconChainError, BeaconChainTypes, + chain_config::FORK_CHOICE_LOOKAHEAD_FACTOR, + shuffling_cache::{CachedPTCs, CachedShuffling}, }; use slot_clock::SlotClock; use state_processing::per_slot_processing; @@ -394,19 +396,30 @@ fn advance_head(beacon_chain: &Arc>) -> Resu .map_err(BeaconChainError::from)?; let committee_cache = state .committee_cache(RelativeEpoch::Next) - .map_err(BeaconChainError::from)?; - beacon_chain - .shuffling_cache - .write() - .insert_committee_cache(shuffling_id.clone(), committee_cache); + .map_err(BeaconChainError::from)? + .clone(); + let shuffling_epoch = RelativeEpoch::Next.into_epoch(state.current_epoch()); - debug!( - ?head_block_root, - next_epoch_shuffling_root = ?shuffling_id.shuffling_decision_block, - state_epoch = %state.current_epoch(), - current_epoch = %current_slot.epoch(T::EthSpec::slots_per_epoch()), - "Primed proposer and attester caches" - ); + if let Some(ptcs) = CachedPTCs::try_from_state(&state, shuffling_epoch, &beacon_chain.spec)? + { + beacon_chain.shuffling_cache.write().insert_committee_cache( + shuffling_id.clone(), + CachedShuffling::new(committee_cache, ptcs), + ); + + debug!( + ?head_block_root, + next_epoch_shuffling_root = ?shuffling_id.shuffling_decision_block, + state_epoch = %state.current_epoch(), + current_epoch = %current_slot.epoch(T::EthSpec::slots_per_epoch()), + "Primed proposer and attester caches" + ); + } else { + debug!( + %shuffling_epoch, + "Skipping priming of attester cache for Gloas boundary epoch" + ); + } } let final_slot = state.slot(); diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index 7e50f4e5ac..0ac77dcfaa 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -1209,7 +1209,8 @@ fn check_shuffling_compatible( .with_committee_cache( block_root, head_state.current_epoch(), - |committee_cache, _| { + |cached_shuffling, _| { + let committee_cache = cached_shuffling.committee_cache.as_ref(); let state_cache = head_state.committee_cache(RelativeEpoch::Current).unwrap(); // We used to check for false negatives here, but had to remove that check // because `shuffling_is_compatible` does not guarantee their absence. @@ -1247,7 +1248,8 @@ fn check_shuffling_compatible( .with_committee_cache( block_root, head_state.previous_epoch(), - |committee_cache, _| { + |cached_shuffling, _| { + let committee_cache = cached_shuffling.committee_cache.as_ref(); let state_cache = head_state.committee_cache(RelativeEpoch::Previous).unwrap(); if previous_epoch_shuffling_is_compatible { assert_eq!(committee_cache, state_cache.as_ref()); diff --git a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs index 3e8845f017..14cda1b483 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -4258,8 +4258,7 @@ impl NetworkBeaconProcessor { "payload_attn_invalid_sig", ); } - PayloadAttestationError::BeaconChainError(_) - | PayloadAttestationError::BeaconStateError(_) => { + PayloadAttestationError::BeaconChainError(_) => { debug!( %peer_id, %message_slot, 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 ef7109dd94..f56cb17554 100644 --- a/consensus/state_processing/src/per_block_processing/signature_sets.rs +++ b/consensus/state_processing/src/per_block_processing/signature_sets.rs @@ -363,6 +363,26 @@ pub fn indexed_payload_attestation_signature_set<'a, 'b, E, F>( indexed_payload_attestation: &'b IndexedPayloadAttestation, spec: &'a ChainSpec, ) -> Result> +where + E: EthSpec, + F: Fn(usize) -> Option>, +{ + indexed_payload_attestation_signature_set_from_pubkeys( + get_pubkey, + signature, + indexed_payload_attestation, + state.genesis_validators_root(), + spec, + ) +} + +pub fn indexed_payload_attestation_signature_set_from_pubkeys<'a, 'b, E, F>( + get_pubkey: F, + signature: &'a AggregateSignature, + indexed_payload_attestation: &'b IndexedPayloadAttestation, + genesis_validators_root: Hash256, + spec: &'a ChainSpec, +) -> Result> where E: EthSpec, F: Fn(usize) -> Option>, @@ -379,12 +399,7 @@ where .slot .epoch(E::slots_per_epoch()); let fork = spec.fork_at_epoch(epoch); - let domain = spec.get_domain( - epoch, - Domain::PTCAttester, - &fork, - state.genesis_validators_root(), - ); + let domain = spec.get_domain(epoch, Domain::PTCAttester, &fork, genesis_validators_root); let message = indexed_payload_attestation.data.signing_root(domain); From dfb259171a65cacd6db57b8874af8f543cabcb7a Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Sun, 24 May 2026 22:09:38 -0700 Subject: [PATCH 14/18] Ensure we can serve blocks and columns after `head` event is emitted (#9338) See related issue: https://github.com/ethpandaops/dora/pull/713 When LH emits a `head` event the block isn't written to disk yet. Some upstream consumers may expect that after a `head` event that the block should be queryable via the beacon api. This PR falls back to fetching the block from the early attester cache if it wasn't found in the store. This should ensure that a block is always queryable immediately after a `head` event is emitted. Additionally I noticed that when serving columns we always default to using the store. We already have `get_data_columns_checking_all_caches ` which tries the da cache, then the store and finally the early attester cache. Co-Authored-By: Eitan Seri-Levi Co-Authored-By: Michael Sproul --- beacon_node/http_api/src/block_id.rs | 236 ++++++++++++++++++++++++--- 1 file changed, 216 insertions(+), 20 deletions(-) diff --git a/beacon_node/http_api/src/block_id.rs b/beacon_node/http_api/src/block_id.rs index e6b1ed0879..ca980b96a4 100644 --- a/beacon_node/http_api/src/block_id.rs +++ b/beacon_node/http_api/src/block_id.rs @@ -129,6 +129,15 @@ impl BlockId { .is_finalized_block(root, block_slot) .map_err(warp_utils::reject::unhandled_error)?; Ok((*root, execution_optimistic, finalized)) + } else if chain.early_attester_cache.get_block(*root).is_some() { + // Fall back to the early attester cache for blocks that are in fork choice + // but haven't been written to disk yet. + let execution_optimistic = chain + .canonical_head + .fork_choice_read_lock() + .is_optimistic_or_invalid_block(root) + .unwrap_or(false); + Ok((*root, execution_optimistic, false)) } else { Err(warp_utils::reject::custom_not_found(format!( "beacon block with root {}", @@ -143,9 +152,18 @@ impl BlockId { root: &Hash256, chain: &BeaconChain, ) -> Result>, warp::Rejection> { - chain + if let Some(block) = chain .get_blinded_block(root) - .map_err(warp_utils::reject::unhandled_error) + .map_err(warp_utils::reject::unhandled_error)? + { + return Ok(Some(block)); + } + // Fall back to the early attester cache for blocks that are in fork choice + // but haven't been written to disk yet. + Ok(chain + .early_attester_cache + .get_block(*root) + .map(|b| b.clone_as_blinded())) } /// Return the `SignedBeaconBlock` identified by `self`. @@ -253,20 +271,20 @@ impl BlockId { } _ => { let (root, execution_optimistic, finalized) = self.root(chain)?; - chain + let block_opt = chain .get_block(&root) .await - .map_err(warp_utils::reject::unhandled_error) - .and_then(|block_opt| { - block_opt - .map(|block| (Arc::new(block), execution_optimistic, finalized)) - .ok_or_else(|| { - warp_utils::reject::custom_not_found(format!( - "beacon block with root {}", - root - )) - }) - }) + .map_err(warp_utils::reject::unhandled_error)?; + let block = block_opt + .map(Arc::new) + .or_else(|| chain.early_attester_cache.get_block(root)) + .ok_or_else(|| { + warp_utils::reject::custom_not_found(format!( + "beacon block with root {}", + root + )) + })?; + Ok((block, execution_optimistic, finalized)) } } } @@ -290,16 +308,20 @@ impl BlockId { } let data_column_sidecars = if let Some(indices) = query.indices { - indices - .iter() - .filter_map(|index| chain.get_data_column(&root, index, fork_name).transpose()) - .collect::, _>>() + chain + .get_data_columns_checking_all_caches(root, &indices) .map_err(warp_utils::reject::unhandled_error)? } else { chain - .get_data_columns(&root, fork_name) + .early_attester_cache + .get_data_columns(root) + .map(Ok) + .unwrap_or_else(|| { + chain + .get_data_columns(&root, fork_name) + .map(|opt| opt.unwrap_or_default()) + }) .map_err(warp_utils::reject::unhandled_error)? - .unwrap_or_default() }; let fork_name = block @@ -507,3 +529,177 @@ impl fmt::Display for BlockId { write!(f, "{}", self.0) } } + +#[cfg(test)] +mod tests { + use super::*; + use beacon_chain::{ + PayloadVerificationStatus, + block_verification_types::{AvailableBlockData, RangeSyncBlock}, + test_utils::{ + BeaconChainHarness, EphemeralHarnessType, fork_name_from_env, + generate_data_column_sidecars_from_block, + }, + }; + use std::time::Duration; + use types::MinimalEthSpec; + + type TestHarness = BeaconChainHarness>; + + fn harness() -> TestHarness { + BeaconChainHarness::builder(MinimalEthSpec) + .default_spec() + .deterministic_keypairs(8) + .fresh_ephemeral_store() + .mock_execution_layer() + .build() + } + + #[tokio::test] + async fn root_uses_early_attester_cache_for_unpersisted_block() { + let Some(fork_name) = fork_name_from_env().filter(|fork_name| fork_name.fulu_enabled()) + else { + return; + }; + let harness = harness(); + let chain = &harness.chain; + + harness.execution_block_generator().set_min_blob_count(1); + harness.advance_slot(); + + let (block_contents, post_state) = harness + .make_block(harness.get_current_state(), harness.get_current_slot()) + .await; + let (block, _) = block_contents; + let block_root = block.canonical_root(); + let block_fork_name = chain.spec.fork_name_at_epoch(block.epoch()); + + assert_eq!( + block_fork_name, fork_name, + "precondition: test block must be produced at {fork_name:?}" + ); + assert!( + block.num_expected_blobs() > 0, + "precondition: {fork_name:?} test block must have blobs that can be converted to data columns" + ); + + assert!( + !chain.store.block_exists(&block_root).unwrap(), + "precondition: test block must not be persisted" + ); + assert!( + chain.get_blinded_block(&block_root).unwrap().is_none(), + "precondition: test block must not be retrievable from the store" + ); + assert!( + chain + .get_data_columns(&block_root, block_fork_name) + .unwrap() + .is_none(), + "precondition: test data columns must not be retrievable from the store" + ); + assert!( + !chain.block_is_known_to_fork_choice(&block_root), + "precondition: test block must not be imported into fork choice yet" + ); + + let sampling_columns = chain.sampling_columns_for_epoch(block.epoch()); + let data_columns = generate_data_column_sidecars_from_block(&block, &chain.spec) + .into_iter() + .filter(|column| sampling_columns.contains(column.index())) + .collect::>(); + assert!( + !data_columns.is_empty(), + "precondition: {fork_name:?} test block must produce data columns" + ); + + let available_block = RangeSyncBlock::new( + block.clone(), + AvailableBlockData::new_with_data_columns(data_columns), + &chain.data_availability_checker, + chain.spec.clone(), + ) + .unwrap() + .into_available_block(); + + let current_slot = harness.get_current_slot(); + let cached_head = chain.canonical_head.cached_head(); + let canonical_head_proposer_index = chain + .canonical_head_proposer_index(current_slot, &cached_head) + .unwrap(); + + chain + .canonical_head + .fork_choice_write_lock() + .on_block( + current_slot, + block.message(), + block_root, + Duration::ZERO, + &post_state, + PayloadVerificationStatus::Verified, + canonical_head_proposer_index, + &chain.spec, + ) + .unwrap(); + + assert!( + chain.block_is_known_to_fork_choice(&block_root), + "precondition: test block must be imported into fork choice" + ); + assert!( + !chain.store.block_exists(&block_root).unwrap(), + "precondition: fork choice insertion must not persist the block" + ); + + let proto_block = chain + .canonical_head + .fork_choice_read_lock() + .get_block(&block_root) + .unwrap(); + + chain + .early_attester_cache + .add_head_block(block_root, &available_block, proto_block, &post_state) + .unwrap(); + + let cached_data_columns = chain + .early_attester_cache + .get_data_columns(block_root) + .expect("precondition: data columns must be cached"); + assert!( + !cached_data_columns.is_empty(), + "precondition: cached data columns must be non-empty" + ); + + assert_eq!( + BlockId(CoreBlockId::Root(block_root)).root(chain).unwrap(), + (block_root, false, false) + ); + + let (blinded_block, execution_optimistic, finalized) = + BlockId(CoreBlockId::Root(block_root)) + .blinded_block(chain) + .unwrap(); + assert_eq!(blinded_block.canonical_root(), block_root); + assert_eq!(blinded_block.slot(), block.slot()); + assert!(!execution_optimistic); + assert!(!finalized); + + let (data_columns, data_columns_fork_name, execution_optimistic, finalized) = + BlockId(CoreBlockId::Root(block_root)) + .get_data_columns(DataColumnIndicesQuery { indices: None }, chain) + .unwrap(); + assert_eq!(data_columns, cached_data_columns); + assert_eq!(data_columns_fork_name, fork_name); + assert!(!execution_optimistic); + assert!(!finalized); + + chain.early_attester_cache.clear(); + + assert!( + BlockId(CoreBlockId::Root(block_root)).root(chain).is_err(), + "root lookup should fail once the unpersisted block leaves the early attester cache" + ); + } +} From 5636030b49c5524e0192c0118b9cc988c2fea92a Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Thu, 28 May 2026 10:27:16 +1000 Subject: [PATCH 15/18] Add gossip validation spec tests for proposer/attester slashings (#9323) Addresses #9232 partially. This PR covers two topics only. * #9232 Wires up networking test vectors for `gossip_proposer_slashing` and `gossip_attester_slashing` topics. The tests also revealed minor spec non-compliance where invalid slashings were ignored rather than rejected. - Refactor `process_gossip_proposer_slashing` and `process_gossip_attester_slashing` to return `MessageAcceptance`, so it can be verified in the tests - Add `GossipValidation` test case, handler, and test entries - Spec compliance fix: distinguish between internal errors and validation error - return `Reject` when the slashing is invalid and only penalise on invalid messages Co-Authored-By: Jimmy Chen --- Cargo.lock | 2 + beacon_node/network/src/lib.rs | 1 + .../gossip_methods.rs | 134 +++++++----- .../src/network_beacon_processor/mod.rs | 37 +++- testing/ef_tests/Cargo.toml | 2 + testing/ef_tests/check_all_files_accessed.py | 11 +- testing/ef_tests/src/cases.rs | 2 + .../ef_tests/src/cases/gossip_validation.rs | 206 ++++++++++++++++++ testing/ef_tests/src/handler.rs | 30 +++ testing/ef_tests/tests/tests.rs | 12 + 10 files changed, 374 insertions(+), 63 deletions(-) create mode 100644 testing/ef_tests/src/cases/gossip_validation.rs diff --git a/Cargo.lock b/Cargo.lock index 129be32fcd..f246f2b353 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2855,8 +2855,10 @@ dependencies = [ "fs2", "hex", "kzg", + "lighthouse_network", "logging", "milhouse", + "network", "proto_array", "rayon", "serde", diff --git a/beacon_node/network/src/lib.rs b/beacon_node/network/src/lib.rs index 2a7fedb53e..dc45f53c70 100644 --- a/beacon_node/network/src/lib.rs +++ b/beacon_node/network/src/lib.rs @@ -11,6 +11,7 @@ mod subnet_service; mod sync; pub use lighthouse_network::NetworkConfig; +pub use network_beacon_processor::NetworkBeaconProcessor; pub use service::{ NetworkMessage, NetworkReceivers, NetworkSenders, NetworkService, ValidatorSubscriptionMessage, }; 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 14cda1b483..71216b47a7 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -174,6 +174,17 @@ impl FailedAtt { } } +/// `MessageAcceptance` doesn't implement clone so we do a manual match here. +/// TODO: remove this once `Clone` is available on this type: +/// https://github.com/libp2p/rust-libp2p/pull/6445 +fn clone_message_acceptance(a: &MessageAcceptance) -> MessageAcceptance { + match a { + MessageAcceptance::Accept => MessageAcceptance::Accept, + MessageAcceptance::Reject => MessageAcceptance::Reject, + MessageAcceptance::Ignore => MessageAcceptance::Ignore, + } +} + impl NetworkBeaconProcessor { /* Auxiliary functions */ @@ -2194,14 +2205,14 @@ impl NetworkBeaconProcessor { message_id: MessageId, peer_id: PeerId, proposer_slashing: ProposerSlashing, - ) { + ) -> MessageAcceptance { let validator_index = proposer_slashing.signed_header_1.message.proposer_index; - let slashing = match self + let (validation_result, verified_slashing_opt) = match self .chain .verify_proposer_slashing_for_gossip(proposer_slashing) { - Ok(ObservationOutcome::New(slashing)) => slashing, + Ok(ObservationOutcome::New(slashing)) => (MessageAcceptance::Accept, Some(slashing)), Ok(ObservationOutcome::AlreadyKnown) => { debug!( reason = "Already seen a proposer slashing for that validator", @@ -2209,44 +2220,54 @@ impl NetworkBeaconProcessor { peer = %peer_id, "Dropping proposer slashing" ); - self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); - return; + (MessageAcceptance::Ignore, None) } Err(e) => { - // This is likely a fault with the beacon chain and not necessarily a - // malicious message from the peer. debug!( validator_index, %peer_id, error = ?e, - "Dropping invalid proposer slashing" + "Dropping proposer slashing due to an error" ); - self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); - // Penalize peer slightly for invalids. - self.gossip_penalize_peer( - peer_id, - PeerAction::HighToleranceError, - "invalid_gossip_proposer_slashing", - ); - return; + if matches!(e, BeaconChainError::ProposerSlashingValidationError(_)) { + // Penalize peer slightly for invalids. + self.gossip_penalize_peer( + peer_id, + PeerAction::HighToleranceError, + "invalid_gossip_proposer_slashing", + ); + (MessageAcceptance::Reject, None) + } else { + // This is likely a fault with the beacon chain and not necessarily a + // malicious message from the peer. + (MessageAcceptance::Ignore, None) + } } }; - metrics::inc_counter(&metrics::BEACON_PROCESSOR_PROPOSER_SLASHING_VERIFIED_TOTAL); + self.propagate_validation_result( + message_id, + peer_id, + clone_message_acceptance(&validation_result), + ); - self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Accept); + if let Some(slashing) = verified_slashing_opt { + metrics::inc_counter(&metrics::BEACON_PROCESSOR_PROPOSER_SLASHING_VERIFIED_TOTAL); - // Register the slashing with any monitored validators. - self.chain - .validator_monitor - .read() - .register_gossip_proposer_slashing(slashing.as_inner()); + // Register the slashing with any monitored validators. + self.chain + .validator_monitor + .read() + .register_gossip_proposer_slashing(slashing.as_inner()); - self.chain.import_proposer_slashing(slashing); - debug!("Successfully imported proposer slashing"); + self.chain.import_proposer_slashing(slashing); + debug!("Successfully imported proposer slashing"); - metrics::inc_counter(&metrics::BEACON_PROCESSOR_PROPOSER_SLASHING_IMPORTED_TOTAL); + metrics::inc_counter(&metrics::BEACON_PROCESSOR_PROPOSER_SLASHING_IMPORTED_TOTAL); + } + + validation_result } pub fn process_gossip_attester_slashing( @@ -2254,51 +2275,64 @@ impl NetworkBeaconProcessor { message_id: MessageId, peer_id: PeerId, attester_slashing: AttesterSlashing, - ) { - let slashing = match self + ) -> MessageAcceptance { + let (validation_result, verified_slashing_opt) = match self .chain .verify_attester_slashing_for_gossip(attester_slashing) { - Ok(ObservationOutcome::New(slashing)) => slashing, + Ok(ObservationOutcome::New(slashing)) => (MessageAcceptance::Accept, Some(slashing)), Ok(ObservationOutcome::AlreadyKnown) => { debug!( reason = "Slashings already known for all slashed validators", peer = %peer_id, "Dropping attester slashing" ); - self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); - return; + (MessageAcceptance::Ignore, None) } Err(e) => { debug!( %peer_id, error = ?e, - "Dropping invalid attester slashing" + "Dropping attester slashing due to an error" ); - self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); - // Penalize peer slightly for invalids. - self.gossip_penalize_peer( - peer_id, - PeerAction::HighToleranceError, - "invalid_gossip_attester_slashing", - ); - return; + + if matches!(e, BeaconChainError::AttesterSlashingValidationError(_)) { + // Penalize peer slightly for invalids. + self.gossip_penalize_peer( + peer_id, + PeerAction::HighToleranceError, + "invalid_gossip_attester_slashing", + ); + (MessageAcceptance::Reject, None) + } else { + // This is likely a fault with the beacon chain and not necessarily a + // malicious message from the peer. + (MessageAcceptance::Ignore, None) + } } }; - metrics::inc_counter(&metrics::BEACON_PROCESSOR_ATTESTER_SLASHING_VERIFIED_TOTAL); + self.propagate_validation_result( + message_id, + peer_id, + clone_message_acceptance(&validation_result), + ); - self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Accept); + if let Some(slashing) = verified_slashing_opt { + metrics::inc_counter(&metrics::BEACON_PROCESSOR_ATTESTER_SLASHING_VERIFIED_TOTAL); - // Register the slashing with any monitored validators. - self.chain - .validator_monitor - .read() - .register_gossip_attester_slashing(slashing.as_inner().to_ref()); + // Register the slashing with any monitored validators. + self.chain + .validator_monitor + .read() + .register_gossip_attester_slashing(slashing.as_inner().to_ref()); - self.chain.import_attester_slashing(slashing); - debug!("Successfully imported attester slashing"); - metrics::inc_counter(&metrics::BEACON_PROCESSOR_ATTESTER_SLASHING_IMPORTED_TOTAL); + self.chain.import_attester_slashing(slashing); + debug!("Successfully imported attester slashing"); + metrics::inc_counter(&metrics::BEACON_PROCESSOR_ATTESTER_SLASHING_IMPORTED_TOTAL); + } + + validation_result } pub fn process_gossip_bls_to_execution_change( diff --git a/beacon_node/network/src/network_beacon_processor/mod.rs b/beacon_node/network/src/network_beacon_processor/mod.rs index 434f7ecc8b..bbaafec4ea 100644 --- a/beacon_node/network/src/network_beacon_processor/mod.rs +++ b/beacon_node/network/src/network_beacon_processor/mod.rs @@ -7,6 +7,7 @@ use beacon_chain::data_column_verification::{GossipDataColumnError, observe_goss use beacon_chain::fetch_blobs::{ EngineGetBlobsOutput, FetchEngineBlobError, fetch_and_process_engine_blobs, }; +use beacon_chain::test_utils::{BeaconChainHarness, EphemeralHarnessType}; use beacon_chain::{AvailabilityProcessingStatus, BeaconChain, BeaconChainTypes, BlockError}; use beacon_processor::{ BeaconProcessorSend, DuplicateCache, GossipAggregatePackage, GossipAttestationPackage, Work, @@ -20,7 +21,7 @@ use lighthouse_network::rpc::methods::{ }; use lighthouse_network::service::api_types::CustodyBackfillBatchId; use lighthouse_network::{ - Client, GossipTopic, MessageId, NetworkGlobals, PeerId, PubsubMessage, + Client, GossipTopic, MessageId, NetworkConfig, NetworkGlobals, PeerId, PubsubMessage, rpc::{BlocksByRangeRequest, BlocksByRootRequest, LightClientBootstrapRequest, StatusMessage}, }; use rand::prelude::SliceRandom; @@ -31,6 +32,10 @@ use task_executor::TaskExecutor; use tokio::sync::mpsc::{self, error::TrySendError}; use tracing::{debug, error, instrument, trace, warn}; use types::*; +use { + beacon_chain::builder::Witness, beacon_processor::BeaconProcessorChannels, + slot_clock::ManualSlotClock, store::MemoryStore, tokio::sync::mpsc::UnboundedSender, +}; pub use sync_methods::ChainSegmentProcessId; use types::data::FixedBlobSidecarList; @@ -353,7 +358,7 @@ impl NetworkBeaconProcessor { ) -> Result<(), Error> { let processor = self.clone(); let process_fn = move || { - processor.process_gossip_proposer_slashing(message_id, peer_id, *proposer_slashing) + processor.process_gossip_proposer_slashing(message_id, peer_id, *proposer_slashing); }; self.try_send(BeaconWorkEvent { @@ -420,7 +425,7 @@ impl NetworkBeaconProcessor { ) -> Result<(), Error> { let processor = self.clone(); let process_fn = move || { - processor.process_gossip_attester_slashing(message_id, peer_id, *attester_slashing) + processor.process_gossip_attester_slashing(message_id, peer_id, *attester_slashing); }; self.try_send(BeaconWorkEvent { @@ -1260,16 +1265,8 @@ impl NetworkBeaconProcessor { } } -#[cfg(test)] -use { - beacon_chain::builder::Witness, beacon_processor::BeaconProcessorChannels, - slot_clock::ManualSlotClock, store::MemoryStore, tokio::sync::mpsc::UnboundedSender, -}; - -#[cfg(test)] pub(crate) type TestBeaconChainType = Witness; -#[cfg(test)] impl NetworkBeaconProcessor> { // Instantiates a mostly non-functional version of `Self` and returns the // event receiver that would normally go to the beacon processor. This is @@ -1301,4 +1298,22 @@ impl NetworkBeaconProcessor> { (network_beacon_processor, beacon_processor_rx) } + + /// Constructs a mostly non-functional `NetworkBeaconProcessor` from a test harness, + /// suitable for directly calling gossip processing methods in tests. + pub fn null_from_harness(harness: &BeaconChainHarness>) -> Self { + let network_globals = NetworkGlobals::new_test_globals( + vec![], + Arc::new(NetworkConfig::default()), + harness.spec.clone(), + ); + + Self::null_for_testing( + Arc::new(network_globals), + mpsc::unbounded_channel().0, + harness.chain.clone(), + harness.runtime.task_executor.clone(), + ) + .0 + } } diff --git a/testing/ef_tests/Cargo.toml b/testing/ef_tests/Cargo.toml index ac51e827ad..bb7cba0b10 100644 --- a/testing/ef_tests/Cargo.toml +++ b/testing/ef_tests/Cargo.toml @@ -26,8 +26,10 @@ fork_choice = { workspace = true } fs2 = { workspace = true } hex = { workspace = true } kzg = { workspace = true } +lighthouse_network = { workspace = true } logging = { workspace = true } milhouse = { workspace = true } +network = { workspace = true } proto_array = { workspace = true } rayon = { workspace = true } serde = { workspace = true } diff --git a/testing/ef_tests/check_all_files_accessed.py b/testing/ef_tests/check_all_files_accessed.py index 53fb626e7e..723c5e7e9e 100755 --- a/testing/ef_tests/check_all_files_accessed.py +++ b/testing/ef_tests/check_all_files_accessed.py @@ -75,8 +75,15 @@ excluded_paths = [ "tests/.*/compute_challenge/.*", # We don't need these manifest files at the moment. "tests/.*/manifest.yaml", - # TODO: gossip condition tests not implemented yet - "tests/.*/.*/networking/.*", + # TODO: Remaining gossip validation topics not yet implemented + "tests/.*/.*/networking/gossip_beacon_block/.*", + "tests/.*/.*/networking/gossip_beacon_attestation/.*", + "tests/.*/.*/networking/gossip_beacon_aggregate_and_proof/.*", + "tests/.*/.*/networking/gossip_voluntary_exit/.*", + "tests/.*/.*/networking/gossip_bls_to_execution_change/.*", + "tests/.*/.*/networking/gossip_sync_committee_message/.*", + "tests/.*/.*/networking/gossip_sync_committee_contribution_and_proof/.*", + "tests/.*/.*/networking/gossip_blob_sidecar/.*", # TODO: fast confirmation rule not merged yet "tests/.*/.*/fast_confirmation", ] diff --git a/testing/ef_tests/src/cases.rs b/testing/ef_tests/src/cases.rs index b2e0276353..b2386f6fa5 100644 --- a/testing/ef_tests/src/cases.rs +++ b/testing/ef_tests/src/cases.rs @@ -20,6 +20,7 @@ mod fork_choice; mod genesis_initialization; mod genesis_validity; mod get_custody_groups; +mod gossip_validation; mod kzg_blob_to_kzg_commitment; mod kzg_compute_blob_kzg_proof; mod kzg_compute_cells; @@ -57,6 +58,7 @@ pub use fork::ForkTest; pub use genesis_initialization::*; pub use genesis_validity::*; pub use get_custody_groups::*; +pub use gossip_validation::*; pub use kzg_blob_to_kzg_commitment::*; pub use kzg_compute_blob_kzg_proof::*; pub use kzg_compute_cells::*; diff --git a/testing/ef_tests/src/cases/gossip_validation.rs b/testing/ef_tests/src/cases/gossip_validation.rs new file mode 100644 index 0000000000..3dbbcae5a7 --- /dev/null +++ b/testing/ef_tests/src/cases/gossip_validation.rs @@ -0,0 +1,206 @@ +use super::*; +use crate::bls_setting::BlsSetting; +use crate::decode::{ssz_decode_file, ssz_decode_state, yaml_decode_file}; +use crate::type_name::TypeName; +use beacon_chain::test_utils::{BeaconChainHarness, EphemeralHarnessType}; +use lighthouse_network::{MessageAcceptance, MessageId, PeerId}; +use network::NetworkBeaconProcessor; +use serde::Deserialize; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use types::{AttesterSlashing, BeaconState, EthSpec, ForkName, ProposerSlashing}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "snake_case")] +enum ExpectedOutcome { + Valid, + Ignore, + Reject, +} + +impl PartialEq for ExpectedOutcome { + fn eq(&self, other: &MessageAcceptance) -> bool { + matches!( + (self, other), + (Self::Valid, MessageAcceptance::Accept) + | (Self::Ignore, MessageAcceptance::Ignore) + | (Self::Reject, MessageAcceptance::Reject) + ) + } +} + +#[derive(Debug, Clone, Deserialize)] +struct Meta { + topic: Topic, + #[serde(default)] + messages: Vec, + #[serde(default)] + bls_setting: Option, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(deny_unknown_fields)] +struct MessageMeta { + message: String, + expected: ExpectedOutcome, + #[serde(default)] + reason: Option, + #[serde(default)] + #[allow(dead_code)] + subnet_id: Option, + #[serde(default)] + #[allow(dead_code)] + offset_ms: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "snake_case")] +enum Topic { + ProposerSlashing, + AttesterSlashing, + // TODO: add support for these topics + // VoluntaryExit, + // BlsToExecutionChange, + // SyncCommittee, + // SyncCommitteeContributionAndProof, + // BeaconBlock, + // BeaconAttestation, + // BeaconAggregateAndProof, +} + +#[derive(Debug)] +pub struct GossipValidation { + path: PathBuf, + meta: Meta, + state: BeaconState, +} + +impl LoadCase for GossipValidation { + fn load_from_dir(path: &Path, fork_name: ForkName) -> Result { + let meta: Meta = yaml_decode_file(&path.join("meta.yaml"))?; + let spec = &testing_spec::(fork_name); + let state = ssz_decode_state(&path.join("state.ssz_snappy"), spec)?; + + Ok(Self { + path: path.to_path_buf(), + meta, + state, + }) + } +} + +impl Case for GossipValidation { + fn description(&self) -> String { + self.path + .iter() + .next_back() + .map(|s| s.to_string_lossy().to_string()) + .unwrap_or_default() + } + + fn result(&self, _case_index: usize, fork_name: ForkName) -> Result<(), Error> { + if let Some(bls_setting) = self.meta.bls_setting { + bls_setting.check()?; + } + + let spec = testing_spec::(fork_name); + let tester = GossipTester::new(self, spec)?; + + for message_meta in &self.meta.messages { + let actual = + tester.validate_message(&self.path, &self.meta.topic, message_meta, fork_name)?; + + if message_meta.expected != actual { + return Err(Error::NotEqual(format!( + "{}: expected {:?}, got {:?}{}", + self.path.display(), + message_meta.expected, + actual, + message_meta + .reason + .as_ref() + .map(|r| format!(" ({r})")) + .unwrap_or_default() + ))); + } + } + + Ok(()) + } +} + +struct GossipTester { + network_beacon_processor: Arc>>, +} + +impl GossipTester { + fn new(case: &GossipValidation, spec: ChainSpec) -> Result { + let genesis_time = case.state.genesis_time(); + let spec = Arc::new(spec); + + let harness = BeaconChainHarness::>::builder(E::default()) + .spec(spec.clone()) + .keypairs(vec![]) + .genesis_state_ephemeral_store(case.state.clone()) + .mock_execution_layer() + .recalculate_fork_times_with_genesis(genesis_time) + .mock_execution_layer_all_payloads_valid() + .build(); + + let network_beacon_processor = NetworkBeaconProcessor::null_from_harness(&harness); + + Ok(Self { + network_beacon_processor: Arc::new(network_beacon_processor), + }) + } + + fn validate_message( + &self, + path: &Path, + topic: &Topic, + message_meta: &MessageMeta, + fork_name: ForkName, + ) -> Result { + match topic { + Topic::ProposerSlashing => self.validate_proposer_slashing(path, message_meta), + Topic::AttesterSlashing => { + self.validate_attester_slashing(path, message_meta, fork_name) + } + } + } + + fn validate_proposer_slashing( + &self, + path: &Path, + message_meta: &MessageMeta, + ) -> Result { + let slashing: ProposerSlashing = + ssz_decode_file(&path.join(format!("{}.ssz_snappy", message_meta.message)))?; + + let message_id = MessageId::new(&[]); + let peer_id = PeerId::random(); + Ok(self + .network_beacon_processor + .process_gossip_proposer_slashing(message_id, peer_id, slashing)) + } + + fn validate_attester_slashing( + &self, + path: &Path, + message_meta: &MessageMeta, + fork_name: ForkName, + ) -> Result { + let ssz_path = path.join(format!("{}.ssz_snappy", message_meta.message)); + let slashing: AttesterSlashing = if fork_name.electra_enabled() { + ssz_decode_file(&ssz_path).map(AttesterSlashing::Electra)? + } else { + ssz_decode_file(&ssz_path).map(AttesterSlashing::Base)? + }; + + let message_id = MessageId::new(&[]); + let peer_id = PeerId::random(); + Ok(self + .network_beacon_processor + .process_gossip_attester_slashing(message_id, peer_id, slashing)) + } +} diff --git a/testing/ef_tests/src/handler.rs b/testing/ef_tests/src/handler.rs index 52cc5d57ae..df1ece49dd 100644 --- a/testing/ef_tests/src/handler.rs +++ b/testing/ef_tests/src/handler.rs @@ -979,6 +979,36 @@ impl Handler for ComputeColumnsForCustodyGroupHandler } } +pub struct GossipValidationHandler { + handler_name: &'static str, + _phantom: PhantomData, +} + +impl GossipValidationHandler { + pub const fn new(handler_name: &'static str) -> Self { + Self { + handler_name, + _phantom: PhantomData, + } + } +} + +impl Handler for GossipValidationHandler { + type Case = cases::GossipValidation; + + fn config_name() -> &'static str { + E::name() + } + + fn runner_name() -> &'static str { + "networking" + } + + fn handler_name(&self) -> String { + self.handler_name.into() + } +} + #[derive(Educe)] #[educe(Default)] pub struct KZGComputeCellsHandler(PhantomData); diff --git a/testing/ef_tests/tests/tests.rs b/testing/ef_tests/tests/tests.rs index 0ff854bd21..6e1c4fdc10 100644 --- a/testing/ef_tests/tests/tests.rs +++ b/testing/ef_tests/tests/tests.rs @@ -1189,3 +1189,15 @@ fn compute_columns_for_custody_group() { ComputeColumnsForCustodyGroupHandler::::default().run(); ComputeColumnsForCustodyGroupHandler::::default().run(); } + +#[test] +fn gossip_proposer_slashing() { + GossipValidationHandler::::new("gossip_proposer_slashing").run(); + GossipValidationHandler::::new("gossip_proposer_slashing").run(); +} + +#[test] +fn gossip_attester_slashing() { + GossipValidationHandler::::new("gossip_attester_slashing").run(); + GossipValidationHandler::::new("gossip_attester_slashing").run(); +} From ba3abf943fbc06c11b4f4f021978b691b4712065 Mon Sep 17 00:00:00 2001 From: Pawan Dhananjay Date: Fri, 29 May 2026 05:47:53 +0530 Subject: [PATCH 16/18] Rust 1.96 lints (#9368) N/A A rare single line lint update . Co-Authored-By: Pawan Dhananjay --- consensus/types/src/data/partial_data_column_sidecar.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/consensus/types/src/data/partial_data_column_sidecar.rs b/consensus/types/src/data/partial_data_column_sidecar.rs index c0e713b4b8..e70901d76e 100644 --- a/consensus/types/src/data/partial_data_column_sidecar.rs +++ b/consensus/types/src/data/partial_data_column_sidecar.rs @@ -69,7 +69,7 @@ impl PartialDataColumnSidecar { .count(); self.column .get(storage_idx) - .and_then(|cell| self.kzg_proofs.get(storage_idx).map(|proof| (cell, proof))) + .zip(self.kzg_proofs.get(storage_idx)) } /// Creates a reference to this sidecar containing only the blob indices for which the passed From 8396dc87d092609b231b4db2afb5f9c49639cbd4 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Thu, 28 May 2026 19:59:23 -0700 Subject: [PATCH 17/18] Deprecate gossip blobs (#9126) #9124 Deprecate unneeded pre-Fulu blob features - blob gossip - blob lookup sync - engine getBlobsV1 Also deprecates some tests and cleans up production code paths I think this is blocked until gnosis forks to fulu? Co-Authored-By: Eitan Seri-Levi Co-Authored-By: Eitan Seri- Levi Co-Authored-By: dapplion <35266934+dapplion@users.noreply.github.com> Co-Authored-By: Pawan Dhananjay Co-Authored-By: Michael Sproul Co-Authored-By: Daniel Knopik Co-Authored-By: Michael Sproul --- .github/workflows/test-suite.yml | 2 +- Cargo.lock | 1 + Makefile | 4 +- beacon_node/beacon_chain/src/beacon_chain.rs | 197 ++----- .../beacon_chain/src/blob_verification.rs | 482 +---------------- .../beacon_chain/src/block_verification.rs | 37 -- beacon_node/beacon_chain/src/builder.rs | 1 - .../beacon_chain/src/canonical_head.rs | 7 - .../src/data_availability_checker.rs | 22 +- .../overflow_lru_cache.rs | 142 +++-- .../fetch_blobs/fetch_blobs_beacon_adapter.rs | 40 +- .../beacon_chain/src/fetch_blobs/mod.rs | 160 +----- .../beacon_chain/src/fetch_blobs/tests.rs | 291 +---------- beacon_node/beacon_chain/src/test_utils.rs | 71 +-- .../beacon_chain/tests/block_verification.rs | 25 +- beacon_node/beacon_chain/tests/events.rs | 41 -- beacon_node/beacon_processor/src/lib.rs | 10 - .../src/scheduler/work_queue.rs | 7 +- beacon_node/execution_layer/src/engine_api.rs | 15 +- .../execution_layer/src/engine_api/http.rs | 17 - beacon_node/execution_layer/src/lib.rs | 19 +- .../test_utils/execution_block_generator.rs | 14 +- .../src/test_utils/handle_rpc.rs | 14 - .../execution_layer/src/test_utils/mod.rs | 1 - beacon_node/http_api/src/block_id.rs | 18 +- beacon_node/http_api/src/publish_blocks.rs | 129 +---- .../tests/broadcast_validation_tests.rs | 56 +- beacon_node/http_api/tests/tests.rs | 8 +- .../src/service/gossip_cache.rs | 7 - .../lighthouse_network/src/service/utils.rs | 4 - .../lighthouse_network/src/types/pubsub.rs | 40 +- .../lighthouse_network/src/types/topics.rs | 30 -- beacon_node/network/src/metrics.rs | 30 -- .../gossip_methods.rs | 491 ++++-------------- .../src/network_beacon_processor/mod.rs | 138 +---- .../src/network_beacon_processor/tests.rs | 38 -- beacon_node/network/src/router.rs | 13 - .../network/src/sync/block_lookups/mod.rs | 11 +- .../sync/block_lookups/single_block_lookup.rs | 4 +- beacon_node/network/src/sync/manager.rs | 21 - .../requests/blobs_by_range.rs | 1 + .../network_context/requests/blobs_by_root.rs | 2 + beacon_node/network/src/sync/tests/lookups.rs | 59 +-- testing/ef_tests/src/cases/fork_choice.rs | 72 +-- testing/simulator/Cargo.toml | 1 + testing/simulator/src/basic_sim.rs | 12 +- testing/simulator/src/fallback_sim.rs | 15 +- testing/simulator/src/local_network.rs | 11 + 48 files changed, 485 insertions(+), 2346 deletions(-) diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index 1d66bd30e7..3db4804bd1 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -304,7 +304,7 @@ jobs: cache-target: release - name: Create log dir run: mkdir ${{ runner.temp }}/basic_simulator_logs - - name: Run a basic beacon chain sim that starts from Deneb + - name: Run a basic beacon chain sim run: cargo run --release --bin simulator basic-sim --disable-stdout-logging --log-dir ${{ runner.temp }}/basic_simulator_logs - name: Upload logs if: always() diff --git a/Cargo.lock b/Cargo.lock index f246f2b353..a9fdfe70bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8202,6 +8202,7 @@ dependencies = [ name = "simulator" version = "0.2.0" dependencies = [ + "beacon_chain", "clap", "environment", "execution_layer", diff --git a/Makefile b/Makefile index dd57bb038e..3c00883ce9 100644 --- a/Makefile +++ b/Makefile @@ -33,11 +33,11 @@ PROFILE ?= release # List of all hard forks up to gloas. This list is used to set env variables for several tests so that # they run for different forks. # TODO(EIP-7732) Remove this once we extend network tests to support gloas and use RECENT_FORKS instead -RECENT_FORKS_BEFORE_GLOAS=electra fulu +RECENT_FORKS_BEFORE_GLOAS=fulu # List of all recent hard forks. This list is used to set env variables for http_api tests # Include phase0 to test the code paths in sync that are pre blobs -RECENT_FORKS=electra fulu gloas +RECENT_FORKS=fulu gloas # For network tests include phase0 to cover genesis syncing (blocks without blobs or columns) TEST_NETWORK_FORKS=phase0 $(RECENT_FORKS_BEFORE_GLOAS) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index b3d258a2fb..d826895a25 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -5,7 +5,6 @@ use crate::attestation_verification::{ }; use crate::beacon_block_streamer::{BeaconBlockStreamer, CheckCaches}; use crate::beacon_proposer_cache::{BeaconProposerCache, EpochBlockProposers}; -use crate::blob_verification::{GossipBlobError, GossipVerifiedBlob}; use crate::block_times_cache::BlockTimesCache; use crate::block_verification::{ BlockError, ExecutionPendingBlock, GossipVerifiedBlock, IntoExecutionPendingBlock, @@ -26,16 +25,15 @@ use crate::data_availability_checker::{ use crate::data_availability_checker::DataAvailabilityChecker; use crate::data_column_verification::{ GossipDataColumnError, GossipPartialDataColumnError, GossipVerifiedDataColumn, - GossipVerifiedPartialDataColumnHeader, KzgVerifiedCustodyPartialDataColumn, - KzgVerifiedPartialDataColumn, PartialColumnVerificationResult, - validate_partial_data_column_sidecar_for_gossip, + GossipVerifiedPartialDataColumnHeader, KzgVerifiedCustodyDataColumn, + KzgVerifiedCustodyPartialDataColumn, KzgVerifiedPartialDataColumn, + PartialColumnVerificationResult, validate_partial_data_column_sidecar_for_gossip, }; use crate::early_attester_cache::EarlyAttesterCache; use crate::envelope_times_cache::EnvelopeTimesCache; use crate::errors::{BeaconChainError as Error, BlockProductionError}; use crate::events::ServerSentEventHandler; use crate::execution_payload::{NotifyExecutionLayer, PreparePayloadHandle, get_execution_payload}; -use crate::fetch_blobs::EngineGetBlobsOutput; use crate::fork_choice_signal::{ForkChoiceSignalRx, ForkChoiceSignalTx}; use crate::graffiti_calculator::{GraffitiCalculator, GraffitiSettings}; use crate::light_client_finality_update_verification::{ @@ -431,8 +429,6 @@ pub struct BeaconChain { pub(crate) observed_payload_attesters: RwLock>, /// Maintains a record of which validators have proposed blocks for each slot. pub observed_block_producers: RwLock>, - /// Maintains a record of blob sidecars seen over the gossip network. - pub observed_blob_sidecars: RwLock, T::EthSpec>>, /// Maintains a record of column sidecars seen over the gossip network. pub observed_column_sidecars: RwLock, T::EthSpec>>, @@ -2453,19 +2449,6 @@ impl BeaconChain { ret } - #[instrument(skip_all, level = "trace")] - pub fn verify_blob_sidecar_for_gossip( - self: &Arc, - blob_sidecar: Arc>, - subnet_id: u64, - ) -> Result, GossipBlobError> { - metrics::inc_counter(&metrics::BLOBS_SIDECAR_PROCESSING_REQUESTS); - let _timer = metrics::start_timer(&metrics::BLOBS_SIDECAR_GOSSIP_VERIFICATION_TIMES); - GossipVerifiedBlob::new(blob_sidecar, subnet_id, self).inspect(|_| { - metrics::inc_counter(&metrics::BLOBS_SIDECAR_PROCESSING_SUCCESSES); - }) - } - /// Accepts some 'LightClientOptimisticUpdate' from the network and attempts to verify it pub fn verify_optimistic_update_for_gossip( self: &Arc, @@ -3253,35 +3236,6 @@ impl BeaconChain { .map_err(BeaconChainError::TokioJoin)? } - /// Cache the blob in the processing cache, process it, then evict it from the cache if it was - /// imported or errors. - #[instrument(skip_all, level = "debug")] - pub async fn process_gossip_blob( - self: &Arc, - blob: GossipVerifiedBlob, - ) -> Result { - let block_root = blob.block_root(); - - // 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(blob.block_root())); - } - - // No need to process and import blobs beyond the PeerDAS epoch. - if self.spec.is_peer_das_enabled_for_epoch(blob.epoch()) { - return Err(BlockError::BlobNotRequired(blob.slot())); - } - - self.emit_sse_blob_sidecar_events(&block_root, std::iter::once(blob.as_blob())); - - self.check_gossip_blob_availability_and_import(blob).await - } - /// Cache the data columns in the processing cache, process it, then evict it from the cache if it was /// imported or errors. /// Only accepts full columns. Partials are handled via PartialDataColumnAssembler. @@ -3428,19 +3382,21 @@ impl BeaconChain { return Err(BlockError::DuplicateFullyImported(block_root)); } - // Reject RPC blobs referencing unknown parents. Otherwise we allow potentially invalid data - // into the da_checker, where invalid = descendant of invalid blocks. - // Note: blobs should have at least one item and all items have the same parent root. - if let Some(parent_root) = blobs - .iter() - .filter_map(|b| b.as_ref().map(|b| b.block_parent_root())) - .next() - && !self - .canonical_head - .fork_choice_read_lock() - .contains_block(&parent_root) - { - return Err(BlockError::ParentUnknown { parent_root }); + for blob in &blobs { + if let Some(blob) = blob.as_ref() { + // Reject RPC blobs referencing unknown parents. Otherwise we allow potentially invalid data + // into the da_checker, where invalid = descendant of invalid blocks. + // Note: blobs should have at least one item and all items have the same parent root. + if !self + .canonical_head + .fork_choice_read_lock() + .contains_block(&blob.block_parent_root()) + { + return Err(BlockError::ParentUnknown { + parent_root: blob.block_parent_root(), + }); + } + } } self.emit_sse_blob_sidecar_events(&block_root, blobs.iter().flatten().map(Arc::as_ref)); @@ -3454,7 +3410,7 @@ impl BeaconChain { self: &Arc, slot: Slot, block_root: Hash256, - engine_get_blobs_output: EngineGetBlobsOutput, + engine_get_blobs_output: Vec>, ) -> Result { // If this block has already been imported to forkchoice it must have been available, so // we don't need to process its blobs again. @@ -3466,17 +3422,12 @@ impl BeaconChain { return Err(BlockError::DuplicateFullyImported(block_root)); } - match &engine_get_blobs_output { - EngineGetBlobsOutput::Blobs(blobs) => { - self.emit_sse_blob_sidecar_events(&block_root, blobs.iter().map(|b| b.as_blob())); - } - EngineGetBlobsOutput::CustodyColumns(columns) => { - self.emit_sse_data_column_sidecar_events( - &block_root, - columns.iter().map(|column| column.as_data_column()), - ); - } - } + self.emit_sse_data_column_sidecar_events( + &block_root, + engine_get_blobs_output + .iter() + .map(|column| column.as_data_column()), + ); self.check_engine_blobs_availability_and_import(slot, block_root, engine_get_blobs_output) .await @@ -3915,24 +3866,6 @@ impl BeaconChain { .await } - /// Checks if the provided blob can make any cached blocks available, and imports immediately - /// if so, otherwise caches the blob in the data availability checker. - async fn check_gossip_blob_availability_and_import( - self: &Arc, - blob: GossipVerifiedBlob, - ) -> Result { - let slot = blob.slot(); - if let Some(slasher) = self.slasher.as_ref() { - slasher.accept_block_header(blob.signed_block_header()); - } - let availability = self - .data_availability_checker - .put_gossip_verified_blobs(blob.block_root(), std::iter::once(blob))?; - - self.process_availability(slot, availability, || Ok(())) - .await - } - /// Checks if the provided data column can make any cached blocks available, and imports immediately /// if so, otherwise caches the data column in the data availability checker. /// Check gossip data columns for availability and import. Only accepts full columns. @@ -4015,7 +3948,7 @@ impl BeaconChain { ) -> Result { self.check_blob_header_signature_and_slashability( block_root, - blobs.iter().flatten().map(Arc::as_ref), + blobs.iter().flatten().map(|b| b.as_ref()), )?; let availability = self .data_availability_checker @@ -4030,56 +3963,36 @@ impl BeaconChain { self: &Arc, slot: Slot, block_root: Hash256, - engine_get_blobs_output: EngineGetBlobsOutput, + engine_get_blobs_output: Vec>, ) -> Result { - match engine_get_blobs_output { - EngineGetBlobsOutput::Blobs(blobs) => { - self.check_blob_header_signature_and_slashability( - block_root, - blobs.iter().map(|b| b.as_blob()), - )?; - let availability = self - .data_availability_checker - .put_kzg_verified_blobs(block_root, blobs) - .map_err(BlockError::from)?; - - Ok(self - .process_availability(slot, availability, || Ok(())) - .await?) - } - EngineGetBlobsOutput::CustodyColumns(data_columns) => { - // TODO(gloas) verify that this check is no longer relevant for gloas - self.check_data_column_sidecar_header_signature_and_slashability( - block_root, - data_columns - .iter() - .filter_map(|c| match c.as_data_column() { - DataColumnSidecar::Fulu(column) => Some(column), - _ => None, - }), - )?; - if self - .spec - .fork_name_at_slot::(slot) - .gloas_enabled() - { - let availability = self - .pending_payload_cache - .put_kzg_verified_custody_data_columns(block_root, &data_columns) - .map_err(BlockError::from)?; - Ok(self - .process_payload_envelope_availability(slot, availability, || Ok(())) - .await?) - } else { - let availability = self - .data_availability_checker - .put_kzg_verified_custody_data_columns(block_root, data_columns) - .map_err(BlockError::from)?; - Ok(self - .process_availability(slot, availability, || Ok(())) - .await?) - } - } + // TODO(gloas) verify that this check is no longer relevant for gloas + self.check_data_column_sidecar_header_signature_and_slashability( + block_root, + engine_get_blobs_output + .iter() + .filter_map(|c| match c.as_data_column() { + DataColumnSidecar::Fulu(column) => Some(column), + _ => None, + }), + )?; + if self + .spec + .fork_name_at_slot::(slot) + .gloas_enabled() + { + let availability = self + .pending_payload_cache + .put_kzg_verified_custody_data_columns(block_root, &engine_get_blobs_output) + .map_err(BlockError::from)?; + self.process_payload_envelope_availability(slot, availability, || Ok(())) + .await + } else { + let availability = self + .data_availability_checker + .put_kzg_verified_custody_data_columns(block_root, engine_get_blobs_output) + .map_err(BlockError::from)?; + self.process_availability(slot, availability, || Ok(())) + .await } } diff --git a/beacon_node/beacon_chain/src/blob_verification.rs b/beacon_node/beacon_chain/src/blob_verification.rs index e557a24369..79b2969645 100644 --- a/beacon_node/beacon_chain/src/blob_verification.rs +++ b/beacon_node/beacon_chain/src/blob_verification.rs @@ -1,249 +1,11 @@ -use educe::Educe; -use slot_clock::SlotClock; -use std::marker::PhantomData; -use std::sync::Arc; - -use crate::beacon_chain::{BeaconChain, BeaconChainTypes}; -use crate::block_verification::{ - BlockSlashInfo, get_validator_pubkey_cache, process_block_slash_info, -}; use crate::kzg_utils::{validate_blob, validate_blobs}; -use crate::observed_data_sidecars::{ - Error as ObservedDataSidecarsError, ObservationStrategy, Observe, -}; -use crate::{BeaconChainError, metrics}; +use educe::Educe; use kzg::{Error as KzgError, Kzg, KzgCommitment}; use ssz_derive::{Decode, Encode}; +use std::sync::Arc; use std::time::Duration; -use tracing::{debug, instrument}; -use tree_hash::TreeHash; -use types::data::BlobIdentifier; -use types::{ - BeaconStateError, BlobSidecar, Epoch, EthSpec, Hash256, SignedBeaconBlockHeader, Slot, -}; - -/// An error occurred while validating a gossip blob. -#[derive(Debug)] -pub enum GossipBlobError { - /// The blob sidecar is from a slot that is later than the current slot (with respect to the - /// gossip clock disparity). - /// - /// ## Peer scoring - /// - /// Assuming the local clock is correct, the peer has sent an invalid message. - FutureSlot { - message_slot: Slot, - latest_permissible_slot: Slot, - }, - - /// There was an error whilst processing the blob. It is not known if it is - /// valid or invalid. - /// - /// ## Peer scoring - /// - /// We were unable to process this blob due to an internal error. It's - /// unclear if the blob is valid. - BeaconChainError(Box), - - /// The `BlobSidecar` was gossiped over an incorrect subnet. - /// - /// ## Peer scoring - /// - /// The blob is invalid or the peer is faulty. - InvalidSubnet { expected: u64, received: u64 }, - - /// The sidecar corresponds to a slot older than the finalized head slot. - /// - /// ## Peer scoring - /// - /// It's unclear if this blob is valid, but this blob is for a finalized slot and is - /// therefore useless to us. - PastFinalizedSlot { - blob_slot: Slot, - finalized_slot: Slot, - }, - - /// The proposer index specified in the sidecar does not match the locally computed - /// proposer index. - /// - /// ## Peer scoring - /// - /// The blob is invalid and the peer is faulty. - ProposerIndexMismatch { sidecar: usize, local: usize }, - - /// The proposal signature in invalid. - /// - /// ## Peer scoring - /// - /// The blob is invalid and the peer is faulty. - ProposalSignatureInvalid, - - /// The proposal_index corresponding to blob.beacon_block_root is not known. - /// - /// ## Peer scoring - /// - /// The blob is invalid and the peer is faulty. - UnknownValidator(u64), - - /// The provided blob is not from a later slot than its parent. - /// - /// ## Peer scoring - /// - /// The blob is invalid and the peer is faulty. - BlobIsNotLaterThanParent { blob_slot: Slot, parent_slot: Slot }, - - /// The provided blob's parent block is unknown. - /// - /// ## Peer scoring - /// - /// We cannot process the blob without validating its parent, the peer isn't necessarily faulty. - ParentUnknown { parent_root: Hash256 }, - - /// Invalid kzg commitment inclusion proof - /// ## Peer scoring - /// - /// The blob sidecar is invalid and the peer is faulty - InvalidInclusionProof, - - /// A blob has already been seen for the given `(sidecar.block_root, sidecar.index)` tuple - /// over gossip or no gossip sources. - /// - /// ## Peer scoring - /// - /// The peer isn't faulty, but we do not forward it over gossip. - RepeatBlob { - proposer: u64, - slot: Slot, - index: u64, - }, - - /// The kzg verification failed. - /// - /// ## Peer scoring - /// - /// The blob sidecar is invalid and the peer is faulty. - KzgError(kzg::Error), - - /// The pubkey cache timed out. - /// - /// ## Peer scoring - /// - /// The blob sidecar may be valid, this is an internal error. - PubkeyCacheTimeout, - - /// The block conflicts with finalization, no need to propagate. - /// - /// ## Peer scoring - /// - /// It's unclear if this block is valid, but it conflicts with finality and shouldn't be - /// imported. - NotFinalizedDescendant { block_parent_root: Hash256 }, -} - -impl std::fmt::Display for GossipBlobError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?}", self) - } -} - -impl From for GossipBlobError { - fn from(e: BeaconChainError) -> Self { - GossipBlobError::BeaconChainError(e.into()) - } -} - -impl From for GossipBlobError { - fn from(e: BeaconStateError) -> Self { - GossipBlobError::BeaconChainError(BeaconChainError::BeaconStateError(e).into()) - } -} - -/// A wrapper around a `BlobSidecar` that indicates it has been approved for re-gossiping on -/// the p2p network. -#[derive(Debug)] -pub struct GossipVerifiedBlob { - block_root: Hash256, - blob: KzgVerifiedBlob, - _phantom: PhantomData, -} - -impl Clone for GossipVerifiedBlob { - fn clone(&self) -> Self { - Self { - block_root: self.block_root, - blob: self.blob.clone(), - _phantom: PhantomData, - } - } -} - -impl GossipVerifiedBlob { - pub fn new( - blob: Arc>, - subnet_id: u64, - chain: &BeaconChain, - ) -> Result { - 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| { - process_block_slash_info::<_, GossipBlobError>( - chain, - BlockSlashInfo::from_early_error_blob(header, e), - ) - }) - } - /// Construct a `GossipVerifiedBlob` that is assumed to be valid. - /// - /// This should ONLY be used for testing. - pub fn __assumed_valid(blob: Arc>) -> Self { - Self { - block_root: blob.block_root(), - blob: KzgVerifiedBlob { - blob, - seen_timestamp: Duration::from_secs(0), - }, - _phantom: PhantomData, - } - } - pub fn id(&self) -> BlobIdentifier { - BlobIdentifier { - block_root: self.block_root, - index: self.blob.blob_index(), - } - } - pub fn block_root(&self) -> Hash256 { - self.block_root - } - pub fn slot(&self) -> Slot { - self.blob.blob.slot() - } - pub fn epoch(&self) -> Epoch { - self.blob.blob.epoch() - } - pub fn index(&self) -> u64 { - self.blob.blob.index - } - pub fn kzg_commitment(&self) -> KzgCommitment { - self.blob.blob.kzg_commitment - } - pub fn signed_block_header(&self) -> SignedBeaconBlockHeader { - self.blob.blob.signed_block_header.clone() - } - pub fn block_proposer_index(&self) -> u64 { - self.blob.blob.block_proposer_index() - } - pub fn into_inner(self) -> KzgVerifiedBlob { - self.blob - } - pub fn as_blob(&self) -> &BlobSidecar { - self.blob.as_blob() - } - /// This is cheap as we're calling clone on an Arc - pub fn clone_blob(&self) -> Arc> { - self.blob.clone_blob() - } -} +use tracing::instrument; +use types::{BlobSidecar, EthSpec}; /// Wrapper over a `BlobSidecar` for which we have completed kzg verification. /// i.e. `verify_blob_kzg_proof(blob, commitment, proof) == true`. @@ -388,239 +150,3 @@ where .unzip(); validate_blobs::(kzg, commitments.as_slice(), blobs, proofs.as_slice()) } - -pub fn validate_blob_sidecar_for_gossip( - blob_sidecar: Arc>, - subnet: u64, - chain: &BeaconChain, -) -> Result, GossipBlobError> { - let blob_slot = blob_sidecar.slot(); - let blob_index = blob_sidecar.index; - let block_parent_root = blob_sidecar.block_parent_root(); - let blob_proposer_index = blob_sidecar.block_proposer_index(); - let block_root = blob_sidecar.block_root(); - let blob_epoch = blob_slot.epoch(T::EthSpec::slots_per_epoch()); - let signed_block_header = &blob_sidecar.signed_block_header; - - let seen_timestamp = chain.slot_clock.now_duration().unwrap_or_default(); - - // This condition is not possible if we have received the blob from the network - // since we only subscribe to `MaxBlobsPerBlock` subnets over gossip network. - // We include this check only for completeness. - // Getting this error would imply something very wrong with our networking decoding logic. - if blob_index >= chain.spec.max_blobs_per_block(blob_epoch) { - return Err(GossipBlobError::InvalidSubnet { - expected: subnet, - received: blob_index, - }); - } - - // Verify that the blob_sidecar was received on the correct subnet. - if blob_index != subnet { - return Err(GossipBlobError::InvalidSubnet { - expected: subnet, - received: blob_index, - }); - } - - // Verify that the sidecar is not from a future slot. - let latest_permissible_slot = chain - .slot_clock - .now_with_future_tolerance(chain.spec.maximum_gossip_clock_disparity()) - .ok_or(BeaconChainError::UnableToReadSlot)?; - if blob_slot > latest_permissible_slot { - return Err(GossipBlobError::FutureSlot { - message_slot: blob_slot, - latest_permissible_slot, - }); - } - - // Verify that the sidecar slot is greater than the latest finalized slot - let latest_finalized_slot = chain - .head() - .finalized_checkpoint() - .epoch - .start_slot(T::EthSpec::slots_per_epoch()); - if blob_slot <= latest_finalized_slot { - return Err(GossipBlobError::PastFinalizedSlot { - blob_slot, - finalized_slot: latest_finalized_slot, - }); - } - - // Verify that this is the first blob sidecar received for the tuple: - // (block_header.slot, block_header.proposer_index, blob_sidecar.index) - if chain - .observed_blob_sidecars - .read() - .observation_key_is_known(&blob_sidecar) - .map_err(|e| GossipBlobError::BeaconChainError(Box::new(e.into())))? - .is_some() - { - return Err(GossipBlobError::RepeatBlob { - proposer: blob_proposer_index, - slot: blob_slot, - index: blob_index, - }); - } - - // Verify the inclusion proof in the sidecar - let _timer = metrics::start_timer(&metrics::BLOB_SIDECAR_INCLUSION_PROOF_VERIFICATION); - if !blob_sidecar.verify_blob_sidecar_inclusion_proof() { - return Err(GossipBlobError::InvalidInclusionProof); - } - drop(_timer); - - let fork_choice = chain.canonical_head.fork_choice_read_lock(); - - // We have already verified that the blob is past finalization, so we can - // just check fork choice for the block's parent. - let Some(parent_block) = fork_choice.get_block(&block_parent_root) else { - return Err(GossipBlobError::ParentUnknown { - parent_root: block_parent_root, - }); - }; - - // Do not process a blob that does not descend from the finalized root. - // We just loaded the parent_block, so we can be sure that it exists in fork choice. - if !fork_choice.is_finalized_checkpoint_or_descendant(block_parent_root) { - return Err(GossipBlobError::NotFinalizedDescendant { block_parent_root }); - } - drop(fork_choice); - - if parent_block.slot >= blob_slot { - return Err(GossipBlobError::BlobIsNotLaterThanParent { - blob_slot, - parent_slot: parent_block.slot, - }); - } - - let proposer_shuffling_root = - parent_block.proposer_shuffling_root_for_child_block(blob_epoch, &chain.spec); - - let proposer = chain.with_proposer_cache( - proposer_shuffling_root, - blob_epoch, - |proposers| proposers.get_slot::(blob_slot), - || { - debug!( - %block_root, - index = %blob_index, - "Proposer shuffling cache miss for blob verification" - ); - // Blob verification is only relevant pre-Fulu and pre-Gloas, so `Pending` payload - // status is sufficient. - chain - .store - .get_advanced_hot_state(block_parent_root, blob_slot, parent_block.state_root) - .map_err(|e| GossipBlobError::BeaconChainError(Box::new(e.into())))? - .ok_or_else(|| { - GossipBlobError::BeaconChainError(Box::new(BeaconChainError::DBInconsistent( - format!("Missing state for parent block {block_parent_root:?}",), - ))) - }) - }, - )?; - let proposer_index = proposer.index; - let fork = proposer.fork; - - // Signature verify the signed block header. - let signature_is_valid = { - let pubkey_cache = - get_validator_pubkey_cache(chain).map_err(|_| GossipBlobError::PubkeyCacheTimeout)?; - - let pubkey = pubkey_cache - .get(proposer_index) - .ok_or_else(|| GossipBlobError::UnknownValidator(proposer_index as u64))?; - signed_block_header.verify_signature::( - pubkey, - &fork, - chain.genesis_validators_root, - &chain.spec, - ) - }; - - if !signature_is_valid { - return Err(GossipBlobError::ProposalSignatureInvalid); - } - - if proposer_index != blob_proposer_index as usize { - return Err(GossipBlobError::ProposerIndexMismatch { - sidecar: blob_proposer_index as usize, - local: proposer_index, - }); - } - - // Kzg verification for gossip blob sidecar - let kzg = chain.kzg.as_ref(); - - let kzg_verified_blob = KzgVerifiedBlob::new(blob_sidecar.clone(), kzg, seen_timestamp) - .map_err(GossipBlobError::KzgError)?; - let blob_sidecar = &kzg_verified_blob.blob; - - chain - .observed_slashable - .write() - .observe_slashable( - blob_sidecar.slot(), - blob_sidecar.block_proposer_index(), - block_root, - ) - .map_err(|e| GossipBlobError::BeaconChainError(Box::new(e.into())))?; - - if O::observe() { - observe_gossip_blob(&kzg_verified_blob.blob, chain)?; - } - - Ok(GossipVerifiedBlob { - block_root, - blob: kzg_verified_blob, - _phantom: PhantomData, - }) -} - -pub 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. - // - // 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 - .write() - .observe_sidecar(blob_sidecar) - .map_err(|e: ObservedDataSidecarsError| { - GossipBlobError::BeaconChainError(Box::new(e.into())) - })? - .is_some() - { - return Err(GossipBlobError::RepeatBlob { - proposer: blob_sidecar.block_proposer_index(), - slot: blob_sidecar.slot(), - index: blob_sidecar.index, - }); - } - Ok(()) -} - -/// Returns the canonical root of the given `blob`. -/// -/// Use this function to ensure that we report the blob hashing time Prometheus metric. -pub fn get_blob_root(blob: &BlobSidecar) -> Hash256 { - let blob_root_timer = metrics::start_timer(&metrics::BLOCK_PROCESSING_BLOB_ROOT); - - let blob_root = blob.tree_hash_root(); - - metrics::stop_timer(blob_root_timer); - - blob_root -} diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 24f971f736..22e50e4185 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -49,7 +49,6 @@ #![allow(clippy::result_large_err)] use crate::beacon_snapshot::PreProcessingSnapshot; -use crate::blob_verification::GossipBlobError; use crate::block_verification_types::{AsBlock, BlockImportData, LookupBlock, RangeSyncBlock}; use crate::data_availability_checker::{ AvailabilityCheckError, AvailableBlock, AvailableBlockData, MaybeAvailableBlock, @@ -290,14 +289,6 @@ pub enum BlockError { EnvelopeBlockRootUnknown(Hash256), /// Optimistic sync is not supported for Gloas payload envelopes. OptimisticSyncNotSupported { block_root: Hash256 }, - /// A Blob with a slot after PeerDAS is received and is not required to be imported. - /// This can happen because we stay subscribed to the blob subnet after 2 epochs, as we could - /// still receive valid blobs from a Deneb epoch after PeerDAS is activated. - /// - /// ## Peer scoring - /// - /// This indicates the peer is sending an unexpected gossip blob and should be penalised. - BlobNotRequired(Slot), /// An internal error has occurred when processing the block or sidecars. /// /// ## Peer scoring @@ -520,17 +511,6 @@ impl BlockSlashInfo { } } -impl BlockSlashInfo { - pub fn from_early_error_blob(header: SignedBeaconBlockHeader, e: GossipBlobError) -> Self { - match e { - GossipBlobError::ProposalSignatureInvalid => BlockSlashInfo::SignatureInvalid(e), - // `InvalidSignature` could indicate any signature in the block, so we want - // to recheck the proposer signature alone. - _ => BlockSlashInfo::SignatureNotChecked(header, e), - } - } -} - impl BlockSlashInfo { pub fn from_early_error_data_column( header: SignedBeaconBlockHeader, @@ -2038,23 +2018,6 @@ impl BlockBlobError for BlockError { } } -impl BlockBlobError for GossipBlobError { - fn not_later_than_parent_error(blob_slot: Slot, parent_slot: Slot) -> Self { - GossipBlobError::BlobIsNotLaterThanParent { - blob_slot, - parent_slot, - } - } - - fn unknown_validator_error(validator_index: u64) -> Self { - GossipBlobError::UnknownValidator(validator_index) - } - - fn proposer_signature_invalid() -> Self { - GossipBlobError::ProposalSignatureInvalid - } -} - impl BlockBlobError for GossipDataColumnError { fn not_later_than_parent_error(data_column_slot: Slot, parent_slot: Slot) -> Self { GossipDataColumnError::IsNotLaterThanParent { diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index b8da2bcded..6df0b9c1a9 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -1007,7 +1007,6 @@ where // TODO: allow for persisting and loading the pool from disk. observed_block_producers: <_>::default(), observed_column_sidecars: RwLock::new(ObservedDataSidecars::new(self.spec.clone())), - observed_blob_sidecars: RwLock::new(ObservedDataSidecars::new(self.spec.clone())), observed_slashable: <_>::default(), pending_payload_envelopes: <_>::default(), observed_voluntary_exits: <_>::default(), diff --git a/beacon_node/beacon_chain/src/canonical_head.rs b/beacon_node/beacon_chain/src/canonical_head.rs index b3ab2e6975..1eab7ccf7a 100644 --- a/beacon_node/beacon_chain/src/canonical_head.rs +++ b/beacon_node/beacon_chain/src/canonical_head.rs @@ -967,13 +967,6 @@ impl BeaconChain { .start_slot(T::EthSpec::slots_per_epoch()), ); - self.observed_blob_sidecars.write().prune( - new_view - .finalized_checkpoint - .epoch - .start_slot(T::EthSpec::slots_per_epoch()), - ); - self.observed_column_sidecars.write().prune( new_view .finalized_checkpoint diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index cfd8ee7d34..3c2ba13fed 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -1,6 +1,4 @@ -use crate::blob_verification::{ - GossipVerifiedBlob, KzgVerifiedBlob, KzgVerifiedBlobList, verify_kzg_for_blob_list, -}; +use crate::blob_verification::{KzgVerifiedBlob, KzgVerifiedBlobList, verify_kzg_for_blob_list}; use crate::block_verification_types::{AvailabilityPendingExecutedBlock, AvailableExecutedBlock}; use crate::data_availability_checker::overflow_lru_cache::{ DataAvailabilityCheckerInner, ReconstructColumnsDecision, @@ -364,24 +362,6 @@ impl DataAvailabilityChecker { .put_kzg_verified_data_columns(block_root, verified_custody_columns) } - /// Check if we've cached other blobs for this block. If it completes a set and we also - /// have a block cached, return the `Availability` variant triggering block import. - /// Otherwise cache the blob sidecar. - /// - /// This should only accept gossip verified blobs, so we should not have to worry about dupes. - #[instrument(skip_all, level = "trace")] - pub fn put_gossip_verified_blobs< - I: IntoIterator>, - O: ObservationStrategy, - >( - &self, - block_root: Hash256, - blobs: I, - ) -> Result, AvailabilityCheckError> { - self.availability_cache - .put_kzg_verified_blobs(block_root, blobs.into_iter().map(|b| b.into_inner())) - } - #[instrument(skip_all, level = "trace")] pub fn put_kzg_verified_blobs>>( &self, 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 8a80f835ab..2ce0b4cd4a 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 @@ -780,9 +780,11 @@ impl DataAvailabilityCheckerInner { mod test { use super::*; - use crate::test_utils::generate_data_column_indices_rand_order; + use crate::data_column_verification::{GossipVerifiedDataColumn, KzgVerifiedCustodyDataColumn}; + use crate::test_utils::{ + generate_data_column_indices_rand_order, generate_data_column_sidecars_from_block, + }; use crate::{ - blob_verification::GossipVerifiedBlob, block_verification::PayloadVerificationOutcome, block_verification_types::{AsBlock, BlockImportData}, custody_context::NodeCustodyType, @@ -794,8 +796,8 @@ mod test { use store::{HotColdDB, ItemStore, StoreConfig, database::interface::BeaconNodeBackend}; use tempfile::{TempDir, tempdir}; use tracing::info; - use types::MinimalEthSpec; use types::new_non_zero_usize; + use types::{DataColumnSubnetId, MinimalEthSpec}; const LOW_VALIDATOR_COUNT: usize = 32; @@ -819,21 +821,25 @@ mod test { .expect("disk store should initialize") } - // get a beacon chain harness advanced to just before deneb fork - async fn get_deneb_chain( + // get a beacon chain harness advanced to just before fulu fork + async fn get_fulu_chain( db_path: &TempDir, ) -> BeaconChainHarness> { let altair_fork_epoch = Epoch::new(0); let bellatrix_fork_epoch = Epoch::new(0); let capella_fork_epoch = Epoch::new(3); let deneb_fork_epoch = Epoch::new(4); - let deneb_fork_slot = deneb_fork_epoch.start_slot(E::slots_per_epoch()); + let electra_fork_epoch = Epoch::new(5); + let fulu_fork_epoch = Epoch::new(6); + let fulu_fork_slot = fulu_fork_epoch.start_slot(E::slots_per_epoch()); let mut spec = E::default_spec(); spec.altair_fork_epoch = Some(altair_fork_epoch); spec.bellatrix_fork_epoch = Some(bellatrix_fork_epoch); spec.capella_fork_epoch = Some(capella_fork_epoch); spec.deneb_fork_epoch = Some(deneb_fork_epoch); + spec.electra_fork_epoch = Some(electra_fork_epoch); + spec.fulu_fork_epoch = Some(fulu_fork_epoch); let spec = Arc::new(spec); let chain_store = get_store_with_spec::(db_path, spec.clone()); @@ -846,8 +852,10 @@ mod test { .mock_execution_layer() .build(); - // go right before deneb slot - harness.extend_to_slot(deneb_fork_slot - 1).await; + harness.execution_block_generator().set_min_blob_count(1); + + // go right before fulu slot + harness.extend_to_slot(fulu_fork_slot - 1).await; harness } @@ -856,7 +864,7 @@ mod test { harness: &BeaconChainHarness>, ) -> ( AvailabilityPendingExecutedBlock, - Vec>>, + Vec>>, ) where E: EthSpec, @@ -874,7 +882,7 @@ mod test { .expect("should get block") .expect("should have block"); - let (signed_beacon_block_hash, (block, maybe_blobs), state) = harness + let (signed_beacon_block_hash, (block, _maybe_blobs), state) = harness .add_block_at_slot(target_slot, parent_state) .await .expect("should add block"); @@ -892,27 +900,25 @@ mod test { .message() .body() .blob_kzg_commitments() - .expect("should be deneb fork") + .expect("should be fulu fork") .clone(), ) { info!(commitment = ?comm, "kzg commitment"); } info!("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, &chain.spec).unwrap(); - Vec::from(sidecars) - .into_iter() - .map(|sidecar| { - let subnet = sidecar.index; - GossipVerifiedBlob::new(sidecar, subnet, &harness.chain) - .expect("should validate blob") - }) - .collect() - } else { - vec![] - }; + // Generate data columns from the block + let data_columns = generate_data_column_sidecars_from_block(&block, &harness.spec); + + let gossip_verified_columns: Vec<_> = data_columns + .into_iter() + .map(|sidecar| { + let subnet_id = + DataColumnSubnetId::from_column_index(*sidecar.index(), &harness.spec); + GossipVerifiedDataColumn::new(sidecar, subnet_id, &harness.chain) + .expect("should validate data column") + }) + .collect(); let slot = block.slot(); let consensus_context = ConsensusContext::::new(slot); @@ -933,7 +939,7 @@ mod test { payload_verification_outcome, }; - (availability_pending_block, gossip_verified_blobs) + (availability_pending_block, gossip_verified_columns) } async fn setup_harness_and_cache( @@ -953,7 +959,7 @@ mod test { { create_test_tracing_subscriber(); let chain_db_path = tempdir().expect("should get temp dir"); - let harness = get_deneb_chain(&chain_db_path).await; + let harness = get_fulu_chain(&chain_db_path).await; let spec = harness.spec.clone(); let capacity_non_zero = new_non_zero_usize(capacity); let custody_context = Arc::new(CustodyContext::new( @@ -979,20 +985,27 @@ mod test { let capacity = 4; let (harness, cache, _path) = setup_harness_and_cache::(capacity).await; - let (pending_block, blobs) = availability_pending_block(&harness).await; + let (pending_block, columns) = 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(); + let num_blobs_expected = pending_block.num_blobs_expected(); + let columns_expected = cache + .custody_context + .num_of_data_columns_to_sample(epoch, &harness.spec); + + // All columns are returned from availability_pending_block (E::number_of_columns()) + // but we only need custody columns assert_eq!( - blobs.len(), - blobs_expected, - "should have expected number of blobs" + columns.len(), + E::number_of_columns(), + "should have all data columns from block" ); assert!(cache.critical.read().is_empty(), "cache should be empty"); let availability = cache .put_executed_block(pending_block) .expect("should put block"); - if blobs_expected == 0 { + if num_blobs_expected == 0 { assert!( matches!(availability, Availability::Available(_)), "block doesn't have blobs, should be available" @@ -1005,7 +1018,7 @@ mod test { } else { assert!( matches!(availability, Availability::MissingComponents(_)), - "should be pending blobs" + "should be pending columns" ); assert_eq!( cache.critical.read().len(), @@ -1018,13 +1031,26 @@ mod test { ); } - let mut kzg_verified_blobs = Vec::new(); - for (blob_index, gossip_blob) in blobs.into_iter().enumerate() { - kzg_verified_blobs.push(gossip_blob.into_inner()); + // Get sampling column indices for this epoch + let sampling_column_indices = cache + .custody_context + .sampling_columns_for_epoch(epoch, &harness.spec); + + // Filter to only sampling columns + let sampling_columns: Vec<_> = columns + .into_iter() + .filter(|col| sampling_column_indices.contains(&col.index())) + .collect(); + + let mut kzg_verified_columns = Vec::new(); + for (col_index, gossip_column) in sampling_columns.into_iter().enumerate() { + kzg_verified_columns.push(KzgVerifiedCustodyDataColumn::from_asserted_custody( + gossip_column.into_inner(), + )); let availability = cache - .put_kzg_verified_blobs(root, kzg_verified_blobs.clone()) - .expect("should put blob"); - if blob_index == blobs_expected - 1 { + .put_kzg_verified_data_columns(root, kzg_verified_columns.clone()) + .expect("should put column"); + if col_index == columns_expected - 1 { assert!(matches!(availability, Availability::Available(_))); } else { assert!(matches!(availability, Availability::MissingComponents(_))); @@ -1032,20 +1058,36 @@ mod test { } } - let (pending_block, blobs) = availability_pending_block(&harness).await; - let blobs_expected = pending_block.num_blobs_expected(); + let (pending_block, columns) = availability_pending_block(&harness).await; + let _num_blobs_expected = pending_block.num_blobs_expected(); + let epoch = pending_block.block.epoch(); + // All columns returned assert_eq!( - blobs.len(), - blobs_expected, - "should have expected number of blobs" + columns.len(), + E::number_of_columns(), + "should have all data columns" ); let root = pending_block.import_data.block_root; - let mut kzg_verified_blobs = vec![]; - for gossip_blob in blobs { - kzg_verified_blobs.push(gossip_blob.into_inner()); + + // Get sampling column indices for this epoch + let sampling_column_indices = cache + .custody_context + .sampling_columns_for_epoch(epoch, &harness.spec); + + // Filter to only sampling columns + let sampling_columns: Vec<_> = columns + .into_iter() + .filter(|col| sampling_column_indices.contains(&col.index())) + .collect(); + + let mut kzg_verified_columns = vec![]; + for gossip_column in sampling_columns { + kzg_verified_columns.push(KzgVerifiedCustodyDataColumn::from_asserted_custody( + gossip_column.into_inner(), + )); let availability = cache - .put_kzg_verified_blobs(root, kzg_verified_blobs.clone()) - .expect("should put blob"); + .put_kzg_verified_data_columns(root, kzg_verified_columns.clone()) + .expect("should put column"); assert!( matches!(availability, Availability::MissingComponents(_)), "should be pending block" diff --git a/beacon_node/beacon_chain/src/fetch_blobs/fetch_blobs_beacon_adapter.rs b/beacon_node/beacon_chain/src/fetch_blobs/fetch_blobs_beacon_adapter.rs index f5ba647fce..b75fcdac5c 100644 --- a/beacon_node/beacon_chain/src/fetch_blobs/fetch_blobs_beacon_adapter.rs +++ b/beacon_node/beacon_chain/src/fetch_blobs/fetch_blobs_beacon_adapter.rs @@ -1,8 +1,9 @@ -use crate::fetch_blobs::{EngineGetBlobsOutput, FetchEngineBlobError}; +use crate::data_column_verification::KzgVerifiedCustodyDataColumn; +use crate::fetch_blobs::FetchEngineBlobError; use crate::observed_data_sidecars::ObservationKey; use crate::partial_data_column_assembler::PartialDataColumnAssembler; use crate::{AvailabilityProcessingStatus, BeaconChain, BeaconChainTypes}; -use execution_layer::json_structures::{BlobAndProofV1, BlobAndProofV2, BlobAndProofV3}; +use execution_layer::json_structures::{BlobAndProofV2, BlobAndProofV3}; use kzg::Kzg; #[cfg(test)] use mockall::automock; @@ -43,22 +44,6 @@ impl FetchBlobsBeaconAdapter { .cloned() } - pub(crate) async fn get_blobs_v1( - &self, - versioned_hashes: Vec, - ) -> Result>>, FetchEngineBlobError> { - let execution_layer = self - .chain - .execution_layer - .as_ref() - .ok_or(FetchEngineBlobError::ExecutionLayerMissing)?; - - execution_layer - .get_blobs_v1(versioned_hashes) - .await - .map_err(FetchEngineBlobError::RequestFailed) - } - pub(crate) async fn get_blobs_v2( &self, versioned_hashes: Vec, @@ -91,17 +76,6 @@ impl FetchBlobsBeaconAdapter { .map_err(FetchEngineBlobError::RequestFailed) } - pub(crate) fn blobs_known_for_observation_key( - &self, - observation_key: ObservationKey, - ) -> Option> { - self.chain - .observed_blob_sidecars - .read() - .known_for_observation_key(&observation_key) - .cloned() - } - pub(crate) fn data_column_known_for_observation_key( &self, observation_key: ObservationKey, @@ -113,12 +87,6 @@ impl FetchBlobsBeaconAdapter { .cloned() } - pub(crate) fn cached_blob_indexes(&self, block_root: &Hash256) -> Option> { - self.chain - .data_availability_checker - .cached_blob_indexes(block_root) - } - pub(crate) fn cached_data_column_indexes( &self, block_root: &Hash256, @@ -131,7 +99,7 @@ impl FetchBlobsBeaconAdapter { &self, slot: Slot, block_root: Hash256, - blobs: EngineGetBlobsOutput, + blobs: Vec>, ) -> Result { self.chain .process_engine_blobs(slot, block_root, blobs) diff --git a/beacon_node/beacon_chain/src/fetch_blobs/mod.rs b/beacon_node/beacon_chain/src/fetch_blobs/mod.rs index 351e35666a..158cef0003 100644 --- a/beacon_node/beacon_chain/src/fetch_blobs/mod.rs +++ b/beacon_node/beacon_chain/src/fetch_blobs/mod.rs @@ -12,7 +12,6 @@ mod fetch_blobs_beacon_adapter; #[cfg(test)] mod tests; -use crate::blob_verification::{GossipBlobError, KzgVerifiedBlob}; use crate::data_column_verification::{ KzgVerifiedCustodyDataColumn, KzgVerifiedCustodyPartialDataColumn, KzgVerifiedPartialDataColumn, }; @@ -25,26 +24,15 @@ use crate::{ metrics, }; use execution_layer::Error as ExecutionLayerError; -use execution_layer::json_structures::{BlobAndProofV1, BlobAndProofV2, BlobAndProofV3}; +use execution_layer::json_structures::{BlobAndProofV2, BlobAndProofV3}; use metrics::{TryExt, inc_counter}; #[cfg(test)] use mockall_double::double; -use slot_clock::timestamp_now; use state_processing::per_block_processing::deneb::kzg_commitment_to_versioned_hash; use std::sync::Arc; use tracing::{debug, instrument, warn}; use types::data::{BlobSidecarError, ColumnIndex, DataColumnSidecarError, PartialDataColumnHeader}; -use types::{BeaconStateError, BlobSidecar, EthSpec, Hash256, VersionedHash}; - -/// Result from engine get blobs to be passed onto `DataAvailabilityChecker` and published to the -/// gossip network. The blobs / data columns have not been marked as observed yet, as they may not -/// be published immediately. -#[derive(Debug)] -pub enum EngineGetBlobsOutput { - Blobs(Vec>), - /// A filtered list of custody data columns to be imported into the `DataAvailabilityChecker`. - CustodyColumns(Vec>), -} +use types::{BeaconStateError, EthSpec, Hash256, VersionedHash}; #[derive(Debug)] pub enum FetchEngineBlobError { @@ -55,22 +43,21 @@ pub enum FetchEngineBlobError { DataColumnSidecarError(DataColumnSidecarError), ExecutionLayerMissing, InternalError(String), - GossipBlob(GossipBlobError), KzgError(kzg::Error), RequestFailed(ExecutionLayerError), RuntimeShutdown, TokioJoin(tokio::task::JoinError), } -/// 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`. +/// Fetches blobs from the EL mempool and processes them as data columns. It also broadcasts +/// unseen data columns to the network, using the supplied `publish_fn`. #[instrument(skip_all)] pub async fn fetch_and_process_engine_blobs( chain: Arc>, block_root: Hash256, header: Arc>, custody_columns: &[ColumnIndex], - publish_fn: impl Fn(EngineGetBlobsOutput) + Send + 'static, + publish_fn: impl Fn(Vec>) + Send + 'static, ) -> Result, FetchEngineBlobError> { fetch_and_process_engine_blobs_inner( FetchBlobsBeaconAdapter::new(chain), @@ -89,7 +76,7 @@ async fn fetch_and_process_engine_blobs_inner( block_root: Hash256, header: Arc>, custody_columns: &[ColumnIndex], - publish_fn: impl Fn(EngineGetBlobsOutput) + Send + 'static, + publish_fn: impl Fn(Vec>) + Send + 'static, ) -> Result, FetchEngineBlobError> { let versioned_hashes = header .kzg_commitments @@ -120,104 +107,12 @@ async fn fetch_and_process_engine_blobs_inner( ) .await } else { - fetch_and_process_blobs_v1( - chain_adapter, - block_root, - &header, - versioned_hashes, - publish_fn, - ) - .await + Err(FetchEngineBlobError::InternalError( + "fetch blobs v1 no longer supported".to_owned(), + )) } } -#[instrument(skip_all, level = "debug")] -async fn fetch_and_process_blobs_v1( - chain_adapter: FetchBlobsBeaconAdapter, - block_root: Hash256, - header: &PartialDataColumnHeader, - versioned_hashes: Vec, - publish_fn: impl Fn(EngineGetBlobsOutput) + Send + Sized, -) -> Result, FetchEngineBlobError> { - let num_expected_blobs = versioned_hashes.len(); - metrics::observe(&metrics::BLOBS_FROM_EL_EXPECTED, num_expected_blobs as f64); - debug!(num_expected_blobs, "Fetching blobs from the EL"); - let response = chain_adapter - .get_blobs_v1(versioned_hashes) - .await - .inspect_err(|_| { - inc_counter(&metrics::BLOBS_FROM_EL_ERROR_TOTAL); - })?; - - let num_fetched_blobs = response.iter().filter(|opt| opt.is_some()).count(); - metrics::observe(&metrics::BLOBS_FROM_EL_RECEIVED, num_fetched_blobs as f64); - - if num_fetched_blobs == 0 { - debug!(num_expected_blobs, "No blobs fetched from the EL"); - inc_counter(&metrics::BLOBS_FROM_EL_MISS_TOTAL); - return Ok(None); - } else { - debug!( - num_expected_blobs, - num_fetched_blobs, "Received blobs from the EL" - ); - inc_counter(&metrics::BLOBS_FROM_EL_HIT_TOTAL); - } - - if chain_adapter.fork_choice_contains_block(&block_root) { - // Avoid computing sidecars if the block has already been imported. - debug!( - info = "block has already been imported", - "Ignoring EL blobs response" - ); - return Ok(None); - } - - let mut blob_sidecar_list = build_blob_sidecars(header, response)?; - - let observation_key = ObservationKey::new_proposer_key( - header.signed_block_header.message.proposer_index, - header.slot(), - ); - - if let Some(observed_blobs) = chain_adapter.blobs_known_for_observation_key(observation_key) { - blob_sidecar_list.retain(|blob| !observed_blobs.contains(&blob.blob_index())); - if blob_sidecar_list.is_empty() { - debug!( - info = "blobs have already been seen on gossip", - "Ignoring EL blobs response" - ); - return Ok(None); - } - } - - if let Some(known_blobs) = chain_adapter.cached_blob_indexes(&block_root) { - blob_sidecar_list.retain(|blob| !known_blobs.contains(&blob.blob_index())); - if blob_sidecar_list.is_empty() { - debug!( - info = "blobs have already been imported into data availability checker", - "Ignoring EL blobs response" - ); - return Ok(None); - } - } - - // Up until this point we have not observed the blobs in the gossip cache, which allows them to - // arrive independently while this function is running. In `publish_fn` we will observe them - // and then publish any blobs that had not already been observed. - publish_fn(EngineGetBlobsOutput::Blobs(blob_sidecar_list.clone())); - - let availability_processing_status = chain_adapter - .process_engine_blobs( - header.slot(), - block_root, - EngineGetBlobsOutput::Blobs(blob_sidecar_list), - ) - .await?; - - Ok(Some(availability_processing_status)) -} - #[instrument(skip_all, level = "debug")] async fn fetch_and_process_blobs_v2_or_v3( chain_adapter: FetchBlobsBeaconAdapter, @@ -225,7 +120,7 @@ async fn fetch_and_process_blobs_v2_or_v3( header: Arc>, versioned_hashes: Vec, custody_columns_indices: &[ColumnIndex], - publish_fn: impl Fn(EngineGetBlobsOutput) + Send + 'static, + publish_fn: impl Fn(Vec>) + Send + 'static, ) -> Result, FetchEngineBlobError> { let num_expected_blobs = versioned_hashes.len(); let slot = header.slot(); @@ -354,7 +249,7 @@ async fn fetch_and_process_blobs_v2_or_v3( // Publish complete columns if !full_columns.is_empty() { - publish_fn(EngineGetBlobsOutput::CustodyColumns(full_columns.clone())); + publish_fn(full_columns.clone()); } // We publish all partials at the calling site, regardless of result, as previous publishs // have been blocked, waiting for the results of this call @@ -362,11 +257,7 @@ async fn fetch_and_process_blobs_v2_or_v3( // Process complete columns through DA checker let availability_processing_status = if !full_columns.is_empty() { chain_adapter - .process_engine_blobs( - slot, - block_root, - EngineGetBlobsOutput::CustodyColumns(full_columns), - ) + .process_engine_blobs(slot, block_root, full_columns) .await? } else { // No complete columns yet, still missing components @@ -461,30 +352,3 @@ async fn compute_custody_columns_to_import( .await .map_err(FetchEngineBlobError::TokioJoin)? } - -fn build_blob_sidecars( - header: &PartialDataColumnHeader, - response: Vec>>, -) -> Result>, FetchEngineBlobError> { - let mut sidecars = vec![]; - for (index, blob_and_proof) in response - .into_iter() - .enumerate() - .filter_map(|(index, opt_blob)| Some((index, opt_blob?))) - { - let blob_sidecar = BlobSidecar::new_with_existing_proof( - index, - blob_and_proof.blob, - header.clone(), - blob_and_proof.proof, - ) - .map_err(FetchEngineBlobError::BlobSidecarError)?; - - sidecars.push(KzgVerifiedBlob::from_execution_verified( - Arc::new(blob_sidecar), - timestamp_now(), - )); - } - - Ok(sidecars) -} diff --git a/beacon_node/beacon_chain/src/fetch_blobs/tests.rs b/beacon_node/beacon_chain/src/fetch_blobs/tests.rs index 37d40f3a27..99cb4b5a78 100644 --- a/beacon_node/beacon_chain/src/fetch_blobs/tests.rs +++ b/beacon_node/beacon_chain/src/fetch_blobs/tests.rs @@ -1,8 +1,7 @@ use crate::AvailabilityProcessingStatus; +use crate::data_column_verification::KzgVerifiedCustodyDataColumn; use crate::fetch_blobs::fetch_blobs_beacon_adapter::MockFetchBlobsBeaconAdapter; -use crate::fetch_blobs::{ - EngineGetBlobsOutput, FetchEngineBlobError, fetch_and_process_engine_blobs_inner, -}; +use crate::fetch_blobs::{FetchEngineBlobError, fetch_and_process_engine_blobs_inner}; use crate::partial_data_column_assembler::PartialDataColumnAssembler; use crate::test_utils::{EphemeralHarnessType, get_kzg}; use bls::Signature; @@ -226,7 +225,7 @@ mod get_blobs_v2 { assert!( matches!( published_columns, - EngineGetBlobsOutput::CustodyColumns(columns) if columns.len() == custody_columns.len() + columns if columns.len() == custody_columns.len() ), "should publish custody columns" ); @@ -251,284 +250,10 @@ mod get_blobs_v2 { } } -mod get_blobs_v1 { - use super::*; - use crate::block_verification_types::AsBlock; - use std::collections::HashSet; - use types::{ColumnIndex, FullPayload, PartialDataColumnHeader}; - - const ELECTRA_FORK: ForkName = ForkName::Electra; - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_fetch_blobs_v1_no_blobs_in_block() { - let mut mock_adapter = mock_beacon_adapter(ELECTRA_FORK, false); - let spec = mock_adapter.spec(); - let (publish_fn, _s) = mock_publish_fn(); - let block_no_blobs = SignedBeaconBlock::>::from_block( - BeaconBlock::empty(spec), - Signature::empty(), - ); - let block_root = block_no_blobs.canonical_root(); - - // Expectations: engine fetch blobs should not be triggered - mock_adapter.expect_get_blobs_v1().times(0); - - // WHEN: Trigger fetch blobs on the block - let custody_columns: [ColumnIndex; 3] = [0, 1, 2]; - let processing_status = fetch_and_process_engine_blobs_inner( - mock_adapter, - block_root, - Arc::new(PartialDataColumnHeader::try_from(&block_no_blobs).unwrap()), - &custody_columns, - publish_fn, - ) - .await - .expect("fetch blobs should succeed"); - - // THEN: No blob is processed - assert_eq!(processing_status, None); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_fetch_blobs_v1_no_blobs_returned() { - let mut mock_adapter = mock_beacon_adapter(ELECTRA_FORK, false); - let (publish_fn, _) = mock_publish_fn(); - let (block, _blobs_and_proofs) = create_test_block_and_blobs(&mock_adapter, 2); - let block_root = block.canonical_root(); - - // GIVEN: No blobs in EL response - let expected_blob_count = block.message().body().blob_kzg_commitments().unwrap().len(); - mock_get_blobs_v1_response(&mut mock_adapter, vec![None; expected_blob_count]); - - // WHEN: Trigger fetch blobs on the block - let custody_columns: [ColumnIndex; 3] = [0, 1, 2]; - let processing_status = fetch_and_process_engine_blobs_inner( - mock_adapter, - block_root, - Arc::new(PartialDataColumnHeader::try_from(block.as_ref()).unwrap()), - &custody_columns, - publish_fn, - ) - .await - .expect("fetch blobs should succeed"); - - // THEN: No blob is processed - assert_eq!(processing_status, None); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_fetch_blobs_v1_partial_blobs_returned() { - let mut mock_adapter = mock_beacon_adapter(ELECTRA_FORK, false); - let (publish_fn, publish_fn_args) = mock_publish_fn(); - let blob_count = 2; - let (block, blobs_and_proofs) = create_test_block_and_blobs(&mock_adapter, blob_count); - let block_slot = block.slot(); - let block_root = block.canonical_root(); - - // GIVEN: Missing a blob in EL response (remove 1 blob from response) - let mut blob_and_proof_opts = blobs_and_proofs.into_iter().map(Some).collect::>(); - blob_and_proof_opts.first_mut().unwrap().take(); - mock_get_blobs_v1_response(&mut mock_adapter, blob_and_proof_opts); - // AND block is not imported into fork choice - mock_fork_choice_contains_block(&mut mock_adapter, vec![]); - // AND all blobs have not yet been seen - mock_adapter - .expect_cached_blob_indexes() - .returning(|_| None); - mock_adapter - .expect_blobs_known_for_observation_key() - .returning(|_| None); - // Returned blobs should be processed - mock_process_engine_blobs_result( - &mut mock_adapter, - Ok(AvailabilityProcessingStatus::MissingComponents( - block_slot, block_root, - )), - ); - - // WHEN: Trigger fetch blobs on the block - let custody_columns: [ColumnIndex; 3] = [0, 1, 2]; - let processing_status = fetch_and_process_engine_blobs_inner( - mock_adapter, - block_root, - Arc::new(PartialDataColumnHeader::try_from(block.as_ref()).unwrap()), - &custody_columns, - publish_fn, - ) - .await - .expect("fetch blobs should succeed"); - - // THEN: Returned blobs are processed and published - assert_eq!( - processing_status, - Some(AvailabilityProcessingStatus::MissingComponents( - block_slot, block_root, - )) - ); - assert!( - matches!( - extract_published_blobs(publish_fn_args), - EngineGetBlobsOutput::Blobs(blobs) if blobs.len() == blob_count - 1 - ), - "partial blob results should still be published" - ); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_fetch_blobs_v1_block_imported_after_el_response() { - let mut mock_adapter = mock_beacon_adapter(ELECTRA_FORK, false); - let (publish_fn, publish_fn_args) = mock_publish_fn(); - let (block, blobs_and_proofs) = create_test_block_and_blobs(&mock_adapter, 2); - let block_root = block.canonical_root(); - - // GIVEN: All blobs returned, but fork choice already imported the block - let blob_and_proof_opts = blobs_and_proofs.into_iter().map(Some).collect::>(); - mock_get_blobs_v1_response(&mut mock_adapter, blob_and_proof_opts); - mock_fork_choice_contains_block(&mut mock_adapter, vec![block.canonical_root()]); - - // WHEN: Trigger fetch blobs on the block - let custody_columns: [ColumnIndex; 3] = [0, 1, 2]; - let processing_status = fetch_and_process_engine_blobs_inner( - mock_adapter, - block_root, - Arc::new(PartialDataColumnHeader::try_from(block.as_ref()).unwrap()), - &custody_columns, - publish_fn, - ) - .await - .expect("fetch blobs should succeed"); - - // THEN: Returned blobs should NOT be processed or published. - assert_eq!(processing_status, None); - assert_eq!( - publish_fn_args.lock().unwrap().len(), - 0, - "no blobs should be published" - ); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_fetch_blobs_v1_no_new_blobs_to_import() { - let mut mock_adapter = mock_beacon_adapter(ELECTRA_FORK, false); - let (publish_fn, publish_fn_args) = mock_publish_fn(); - let (block, blobs_and_proofs) = create_test_block_and_blobs(&mock_adapter, 2); - let block_root = block.canonical_root(); - - // **GIVEN**: - // All blobs returned - let blob_and_proof_opts = blobs_and_proofs.into_iter().map(Some).collect::>(); - let all_blob_indices = blob_and_proof_opts - .iter() - .enumerate() - .map(|(i, _)| i as u64) - .collect::>(); - - mock_get_blobs_v1_response(&mut mock_adapter, blob_and_proof_opts); - // block not yet imported into fork choice - mock_fork_choice_contains_block(&mut mock_adapter, vec![]); - // All blobs already seen on gossip - mock_adapter - .expect_cached_blob_indexes() - .returning(|_| None); - mock_adapter - .expect_blobs_known_for_observation_key() - .returning(move |_| Some(all_blob_indices.clone())); - - // **WHEN**: Trigger `fetch_blobs` on the block - let custody_columns: [ColumnIndex; 3] = [0, 1, 2]; - let processing_status = fetch_and_process_engine_blobs_inner( - mock_adapter, - block_root, - Arc::new(PartialDataColumnHeader::try_from(block.as_ref()).unwrap()), - &custody_columns, - publish_fn, - ) - .await - .expect("fetch blobs should succeed"); - - // **THEN**: Should NOT be processed and no blobs should be published. - assert_eq!(processing_status, None); - assert_eq!( - publish_fn_args.lock().unwrap().len(), - 0, - "no blobs should be published" - ); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_fetch_blobs_v1_success() { - let mut mock_adapter = mock_beacon_adapter(ELECTRA_FORK, false); - let (publish_fn, publish_fn_args) = mock_publish_fn(); - let blob_count = 2; - let (block, blobs_and_proofs) = create_test_block_and_blobs(&mock_adapter, blob_count); - let block_root = block.canonical_root(); - - // All blobs returned, fork choice doesn't contain block - let blob_and_proof_opts = blobs_and_proofs.into_iter().map(Some).collect::>(); - mock_get_blobs_v1_response(&mut mock_adapter, blob_and_proof_opts); - mock_fork_choice_contains_block(&mut mock_adapter, vec![]); - mock_adapter - .expect_cached_blob_indexes() - .returning(|_| None); - mock_adapter - .expect_blobs_known_for_observation_key() - .returning(|_| None); - mock_process_engine_blobs_result( - &mut mock_adapter, - Ok(AvailabilityProcessingStatus::Imported(block_root)), - ); - - // Trigger fetch blobs on the block - let custody_columns: [ColumnIndex; 3] = [0, 1, 2]; - let processing_status = fetch_and_process_engine_blobs_inner( - mock_adapter, - block_root, - Arc::new(PartialDataColumnHeader::try_from(block.as_ref()).unwrap()), - &custody_columns, - publish_fn, - ) - .await - .expect("fetch blobs should succeed"); - - // THEN all fetched blobs are processed and published - assert_eq!( - processing_status, - Some(AvailabilityProcessingStatus::Imported(block_root)) - ); - - let published_blobs = extract_published_blobs(publish_fn_args); - assert!( - matches!( - published_blobs, - EngineGetBlobsOutput::Blobs(blobs) if blobs.len() == blob_count - ), - "should publish fetched blobs" - ); - } - - fn mock_get_blobs_v1_response( - mock_adapter: &mut MockFetchBlobsBeaconAdapter, - blobs_and_proofs_opt: Vec>>, - ) { - let blobs_and_proofs_v1 = blobs_and_proofs_opt - .into_iter() - .map(|blob_and_proof_opt| { - blob_and_proof_opt.map(|blob_and_proof| match blob_and_proof { - BlobAndProof::V1(inner) => inner, - _ => panic!("BlobAndProofV1 not expected"), - }) - }) - .collect(); - mock_adapter - .expect_get_blobs_v1() - .return_once(move |_| Ok(blobs_and_proofs_v1)); - } -} - -/// Extract the `EngineGetBlobsOutput` passed to the `publish_fn`. +/// Extract the `Vec>` passed to the `publish_fn`. fn extract_published_blobs( - publish_fn_args: Arc>>>, -) -> EngineGetBlobsOutput { + publish_fn_args: Arc>>>>, +) -> Vec> { let mut calls = publish_fn_args.lock().unwrap(); assert_eq!(calls.len(), 1); calls.pop().unwrap() @@ -597,8 +322,8 @@ fn create_test_block_and_blobs( #[allow(clippy::type_complexity)] fn mock_publish_fn() -> ( - impl Fn(EngineGetBlobsOutput) + Send + 'static, - Arc>>>, + impl Fn(Vec>) + Send + 'static, + Arc>>>>, ) { // Keep track of the arguments captured by `publish_fn`. let captured_args = Arc::new(Mutex::new(vec![])); diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index c2ccad7d8c..919bb43bfd 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -1,4 +1,3 @@ -use crate::blob_verification::GossipVerifiedBlob; use crate::block_verification_types::{AsBlock, AvailableBlockData, LookupBlock, RangeSyncBlock}; use crate::custody_context::NodeCustodyType; use crate::data_availability_checker::DataAvailabilityChecker; @@ -3696,55 +3695,39 @@ 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>( + /// Simulate the block's custody data columns (or those in `custody_columns_opt`) being + /// seen on gossip. Panics unless PeerDAS is enabled for the block's epoch. + pub async fn process_gossip_columns( &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 epoch = block.slot().epoch(E::slots_per_epoch()); - self.chain - .sampling_columns_for_epoch(epoch) - .iter() - .copied() - .collect() - }); + assert!(self.chain.spec.is_peer_das_enabled_for_epoch(block.epoch())); + let custody_columns = custody_columns_opt.unwrap_or_else(|| { + let epoch = block.slot().epoch(E::slots_per_epoch()); + self.chain + .sampling_columns_for_epoch(epoch) + .iter() + .copied() + .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 subnet_id = - DataColumnSubnetId::from_column_index(*sidecar.index(), &self.spec); - self.chain - .verify_data_column_sidecar_for_gossip(sidecar, subnet_id) - }) - .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 subnet_id = DataColumnSubnetId::from_column_index(*sidecar.index(), &self.spec); + self.chain + .verify_data_column_sidecar_for_gossip(sidecar, subnet_id) + }) + .collect::, _>>() + .unwrap(); + + if !verified_columns.is_empty() { + self.chain + .process_gossip_data_columns(verified_columns, || Ok(())) + .await .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"); - } } } } diff --git a/beacon_node/beacon_chain/tests/block_verification.rs b/beacon_node/beacon_chain/tests/block_verification.rs index 533ef61219..67fe0eaae0 100644 --- a/beacon_node/beacon_chain/tests/block_verification.rs +++ b/beacon_node/beacon_chain/tests/block_verification.rs @@ -1456,20 +1456,8 @@ async fn verify_and_process_gossip_data_sidecars( data_sidecars: DataSidecars, ) { match data_sidecars { - DataSidecars::Blobs(blob_sidecars) => { - for blob_sidecar in blob_sidecars { - let blob_index = blob_sidecar.index; - let gossip_verified = harness - .chain - .verify_blob_sidecar_for_gossip(blob_sidecar.clone(), blob_index) - .expect("should obtain gossip verified blob"); - - harness - .chain - .process_gossip_blob(gossip_verified) - .await - .expect("should import valid gossip verified blob"); - } + DataSidecars::Blobs(_blob_sidecars) => { + // Blob gossip is deprecated, blobs are available via RPC. } DataSidecars::DataColumns(column_sidecars) => { let gossip_verified = column_sidecars @@ -1521,14 +1509,9 @@ async fn verify_block_for_gossip_slashing_detection() { let verified_block = harness.chain.verify_block_for_gossip(block1).await.unwrap(); - if let Some((kzg_proofs, blobs)) = blobs1 { + if blobs1.is_some() { harness - .process_gossip_blobs_or_columns( - verified_block.block(), - blobs.iter(), - kzg_proofs.iter(), - None, - ) + .process_gossip_columns(verified_block.block(), None) .await; } harness diff --git a/beacon_node/beacon_chain/tests/events.rs b/beacon_node/beacon_chain/tests/events.rs index 29d0e38b93..baa6975303 100644 --- a/beacon_node/beacon_chain/tests/events.rs +++ b/beacon_node/beacon_chain/tests/events.rs @@ -1,12 +1,9 @@ use arbitrary::Arbitrary; -use beacon_chain::blob_verification::GossipVerifiedBlob; use beacon_chain::data_column_verification::GossipVerifiedDataColumn; use beacon_chain::test_utils::{ BeaconChainHarness, fork_name_from_env, generate_data_column_sidecars_from_block, test_spec, }; use eth2::types::{EventKind, SseBlobSidecar, SseDataColumnSidecar}; -use rand::SeedableRng; -use rand::rngs::StdRng; use std::sync::Arc; use types::data::FixedBlobSidecarList; use types::{ @@ -17,44 +14,6 @@ use types::{ type E = MinimalEthSpec; -/// Verifies that a blob event is emitted when a gossip verified blob is received via gossip or the publish block API. -#[tokio::test] -async fn blob_sidecar_event_on_process_gossip_blob() { - if fork_name_from_env().is_some_and(|f| !f.deneb_enabled() || f.fulu_enabled()) { - return; - }; - - let spec = Arc::new(test_spec::()); - let harness = BeaconChainHarness::builder(E::default()) - .spec(spec) - .deterministic_keypairs(8) - .fresh_ephemeral_store() - .mock_execution_layer() - .build(); - - // subscribe to blob sidecar events - let event_handler = harness.chain.event_handler.as_ref().unwrap(); - let mut blob_event_receiver = event_handler.subscribe_blob_sidecar(); - - // build and process a gossip verified blob - let kzg = harness.chain.kzg.as_ref(); - let mut rng = StdRng::seed_from_u64(0xDEADBEEF0BAD5EEDu64); - let sidecar = BlobSidecar::random_valid(&mut rng, kzg) - .map(Arc::new) - .unwrap(); - let gossip_verified_blob = GossipVerifiedBlob::__assumed_valid(sidecar); - let expected_sse_blobs = SseBlobSidecar::from_blob_sidecar(gossip_verified_blob.as_blob()); - - let _ = harness - .chain - .process_gossip_blob(gossip_verified_blob) - .await - .unwrap(); - - let sidecar_event = blob_event_receiver.try_recv().unwrap(); - assert_eq!(sidecar_event, EventKind::BlobSidecar(expected_sse_blobs)); -} - /// Verifies that a data column event is emitted when a gossip verified data column is received via gossip or the publish block API. #[tokio::test] async fn data_column_sidecar_event_on_process_gossip_data_column() { diff --git a/beacon_node/beacon_processor/src/lib.rs b/beacon_node/beacon_processor/src/lib.rs index ce3851ea54..af3ff09c8a 100644 --- a/beacon_node/beacon_processor/src/lib.rs +++ b/beacon_node/beacon_processor/src/lib.rs @@ -390,7 +390,6 @@ pub enum Work { process_batch: Box>) + Send + Sync>, }, GossipBlock(AsyncFn), - GossipBlobSidecar(AsyncFn), GossipDataColumnSidecar(AsyncFn), GossipPartialDataColumnSidecar(AsyncFn), DelayedImportBlock { @@ -471,7 +470,6 @@ pub enum WorkType { UnknownLightClientOptimisticUpdate, GossipAggregateBatch, GossipBlock, - GossipBlobSidecar, GossipDataColumnSidecar, GossipPartialDataColumnSidecar, DelayedImportBlock, @@ -528,7 +526,6 @@ impl Work { Work::GossipAggregate { .. } => WorkType::GossipAggregate, Work::GossipAggregateBatch { .. } => WorkType::GossipAggregateBatch, Work::GossipBlock(_) => WorkType::GossipBlock, - Work::GossipBlobSidecar(_) => WorkType::GossipBlobSidecar, Work::GossipDataColumnSidecar(_) => WorkType::GossipDataColumnSidecar, Work::GossipPartialDataColumnSidecar(_) => WorkType::GossipPartialDataColumnSidecar, Work::DelayedImportBlock { .. } => WorkType::DelayedImportBlock, @@ -843,8 +840,6 @@ impl BeaconProcessor { } else if let Some(item) = work_queues.gossip_execution_payload_queue.pop() { Some(item) - } else if let Some(item) = work_queues.gossip_blob_queue.pop() { - Some(item) } else if let Some(item) = work_queues.gossip_data_column_queue.pop() { Some(item) } else if let Some(item) = @@ -1157,9 +1152,6 @@ impl BeaconProcessor { Work::GossipBlock { .. } => { work_queues.gossip_block_queue.push(work, work_id) } - Work::GossipBlobSidecar { .. } => { - work_queues.gossip_blob_queue.push(work, work_id) - } Work::GossipDataColumnSidecar { .. } => { work_queues.gossip_data_column_queue.push(work, work_id) } @@ -1306,7 +1298,6 @@ impl BeaconProcessor { } WorkType::GossipAggregateBatch => 0, // No queue WorkType::GossipBlock => work_queues.gossip_block_queue.len(), - WorkType::GossipBlobSidecar => work_queues.gossip_blob_queue.len(), WorkType::GossipDataColumnSidecar => { work_queues.gossip_data_column_queue.len() } @@ -1536,7 +1527,6 @@ impl BeaconProcessor { | Work::ColumnReconstruction(process_fn) => task_spawner.spawn_async(process_fn), Work::IgnoredRpcBlock { process_fn } => task_spawner.spawn_blocking(process_fn), Work::GossipBlock(work) - | Work::GossipBlobSidecar(work) | Work::GossipDataColumnSidecar(work) | Work::GossipPartialDataColumnSidecar(work) | Work::GossipExecutionPayload(work) => task_spawner.spawn_async(async move { diff --git a/beacon_node/beacon_processor/src/scheduler/work_queue.rs b/beacon_node/beacon_processor/src/scheduler/work_queue.rs index 2fdc15182c..ebd66e743d 100644 --- a/beacon_node/beacon_processor/src/scheduler/work_queue.rs +++ b/beacon_node/beacon_processor/src/scheduler/work_queue.rs @@ -125,7 +125,6 @@ pub struct BeaconProcessorQueueLengths { chain_segment_queue: usize, backfill_chain_segment: usize, gossip_block_queue: usize, - gossip_blob_queue: usize, gossip_data_column_queue: usize, gossip_partial_data_column_queue: usize, delayed_block_queue: usize, @@ -202,7 +201,6 @@ impl BeaconProcessorQueueLengths { chain_segment_queue: 64, backfill_chain_segment: 64, gossip_block_queue: 1024, - gossip_blob_queue: 1024, gossip_data_column_queue: 1024, gossip_partial_data_column_queue: 1024, delayed_block_queue: 1024, @@ -218,7 +216,7 @@ impl BeaconProcessorQueueLengths { payload_envelopes_brange_queue: 1024, payload_envelopes_broots_queue: 1024, gossip_bls_to_execution_change_queue: 16384, - // TODO(EIP-7732): verify 1024 is preferable. I used same value as `gossip_block_queue` and `gossip_blob_queue` + // TODO(EIP-7732): verify 1024 is preferable. gossip_execution_payload_queue: 1024, // TODO(EIP-7732) how big should this queue be? gossip_execution_payload_bid_queue: 1024, @@ -261,7 +259,6 @@ pub struct WorkQueues { pub chain_segment_queue: FifoQueue>, pub backfill_chain_segment: FifoQueue>, pub gossip_block_queue: FifoQueue>, - pub gossip_blob_queue: FifoQueue>, pub gossip_data_column_queue: FifoQueue>, pub gossip_partial_data_column_queue: FifoQueue>, pub delayed_block_queue: FifoQueue>, @@ -332,7 +329,6 @@ impl WorkQueues { let chain_segment_queue = FifoQueue::new(queue_lengths.chain_segment_queue); let backfill_chain_segment = FifoQueue::new(queue_lengths.backfill_chain_segment); let gossip_block_queue = FifoQueue::new(queue_lengths.gossip_block_queue); - let gossip_blob_queue = FifoQueue::new(queue_lengths.gossip_blob_queue); let gossip_data_column_queue = FifoQueue::new(queue_lengths.gossip_data_column_queue); let gossip_partial_data_column_queue = FifoQueue::new(queue_lengths.gossip_partial_data_column_queue); @@ -401,7 +397,6 @@ impl WorkQueues { column_reconstruction_queue, backfill_chain_segment, gossip_block_queue, - gossip_blob_queue, gossip_data_column_queue, gossip_partial_data_column_queue, delayed_block_queue, diff --git a/beacon_node/execution_layer/src/engine_api.rs b/beacon_node/execution_layer/src/engine_api.rs index 7337a29c8f..d9dd9aaf4c 100644 --- a/beacon_node/execution_layer/src/engine_api.rs +++ b/beacon_node/execution_layer/src/engine_api.rs @@ -1,12 +1,11 @@ use crate::engines::ForkchoiceState; use crate::http::{ ENGINE_FORKCHOICE_UPDATED_V1, ENGINE_FORKCHOICE_UPDATED_V2, ENGINE_FORKCHOICE_UPDATED_V3, - ENGINE_FORKCHOICE_UPDATED_V4, ENGINE_GET_BLOBS_V1, ENGINE_GET_BLOBS_V2, - 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_GET_PAYLOAD_V5, ENGINE_GET_PAYLOAD_V6, - ENGINE_NEW_PAYLOAD_V1, ENGINE_NEW_PAYLOAD_V2, ENGINE_NEW_PAYLOAD_V3, ENGINE_NEW_PAYLOAD_V4, - ENGINE_NEW_PAYLOAD_V5, + ENGINE_FORKCHOICE_UPDATED_V4, ENGINE_GET_BLOBS_V2, 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_GET_PAYLOAD_V5, ENGINE_GET_PAYLOAD_V6, 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, @@ -607,7 +606,6 @@ pub struct EngineCapabilities { pub get_payload_v5: bool, pub get_payload_v6: bool, pub get_client_version_v1: bool, - pub get_blobs_v1: bool, pub get_blobs_v2: bool, pub get_blobs_v3: bool, } @@ -669,9 +667,6 @@ 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); - } if self.get_blobs_v2 { response.push(ENGINE_GET_BLOBS_V2); } diff --git a/beacon_node/execution_layer/src/engine_api/http.rs b/beacon_node/execution_layer/src/engine_api/http.rs index 7c63f78a22..8df7d2a54b 100644 --- a/beacon_node/execution_layer/src/engine_api/http.rs +++ b/beacon_node/execution_layer/src/engine_api/http.rs @@ -62,7 +62,6 @@ 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_V2: &str = "engine_getBlobsV2"; pub const ENGINE_GET_BLOBS_V3: &str = "engine_getBlobsV3"; pub const ENGINE_GET_BLOBS_TIMEOUT: Duration = Duration::from_secs(1); @@ -92,7 +91,6 @@ 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, ENGINE_GET_BLOBS_V2, ENGINE_GET_BLOBS_V3, ]; @@ -717,20 +715,6 @@ impl HttpJsonRpc { } } - pub async fn get_blobs_v1( - &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_blobs_v2( &self, versioned_hashes: Vec, @@ -1272,7 +1256,6 @@ impl HttpJsonRpc { get_payload_v5: capabilities.contains(ENGINE_GET_PAYLOAD_V5), get_payload_v6: capabilities.contains(ENGINE_GET_PAYLOAD_V6), get_client_version_v1: capabilities.contains(ENGINE_GET_CLIENT_VERSION_V1), - get_blobs_v1: capabilities.contains(ENGINE_GET_BLOBS_V1), get_blobs_v2: capabilities.contains(ENGINE_GET_BLOBS_V2), get_blobs_v3: capabilities.contains(ENGINE_GET_BLOBS_V3), }) diff --git a/beacon_node/execution_layer/src/lib.rs b/beacon_node/execution_layer/src/lib.rs index b1b8b0deaa..78076bee6c 100644 --- a/beacon_node/execution_layer/src/lib.rs +++ b/beacon_node/execution_layer/src/lib.rs @@ -4,7 +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, BlobAndProofV2, BlobAndProofV3}; +use crate::json_structures::{BlobAndProofV2, BlobAndProofV3}; use crate::payload_cache::PayloadCache; use arc_swap::ArcSwapOption; use auth::{Auth, JwtKey, strip_prefix}; @@ -1722,23 +1722,6 @@ impl ExecutionLayer { } } - pub async fn get_blobs_v1( - &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_v1(query).await }) - .await - .map_err(Box::new) - .map_err(Error::EngineError) - } else { - Err(Error::GetBlobsNotSupported) - } - } - pub async fn get_blobs_v2( &self, query: Vec, 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 4a46ce0f88..b05db6e8bd 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 @@ -565,20 +565,16 @@ impl ExecutionBlockGenerator { self.insert_block(Block::PoS(payload))?; } - // Post-Gloas, the justified and finalized block hashes must be non-zero, since the - // CL always has a known parent_block_hash to reference. - if let Some(head_block) = self.blocks.get(&head_block_hash) - && self - .get_fork_at_timestamp(head_block.timestamp()) - .gloas_enabled() - { + // If Gloas was enabled from genesis, the justified and finalized block hashes must be + // non-zero, since the CL always has a known parent_block_hash to reference. + if self.get_fork_at_timestamp(0).gloas_enabled() { assert!( forkchoice_state.safe_block_hash != ExecutionBlockHash::zero(), - "post-Gloas safe_block_hash must not be zero" + "for Gloas genesis safe_block_hash must not be zero" ); assert!( forkchoice_state.finalized_block_hash != ExecutionBlockHash::zero(), - "post-Gloas finalized_block_hash must not be zero" + "for Gloas genesis finalized_block_hash must not be zero" ); } 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 64eecccc58..9924fbe474 100644 --- a/beacon_node/execution_layer/src/test_utils/handle_rpc.rs +++ b/beacon_node/execution_layer/src/test_utils/handle_rpc.rs @@ -494,20 +494,6 @@ pub async fn handle_rpc( _ => unreachable!(), } } - ENGINE_GET_BLOBS_V1 => { - let versioned_hashes = - get_param::>(params, 0).map_err(|s| (s, BAD_PARAMS_ERROR_CODE))?; - let generator = ctx.execution_block_generator.read(); - // V1: per-element nullable array, positionally matching the request. - let response: Vec>> = versioned_hashes - .iter() - .map(|hash| match generator.get_blob_and_proof(hash) { - Some(BlobAndProof::V1(v1)) => Some(v1), - _ => None, - }) - .collect(); - Ok(serde_json::to_value(response).unwrap()) - } ENGINE_GET_BLOBS_V2 => { let versioned_hashes = get_param::>(params, 0).map_err(|s| (s, BAD_PARAMS_ERROR_CODE))?; diff --git a/beacon_node/execution_layer/src/test_utils/mod.rs b/beacon_node/execution_layer/src/test_utils/mod.rs index 4eb03778f8..570008b62a 100644 --- a/beacon_node/execution_layer/src/test_utils/mod.rs +++ b/beacon_node/execution_layer/src/test_utils/mod.rs @@ -57,7 +57,6 @@ pub const DEFAULT_ENGINE_CAPABILITIES: EngineCapabilities = EngineCapabilities { get_payload_v5: true, get_payload_v6: true, get_client_version_v1: true, - get_blobs_v1: true, get_blobs_v2: true, get_blobs_v3: true, }; diff --git a/beacon_node/http_api/src/block_id.rs b/beacon_node/http_api/src/block_id.rs index ca980b96a4..8843541c11 100644 --- a/beacon_node/http_api/src/block_id.rs +++ b/beacon_node/http_api/src/block_id.rs @@ -455,20 +455,18 @@ impl BlockId { warp_utils::reject::custom_not_found(format!("no blobs stored for block {root}")) })?; - let blob_sidecar_list_filtered = match indices { - Some(vec) => { - let list: Vec<_> = vec - .into_iter() - .flat_map(|index| blob_sidecar_list.get(index as usize).cloned()) - .collect(); + let blob_sidecar_list: Vec<_> = blob_sidecar_list.into_iter().collect(); - BlobSidecarList::new(list, max_blobs_per_block) - .map_err(|e| warp_utils::reject::custom_server_error(format!("{:?}", e)))? - } + let blob_sidecar_list = match indices { + Some(indices) => indices + .into_iter() + .filter_map(|i| blob_sidecar_list.get(i as usize).cloned()) + .collect(), None => blob_sidecar_list, }; - Ok(blob_sidecar_list_filtered) + BlobSidecarList::new(blob_sidecar_list, max_blobs_per_block) + .map_err(|e| warp_utils::reject::custom_server_error(format!("{:?}", e))) } fn get_blobs_from_data_columns( diff --git a/beacon_node/http_api/src/publish_blocks.rs b/beacon_node/http_api/src/publish_blocks.rs index ca4ab85524..b46576ddad 100644 --- a/beacon_node/http_api/src/publish_blocks.rs +++ b/beacon_node/http_api/src/publish_blocks.rs @@ -1,7 +1,6 @@ use crate::metrics; use std::future::Future; -use beacon_chain::blob_verification::{GossipBlobError, GossipVerifiedBlob}; use beacon_chain::block_verification_types::{AsBlock, LookupBlock}; use beacon_chain::data_column_verification::GossipVerifiedDataColumn; use beacon_chain::validator_monitor::get_block_delay_ms; @@ -26,13 +25,12 @@ use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use std::time::Duration; use tokio::sync::mpsc::UnboundedSender; -use tracing::{Span, debug, debug_span, error, field, info, instrument, warn}; +use tracing::{Span, debug, error, field, info, instrument, warn}; use tree_hash::TreeHash; use types::{ - AbstractExecPayload, BeaconBlockRef, BlobSidecar, BlobsList, BlockImportSource, - DataColumnSidecar, DataColumnSubnetId, EthSpec, ExecPayload, ExecutionBlockHash, ForkName, - FullPayload, FullPayloadBellatrix, Hash256, KzgProofs, SignedBeaconBlock, - SignedBlindedBeaconBlock, + AbstractExecPayload, BeaconBlockRef, BlobsList, BlockImportSource, DataColumnSidecar, + DataColumnSubnetId, EthSpec, ExecPayload, ExecutionBlockHash, ForkName, FullPayload, + FullPayloadBellatrix, Hash256, KzgProofs, SignedBeaconBlock, SignedBlindedBeaconBlock, }; use warp::{Rejection, Reply, reply::Response}; @@ -195,23 +193,8 @@ pub async fn publish_block>( 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() { - 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)) - } else { - error!(reason = &msg, "Invalid blob provided to HTTP API"); - Err(warp_utils::reject::custom_bad_request(msg)) - }; - } - } + // Wait for columns to get gossip verified before proceeding further as we need them for import. + let gossip_verified_columns = build_sidecar_task_handle.await?; if !gossip_verified_columns.is_empty() { if let Some(data_column_publishing_delay) = data_column_publishing_delay_for_testing { @@ -342,18 +325,9 @@ pub async fn publish_block>( } } -type BuildDataSidecarTaskResult = Result< - ( - Vec>>, - Vec>, - ), - Rejection, ->; +type BuildDataSidecarTaskResult = Result>, Rejection>; -/// Convert blobs to either: -/// -/// 1. Blob sidecars if prior to peer DAS, or -/// 2. Data column sidecars if post peer DAS. +/// Convert blobs to data column sidecars. fn spawn_build_data_sidecar_task( chain: Arc>, block: Arc>>, @@ -365,22 +339,9 @@ fn spawn_build_data_sidecar_task( .spawn_blocking_handle( move || { let Some((kzg_proofs, blobs)) = proofs_and_blobs else { - return Ok((vec![], vec![])); + return Ok(vec![]); }; - let _span = debug_span!("build_data_sidecars").entered(); - - 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)?; - Ok((gossip_verified_blobs, vec![])) - } else { - // Post PeerDAS: construct data columns. - let gossip_verified_data_columns = - build_data_columns(&chain, &block, blobs, kzg_proofs)?; - Ok((vec![], gossip_verified_data_columns)) - } + build_data_columns(&chain, &block, blobs, kzg_proofs) }, "build_data_sidecars", ) @@ -424,76 +385,6 @@ fn build_data_columns( Ok(gossip_verified_data_columns) } -fn build_gossip_verified_blobs( - chain: &BeaconChain, - block: &SignedBeaconBlock>, - blobs: BlobsList, - kzg_proofs: KzgProofs, -) -> 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!( - error = ?e, - blob_index = i, - %slot, - "Invalid blob - not publishing block" - ); - 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!( - blob_index = blob_sidecar.index, - %slot, - proposer, - "Blob for publication already known" - ); - Ok(None) - } - Err(e) => { - error!( - blob_index = blob_sidecar.index, - %slot, - error = ?e, - "Blob for publication is gossip-invalid" - ); - Err(warp_utils::reject::custom_bad_request(e.to_string())) - } - } - }) - .collect::, Rejection>>()?; - - Ok(gossip_verified_blobs) -} - -fn publish_blob_sidecars( - sender_clone: &UnboundedSender>, - blob: &GossipVerifiedBlob, -) -> Result<(), BlockError> { - let pubsub_message = PubsubMessage::BlobSidecar(Box::new((blob.index(), blob.clone_blob()))); - crate::utils::publish_pubsub_message(sender_clone, pubsub_message) - .map_err(|_| BlockError::BeaconChainError(Box::new(BeaconChainError::UnableToPublish))) -} - pub(crate) fn publish_column_sidecars( sender_clone: &UnboundedSender>, data_column_sidecars: &[GossipVerifiedDataColumn], diff --git a/beacon_node/http_api/tests/broadcast_validation_tests.rs b/beacon_node/http_api/tests/broadcast_validation_tests.rs index a189be1cfc..98629a1c5e 100644 --- a/beacon_node/http_api/tests/broadcast_validation_tests.rs +++ b/beacon_node/http_api/tests/broadcast_validation_tests.rs @@ -1587,7 +1587,7 @@ pub async fn block_seen_on_gossip_without_blobs_or_columns() { let state = tester.harness.get_current_state(); let fork_name = state.fork_name(&tester.harness.spec).unwrap(); // Gloas blocks don't carry blobs (execution data comes via envelopes). - if !fork_name.deneb_enabled() || fork_name.gloas_enabled() { + if !fork_name.fulu_enabled() || fork_name.gloas_enabled() { return; } @@ -1647,7 +1647,7 @@ pub async fn block_seen_on_gossip_without_blobs_or_columns() { /// This test checks that an HTTP POST request with the block & blobs/columns succeeds with a 200 response /// even if the block has already been seen on gossip without all blobs/columns. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -pub async fn block_seen_on_gossip_with_some_blobs_or_columns() { +pub async fn block_seen_on_gossip_with_columns() { let validation_level: Option = Some(BroadcastValidation::Gossip); // Validator count needs to be at least 32 or proposer boost gets set to 0 when computing @@ -1658,7 +1658,7 @@ pub async fn block_seen_on_gossip_with_some_blobs_or_columns() { let state = tester.harness.get_current_state(); let fork_name = state.fork_name(&tester.harness.spec).unwrap(); // Gloas blocks don't carry blobs (execution data comes via envelopes). - if !fork_name.deneb_enabled() || fork_name.gloas_enabled() { + if !fork_name.fulu_enabled() || fork_name.gloas_enabled() { return; } @@ -1690,9 +1690,6 @@ pub async fn block_seen_on_gossip_with_some_blobs_or_columns() { blobs.0.len() ); - let partial_kzg_proofs = [*blobs.0.first().unwrap()]; - let partial_blobs = [blobs.1.first().unwrap().clone()]; - // Simulate the block being seen on gossip. block .clone() @@ -1702,12 +1699,7 @@ pub async fn block_seen_on_gossip_with_some_blobs_or_columns() { // Simulate some of the blobs being seen on gossip. tester .harness - .process_gossip_blobs_or_columns( - &block, - partial_blobs.iter(), - partial_kzg_proofs.iter(), - Some(get_custody_columns(&tester, block.slot())), - ) + .process_gossip_columns(&block, Some(get_custody_columns(&tester, block.slot()))) .await; // It should not yet be added to fork choice because all blobs have not been seen. @@ -1740,7 +1732,7 @@ pub async fn block_seen_on_gossip_with_some_blobs_or_columns() { /// This test checks that an HTTP POST request with the block & blobs/columns succeeds with a 200 response /// even if the blobs/columns have already been seen on gossip. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -pub async fn blobs_or_columns_seen_on_gossip_without_block() { +pub async fn columns_seen_on_gossip_without_block() { let spec = test_spec::(); let validation_level: Option = Some(BroadcastValidation::Gossip); @@ -1752,7 +1744,7 @@ pub async fn blobs_or_columns_seen_on_gossip_without_block() { let state = tester.harness.get_current_state(); let fork_name = state.fork_name(&tester.harness.spec).unwrap(); // Gloas blocks don't carry blobs (execution data comes via envelopes). - if !fork_name.deneb_enabled() || fork_name.gloas_enabled() { + if !fork_name.fulu_enabled() || fork_name.gloas_enabled() { return; } @@ -1778,12 +1770,7 @@ pub async fn blobs_or_columns_seen_on_gossip_without_block() { // Simulate the blobs being seen on gossip. tester .harness - .process_gossip_blobs_or_columns( - &block, - blobs.iter(), - kzg_proofs.iter(), - Some(get_custody_columns(&tester, block.slot())), - ) + .process_gossip_columns(&block, Some(get_custody_columns(&tester, block.slot()))) .await; // It should not yet be added to fork choice because the block has not been seen. @@ -1816,7 +1803,7 @@ pub async fn blobs_or_columns_seen_on_gossip_without_block() { /// This test checks that an HTTP POST request with the block succeeds with a 200 response /// if just the blobs have already been seen on gossip. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn blobs_or_columns_seen_on_gossip_without_block_and_no_http_blobs_or_columns() { +async fn columns_seen_on_gossip_without_block_and_no_http_columns() { let validation_level: Option = Some(BroadcastValidation::Gossip); // Validator count needs to be at least 32 or proposer boost gets set to 0 when computing @@ -1827,7 +1814,7 @@ async fn blobs_or_columns_seen_on_gossip_without_block_and_no_http_blobs_or_colu let state = tester.harness.get_current_state(); let fork_name = state.fork_name(&tester.harness.spec).unwrap(); // Gloas blocks don't carry blobs (execution data comes via envelopes). - if !fork_name.deneb_enabled() || fork_name.gloas_enabled() { + if !fork_name.fulu_enabled() || fork_name.gloas_enabled() { return; } @@ -1848,18 +1835,13 @@ async fn blobs_or_columns_seen_on_gossip_without_block_and_no_http_blobs_or_colu let state_a = tester.harness.get_current_state(); let ((block, blobs), _) = tester.harness.make_block(state_a, slot_b).await; - let (kzg_proofs, blobs) = blobs.expect("should have some blobs"); + let (_, blobs) = blobs.expect("should have some blobs"); assert!(!blobs.is_empty()); // Simulate the blobs being seen on gossip. tester .harness - .process_gossip_blobs_or_columns( - &block, - blobs.iter(), - kzg_proofs.iter(), - Some(get_custody_columns(&tester, block.slot())), - ) + .process_gossip_columns(&block, Some(get_custody_columns(&tester, block.slot()))) .await; // It should not yet be added to fork choice because the block has not been seen. @@ -1893,7 +1875,7 @@ async fn blobs_or_columns_seen_on_gossip_without_block_and_no_http_blobs_or_colu } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn slashable_blobs_or_columns_seen_on_gossip_cause_failure() { +async fn slashable_columns_seen_on_gossip_cause_failure() { let validation_level: Option = Some(BroadcastValidation::ConsensusAndEquivocation); @@ -1905,7 +1887,7 @@ async fn slashable_blobs_or_columns_seen_on_gossip_cause_failure() { let state = tester.harness.get_current_state(); let fork_name = state.fork_name(&tester.harness.spec).unwrap(); // Gloas blocks don't carry blobs (execution data comes via envelopes). - if !fork_name.deneb_enabled() || fork_name.gloas_enabled() { + if !fork_name.fulu_enabled() || fork_name.gloas_enabled() { return; } @@ -1926,19 +1908,13 @@ async fn slashable_blobs_or_columns_seen_on_gossip_cause_failure() { let state_a = tester.harness.get_current_state(); let ((block_a, blobs_a), _) = tester.harness.make_block(state_a.clone(), slot_b).await; - let ((block_b, blobs_b), _) = tester.harness.make_block(state_a, slot_b).await; + let ((block_b, _), _) = tester.harness.make_block(state_a, slot_b).await; let (kzg_proofs_a, blobs_a) = blobs_a.expect("should have some blobs"); - let (kzg_proofs_b, blobs_b) = blobs_b.expect("should have some blobs"); // Simulate the blobs of block B being seen on gossip. tester .harness - .process_gossip_blobs_or_columns( - &block_b, - blobs_b.iter(), - kzg_proofs_b.iter(), - Some(get_custody_columns(&tester, block_b.slot())), - ) + .process_gossip_columns(&block_b, Some(get_custody_columns(&tester, block_b.slot()))) .await; // It should not yet be added to fork choice because block B has not been seen. @@ -1984,7 +1960,7 @@ pub async fn duplicate_block_status_code() { // Gloas blocks don't carry blobs (execution data comes via envelopes). let spec = test_spec::(); let genesis_fork = spec.fork_name_at_slot::(Slot::new(0)); - if !genesis_fork.deneb_enabled() || genesis_fork.gloas_enabled() { + if !genesis_fork.fulu_enabled() || genesis_fork.gloas_enabled() { return; } diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 3da0841a4e..06b3a6197b 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -9167,11 +9167,17 @@ async fn builder_works_post_deneb() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn get_blob_sidecars() { - let mut config = ApiTesterConfig::default(); + let mut config = ApiTesterConfig { + retain_historic_states: false, + spec: E::default_spec(), + node_custody_type: NodeCustodyType::Supernode, + }; 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)); + config.spec.fulu_fork_epoch = Some(Epoch::new(0)); ApiTester::new_from_config(config) .await diff --git a/beacon_node/lighthouse_network/src/service/gossip_cache.rs b/beacon_node/lighthouse_network/src/service/gossip_cache.rs index e9862e3f74..4b96fe884e 100644 --- a/beacon_node/lighthouse_network/src/service/gossip_cache.rs +++ b/beacon_node/lighthouse_network/src/service/gossip_cache.rs @@ -20,8 +20,6 @@ pub struct GossipCache { topic_msgs: HashMap, Key>>, /// Timeout for blocks. beacon_block: Option, - /// Timeout for blobs. - blob_sidecar: Option, /// Timeout for data columns. data_column_sidecar: Option, /// Timeout for aggregate attestations. @@ -59,8 +57,6 @@ pub struct GossipCacheBuilder { default_timeout: Option, /// Timeout for blocks. beacon_block: Option, - /// Timeout for blob sidecars. - blob_sidecar: Option, /// Timeout for data column sidecars. data_column_sidecar: Option, /// Timeout for aggregate attestations. @@ -195,7 +191,6 @@ impl GossipCacheBuilder { let GossipCacheBuilder { default_timeout, beacon_block, - blob_sidecar, data_column_sidecar, aggregates, attestation, @@ -216,7 +211,6 @@ impl GossipCacheBuilder { expirations: DelayQueue::default(), topic_msgs: HashMap::default(), beacon_block: beacon_block.or(default_timeout), - blob_sidecar: blob_sidecar.or(default_timeout), data_column_sidecar: data_column_sidecar.or(default_timeout), aggregates: aggregates.or(default_timeout), attestation: attestation.or(default_timeout), @@ -247,7 +241,6 @@ impl GossipCache { pub fn insert(&mut self, topic: GossipTopic, data: Vec) { let expire_timeout = match topic.kind() { GossipKind::BeaconBlock => self.beacon_block, - GossipKind::BlobSidecar(_) => self.blob_sidecar, GossipKind::DataColumnSidecar(_) => self.data_column_sidecar, GossipKind::BeaconAggregateAndProof => self.aggregates, GossipKind::Attestation(_) => self.attestation, diff --git a/beacon_node/lighthouse_network/src/service/utils.rs b/beacon_node/lighthouse_network/src/service/utils.rs index d235e4b28f..c7dabcb391 100644 --- a/beacon_node/lighthouse_network/src/service/utils.rs +++ b/beacon_node/lighthouse_network/src/service/utils.rs @@ -284,10 +284,6 @@ pub(crate) fn create_whitelist_filter( for id in 0..sync_committee_subnet_count { add(SyncCommitteeMessage(SyncSubnetId::new(id))); } - let blob_subnet_count = spec.blob_sidecar_subnet_count_max(); - for id in 0..blob_subnet_count { - add(BlobSidecar(id)); - } for id in 0..spec.data_column_sidecar_subnet_count { add(DataColumnSidecar(DataColumnSubnetId::new(id))); } diff --git a/beacon_node/lighthouse_network/src/types/pubsub.rs b/beacon_node/lighthouse_network/src/types/pubsub.rs index e5a703ff1e..043d1cfb88 100644 --- a/beacon_node/lighthouse_network/src/types/pubsub.rs +++ b/beacon_node/lighthouse_network/src/types/pubsub.rs @@ -7,10 +7,10 @@ use ssz::{Decode, Encode}; use std::io::{Error, ErrorKind}; use std::sync::Arc; use types::{ - AttesterSlashing, AttesterSlashingBase, AttesterSlashingElectra, BlobSidecar, - DataColumnSidecar, DataColumnSubnetId, EthSpec, ForkContext, ForkName, Hash256, - LightClientFinalityUpdate, LightClientOptimisticUpdate, PartialDataColumn, - PartialDataColumnSidecar, PayloadAttestationMessage, ProposerSlashing, SignedAggregateAndProof, + AttesterSlashing, AttesterSlashingBase, AttesterSlashingElectra, DataColumnSidecar, + DataColumnSubnetId, EthSpec, ForkContext, ForkName, Hash256, LightClientFinalityUpdate, + LightClientOptimisticUpdate, PartialDataColumn, PartialDataColumnSidecar, + PayloadAttestationMessage, ProposerSlashing, SignedAggregateAndProof, SignedAggregateAndProofBase, SignedAggregateAndProofElectra, SignedBeaconBlock, SignedBeaconBlockAltair, SignedBeaconBlockBase, SignedBeaconBlockBellatrix, SignedBeaconBlockCapella, SignedBeaconBlockDeneb, SignedBeaconBlockElectra, @@ -24,8 +24,6 @@ use types::{ pub enum PubsubMessage { /// Gossipsub message providing notification of a new block. BeaconBlock(Arc>), - /// Gossipsub message providing notification of a [`BlobSidecar`] along with the subnet id where it was received. - BlobSidecar(Box<(u64, Arc>)>), /// Gossipsub message providing notification of a [`DataColumnSidecar`] along with the subnet id where it was received. DataColumnSidecar(Box<(DataColumnSubnetId, Arc>)>), /// Gossipsub message providing notification of a Aggregate attestation and associated proof. @@ -139,9 +137,6 @@ impl PubsubMessage { pub fn kind(&self) -> GossipKind { match self { PubsubMessage::BeaconBlock(_) => GossipKind::BeaconBlock, - PubsubMessage::BlobSidecar(blob_sidecar_data) => { - GossipKind::BlobSidecar(blob_sidecar_data.0) - } PubsubMessage::DataColumnSidecar(column_sidecar_data) => { GossipKind::DataColumnSidecar(column_sidecar_data.0) } @@ -266,26 +261,6 @@ impl PubsubMessage { }; Ok(PubsubMessage::BeaconBlock(Arc::new(beacon_block))) } - GossipKind::BlobSidecar(blob_index) => { - if let Some(fork_name) = - fork_context.get_fork_from_context_bytes(gossip_topic.fork_digest) - && fork_name.deneb_enabled() - { - let blob_sidecar = Arc::new( - BlobSidecar::from_ssz_bytes(data) - .map_err(|e| format!("{:?}", e))?, - ); - return Ok(PubsubMessage::BlobSidecar(Box::new(( - *blob_index, - blob_sidecar, - )))); - } - - Err(format!( - "beacon_blobs_and_sidecar topic invalid for given fork digest {:?}", - gossip_topic.fork_digest - )) - } GossipKind::DataColumnSidecar(subnet_id) => { match fork_context.get_fork_from_context_bytes(gossip_topic.fork_digest) { Some(fork) if fork.fulu_enabled() => { @@ -444,7 +419,6 @@ impl PubsubMessage { // messages for us. match &self { PubsubMessage::BeaconBlock(data) => data.as_ssz_bytes(), - PubsubMessage::BlobSidecar(data) => data.1.as_ssz_bytes(), PubsubMessage::DataColumnSidecar(data) => data.1.as_ssz_bytes(), PubsubMessage::AggregateAndProofAttestation(data) => data.as_ssz_bytes(), PubsubMessage::VoluntaryExit(data) => data.as_ssz_bytes(), @@ -502,12 +476,6 @@ impl std::fmt::Display for PubsubMessage { block.slot(), block.message().proposer_index() ), - PubsubMessage::BlobSidecar(data) => write!( - f, - "BlobSidecar: slot: {}, blob index: {}", - data.1.slot(), - data.1.index, - ), PubsubMessage::DataColumnSidecar(data) => write!( f, "DataColumnSidecar: slot: {}, column index: {}", diff --git a/beacon_node/lighthouse_network/src/types/topics.rs b/beacon_node/lighthouse_network/src/types/topics.rs index b51c459a80..1a5acd79b4 100644 --- a/beacon_node/lighthouse_network/src/types/topics.rs +++ b/beacon_node/lighthouse_network/src/types/topics.rs @@ -21,7 +21,6 @@ pub const SSZ_SNAPPY_ENCODING_POSTFIX: &str = "ssz_snappy"; pub const BEACON_BLOCK_TOPIC: &str = "beacon_block"; pub const BEACON_AGGREGATE_AND_PROOF_TOPIC: &str = "beacon_aggregate_and_proof"; pub const BEACON_ATTESTATION_PREFIX: &str = "beacon_attestation_"; -pub const BLOB_SIDECAR_PREFIX: &str = "blob_sidecar_"; pub const DATA_COLUMN_SIDECAR_PREFIX: &str = "data_column_sidecar_"; pub const VOLUNTARY_EXIT_TOPIC: &str = "voluntary_exit"; pub const PROPOSER_SLASHING_TOPIC: &str = "proposer_slashing"; @@ -82,13 +81,6 @@ pub fn core_topics_to_subscribe( topics.push(GossipKind::BlsToExecutionChange); } - if fork_name.deneb_enabled() && !fork_name.fulu_enabled() { - // All of deneb blob topics are core topics - for i in 0..spec.blob_sidecar_subnet_count(fork_name) { - topics.push(GossipKind::BlobSidecar(i)); - } - } - if fork_name.fulu_enabled() { for subnet in &opts.sampling_subnets { topics.push(GossipKind::DataColumnSidecar(*subnet)); @@ -118,7 +110,6 @@ pub fn is_fork_non_core_topic(topic: &GossipTopic, _fork_name: ForkName) -> bool // All these topics are core-only GossipKind::BeaconBlock | GossipKind::BeaconAggregateAndProof - | GossipKind::BlobSidecar(_) | GossipKind::DataColumnSidecar(_) | GossipKind::VoluntaryExit | GossipKind::ProposerSlashing @@ -166,8 +157,6 @@ pub enum GossipKind { BeaconBlock, /// Topic for publishing aggregate attestations and proofs. BeaconAggregateAndProof, - /// Topic for publishing BlobSidecars. - BlobSidecar(u64), /// Topic for publishing DataColumnSidecars. DataColumnSidecar(DataColumnSubnetId), /// Topic for publishing raw attestations on a particular subnet. @@ -216,9 +205,6 @@ impl std::fmt::Display for GossipKind { GossipKind::SyncCommitteeMessage(subnet_id) => { write!(f, "sync_committee_{}", **subnet_id) } - GossipKind::BlobSidecar(blob_index) => { - write!(f, "{}{}", BLOB_SIDECAR_PREFIX, blob_index) - } GossipKind::DataColumnSidecar(column_subnet_id) => { write!(f, "{}{}", DATA_COLUMN_SIDECAR_PREFIX, **column_subnet_id) } @@ -349,9 +335,6 @@ impl std::fmt::Display for GossipTopic { GossipKind::SyncCommitteeMessage(index) => { format!("{}{}", SYNC_COMMITTEE_PREFIX_TOPIC, *index) } - GossipKind::BlobSidecar(blob_index) => { - format!("{}{}", BLOB_SIDECAR_PREFIX, blob_index) - } GossipKind::DataColumnSidecar(column_subnet_id) => { format!("{}{}", DATA_COLUMN_SIDECAR_PREFIX, *column_subnet_id) } @@ -401,8 +384,6 @@ fn subnet_topic_index(topic: &str) -> Option { return Some(GossipKind::SyncCommitteeMessage(SyncSubnetId::new( index.parse::().ok()?, ))); - } else if let Some(index) = topic.strip_prefix(BLOB_SIDECAR_PREFIX) { - return Some(GossipKind::BlobSidecar(index.parse::().ok()?)); } else if let Some(index) = topic.strip_prefix(DATA_COLUMN_SIDECAR_PREFIX) { return Some(GossipKind::DataColumnSidecar(DataColumnSubnetId::new( index.parse::().ok()?, @@ -576,17 +557,6 @@ mod tests { } } - #[test] - fn blobs_are_not_subscribed_in_peerdas() { - let spec = get_spec(); - let s = get_sampling_subnets(); - let topic_config = get_topic_config(&s); - assert!( - !core_topics_to_subscribe::(ForkName::Fulu, &topic_config, &spec,) - .contains(&GossipKind::BlobSidecar(0)) - ); - } - #[test] fn columns_are_subscribed_in_peerdas() { let spec = get_spec(); diff --git a/beacon_node/network/src/metrics.rs b/beacon_node/network/src/metrics.rs index 4b34d7bfc0..c043133cee 100644 --- a/beacon_node/network/src/metrics.rs +++ b/beacon_node/network/src/metrics.rs @@ -128,13 +128,6 @@ pub static BEACON_PROCESSOR_GOSSIP_BLOCK_EARLY_SECONDS: LazyLock> = - LazyLock::new(|| { - try_create_int_counter( - "beacon_processor_gossip_blob_verified_total", - "Total number of gossip blob verified for propagation.", - ) - }); pub static BEACON_PROCESSOR_GOSSIP_DATA_COLUMN_SIDECAR_VERIFIED_TOTAL: LazyLock< Result, > = LazyLock::new(|| { @@ -600,12 +593,6 @@ pub static BEACON_BLOCK_DELAY_GOSSIP_ARRIVED_LATE_TOTAL: LazyLock> = LazyLock::new(|| { - try_create_int_gauge( - "beacon_blob_delay_gossip_last_delay", - "The first time we see this blob as a delay from the start of the slot", - ) -}); pub static BEACON_DATA_COLUMN_GOSSIP_PROPAGATION_VERIFICATION_DELAY_TIME: LazyLock< Result, @@ -664,14 +651,6 @@ pub static BEACON_USEFUL_FULL_COLUMNS_RECEIVED_TOTAL: LazyLock> = LazyLock::new( - || { - try_create_int_gauge( - "beacon_blob_delay_gossip_verification", - "Keeps track of the time delay from the start of the slot to the point we propagate the blob", - ) - }, -); pub static BEACON_BLOB_DELAY_FULL_VERIFICATION: LazyLock> = LazyLock::new(|| { try_create_int_gauge( "beacon_blob_last_full_verification_delay", @@ -695,15 +674,6 @@ pub static BEACON_BLOB_RPC_SLOT_START_DELAY_TIME: LazyLock> = }, ); -pub static BEACON_BLOB_GOSSIP_ARRIVED_LATE_TOTAL: LazyLock> = LazyLock::new( - || { - try_create_int_counter( - "beacon_blob_gossip_arrived_late_total", - "Count of times when a gossip blob arrived from the network later than the attestation deadline.", - ) - }, -); - /* * Light client update reprocessing queue metrics. */ 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 71216b47a7..65c95eff35 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -11,6 +11,9 @@ use beacon_chain::data_column_verification::{ PartialColumnVerificationResult, }; use beacon_chain::payload_bid_verification::PayloadBidError; +use beacon_chain::payload_envelope_verification::{ + EnvelopeError, gossip_verified_envelope::GossipVerifiedEnvelope, +}; use beacon_chain::proposer_preferences_verification::ProposerPreferencesError; use beacon_chain::store::Error; use beacon_chain::{ @@ -27,12 +30,6 @@ use beacon_chain::{ sync_committee_verification::{self, Error as SyncCommitteeError}, validator_monitor::{get_block_delay_ms, get_slot_delay_ms}, }; -use beacon_chain::{ - blob_verification::{GossipBlobError, GossipVerifiedBlob}, - payload_envelope_verification::{ - EnvelopeError, gossip_verified_envelope::GossipVerifiedEnvelope, - }, -}; use beacon_processor::{Work, WorkEvent}; use lighthouse_network::{ Client, GossipTopic, MessageAcceptance, MessageId, PeerAction, PeerId, PubsubMessage, @@ -50,13 +47,13 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use store::hot_cold_store::HotColdDBError; use tracing::{Instrument, Span, debug, error, info, instrument, trace, warn}; use types::{ - Attestation, AttestationData, AttestationRef, AttesterSlashing, BlobSidecar, ColumnIndex, - DataColumnSidecar, DataColumnSubnetId, EthSpec, Hash256, IndexedAttestation, - LightClientFinalityUpdate, LightClientOptimisticUpdate, PartialDataColumn, - PartialDataColumnHeader, PayloadAttestationMessage, ProposerSlashing, SignedAggregateAndProof, - SignedBeaconBlock, SignedBlsToExecutionChange, SignedContributionAndProof, - SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope, SignedProposerPreferences, - SignedVoluntaryExit, SingleAttestation, Slot, SubnetId, SyncCommitteeMessage, SyncSubnetId, + Attestation, AttestationData, AttestationRef, AttesterSlashing, ColumnIndex, DataColumnSidecar, + DataColumnSubnetId, EthSpec, Hash256, IndexedAttestation, LightClientFinalityUpdate, + LightClientOptimisticUpdate, PartialDataColumn, PartialDataColumnHeader, + PayloadAttestationMessage, ProposerSlashing, SignedAggregateAndProof, SignedBeaconBlock, + SignedBlsToExecutionChange, SignedContributionAndProof, SignedExecutionPayloadBid, + SignedExecutionPayloadEnvelope, SignedProposerPreferences, SignedVoluntaryExit, + SingleAttestation, Slot, SubnetId, SyncCommitteeMessage, SyncSubnetId, block::BlockImportSource, }; @@ -844,13 +841,109 @@ impl NetworkBeaconProcessor { } } - #[instrument( - name = "lh_process_gossip_partial_data_column", - parent = None, - level = "debug", - skip_all, - fields(block_root = ?column.block_root, index = column.index), - )] + async fn process_gossip_verified_data_column( + self: &Arc, + peer_id: PeerId, + verified_data_column: GossipVerifiedDataColumn, + // This value is not used presently, but it might come in handy for debugging. + _seen_duration: Duration, + ) { + let processing_start_time = Instant::now(); + let block_root = verified_data_column.block_root(); + let data_column_slot = verified_data_column.slot(); + let data_column_index = verified_data_column.index(); + + // TODO(gloas): implement partial messages + if let DataColumnSidecar::Fulu(col) = verified_data_column.as_data_column() + && self + .chain + .data_availability_checker + .partial_assembler() + .is_some_and(|a| !a.is_complete(block_root, verified_data_column.index())) + { + metrics::inc_counter_vec( + &metrics::BEACON_USEFUL_FULL_COLUMNS_RECEIVED_TOTAL, + &[&data_column_index.to_string()], + ); + + match col.to_partial() { + Ok(mut column) => { + let header = column.sidecar.header.take(); + if let Some(header) = header { + self.send_network_message(NetworkMessage::PublishPartialColumns { + columns: vec![Arc::new(column)], + header: Arc::new(header), + }); + } else { + crit!("Converting from full to partial yielded headerless partial") + }; + } + Err(err) => crit!(?err, "Could not convert from full to partial"), + } + } + + let result = self + .chain + .process_gossip_data_columns(vec![verified_data_column], || Ok(())) + .await; + register_process_result_metrics(&result, metrics::BlockSource::Gossip, "data_column"); + + match result { + Ok(availability) => match availability { + AvailabilityProcessingStatus::Imported(block_root) => { + debug!( + %block_root, + "Gossipsub data column processed, imported fully available block" + ); + self.chain.recompute_head_at_current_slot().await; + + metrics::set_gauge( + &metrics::BEACON_BLOB_DELAY_FULL_VERIFICATION, + processing_start_time.elapsed().as_millis() as i64, + ); + + // If a block is in the da_checker, sync maybe awaiting for an event when block is finally + // imported. A block can become imported both after processing a block or data column. If + // importing a block results in `Imported`, notify. Do not notify of data column errors. + self.send_sync_message(SyncMessage::GossipBlockProcessResult { + block_root, + imported: true, + }); + } + AvailabilityProcessingStatus::MissingComponents(slot, block_root) => { + trace!( + %slot, + %data_column_index, + %block_root, + "Processed data column, waiting for other components" + ); + + self.check_reconstruction_trigger(slot, &block_root).await; + } + }, + Err(BlockError::DuplicateFullyImported(_)) => { + debug!( + ?block_root, + data_column_index, "Ignoring gossip column already imported" + ); + } + Err(err) => { + debug!( + outcome = ?err, + ?block_root, + block_slot = %data_column_slot, + data_column_index, + "Invalid gossip data column" + ); + self.gossip_penalize_peer( + peer_id, + PeerAction::MidToleranceError, + "bad_gossip_data_column_ssz", + ); + } + } + } + pub async fn process_gossip_partial_data_column_sidecar( self: &Arc, peer_id: PeerId, @@ -1008,7 +1101,6 @@ impl NetworkBeaconProcessor { %index, "Could not verify partial column for gossip. Rejecting the column sidecar" ); - // Prevent recurring behaviour by penalizing the peer slightly. self.gossip_penalize_peer( peer_id, PeerAction::LowToleranceError, @@ -1017,9 +1109,6 @@ impl NetworkBeaconProcessor { self.propagate_partial_validation_failure(peer_id, topic); } 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!( %block_root, %index, @@ -1034,7 +1123,6 @@ impl NetworkBeaconProcessor { %index, "Could not verify column sidecar for gossip. Ignoring the partial column sidecar" ); - // Prevent recurring behaviour by penalizing the peer slightly. self.gossip_penalize_peer( peer_id, PeerAction::HighToleranceError, @@ -1119,357 +1207,6 @@ impl NetworkBeaconProcessor { } } - #[allow(clippy::too_many_arguments)] - #[instrument( - name = "lh_process_gossip_blob", - parent = None, - level = "debug", - skip_all, - fields( - slot = ?blob_sidecar.slot(), - block_root = ?blob_sidecar.block_root(), - index = blob_sidecar.index), - )] - pub async fn process_gossip_blob( - self: &Arc, - message_id: MessageId, - peer_id: PeerId, - _peer_client: Client, - blob_index: u64, - blob_sidecar: Arc>, - seen_duration: Duration, - ) { - let slot = blob_sidecar.slot(); - let root = blob_sidecar.block_root(); - let index = blob_sidecar.index; - let commitment = blob_sidecar.kzg_commitment; - let delay = get_slot_delay_ms(seen_duration, slot, &self.chain.slot_clock); - // Log metrics to track delay from other nodes on the network. - metrics::set_gauge(&metrics::BEACON_BLOB_DELAY_GOSSIP, delay.as_millis() as i64); - match self - .chain - .verify_blob_sidecar_for_gossip(blob_sidecar.clone(), blob_index) - { - Ok(gossip_verified_blob) => { - metrics::inc_counter(&metrics::BEACON_PROCESSOR_GOSSIP_BLOB_VERIFIED_TOTAL); - - if delay >= self.chain.spec.get_unaggregated_attestation_due() { - metrics::inc_counter(&metrics::BEACON_BLOB_GOSSIP_ARRIVED_LATE_TOTAL); - debug!( - block_root = ?gossip_verified_blob.block_root(), - proposer_index = gossip_verified_blob.block_proposer_index(), - slot = %gossip_verified_blob.slot(), - delay = ?delay, - commitment = %gossip_verified_blob.kzg_commitment(), - "Gossip blob arrived late" - ); - } - - debug!( - %slot, - %root, - %index, - commitment = %gossip_verified_blob.kzg_commitment(), - "Successfully verified gossip blob" - ); - - self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Accept); - - // Log metrics to keep track of propagation delay times. - if let Some(duration) = SystemTime::now() - .duration_since(UNIX_EPOCH) - .ok() - .and_then(|now| now.checked_sub(seen_duration)) - { - metrics::set_gauge( - &metrics::BEACON_BLOB_DELAY_GOSSIP_VERIFICATION, - duration.as_millis() as i64, - ); - } - self.process_gossip_verified_blob(peer_id, gossip_verified_blob, seen_duration) - .await - } - Err(err) => { - match err { - GossipBlobError::ParentUnknown { parent_root } => { - debug!( - action = "requesting parent", - block_root = %root, - parent_root = %parent_root, - %commitment, - "Unknown parent hash for blob" - ); - self.send_sync_message(SyncMessage::UnknownParentBlob( - peer_id, - blob_sidecar, - )); - } - GossipBlobError::PubkeyCacheTimeout | GossipBlobError::BeaconChainError(_) => { - crit!( - error = ?err, - "Internal error when verifying blob sidecar" - ) - } - GossipBlobError::ProposalSignatureInvalid - | GossipBlobError::UnknownValidator(_) - | GossipBlobError::ProposerIndexMismatch { .. } - | GossipBlobError::BlobIsNotLaterThanParent { .. } - | GossipBlobError::InvalidSubnet { .. } - | GossipBlobError::InvalidInclusionProof - | GossipBlobError::KzgError(_) - | GossipBlobError::NotFinalizedDescendant { .. } => { - warn!( - error = ?err, - %slot, - %root, - %index, - %commitment, - "Could not verify blob sidecar for gossip. Rejecting the blob sidecar" - ); - // Prevent recurring behaviour by penalizing the peer. - self.gossip_penalize_peer( - peer_id, - PeerAction::LowToleranceError, - "gossip_blob_low", - ); - self.propagate_validation_result( - message_id, - peer_id, - MessageAcceptance::Reject, - ); - } - 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!( - %slot, - %root, - %index, - "Received already available blob sidecar. Ignoring the blob sidecar" - ) - } - GossipBlobError::FutureSlot { .. } => { - debug!( - error = ?err, - %slot, - %root, - %index, - %commitment, - "Could not verify blob sidecar for gossip. Ignoring the blob sidecar" - ); - // Prevent recurring behaviour by penalizing the peer slightly. - self.gossip_penalize_peer( - peer_id, - PeerAction::HighToleranceError, - "gossip_blob_high", - ); - self.propagate_validation_result( - message_id, - peer_id, - MessageAcceptance::Ignore, - ); - } - GossipBlobError::PastFinalizedSlot { .. } => { - debug!( - error = ?err, - %slot, - %root, - %index, - %commitment, - "Could not verify blob sidecar for gossip. Ignoring the blob sidecar" - ); - // Prevent recurring behaviour by penalizing the peer. A low-tolerance - // error is fine because there's no reason for peers to be propagating old - // blobs on gossip, even if their view of finality is lagging. - self.gossip_penalize_peer( - peer_id, - PeerAction::LowToleranceError, - "gossip_blob_low", - ); - self.propagate_validation_result( - message_id, - peer_id, - MessageAcceptance::Ignore, - ); - } - } - } - } - } - - async fn process_gossip_verified_blob( - self: &Arc, - peer_id: PeerId, - verified_blob: GossipVerifiedBlob, - _seen_duration: Duration, - ) { - let processing_start_time = Instant::now(); - let block_root = verified_blob.block_root(); - let blob_slot = verified_blob.slot(); - 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)) => { - debug!( - %block_root, - "Gossipsub blob processed - imported fully available block" - ); - self.chain.recompute_head_at_current_slot().await; - - metrics::set_gauge( - &metrics::BEACON_BLOB_DELAY_FULL_VERIFICATION, - processing_start_time.elapsed().as_millis() as i64, - ); - } - Ok(AvailabilityProcessingStatus::MissingComponents(slot, block_root)) => { - debug!( - %slot, - %blob_index, - %block_root, - "Processed gossip blob - waiting for other components" - ); - } - Err(BlockError::DuplicateFullyImported(_)) => { - debug!( - ?block_root, - blob_index, "Ignoring gossip blob already imported" - ); - } - Err(err) => { - debug!( - outcome = ?err, - ?block_root, - %blob_slot, - blob_index, - "Invalid gossip blob" - ); - self.gossip_penalize_peer( - peer_id, - PeerAction::MidToleranceError, - "bad_gossip_blob_ssz", - ); - } - } - - // If a block is in the da_checker, sync maybe awaiting for an event when block is finally - // imported. A block can become imported both after processing a block or blob. If a - // importing a block results in `Imported`, notify. Do not notify of blob errors. - if matches!(result, Ok(AvailabilityProcessingStatus::Imported(_))) { - self.send_sync_message(SyncMessage::GossipBlockProcessResult { - block_root, - imported: true, - }); - } - } - - /// Process a gossip-verified full data column (not partial). - /// Partials are handled by process_gossip_verified_partial_data_column. - async fn process_gossip_verified_data_column( - self: &Arc, - peer_id: PeerId, - verified_data_column: GossipVerifiedDataColumn, - // This value is not used presently, but it might come in handy for debugging. - _seen_duration: Duration, - ) { - let processing_start_time = Instant::now(); - let block_root = verified_data_column.block_root(); - let data_column_slot = verified_data_column.slot(); - let data_column_index = verified_data_column.index(); - - // TODO(gloas): implement partial messages - if let DataColumnSidecar::Fulu(col) = verified_data_column.as_data_column() - && self - .chain - .data_availability_checker - .partial_assembler() - .is_some_and(|a| !a.is_complete(block_root, verified_data_column.index())) - { - metrics::inc_counter_vec( - &metrics::BEACON_USEFUL_FULL_COLUMNS_RECEIVED_TOTAL, - &[&data_column_index.to_string()], - ); - - match col.to_partial() { - Ok(mut column) => { - let header = column.sidecar.header.take(); - if let Some(header) = header { - self.send_network_message(NetworkMessage::PublishPartialColumns { - columns: vec![Arc::new(column)], - header: Arc::new(header), - }); - } else { - crit!("Converting from full to partial yielded headerless partial") - }; - } - Err(err) => crit!(?err, "Could not convert from full to partial"), - } - } - - let result = self - .chain - .process_gossip_data_columns(vec![verified_data_column], || Ok(())) - .await; - register_process_result_metrics(&result, metrics::BlockSource::Gossip, "data_column"); - - match result { - Ok(availability) => match availability { - AvailabilityProcessingStatus::Imported(block_root) => { - debug!( - %block_root, - "Gossipsub data column processed, imported fully available block" - ); - self.chain.recompute_head_at_current_slot().await; - - metrics::set_gauge( - &metrics::BEACON_BLOB_DELAY_FULL_VERIFICATION, - processing_start_time.elapsed().as_millis() as i64, - ); - - // If a block is in the da_checker, sync maybe awaiting for an event when block is finally - // imported. A block can become imported both after processing a block or data column. If - // importing a block results in `Imported`, notify. Do not notify of data column errors. - self.send_sync_message(SyncMessage::GossipBlockProcessResult { - block_root, - imported: true, - }); - } - AvailabilityProcessingStatus::MissingComponents(slot, block_root) => { - trace!( - %slot, - %data_column_index, - %block_root, - "Processed data column, waiting for other components" - ); - - self.check_reconstruction_trigger(slot, &block_root).await; - } - }, - Err(BlockError::DuplicateFullyImported(_)) => { - debug!( - ?block_root, - data_column_index, "Ignoring gossip column already imported" - ); - } - Err(err) => { - debug!( - outcome = ?err, - ?block_root, - block_slot = %data_column_slot, - data_column_index, - "Invalid gossip data column" - ); - self.gossip_penalize_peer( - peer_id, - PeerAction::MidToleranceError, - "bad_gossip_data_column_ssz", - ); - } - } - } - /// Process a gossip-verified partial data column by merging it in the assembler async fn process_gossip_verified_partial_data_column( self: &Arc, @@ -1885,9 +1622,7 @@ impl NetworkBeaconProcessor { crit!(error = %e, "Internal block gossip validation error. Availability check during gossip validation"); return None; } - // BlobNotRequired is unreachable. Only constructed in `process_gossip_blob` Err(e @ BlockError::InternalError(_)) - | Err(e @ BlockError::BlobNotRequired(_)) | Err(e @ BlockError::EnvelopeBlockRootUnknown(_)) | Err(e @ BlockError::OptimisticSyncNotSupported { .. }) => { error!(error = %e, "Internal block gossip validation error"); diff --git a/beacon_node/network/src/network_beacon_processor/mod.rs b/beacon_node/network/src/network_beacon_processor/mod.rs index bbaafec4ea..97673aa8b8 100644 --- a/beacon_node/network/src/network_beacon_processor/mod.rs +++ b/beacon_node/network/src/network_beacon_processor/mod.rs @@ -1,12 +1,11 @@ use crate::sync::manager::BlockProcessType; use crate::{service::NetworkMessage, sync::manager::SyncMessage}; -use beacon_chain::blob_verification::{GossipBlobError, observe_gossip_blob}; use beacon_chain::block_verification_types::LookupBlock; use beacon_chain::block_verification_types::RangeSyncBlock; -use beacon_chain::data_column_verification::{GossipDataColumnError, observe_gossip_data_column}; -use beacon_chain::fetch_blobs::{ - EngineGetBlobsOutput, FetchEngineBlobError, fetch_and_process_engine_blobs, +use beacon_chain::data_column_verification::{ + GossipDataColumnError, KzgVerifiedCustodyDataColumn, observe_gossip_data_column, }; +use beacon_chain::fetch_blobs::{FetchEngineBlobError, fetch_and_process_engine_blobs}; use beacon_chain::test_utils::{BeaconChainHarness, EphemeralHarnessType}; use beacon_chain::{AvailabilityProcessingStatus, BeaconChain, BeaconChainTypes, BlockError}; use beacon_processor::{ @@ -70,9 +69,6 @@ pub struct NetworkBeaconProcessor { pub executor: TaskExecutor, } -// 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.try_send(event) @@ -198,36 +194,6 @@ impl NetworkBeaconProcessor { }) } - /// Create a new `Work` event for some blob sidecar. - pub fn send_gossip_blob_sidecar( - self: &Arc, - message_id: MessageId, - peer_id: PeerId, - peer_client: Client, - blob_index: u64, - blob_sidecar: Arc>, - seen_timestamp: Duration, - ) -> Result<(), Error> { - let processor = self.clone(); - let process_fn = async move { - processor - .process_gossip_blob( - message_id, - peer_id, - peer_client, - blob_index, - blob_sidecar, - seen_timestamp, - ) - .await - }; - - self.try_send(BeaconWorkEvent { - drop_during_sync: false, - work: Work::GossipBlobSidecar(Box::pin(process_fn)), - }) - } - /// Create a new `Work` event for some data column sidecar. pub fn send_gossip_data_column_sidecar( self: &Arc, @@ -970,22 +936,12 @@ impl NetworkBeaconProcessor { let epoch = header.slot().epoch(T::EthSpec::slots_per_epoch()); let custody_columns = self.chain.sampling_columns_for_epoch(epoch); let self_cloned = self.clone(); - let publish_fn = move |blobs_or_data_column| { + let publish_fn = move |columns: Vec>| { if publish_blobs { - match blobs_or_data_column { - EngineGetBlobsOutput::Blobs(blobs) => { - self_cloned.publish_blobs_gradually( - blobs.into_iter().map(|b| b.to_blob()).collect(), - block_root, - ); - } - EngineGetBlobsOutput::CustodyColumns(columns) => { - self_cloned.publish_data_columns_gradually( - columns.into_iter().map(|c| c.clone_arc()).collect(), - block_root, - ); - } - }; + self_cloned.publish_data_columns_gradually( + columns.into_iter().map(|c| c.clone_arc()).collect(), + block_root, + ); } }; @@ -1103,84 +1059,6 @@ 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 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::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(|blob| match observe_gossip_blob(&blob, &chain) { - Ok(()) => Some(blob), - Err(GossipBlobError::RepeatBlob { .. }) => None, - Err(e) => { - warn!( - error = ?e, - "Previously verified blob is invalid" - ); - None - } - }) - .collect::>(); - - if !publishable.is_empty() { - debug!( - publish_count = publishable.len(), - ?block_root, - "Publishing blob batch" - ); - publish_count += publishable.len(); - publish_fn(publishable); - } - - tokio::time::sleep(blob_publication_batch_interval).await; - batch_size *= BLOB_PUBLICATION_EXP_FACTOR; - } - - debug!( - batch_interval = blob_publication_batch_interval.as_millis(), - blob_count, - publish_count, - ?block_root, - "Batch blob publication complete" - ) - }, - "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 diff --git a/beacon_node/network/src/network_beacon_processor/tests.rs b/beacon_node/network/src/network_beacon_processor/tests.rs index 18d34b40b3..42d3b8f33d 100644 --- a/beacon_node/network/src/network_beacon_processor/tests.rs +++ b/beacon_node/network/src/network_beacon_processor/tests.rs @@ -409,22 +409,6 @@ impl TestRig { .unwrap(); } - pub fn enqueue_gossip_blob(&self, blob_index: usize) { - if let Some(blobs) = self.next_blobs.as_ref() { - let blob = blobs.get(blob_index).unwrap(); - self.network_beacon_processor - .send_gossip_blob_sidecar( - junk_message_id(), - junk_peer_id(), - Client::default(), - blob.index, - blob.clone(), - Duration::from_secs(0), - ) - .unwrap(); - } - } - pub fn enqueue_gossip_data_columns(&self, col_index: usize) { if let Some(data_columns) = self.next_data_columns.as_ref() { let data_column = data_columns.get(col_index).unwrap(); @@ -1101,13 +1085,6 @@ async fn import_gossip_block_acceptably_early() { rig.assert_event_journal_completes(&[WorkType::GossipBlock]) .await; - let num_blobs = rig.next_blobs.as_ref().map(|b| b.len()).unwrap_or(0); - for i in 0..num_blobs { - rig.enqueue_gossip_blob(i); - rig.assert_event_journal_completes(&[WorkType::GossipBlobSidecar]) - .await; - } - let num_data_columns = rig.next_data_columns.as_ref().map(|c| c.len()).unwrap_or(0); for i in 0..num_data_columns { rig.enqueue_gossip_data_columns(i); @@ -1242,13 +1219,6 @@ async fn import_gossip_block_at_current_slot() { rig.assert_event_journal_completes(&[WorkType::GossipBlock]) .await; - let num_blobs = rig.next_blobs.as_ref().map(|b| b.len()).unwrap_or(0); - for i in 0..num_blobs { - rig.enqueue_gossip_blob(i); - rig.assert_event_journal_completes(&[WorkType::GossipBlobSidecar]) - .await; - } - let num_data_columns = rig.next_data_columns.as_ref().map(|c| c.len()).unwrap_or(0); for i in 0..num_data_columns { rig.enqueue_gossip_data_columns(i); @@ -1315,10 +1285,6 @@ async fn attestation_to_unknown_block_processed(import_method: BlockImportMethod BlockImportMethod::Gossip => { rig.enqueue_gossip_block(); events.push(WorkType::GossipBlock); - for i in 0..num_blobs { - rig.enqueue_gossip_blob(i); - events.push(WorkType::GossipBlobSidecar); - } for i in 0..num_data_columns { rig.enqueue_gossip_data_columns(i); events.push(WorkType::GossipDataColumnSidecar); @@ -1401,10 +1367,6 @@ async fn aggregate_attestation_to_unknown_block(import_method: BlockImportMethod BlockImportMethod::Gossip => { rig.enqueue_gossip_block(); events.push(WorkType::GossipBlock); - for i in 0..num_blobs { - rig.enqueue_gossip_blob(i); - events.push(WorkType::GossipBlobSidecar); - } for i in 0..num_data_columns { rig.enqueue_gossip_data_columns(i); events.push(WorkType::GossipDataColumnSidecar) diff --git a/beacon_node/network/src/router.rs b/beacon_node/network/src/router.rs index 35939c6f39..d2098d341e 100644 --- a/beacon_node/network/src/router.rs +++ b/beacon_node/network/src/router.rs @@ -412,19 +412,6 @@ impl Router { seen_timestamp, ), ), - PubsubMessage::BlobSidecar(data) => { - let (blob_index, blob_sidecar) = *data; - self.handle_beacon_processor_send_result( - self.network_beacon_processor.send_gossip_blob_sidecar( - message_id, - peer_id, - self.network_globals.client(&peer_id), - blob_index, - blob_sidecar, - seen_timestamp, - ), - ) - } PubsubMessage::DataColumnSidecar(data) => { let (subnet_id, column_sidecar) = *data; self.handle_beacon_processor_send_result( diff --git a/beacon_node/network/src/sync/block_lookups/mod.rs b/beacon_node/network/src/sync/block_lookups/mod.rs index f10610c751..ff3bf6f998 100644 --- a/beacon_node/network/src/sync/block_lookups/mod.rs +++ b/beacon_node/network/src/sync/block_lookups/mod.rs @@ -80,7 +80,6 @@ const MAX_LOOKUPS: usize = 200; /// The values for `Blob`, `DataColumn` and `PartialDataColumn` is the parent root of the column. pub enum BlockComponent { Block(DownloadResult>>), - Blob(DownloadResult), DataColumn(DownloadResult), PartialDataColumn(DownloadResult), } @@ -89,15 +88,13 @@ impl BlockComponent { fn parent_root(&self) -> Hash256 { match self { BlockComponent::Block(block) => block.value.parent_root(), - BlockComponent::Blob(parent_root) - | BlockComponent::DataColumn(parent_root) + BlockComponent::DataColumn(parent_root) | BlockComponent::PartialDataColumn(parent_root) => parent_root.value, } } fn get_type(&self) -> &'static str { match self { BlockComponent::Block(_) => "block", - BlockComponent::Blob(_) => "blob", BlockComponent::DataColumn(_) => "data_column", BlockComponent::PartialDataColumn(_) => "partial_data_column", } @@ -214,9 +211,9 @@ impl BlockLookups { block_root, Some(block_component), Some(parent_root), - // On a `UnknownParentBlock` or `UnknownParentBlob` event the peer is not required - // to have the rest of the block components (refer to decoupled blob gossip). Create - // the lookup with zero peers to house the block components. + // On a `UnknownParentBlock` or `UnknownParentDataColumn` event the peer is not + // required to have the rest of the block components. Create the lookup with zero + // peers to house the block components. &[], 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 23bfd531f0..d54480e8e5 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 @@ -156,9 +156,7 @@ impl SingleBlockLookup { .block_request_state .state .insert_verified_response(block), - BlockComponent::Blob(_) - | BlockComponent::DataColumn(_) - | BlockComponent::PartialDataColumn(_) => { + BlockComponent::DataColumn(_) | BlockComponent::PartialDataColumn(_) => { // For now ignore single blobs and columns, as the blob request state assumes all blobs are // attributed to the same peer = the peer serving the remaining blobs. Ignoring this // block component has a minor effect, causing the node to re-request this blob diff --git a/beacon_node/network/src/sync/manager.rs b/beacon_node/network/src/sync/manager.rs index 14a38f0e72..534e0bc7c8 100644 --- a/beacon_node/network/src/sync/manager.rs +++ b/beacon_node/network/src/sync/manager.rs @@ -144,9 +144,6 @@ pub enum SyncMessage { /// A block with an unknown parent has been received. UnknownParentBlock(PeerId, Arc>, Hash256), - /// A blob with an unknown parent has been received. - UnknownParentBlob(PeerId, Arc>), - /// A data column with an unknown parent has been received. UnknownParentDataColumn(PeerId, Arc>), @@ -890,24 +887,6 @@ impl SyncManager { }), ); } - SyncMessage::UnknownParentBlob(peer_id, blob) => { - let blob_slot = blob.slot(); - let block_root = blob.block_root(); - let parent_root = blob.block_parent_root(); - debug!(%block_root, %parent_root, "Received unknown parent blob message"); - self.handle_unknown_parent( - peer_id, - block_root, - parent_root, - blob_slot, - BlockComponent::Blob(DownloadResult { - value: parent_root, - block_root, - seen_timestamp: self.chain.slot_clock.now_duration().unwrap_or_default(), - peer_group: PeerGroup::from_single(peer_id), - }), - ); - } SyncMessage::UnknownParentDataColumn(peer_id, data_column) => { let data_column_slot = data_column.slot(); let block_root = data_column.block_root(); diff --git a/beacon_node/network/src/sync/network_context/requests/blobs_by_range.rs b/beacon_node/network/src/sync/network_context/requests/blobs_by_range.rs index 9c6f516199..1da0fb52f7 100644 --- a/beacon_node/network/src/sync/network_context/requests/blobs_by_range.rs +++ b/beacon_node/network/src/sync/network_context/requests/blobs_by_range.rs @@ -33,6 +33,7 @@ impl ActiveRequestItems for BlobsByRangeRequestItems { if blob.index >= self.max_blobs_per_block { return Err(LookupVerifyError::UnrequestedIndex(blob.index)); } + if !blob.verify_blob_sidecar_inclusion_proof() { return Err(LookupVerifyError::InvalidInclusionProof); } 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 556985c2b4..f0ff99867b 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 @@ -50,9 +50,11 @@ impl ActiveRequestItems for BlobsByRootRequestItems { if self.request.block_root != block_root { return Err(LookupVerifyError::UnrequestedBlockRoot(block_root)); } + if !blob.verify_blob_sidecar_inclusion_proof() { return Err(LookupVerifyError::InvalidInclusionProof); } + if !self.request.indices.contains(&blob.index) { return Err(LookupVerifyError::UnrequestedIndex(blob.index)); } diff --git a/beacon_node/network/src/sync/tests/lookups.rs b/beacon_node/network/src/sync/tests/lookups.rs index c1b2793491..5c9e18362c 100644 --- a/beacon_node/network/src/sync/tests/lookups.rs +++ b/beacon_node/network/src/sync/tests/lookups.rs @@ -1205,17 +1205,6 @@ impl TestRig { self.trigger_unknown_parent_block(peer_id, last_block); } - fn trigger_with_last_unknown_blob_parent(&mut self) { - let peer_id = self.new_connected_supernode_peer(); - let blobs = self - .get_last_block() - .block_data() - .blobs() - .expect("no blobs"); - let blob = blobs.first().expect("empty blobs"); - self.trigger_unknown_parent_blob(peer_id, blob.clone()); - } - fn trigger_with_last_unknown_data_column_parent(&mut self) { let peer_id = self.new_connected_supernode_peer(); let columns = self @@ -1224,7 +1213,7 @@ impl TestRig { .data_columns() .expect("No data columns"); let column = columns.first().expect("empty columns"); - self.trigger_unknown_parent_column(peer_id, column.clone()); + self.trigger_unknown_parent_data_column(peer_id, column.clone()); } // Post-test assertions @@ -1428,6 +1417,10 @@ impl TestRig { genesis_fork().deneb_enabled().then(Self::default) } + fn new_after_fulu() -> Option { + genesis_fork().fulu_enabled().then(Self::default) + } + fn new_after_deneb_before_fulu() -> Option { let fork = genesis_fork(); if fork.deneb_enabled() && !fork.fulu_enabled() { @@ -1463,16 +1456,12 @@ impl TestRig { self.send_sync_message(SyncMessage::UnknownParentBlock(peer_id, block, block_root)) } - fn trigger_unknown_parent_blob(&mut self, peer_id: PeerId, blob: Arc>) { - self.send_sync_message(SyncMessage::UnknownParentBlob(peer_id, blob)); - } - - fn trigger_unknown_parent_column( + fn trigger_unknown_parent_data_column( &mut self, peer_id: PeerId, - column: Arc>, + data_column: Arc>, ) { - self.send_sync_message(SyncMessage::UnknownParentDataColumn(peer_id, column)); + self.send_sync_message(SyncMessage::UnknownParentDataColumn(peer_id, data_column)); } fn trigger_unknown_block_from_attestation(&mut self, block_root: Hash256, peer_id: PeerId) { @@ -1757,9 +1746,9 @@ impl TestRig { ) .unwrap() { - Availability::Available(_) => panic!("blob removed from da_checker, available"), + Availability::Available(_) => panic!("column removed from da_checker, available"), Availability::MissingComponents(block_root) => { - self.log(&format!("inserted blob to da_checker {block_root:?}")) + self.log(&format!("inserted column to da_checker {block_root:?}")) } }; } @@ -1944,35 +1933,29 @@ async fn happy_path_unknown_block_parent(depth: usize) { } } -/// Assert that sync completes from a GossipUnknownParentBlob / UnknownDataColumnParent +/// Assert that sync completes from an UnknownDataColumnParent async fn happy_path_unknown_data_parent(depth: usize) { - let Some(mut r) = TestRig::new_after_deneb() else { + let Some(mut r) = TestRig::new_after_fulu() else { return; }; r.build_chain(depth).await; - if r.is_after_fulu() { - r.trigger_with_last_unknown_data_column_parent(); - } else if r.is_after_deneb() { - r.trigger_with_last_unknown_blob_parent(); - } + r.trigger_with_last_unknown_data_column_parent(); r.simulate(SimulateConfig::happy_path()).await; r.assert_successful_lookup_sync_parent_trigger(); } /// Assert that multiple trigger types don't create extra lookups async fn happy_path_multiple_triggers(depth: usize) { - let mut r = TestRig::default(); + let Some(mut r) = TestRig::new_after_fulu() else { + return; + }; // + 1, because the unknown parent trigger needs two new blocks r.build_chain(depth + 1).await; r.trigger_with_last_block(); r.trigger_with_last_block(); r.trigger_with_last_unknown_block_parent(); r.trigger_with_last_unknown_block_parent(); - if r.is_after_fulu() { - r.trigger_with_last_unknown_data_column_parent(); - } else if r.is_after_deneb() { - r.trigger_with_last_unknown_blob_parent(); - } + r.trigger_with_last_unknown_data_column_parent(); r.simulate(SimulateConfig::happy_path()).await; assert_eq!(r.created_lookups(), depth + 1, "Don't create extra lookups"); r.assert_successful_lookup_sync(); @@ -2105,18 +2088,14 @@ async fn too_many_processing_failures(depth: usize) { #[tokio::test] /// Assert that multiple trigger types don't create extra lookups async fn unknown_parent_does_not_add_peers_to_itself() { - let Some(mut r) = TestRig::new_after_deneb() else { + let Some(mut r) = TestRig::new_after_fulu() else { return; }; // 2, because the unknown parent trigger needs two new blocks r.build_chain(2).await; r.trigger_with_last_unknown_block_parent(); r.trigger_with_last_unknown_block_parent(); - if r.is_after_fulu() { - r.trigger_with_last_unknown_data_column_parent(); - } else if r.is_after_deneb() { - r.trigger_with_last_unknown_blob_parent(); - } + r.trigger_with_last_unknown_data_column_parent(); r.simulate(SimulateConfig::happy_path()).await; r.assert_peers_at_lookup_of_slot(2, 0); r.assert_peers_at_lookup_of_slot(1, 3); diff --git a/testing/ef_tests/src/cases/fork_choice.rs b/testing/ef_tests/src/cases/fork_choice.rs index 2954ee7eb4..1736cd951f 100644 --- a/testing/ef_tests/src/cases/fork_choice.rs +++ b/testing/ef_tests/src/cases/fork_choice.rs @@ -2,7 +2,6 @@ use super::*; use crate::decode::{ssz_decode_file, ssz_decode_file_with, ssz_decode_state, yaml_decode_file}; use ::fork_choice::{AttestationFromBlock, PayloadVerificationStatus, ProposerHeadError}; use beacon_chain::beacon_proposer_cache::compute_proposer_duties_from_head; -use beacon_chain::blob_verification::GossipBlobError; use beacon_chain::block_verification_types::LookupBlock; use beacon_chain::chain_config::DisallowedReOrgOffsets; use beacon_chain::data_column_verification::GossipVerifiedDataColumn; @@ -12,7 +11,7 @@ use beacon_chain::{ attestation_verification::{ VerifiedAttestation, obtain_indexed_attestation_and_committees_per_slot, }, - blob_verification::GossipVerifiedBlob, + blob_verification::KzgVerifiedBlob, custody_context::NodeCustodyType, test_utils::{BeaconChainHarness, EphemeralHarnessType}, }; @@ -696,7 +695,6 @@ impl Tester { let mut blob_success = true; - // Convert blobs and kzg_proofs into sidecars, then plumb them into the availability tracker if let Some(blobs) = blobs.clone() { let proofs = kzg_proofs.unwrap(); let commitments = block @@ -709,37 +707,51 @@ impl Tester { // Zipping will stop when any of the zipped lists runs out, which is what we want. Some // of the tests don't provide enough proofs/blobs, and should fail the availability // check. - for (i, ((blob, kzg_proof), kzg_commitment)) in - blobs.into_iter().zip(proofs).zip(commitments).enumerate() - { - let blob_sidecar = Arc::new(BlobSidecar { - index: i as u64, - blob, - kzg_commitment, - kzg_proof, - signed_block_header: block.signed_block_header(), - kzg_commitment_inclusion_proof: block - .message() - .body() - .kzg_commitment_merkle_proof(i) - .unwrap(), - }); + let verified_blobs: Vec> = blobs + .into_iter() + .zip(proofs) + .zip(commitments) + .enumerate() + .filter_map(|(i, ((blob, kzg_proof), kzg_commitment))| { + let blob_sidecar = Arc::new(BlobSidecar { + index: i as u64, + blob, + kzg_commitment, + kzg_proof, + signed_block_header: block.signed_block_header(), + kzg_commitment_inclusion_proof: block + .message() + .body() + .kzg_commitment_merkle_proof(i) + .unwrap(), + }); - let chain = self.harness.chain.clone(); - let blob = - match GossipVerifiedBlob::new(blob_sidecar.clone(), blob_sidecar.index, &chain) - { - Ok(gossip_verified_blob) => gossip_verified_blob, - Err(GossipBlobError::KzgError(_)) => { + match KzgVerifiedBlob::new( + blob_sidecar.clone(), + &self.harness.chain.kzg, + Duration::default(), + ) { + Ok(verified) => Some(verified), + Err(_) => { blob_success = false; - GossipVerifiedBlob::__assumed_valid(blob_sidecar) + None } - Err(_) => GossipVerifiedBlob::__assumed_valid(blob_sidecar), - }; - let result = - self.block_on_dangerous(self.harness.chain.process_gossip_blob(blob))?; + } + }) + .collect(); + + if !verified_blobs.is_empty() { + let result = self + .harness + .chain + .data_availability_checker + .put_kzg_verified_blobs(block_root, verified_blobs); if valid { - assert!(result.is_ok()); + assert!( + result.is_ok(), + "put_kzg_verified_blobs failed: {:?}", + result + ); } } }; diff --git a/testing/simulator/Cargo.toml b/testing/simulator/Cargo.toml index a1b1b6f95d..29972648f3 100644 --- a/testing/simulator/Cargo.toml +++ b/testing/simulator/Cargo.toml @@ -6,6 +6,7 @@ edition = { workspace = true } # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +beacon_chain = { workspace = true } clap = { workspace = true } environment = { workspace = true } execution_layer = { workspace = true } diff --git a/testing/simulator/src/basic_sim.rs b/testing/simulator/src/basic_sim.rs index 79581ee529..688cfb31ec 100644 --- a/testing/simulator/src/basic_sim.rs +++ b/testing/simulator/src/basic_sim.rs @@ -30,9 +30,10 @@ const ALTAIR_FORK_EPOCH: u64 = 0; const BELLATRIX_FORK_EPOCH: u64 = 0; const CAPELLA_FORK_EPOCH: u64 = 0; const DENEB_FORK_EPOCH: u64 = 0; -const ELECTRA_FORK_EPOCH: u64 = 2; -// const FULU_FORK_EPOCH: u64 = 3; -// const GLOAS_FORK_EPOCH: u64 = 4; +const ELECTRA_FORK_EPOCH: u64 = 0; +const FULU_FORK_EPOCH: u64 = 0; +// TODO(gloas): enable Gloas in simulator, current blocker is lack of data column gossip verification +// const GLOAS_FORK_EPOCH: u64 = 2; 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]; @@ -171,8 +172,8 @@ pub fn run_basic_sim(matches: &ArgMatches) -> Result<(), String> { let genesis_delay = GENESIS_DELAY; // Convenience variables. Update these values when adding a newer fork. - let latest_fork_version = spec.electra_fork_version; - let latest_fork_start_epoch = ELECTRA_FORK_EPOCH; + let latest_fork_version = spec.fulu_fork_version; + let latest_fork_start_epoch = FULU_FORK_EPOCH; let mut slot_duration_ms = spec.get_slot_duration().as_millis() as u64; slot_duration_ms /= speed_up_factor; @@ -187,6 +188,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 06f4478c5e..aed113eca0 100644 --- a/testing/simulator/src/fallback_sim.rs +++ b/testing/simulator/src/fallback_sim.rs @@ -25,11 +25,12 @@ const END_EPOCH: u64 = 16; const GENESIS_DELAY: u64 = 38; 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 FULU_FORK_EPOCH: u64 = 4; -// const GLOAS_FORK_EPOCH: u64 = 5; +const CAPELLA_FORK_EPOCH: u64 = 0; +const DENEB_FORK_EPOCH: u64 = 0; +const ELECTRA_FORK_EPOCH: u64 = 0; +const FULU_FORK_EPOCH: u64 = 0; +// TODO(gloas): enable Gloas in simulator, current blocker is lack of data column gossip verification +// const GLOAS_FORK_EPOCH: u64 = 2; // Since simulator tests are non-deterministic and there is a non-zero chance of missed // attestations, define an acceptable network-wide attestation performance. @@ -191,8 +192,8 @@ pub fn run_fallback_sim(matches: &ArgMatches) -> Result<(), String> { spec.bellatrix_fork_epoch = Some(Epoch::new(BELLATRIX_FORK_EPOCH)); 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)); + 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 2beb9c0efc..780a09e543 100644 --- a/testing/simulator/src/local_network.rs +++ b/testing/simulator/src/local_network.rs @@ -1,4 +1,5 @@ use crate::checks::epoch_delay; +use beacon_chain::custody_context::NodeCustodyType; use kzg::trusted_setup::get_trusted_setup; use node_test_rig::{ ClientConfig, ClientGenesis, LocalBeaconNode, LocalExecutionNode, LocalValidatorClient, @@ -46,6 +47,7 @@ fn default_client_config(network_params: LocalNetworkParams, genesis_time: u64) beacon_config.network.discv5_config.enable_packet_filter = false; beacon_config.chain.enable_light_client_server = true; beacon_config.chain.optimistic_finalized_sync = false; + beacon_config.chain.node_custody_type = NodeCustodyType::Supernode; beacon_config.trusted_setup = get_trusted_setup(); let el_config = execution_layer::Config { @@ -103,6 +105,15 @@ fn default_mock_execution_config( ) } + if let Some(gloas_fork_epoch) = spec.gloas_fork_epoch { + mock_execution_config.amsterdam_time = Some( + genesis_time + + (spec.get_slot_duration().as_secs()) + * E::slots_per_epoch() + * gloas_fork_epoch.as_u64(), + ) + } + mock_execution_config } From 74a5609ab1f059cb9bb5dfcee4d82e1e8a01904c Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Mon, 1 Jun 2026 10:46:58 +1000 Subject: [PATCH 18/18] Delete bogus `InvalidBestNode` error (#9364) On Glamsterdam devnets we started seeing Lighthouse nodes unable to start with errors like: > May 26 04:34:01.582 CRIT Failed to start beacon node reason: "Unable to load fork choice from disk: ForkChoiceError(ProtoArrayStringError(\"find_head failed: InvalidBestNode(InvalidBestNodeInfo { current_slot: Slot(23550), start_root: 0x2c70b1641c29ec46360c99f9a8512f077862cbbc603e16f4a423007d210b0c5f, justified_checkpoint: Checkpoint { epoch: Epoch(712), root: 0x2c70b1641c29ec46360c99f9a8512f077862cbbc603e16f4a423007d210b0c5f }, finalized_checkpoint: Checkpoint { epoch: Epoch(710), root: 0xede5e0b09b51bdb5445ade3398e685bd193b845e0b0ffb827f0c3fec8277ea51 }, head_root: 0x2c70b1641c29ec46360c99f9a8512f077862cbbc603e16f4a423007d210b0c5f, head_justified_checkpoint: Checkpoint { epoch: Epoch(710), root: 0xede5e0b09b51bdb5445ade3398e685bd193b845e0b0ffb827f0c3fec8277ea51 }, head_finalized_checkpoint: Checkpoint { epoch: Epoch(709), root: 0xbb243eff616ff362c52b83113e7c536d0a68cb9ca3d6a1cb1055e732219d9736 } })\"))" This error was the result of an overly-strict sanity check, based on assumptions that are not true under extreme network conditions. Completely remove the `InvalidBestNode` failure path: it is not compliant with the spec, and is actively harmful when triggered (it prevents Lighthouse from starting at all). The error was reachable in any situation where all leaf nodes of fork choice were ineligible to be the head. The payload invalidation tests show some examples of cases where this would happen, and the [newly-added regression test](9a5df1d982b145992c0bafce634cd9a7e5907098) shows a contrived case where it can happen on a Gloas network without _any_ slashings or invalid blocks. There are probably many more cases where it can happen. We do not lose anything by removing it. The spec's implementation of `get_head` _always_ returns something (unless it crashes), and in these cases it is correct to return the starting node of the traversal: the justified checkpoint block. This is what we now do, and what the new test verifies. I've also added some facilities to the harness for injecting attestations with fixed `payload_present` fields. @hopinheimer found himself needing something similar when messing with reorg tests, so I think these are probably useful. It might be possible to do without them by juggling the payload reveal timing in just the right way, but I think this approach is just way simpler. Co-Authored-By: Michael Sproul --- beacon_node/beacon_chain/src/test_utils.rs | 47 ++++-- .../tests/attestation_verification.rs | 4 + .../beacon_chain/tests/block_verification.rs | 151 +++++++++++++++++- .../tests/payload_invalidation.rs | 42 ++--- consensus/proto_array/src/error.rs | 16 +- consensus/proto_array/src/proto_array.rs | 23 --- 6 files changed, 200 insertions(+), 83 deletions(-) diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index 919bb43bfd..db2a9a902d 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -1568,6 +1568,7 @@ where beacon_block_root: Hash256, mut state: Cow>, state_root: Hash256, + payload_present_override: Option, ) -> Result, BeaconChainError> { assert_eq!( state.get_latest_block_root(state_root), @@ -1602,12 +1603,17 @@ where *state.get_block_root(target_slot)? }; - let payload_present = state.fork_name_unchecked().gloas_enabled() - && state.latest_block_header().slot != slot - && self - .chain - .canonical_head - .block_has_canonical_payload(&beacon_block_root, &self.spec)?; + let payload_present = match payload_present_override { + Some(payload_present) => payload_present, + None => { + state.fork_name_unchecked().gloas_enabled() + && state.latest_block_header().slot != slot + && self + .chain + .canonical_head + .block_has_canonical_payload(&beacon_block_root, &self.spec)? + } + }; Ok(Attestation::empty_for_signing( index, @@ -1646,7 +1652,11 @@ where state_root, head_block_root, attestation_slot, - MakeAttestationOptions { limit: None, fork }, + MakeAttestationOptions { + limit: None, + fork, + payload_present_override: None, + }, ) .0 } @@ -1673,7 +1683,11 @@ where state_root, head_block_root, attestation_slot, - MakeAttestationOptions { limit: None, fork }, + MakeAttestationOptions { + limit: None, + fork, + payload_present_override: None, + }, ) .0 } @@ -1687,7 +1701,7 @@ where attestation_slot: Slot, opts: MakeAttestationOptions, ) -> (Vec, Vec) { - let MakeAttestationOptions { limit, fork } = opts; + let MakeAttestationOptions { limit, fork, .. } = opts; let committee_count = state.get_committee_count_at_slot(state.slot()).unwrap(); let num_attesters = AtomicUsize::new(0); @@ -1780,7 +1794,11 @@ where attestation_slot: Slot, opts: MakeAttestationOptions, ) -> (Vec>, Vec) { - let MakeAttestationOptions { limit, fork } = opts; + let MakeAttestationOptions { + limit, + fork, + payload_present_override, + } = opts; let committee_count = state.get_committee_count_at_slot(state.slot()).unwrap(); let num_attesters = AtomicUsize::new(0); @@ -1813,6 +1831,7 @@ where head_block_root.into(), Cow::Borrowed(state), state_root, + payload_present_override, ) .unwrap(); @@ -2015,7 +2034,11 @@ where state_root, block_hash, slot, - MakeAttestationOptions { limit, fork }, + MakeAttestationOptions { + limit, + fork, + payload_present_override: None, + }, ) } @@ -3744,6 +3767,8 @@ pub struct MakeAttestationOptions { pub limit: Option, /// Fork to use for signing attestations. pub fork: Fork, + /// Override post-Gloas regular attestation payload-present encoding. + pub payload_present_override: Option, } pub enum NumBlobs { diff --git a/beacon_node/beacon_chain/tests/attestation_verification.rs b/beacon_node/beacon_chain/tests/attestation_verification.rs index da7f380e36..03b8ae58ac 100644 --- a/beacon_node/beacon_chain/tests/attestation_verification.rs +++ b/beacon_node/beacon_chain/tests/attestation_verification.rs @@ -1636,6 +1636,7 @@ async fn attestation_verification_use_head_state_fork() { MakeAttestationOptions { fork: capella_fork, limit: None, + payload_present_override: None, }, ) .0 @@ -1667,6 +1668,7 @@ async fn attestation_verification_use_head_state_fork() { MakeAttestationOptions { fork: bellatrix_fork, limit: None, + payload_present_override: None, }, ) .0 @@ -1741,6 +1743,7 @@ async fn aggregated_attestation_verification_use_head_state_fork() { MakeAttestationOptions { fork: capella_fork, limit: None, + payload_present_override: None, }, ) .0 @@ -1768,6 +1771,7 @@ async fn aggregated_attestation_verification_use_head_state_fork() { MakeAttestationOptions { fork: bellatrix_fork, limit: None, + payload_present_override: None, }, ) .0 diff --git a/beacon_node/beacon_chain/tests/block_verification.rs b/beacon_node/beacon_chain/tests/block_verification.rs index 67fe0eaae0..e0c39c350b 100644 --- a/beacon_node/beacon_chain/tests/block_verification.rs +++ b/beacon_node/beacon_chain/tests/block_verification.rs @@ -8,7 +8,8 @@ use beacon_chain::{ WhenSlotSkipped, custody_context::NodeCustodyType, test_utils::{ - AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType, test_spec, + AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType, + MakeAttestationOptions, test_spec, }, }; use beacon_chain::{ @@ -17,6 +18,7 @@ use beacon_chain::{ }; use bls::{AggregateSignature, Keypair, Signature}; use fixed_bytes::FixedBytesExtended; +use fork_choice::PayloadStatus; use logging::create_test_tracing_subscriber; use slasher::{Config as SlasherConfig, Slasher}; use state_processing::{ @@ -1909,6 +1911,153 @@ async fn add_altair_block_to_base_chain() { )); } +// This is a regression test for the bogus `InvalidBestNode` error which was reachable in Gloas +// networks. Previously Lighthouse would return an `InvalidBestNode` error from `get_head` in +// contradiction to the spec, which states that the justified root should be returned when no leaf +// node is viable. +// +// The chain construction in this test is contrived but not impossible: the justified block's full +// branch is what contained the evidence to justify it, but the empty branch is more weighty and +// wins out. +#[tokio::test] +async fn gloas_get_head_can_return_justified_empty_payload_branch() { + let spec = test_spec::(); + if !spec.fork_name_at_epoch(Epoch::new(0)).gloas_enabled() { + return; + } + + let harness = BeaconChainHarness::builder(MainnetEthSpec) + .spec(spec.clone().into()) + .chain_config(ChainConfig { + archive: true, + ..ChainConfig::default() + }) + .keypairs(KEYPAIRS[0..VALIDATOR_COUNT].to_vec()) + .node_custody_type(NodeCustodyType::Supernode) + .fresh_ephemeral_store() + .mock_execution_layer() + .build(); + + harness + .extend_slots(E::slots_per_epoch() as usize * 3) + .await; + + let justified_checkpoint = harness.justified_checkpoint(); + assert_ne!(justified_checkpoint.epoch, Epoch::new(0)); + let justified_root = justified_checkpoint.root; + let justified_block = harness + .chain + .get_blinded_block(&justified_root) + .unwrap() + .unwrap(); + let justified_slot = justified_block.message().slot(); + let justified_state_root = justified_block.message().state_root(); + + harness.advance_slot(); + harness + .extend_chain( + E::slots_per_epoch() as usize * 2, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::SomeValidators(vec![]), + ) + .await; + + let current_slot = harness.get_current_slot(); + let current_epoch = current_slot.epoch(E::slots_per_epoch()); + assert_eq!( + harness + .chain + .canonical_head + .cached_head() + .head_payload_status(), + PayloadStatus::Full + ); + + { + let fork_choice = harness.chain.canonical_head.fork_choice_read_lock(); + assert!(fork_choice.is_payload_received(&justified_root)); + let justified_node = fork_choice.get_block(&justified_root).unwrap(); + let voting_source = justified_node + .unrealized_justified_checkpoint + .unwrap_or(justified_node.justified_checkpoint); + assert!( + voting_source.epoch + 2 < current_epoch, + "the justified node's own voting source must be stale" + ); + } + + let mut attestation_state = harness + .chain + .get_state(&justified_state_root, Some(justified_slot), true) + .unwrap() + .unwrap(); + assert!( + attestation_state + .validators() + .iter() + .all(|validator| !validator.slashed), + "reproducer must not rely on slashed validators" + ); + + let all_validators = harness.get_all_validators(); + let mut validators_with_empty_vote = [false; VALIDATOR_COUNT]; + let attestation_start_slot = (current_epoch - 1).start_slot(E::slots_per_epoch()); + let attestation_slot = current_slot - 1; + assert_eq!( + attestation_start_slot + E::slots_per_epoch() - 1, + attestation_slot + ); + + // Create two epochs worth of attestations with `payload_present=false`, all pointing at the + // justified block. This ensures it's very much the canonical head, instead of the justifying + // chain built off its `Full` branch. + for slot in (attestation_start_slot.as_u64()..current_slot.as_u64()).map(Slot::new) { + while attestation_state.slot() < slot { + per_slot_processing(&mut attestation_state, None, &spec).unwrap(); + } + attestation_state.build_caches(&spec).unwrap(); + let attestation_state_root = attestation_state.update_tree_hash_cache().unwrap(); + assert_eq!( + attestation_state.get_latest_block_root(attestation_state_root), + justified_root + ); + + let fork = spec.fork_at_epoch(slot.epoch(E::slots_per_epoch())); + let (attestations, attesters) = harness.make_attestations_with_opts( + &all_validators, + &attestation_state, + attestation_state_root, + justified_root.into(), + slot, + MakeAttestationOptions { + limit: None, + fork, + payload_present_override: Some(false), + }, + ); + + for validator_index in attesters { + validators_with_empty_vote[validator_index] = true; + } + harness.process_attestations(attestations, &attestation_state); + } + + assert!( + validators_with_empty_vote.iter().all(|attested| *attested), + "all validators should have a latest regular attestation to the justified root" + ); + + let (head_root, payload_status) = harness + .chain + .canonical_head + .fork_choice_write_lock() + .get_head(current_slot, &spec) + .expect("fork choice should return the justified root on the empty payload branch"); + + assert_eq!(head_root, justified_root); + assert_eq!(payload_status, PayloadStatus::Empty); +} + // This is a regression test for this bug: // https://github.com/sigp/lighthouse/issues/4332#issuecomment-1565092279 #[tokio::test] diff --git a/beacon_node/beacon_chain/tests/payload_invalidation.rs b/beacon_node/beacon_chain/tests/payload_invalidation.rs index abf1fe48a6..42a78d740f 100644 --- a/beacon_node/beacon_chain/tests/payload_invalidation.rs +++ b/beacon_node/beacon_chain/tests/payload_invalidation.rs @@ -6,7 +6,7 @@ use beacon_chain::{ BeaconChainError, BlockError, ChainConfig, ExecutionPayloadError, INVALID_JUSTIFIED_PAYLOAD_SHUTDOWN_REASON, NotifyExecutionLayer, StateSkipConfig, WhenSlotSkipped, - canonical_head::{CachedHead, CanonicalHead}, + canonical_head::CachedHead, test_utils::{BeaconChainHarness, EphemeralHarnessType, fork_name_from_env, test_spec}, }; use execution_layer::{ @@ -108,10 +108,6 @@ impl InvalidPayloadRig { self.harness.chain.canonical_head.cached_head() } - fn canonical_head(&self) -> &CanonicalHead> { - &self.harness.chain.canonical_head - } - fn previous_forkchoice_update_params(&self) -> (ForkchoiceState, PayloadAttributes) { let mock_execution_layer = self.harness.mock_execution_layer.as_ref().unwrap(); let json = mock_execution_layer @@ -353,19 +349,6 @@ impl InvalidPayloadRig { .await .unwrap(); } - - fn assert_get_head_error_contains(&self, s: &str) { - match self - .harness - .chain - .canonical_head - .fork_choice_write_lock() - .get_head(self.harness.chain.slot().unwrap(), &self.harness.chain.spec) - { - Err(ForkChoiceError::ProtoArrayStringError(e)) if e.contains(s) => (), - other => panic!("expected {} error, got {:?}", s, other), - }; - } } /// Simple test of the different import types. @@ -1297,21 +1280,14 @@ impl InvalidHeadSetup { rig.invalidate_manually(invalid_head.head_block_root()) .await; - // Since our setup ensures that there is only a single, invalid block - // that's viable for head (according to FFG filtering), setting the - // head block as invalid should not result in another head being chosen. - // Rather, it should fail to run fork choice and leave the invalid block as - // the head. - assert!( - rig.canonical_head() - .head_execution_status() - .unwrap() - .is_invalid() - ); - - // Ensure that we're getting the correct error when trying to find a new - // head. - rig.assert_get_head_error_contains("InvalidBestNode"); + // Ensure the justified root is the head. This is the spec-correct choice of head when + // all leaves are ineligible. + let mut fork_choice = rig.harness.chain.canonical_head.fork_choice_write_lock(); + let head = fork_choice + .get_head(rig.harness.chain.slot().unwrap(), &rig.harness.chain.spec) + .unwrap(); + assert_eq!(head.0, fork_choice.justified_checkpoint().root); + drop(fork_choice); Self { rig, diff --git a/consensus/proto_array/src/error.rs b/consensus/proto_array/src/error.rs index d185ed371c..eb0f30cc87 100644 --- a/consensus/proto_array/src/error.rs +++ b/consensus/proto_array/src/error.rs @@ -1,6 +1,6 @@ use crate::PayloadStatus; use safe_arith::ArithError; -use types::{Checkpoint, Epoch, ExecutionBlockHash, Hash256, Slot}; +use types::{Epoch, ExecutionBlockHash, Hash256}; #[derive(Clone, PartialEq, Debug)] pub enum Error { @@ -9,8 +9,6 @@ pub enum Error { NodeUnknown(Hash256), InvalidFinalizedRootChange, InvalidNodeIndex(usize), - InvalidParentIndex(usize), - InvalidBestChildIndex(usize), InvalidJustifiedIndex(usize), InvalidBestDescendant(usize), InvalidParentDelta(usize), @@ -30,7 +28,6 @@ pub enum Error { current_finalized_epoch: Epoch, new_finalized_epoch: Epoch, }, - InvalidBestNode(Box), InvalidAncestorOfValidPayload { ancestor_block_root: Hash256, ancestor_payload_block_hash: ExecutionBlockHash, @@ -74,14 +71,3 @@ impl From for Error { Error::Arith(e) } } - -#[derive(Clone, PartialEq, Debug)] -pub struct InvalidBestNodeInfo { - pub current_slot: Slot, - pub start_root: Hash256, - pub justified_checkpoint: Checkpoint, - pub finalized_checkpoint: Checkpoint, - pub head_root: Hash256, - pub head_justified_checkpoint: Checkpoint, - pub head_finalized_checkpoint: Checkpoint, -} diff --git a/consensus/proto_array/src/proto_array.rs b/consensus/proto_array/src/proto_array.rs index 6ff5eabb04..48efa480b0 100644 --- a/consensus/proto_array/src/proto_array.rs +++ b/consensus/proto_array/src/proto_array.rs @@ -1,4 +1,3 @@ -use crate::error::InvalidBestNodeInfo; use crate::proto_array_fork_choice::IndexedForkChoiceNode; use crate::{ Block, ExecutionStatus, JustifiedBalances, LatestMessage, PayloadStatus, error::Error, @@ -1093,28 +1092,6 @@ impl ProtoArray { spec, )?; - // Perform a sanity check that the node is indeed valid to be the head. - let best_node = self - .nodes - .get(best_fc_node.proto_node_index) - .ok_or(Error::InvalidNodeIndex(best_fc_node.proto_node_index))?; - if !self.node_is_viable_for_head::( - best_node, - current_slot, - best_justified_checkpoint, - best_finalized_checkpoint, - ) { - return Err(Error::InvalidBestNode(Box::new(InvalidBestNodeInfo { - current_slot, - start_root: *justified_root, - justified_checkpoint: best_justified_checkpoint, - finalized_checkpoint: best_finalized_checkpoint, - head_root: best_node.root(), - head_justified_checkpoint: *best_node.justified_checkpoint(), - head_finalized_checkpoint: *best_node.finalized_checkpoint(), - }))); - } - Ok((best_fc_node.root, best_fc_node.payload_status)) }