From 74a5609ab1f059cb9bb5dfcee4d82e1e8a01904c Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Mon, 1 Jun 2026 10:46:58 +1000 Subject: [PATCH 01/26] 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)) } From f0aaf655533cf838cb3470de0be9936ef500815e Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Mon, 1 Jun 2026 08:17:00 +0200 Subject: [PATCH 02/26] Use correct slot in custody request (#9380) Co-Authored-By: dapplion <35266934+dapplion@users.noreply.github.com> --- .../network/src/sync/block_lookups/common.rs | 2 +- .../sync/block_lookups/single_block_lookup.rs | 6 +++-- .../network/src/sync/network_context.rs | 22 +++---------------- 3 files changed, 8 insertions(+), 22 deletions(-) diff --git a/beacon_node/network/src/sync/block_lookups/common.rs b/beacon_node/network/src/sync/block_lookups/common.rs index edd99345b4..bf11f0b658 100644 --- a/beacon_node/network/src/sync/block_lookups/common.rs +++ b/beacon_node/network/src/sync/block_lookups/common.rs @@ -172,7 +172,7 @@ impl RequestState for CustodyRequestState { _: usize, cx: &mut SyncNetworkContext, ) -> Result { - cx.custody_lookup_request(id, self.block_root, lookup_peers) + cx.custody_lookup_request(id, self.block_root, self.slot, lookup_peers) .map_err(LookupRequestError::SendFailedNetwork) } diff --git a/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs b/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs index d54480e8e5..536a8c5cb0 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 @@ -239,7 +239,7 @@ impl SingleBlockLookup { ); } else if cx.chain.should_fetch_custody_columns(block_epoch) { self.component_requests = ComponentRequests::ActiveCustodyRequest( - CustodyRequestState::new(self.block_root), + CustodyRequestState::new(self.block_root, block.slot()), ); } else { self.component_requests = ComponentRequests::NotNeeded("outside da window"); @@ -397,13 +397,15 @@ impl BlobRequestState { pub struct CustodyRequestState { #[educe(Debug(ignore))] pub block_root: Hash256, + pub slot: Slot, pub state: SingleLookupRequestState>, } impl CustodyRequestState { - pub fn new(block_root: Hash256) -> Self { + pub fn new(block_root: Hash256, slot: Slot) -> Self { Self { block_root, + slot, state: SingleLookupRequestState::new(), } } diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index 9d5ac40c0a..3aaf4dbaa0 100644 --- a/beacon_node/network/src/sync/network_context.rs +++ b/beacon_node/network/src/sync/network_context.rs @@ -1174,34 +1174,18 @@ impl SyncNetworkContext { &mut self, lookup_id: SingleLookupId, block_root: Hash256, + block_slot: Slot, lookup_peers: Arc>>, ) -> Result { - let slot = self - .chain - .canonical_head - .fork_choice_read_lock() - .get_block(&block_root) - .map(|block| block.slot) - .or_else(|| self.chain.slot().ok()) - .ok_or_else(|| { - RpcRequestSendError::InternalError(format!( - "Unable to determine slot for block {block_root:?}" - )) - })?; - let custody_indexes_imported = self .chain - .cached_data_column_indexes(&block_root, slot) + .cached_data_column_indexes(&block_root, block_slot) .unwrap_or_default(); - let current_epoch = self.chain.epoch().map_err(|e| { - RpcRequestSendError::InternalError(format!("Unable to read slot clock {:?}", e)) - })?; - // Include only the blob indexes not yet imported (received through gossip) let mut custody_indexes_to_fetch = self .chain - .sampling_columns_for_epoch(current_epoch) + .sampling_columns_for_epoch(block_slot.epoch(T::EthSpec::slots_per_epoch())) .iter() .copied() .filter(|index| !custody_indexes_imported.contains(index)) From cf259e7c501187a9079c525418fa792b6ab271ec Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Mon, 1 Jun 2026 10:43:40 +0200 Subject: [PATCH 03/26] Make proposer_score_boost non-optional in ChainSpec (#9386) Co-Authored-By: dapplion <35266934+dapplion@users.noreply.github.com> --- .../beacon_chain/src/block_production/mod.rs | 8 -------- .../proto_array/src/fork_choice_test_definition.rs | 2 +- .../fork_choice_test_definition/gloas_payload.rs | 4 ++-- consensus/proto_array/src/proto_array.rs | 5 +---- consensus/types/src/core/chain_spec.rs | 14 +++++++++----- 5 files changed, 13 insertions(+), 20 deletions(-) diff --git a/beacon_node/beacon_chain/src/block_production/mod.rs b/beacon_node/beacon_chain/src/block_production/mod.rs index a94bc697b9..17fa34ce02 100644 --- a/beacon_node/beacon_chain/src/block_production/mod.rs +++ b/beacon_node/beacon_chain/src/block_production/mod.rs @@ -179,14 +179,6 @@ impl BeaconChain { 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!( - reason = "this network does not have proposer boost enabled", - "Ignoring proposer re-org configuration" - ); - return None; - } - let slot_delay = self .slot_clock .seconds_from_current_slot_start() diff --git a/consensus/proto_array/src/fork_choice_test_definition.rs b/consensus/proto_array/src/fork_choice_test_definition.rs index 43b76ec7cb..3dc5406212 100644 --- a/consensus/proto_array/src/fork_choice_test_definition.rs +++ b/consensus/proto_array/src/fork_choice_test_definition.rs @@ -144,7 +144,7 @@ impl ForkChoiceTestDefinition { pub fn run(self) { let spec = self.spec.unwrap_or_else(|| { let mut spec = MainnetEthSpec::default_spec(); - spec.proposer_score_boost = Some(50); + spec.proposer_score_boost = 50; // Legacy test definitions target pre-Gloas behaviour unless explicitly overridden. spec.gloas_fork_epoch = None; spec diff --git a/consensus/proto_array/src/fork_choice_test_definition/gloas_payload.rs b/consensus/proto_array/src/fork_choice_test_definition/gloas_payload.rs index ac4f8992c4..bf79a0170f 100644 --- a/consensus/proto_array/src/fork_choice_test_definition/gloas_payload.rs +++ b/consensus/proto_array/src/fork_choice_test_definition/gloas_payload.rs @@ -2,7 +2,7 @@ use super::*; fn gloas_spec() -> ChainSpec { let mut spec = MainnetEthSpec::default_spec(); - spec.proposer_score_boost = Some(50); + spec.proposer_score_boost = 50; spec.gloas_fork_epoch = Some(Epoch::new(0)); spec } @@ -977,7 +977,7 @@ mod tests { fn gloas_fork_boundary_spec() -> ChainSpec { let mut spec = MainnetEthSpec::default_spec(); - spec.proposer_score_boost = Some(50); + spec.proposer_score_boost = 50; spec.gloas_fork_epoch = Some(Epoch::new(1)); spec } diff --git a/consensus/proto_array/src/proto_array.rs b/consensus/proto_array/src/proto_array.rs index 48efa480b0..1e3303afbb 100644 --- a/consensus/proto_array/src/proto_array.rs +++ b/consensus/proto_array/src/proto_array.rs @@ -1861,10 +1861,7 @@ fn get_proposer_score( justified_balances: &JustifiedBalances, spec: &ChainSpec, ) -> Result { - let Some(proposer_score_boost) = spec.proposer_score_boost else { - return Ok(0); - }; - calculate_committee_fraction::(justified_balances, proposer_score_boost) + calculate_committee_fraction::(justified_balances, spec.proposer_score_boost) .ok_or(Error::ProposerBoostOverflow(0)) } diff --git a/consensus/types/src/core/chain_spec.rs b/consensus/types/src/core/chain_spec.rs index 25dcb4ba06..9ccaa86579 100644 --- a/consensus/types/src/core/chain_spec.rs +++ b/consensus/types/src/core/chain_spec.rs @@ -151,7 +151,7 @@ pub struct ChainSpec { /* * Fork choice */ - pub proposer_score_boost: Option, + pub proposer_score_boost: u64, pub reorg_head_weight_threshold: u64, pub reorg_parent_weight_threshold: u64, pub reorg_max_epochs_since_finalization: u64, @@ -1162,7 +1162,7 @@ impl ChainSpec { /* * Fork choice */ - proposer_score_boost: Some(40), + proposer_score_boost: 40, reorg_head_weight_threshold: 20, reorg_parent_weight_threshold: 160, reorg_max_epochs_since_finalization: 2, @@ -1587,7 +1587,7 @@ impl ChainSpec { /* * Fork choice */ - proposer_score_boost: Some(40), + proposer_score_boost: 40, reorg_head_weight_threshold: 20, reorg_parent_weight_threshold: 160, reorg_max_epochs_since_finalization: 2, @@ -2640,7 +2640,9 @@ impl Config { min_per_epoch_churn_limit: spec.min_per_epoch_churn_limit, max_per_epoch_activation_churn_limit: spec.max_per_epoch_activation_churn_limit, - proposer_score_boost: spec.proposer_score_boost.map(|value| MaybeQuoted { value }), + proposer_score_boost: Some(MaybeQuoted { + value: spec.proposer_score_boost, + }), 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, @@ -2854,7 +2856,9 @@ impl Config { min_per_epoch_churn_limit, max_per_epoch_activation_churn_limit, churn_limit_quotient, - proposer_score_boost: proposer_score_boost.map(|q| q.value), + proposer_score_boost: proposer_score_boost + .map(|q| q.value) + .unwrap_or(chain_spec.proposer_score_boost), reorg_head_weight_threshold, reorg_parent_weight_threshold, reorg_max_epochs_since_finalization, From 578b6a62c7b3c77e6c1c865e6a8b609d84d11570 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Mon, 1 Jun 2026 05:10:40 -0700 Subject: [PATCH 04/26] Add `POST beacon/bid` endpoint (#9347) This endpoint is needed for buildoor (and eventually our builder client once its implemented) Co-Authored-By: Eitan Seri-Levi --- .../src/beacon/execution_payload_bid.rs | 112 ++++++++++++++++++ .../src/beacon/execution_payload_envelope.rs | 5 - beacon_node/http_api/src/beacon/mod.rs | 1 + beacon_node/http_api/src/lib.rs | 21 ++++ beacon_node/http_api/tests/tests.rs | 84 ++++++++++++- common/eth2/src/lib.rs | 53 ++++++++- 6 files changed, 266 insertions(+), 10 deletions(-) create mode 100644 beacon_node/http_api/src/beacon/execution_payload_bid.rs diff --git a/beacon_node/http_api/src/beacon/execution_payload_bid.rs b/beacon_node/http_api/src/beacon/execution_payload_bid.rs new file mode 100644 index 0000000000..f6041b55c8 --- /dev/null +++ b/beacon_node/http_api/src/beacon/execution_payload_bid.rs @@ -0,0 +1,112 @@ +use crate::task_spawner::{Priority, TaskSpawner}; +use crate::utils::{ + ChainFilter, EthV1Filter, NetworkTxFilter, ResponseFilter, TaskSpawnerFilter, + publish_pubsub_message, +}; +use beacon_chain::{BeaconChain, BeaconChainTypes}; +use bytes::Bytes; +use lighthouse_network::PubsubMessage; +use network::NetworkMessage; +use ssz::Decode; +use std::sync::Arc; +use tokio::sync::mpsc::UnboundedSender; +use tracing::{debug, warn}; +use types::SignedExecutionPayloadBid; +use warp::{Filter, Rejection, Reply, hyper::Body, hyper::Response}; + +// POST /eth/v1/beacon/execution_payload_bid (SSZ) +pub(crate) fn post_beacon_execution_payload_bid_ssz( + eth_v1: EthV1Filter, + task_spawner_filter: TaskSpawnerFilter, + chain_filter: ChainFilter, + network_tx_filter: NetworkTxFilter, +) -> ResponseFilter { + eth_v1 + .and(warp::path("beacon")) + .and(warp::path("execution_payload_bid")) + .and(warp::path::end()) + .and(warp::body::bytes()) + .and(task_spawner_filter) + .and(chain_filter) + .and(network_tx_filter) + .then( + |body_bytes: Bytes, + task_spawner: TaskSpawner, + chain: Arc>, + network_tx: UnboundedSender>| { + task_spawner.blocking_response_task(Priority::P0, move || { + let bid = SignedExecutionPayloadBid::::from_ssz_bytes(&body_bytes) + .map_err(|e| { + warp_utils::reject::custom_bad_request(format!("invalid SSZ: {e:?}")) + })?; + publish_execution_payload_bid(bid, &chain, &network_tx) + }) + }, + ) + .boxed() +} + +// POST /eth/v1/beacon/execution_payload_bid +pub(crate) fn post_beacon_execution_payload_bid( + eth_v1: EthV1Filter, + task_spawner_filter: TaskSpawnerFilter, + chain_filter: ChainFilter, + network_tx_filter: NetworkTxFilter, +) -> ResponseFilter { + eth_v1 + .and(warp::path("beacon")) + .and(warp::path("execution_payload_bid")) + .and(warp::path::end()) + .and(warp::body::json()) + .and(task_spawner_filter) + .and(chain_filter) + .and(network_tx_filter) + .then( + |bid: SignedExecutionPayloadBid, + task_spawner: TaskSpawner, + chain: Arc>, + network_tx: UnboundedSender>| { + task_spawner.blocking_response_task(Priority::P0, move || { + publish_execution_payload_bid(bid, &chain, &network_tx) + }) + }, + ) + .boxed() +} + +pub fn publish_execution_payload_bid( + bid: SignedExecutionPayloadBid, + chain: &Arc>, + network_tx: &UnboundedSender>, +) -> Result, Rejection> { + let slot = bid.slot(); + let builder_index = bid.message.builder_index; + + if !chain.spec.is_gloas_scheduled() { + return Err(warp_utils::reject::custom_bad_request( + "Execution payload bids are not supported before the Gloas fork".into(), + )); + } + + debug!( + %slot, + builder_index, + "Publishing signed execution payload bid to network" + ); + + let gossip_verified_bid = chain + .verify_payload_bid_for_gossip(Arc::new(bid)) + .map_err(|e| { + warn!(%slot, error = ?e, "Execution payload bid failed gossip verification"); + warp_utils::reject::custom_bad_request(format!("bid failed gossip verification: {e}")) + })?; + + let bid_for_gossip = gossip_verified_bid.signed_bid.as_ref().clone(); + + publish_pubsub_message( + network_tx, + PubsubMessage::ExecutionPayloadBid(Box::new(bid_for_gossip)), + )?; + + Ok(warp::reply().into_response()) +} diff --git a/beacon_node/http_api/src/beacon/execution_payload_envelope.rs b/beacon_node/http_api/src/beacon/execution_payload_envelope.rs index 2e7fe693d6..d8813b0db5 100644 --- a/beacon_node/http_api/src/beacon/execution_payload_envelope.rs +++ b/beacon_node/http_api/src/beacon/execution_payload_envelope.rs @@ -11,7 +11,6 @@ use beacon_chain::payload_envelope_verification::EnvelopeError; use beacon_chain::{BeaconChain, BeaconChainTypes, NotifyExecutionLayer}; use bytes::Bytes; use eth2::types as api_types; -use eth2::{CONTENT_TYPE_HEADER, SSZ_CONTENT_TYPE_HEADER}; use lighthouse_network::PubsubMessage; use network::NetworkMessage; use ssz::{Decode, Encode}; @@ -36,10 +35,6 @@ pub(crate) fn post_beacon_execution_payload_envelope_ssz( .and(warp::path("beacon")) .and(warp::path("execution_payload_envelope")) .and(warp::path::end()) - .and(warp::header::exact( - CONTENT_TYPE_HEADER, - SSZ_CONTENT_TYPE_HEADER, - )) .and(warp::body::bytes()) .and(task_spawner_filter) .and(chain_filter) diff --git a/beacon_node/http_api/src/beacon/mod.rs b/beacon_node/http_api/src/beacon/mod.rs index 9ec1c476f6..db0062c14f 100644 --- a/beacon_node/http_api/src/beacon/mod.rs +++ b/beacon_node/http_api/src/beacon/mod.rs @@ -1,3 +1,4 @@ +pub mod execution_payload_bid; pub mod execution_payload_envelope; pub mod pool; pub mod states; diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index 74bf1ccd76..ff88c12925 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -36,6 +36,9 @@ mod validator_inclusion; mod validators; mod version; +use crate::beacon::execution_payload_bid::{ + post_beacon_execution_payload_bid, post_beacon_execution_payload_bid_ssz, +}; use crate::beacon::execution_payload_envelope::{ get_beacon_execution_payload_envelope, post_beacon_execution_payload_envelope, post_beacon_execution_payload_envelope_ssz, @@ -1555,6 +1558,22 @@ pub fn serve( network_tx_filter.clone(), ); + // POST beacon/execution_payload_bid + let post_beacon_execution_payload_bid = post_beacon_execution_payload_bid( + eth_v1.clone(), + task_spawner_filter.clone(), + chain_filter.clone(), + network_tx_filter.clone(), + ); + + // POST beacon/execution_payload_bid (SSZ) + let post_beacon_execution_payload_bid_ssz = post_beacon_execution_payload_bid_ssz( + eth_v1.clone(), + task_spawner_filter.clone(), + chain_filter.clone(), + network_tx_filter.clone(), + ); + // GET beacon/execution_payload_envelope/{block_id} let get_beacon_execution_payload_envelope = get_beacon_execution_payload_envelope( eth_v1.clone(), @@ -3445,6 +3464,7 @@ pub fn serve( .uor(post_beacon_blinded_blocks_ssz) .uor(post_beacon_blinded_blocks_v2_ssz) .uor(post_beacon_execution_payload_envelope_ssz) + .uor(post_beacon_execution_payload_bid_ssz) .uor(post_beacon_pool_payload_attestations_ssz) .uor(post_validator_proposer_preferences_ssz), ) @@ -3461,6 +3481,7 @@ pub fn serve( .uor(post_beacon_pool_bls_to_execution_changes) .uor(post_validator_proposer_preferences) .uor(post_beacon_execution_payload_envelope) + .uor(post_beacon_execution_payload_bid) .uor(post_beacon_state_validators) .uor(post_beacon_state_validator_balances) .uor(post_beacon_state_validator_identities) diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 06b3a6197b..40cb2e592f 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -48,10 +48,10 @@ use tokio::time::Duration; use tree_hash::TreeHash; use types::ApplicationDomain; use types::{ - Address, Domain, EthSpec, ExecutionBlockHash, Hash256, MainnetEthSpec, ProposerPreferences, - RelativeEpoch, SelectionProof, SignedExecutionPayloadEnvelope, SignedProposerPreferences, - SignedRoot, SingleAttestation, Slot, attestation::AttestationBase, - consts::gloas::BUILDER_INDEX_SELF_BUILD, + Address, Domain, EthSpec, ExecutionBlockHash, ExecutionPayloadBid, Hash256, MainnetEthSpec, + ProposerPreferences, RelativeEpoch, SelectionProof, SignedExecutionPayloadBid, + SignedExecutionPayloadEnvelope, SignedProposerPreferences, SignedRoot, SingleAttestation, Slot, + attestation::AttestationBase, consts::gloas::BUILDER_INDEX_SELF_BUILD, }; type E = MainnetEthSpec; @@ -3055,6 +3055,69 @@ impl ApiTester { self } + /// Build a `SignedExecutionPayloadBid` + fn make_signed_execution_payload_bid(&self) -> (SignedExecutionPayloadBid, ForkName) { + let head = self.chain.canonical_head.cached_head(); + let slot = self.chain.slot().unwrap(); + let fork_name = self.chain.spec.fork_name_at_slot::(slot); + + let bid = ExecutionPayloadBid { + parent_block_hash: ExecutionBlockHash::zero(), + parent_block_root: head.head_block_root(), + block_hash: ExecutionBlockHash::zero(), + prev_randao: Hash256::zero(), + fee_recipient: Address::zero(), + gas_limit: 30_000_000, + builder_index: 0, + slot, + value: 100, + execution_payment: 0, + blob_kzg_commitments: Default::default(), + execution_requests_root: Hash256::zero(), + }; + + let signed = SignedExecutionPayloadBid { + message: bid, + signature: bls::Signature::empty(), + }; + + (signed, fork_name) + } + + /// JSON bid with a valid structure reaches gossip verification and is rejected with 400. + pub async fn test_post_beacon_execution_payload_bid_json(self) -> Self { + let (bid, fork_name) = self.make_signed_execution_payload_bid(); + + let result = self + .client + .post_beacon_execution_payload_bid(&bid, fork_name) + .await; + + assert!( + result.is_err(), + "bid should be rejected by gossip verification" + ); + + self + } + + /// SSZ bid with a valid structure reaches gossip verification and is rejected with 400. + pub async fn test_post_beacon_execution_payload_bid_ssz(self) -> Self { + let (bid, fork_name) = self.make_signed_execution_payload_bid(); + + let result = self + .client + .post_beacon_execution_payload_bid_ssz(&bid, fork_name) + .await; + + assert!( + result.is_err(), + "bid (SSZ) should be rejected by gossip verification" + ); + + self + } + pub async fn test_get_config_fork_schedule(self) -> Self { let result = self.client.get_config_fork_schedule().await.unwrap().data; @@ -9416,3 +9479,16 @@ async fn post_validator_proposer_preferences() { .test_post_validator_proposer_preferences_duplicate() .await; } + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn post_beacon_execution_payload_bid() { + if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } + ApiTester::new_with_hard_forks() + .await + .test_post_beacon_execution_payload_bid_json() + .await + .test_post_beacon_execution_payload_bid_ssz() + .await; +} diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index e9fb44209b..e5c66ab5ff 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -46,7 +46,10 @@ use ssz::{Decode, Encode}; use std::fmt; use std::future::Future; use std::time::Duration; -use types::{PayloadAttestationData, PayloadAttestationMessage, SignedProposerPreferences}; +use types::{ + PayloadAttestationData, PayloadAttestationMessage, SignedExecutionPayloadBid, + SignedProposerPreferences, +}; pub const V1: EndpointVersion = EndpointVersion(1); pub const V2: EndpointVersion = EndpointVersion(2); @@ -2838,6 +2841,54 @@ impl BeaconNodeHttpClient { Ok(()) } + /// `POST v1/beacon/execution_payload_bid` + pub async fn post_beacon_execution_payload_bid( + &self, + bid: &SignedExecutionPayloadBid, + fork_name: ForkName, + ) -> Result<(), Error> { + let mut path = self.eth_path(V1)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("beacon") + .push("execution_payload_bid"); + + self.post_generic_with_consensus_version( + path, + bid, + Some(self.timeouts.proposal), + fork_name, + ) + .await?; + + Ok(()) + } + + /// `POST v1/beacon/execution_payload_bid` in SSZ format + pub async fn post_beacon_execution_payload_bid_ssz( + &self, + bid: &SignedExecutionPayloadBid, + fork_name: ForkName, + ) -> Result<(), Error> { + let mut path = self.eth_path(V1)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("beacon") + .push("execution_payload_bid"); + + self.post_generic_with_consensus_version_and_ssz_body( + path, + bid.as_ssz_bytes(), + Some(self.timeouts.proposal), + fork_name, + ) + .await?; + + Ok(()) + } + /// Path for `v1/beacon/execution_payload_envelope/{block_id}` pub fn get_beacon_execution_payload_envelope_path( &self, From b781227f1d676a549f4e0b687d277bcdaaf2fd82 Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Mon, 1 Jun 2026 14:10:47 +0200 Subject: [PATCH 05/26] Deprecate blob lookup sync (#9383) - Extends https://github.com/sigp/lighthouse/pull/9126 to cover blob lookup sync Lookup sync is only for unfinalized blocks, which will never contains blobs in any network we support. Co-Authored-By: dapplion <35266934+dapplion@users.noreply.github.com> Co-Authored-By: Eitan Seri-Levi --- .../src/data_availability_checker.rs | 8 +- .../overflow_lru_cache.rs | 33 +-- .../src/service/api_types.rs | 2 - .../src/network_beacon_processor/mod.rs | 26 --- .../network_beacon_processor/sync_methods.rs | 111 --------- .../src/network_beacon_processor/tests.rs | 57 +---- beacon_node/network/src/router.rs | 38 +--- .../network/src/sync/block_lookups/common.rs | 67 +----- .../network/src/sync/block_lookups/mod.rs | 6 +- .../sync/block_lookups/single_block_lookup.rs | 32 --- .../src/sync/block_sidecar_coupling.rs | 7 +- beacon_node/network/src/sync/manager.rs | 30 +-- .../network/src/sync/network_context.rs | 194 +--------------- .../src/sync/network_context/requests.rs | 2 - .../network_context/requests/blobs_by_root.rs | 73 ------ beacon_node/network/src/sync/tests/lookups.rs | 215 ++---------------- testing/ef_tests/src/cases/fork_choice.rs | 3 +- 17 files changed, 49 insertions(+), 855 deletions(-) delete mode 100644 beacon_node/network/src/sync/network_context/requests/blobs_by_root.rs diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index 3c2ba13fed..4dfb476686 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -895,12 +895,8 @@ impl AvailableBlock { match &block_data { AvailableBlockData::NoData => { // For Gloas, DA is checked for the PayloadEnvelope, not for the block. - if !block.fork_name_unchecked().gloas_enabled() { - if columns_required { - return Err(AvailabilityCheckError::MissingCustodyColumns); - } else if blobs_required { - return Err(AvailabilityCheckError::MissingBlobs); - } + if !block.fork_name_unchecked().gloas_enabled() && columns_required { + return Err(AvailabilityCheckError::MissingCustodyColumns); } } AvailableBlockData::Blobs(blobs) => { 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 2ce0b4cd4a..3e325cec02 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 @@ -9,7 +9,7 @@ use crate::data_column_verification::KzgVerifiedCustodyDataColumn; use crate::{BeaconChainTypes, BlockProcessStatus}; use lru::LruCache; use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard, RwLockWriteGuard}; -use ssz_types::{RuntimeFixedVector, RuntimeVariableList}; +use ssz_types::RuntimeFixedVector; use std::cmp::Ordering; use std::num::NonZeroUsize; use std::sync::Arc; @@ -235,36 +235,7 @@ impl PendingComponents { } } } else { - // Before PeerDAS, blobs - let num_received_blobs = self.verified_blobs.iter().flatten().count(); - match num_received_blobs.cmp(&num_expected_blobs) { - Ordering::Greater => { - // Should never happen - return Err(AvailabilityCheckError::Unexpected(format!( - "too many blobs got {num_received_blobs} expected {num_expected_blobs}" - ))); - } - Ordering::Equal => { - let max_blobs = spec.max_blobs_per_block(block.block.epoch()) as usize; - let blobs_vec = self - .verified_blobs - .iter() - .flatten() - .map(|blob| blob.clone().to_blob()) - .collect::>(); - let blobs_len = blobs_vec.len(); - let blobs = RuntimeVariableList::new(blobs_vec, max_blobs).map_err(|_| { - AvailabilityCheckError::Unexpected(format!( - "over max_blobs len {blobs_len} max {max_blobs}" - )) - })?; - Some(AvailableBlockData::Blobs(blobs)) - } - Ordering::Less => { - // Not enough blobs received yet - None - } - } + Some(AvailableBlockData::NoData) }; // Block's data not available yet diff --git a/beacon_node/lighthouse_network/src/service/api_types.rs b/beacon_node/lighthouse_network/src/service/api_types.rs index 2429b813e9..1d0d181cb3 100644 --- a/beacon_node/lighthouse_network/src/service/api_types.rs +++ b/beacon_node/lighthouse_network/src/service/api_types.rs @@ -21,8 +21,6 @@ pub struct SingleLookupReqId { pub enum SyncRequestId { /// Request searching for a block given a hash. 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. diff --git a/beacon_node/network/src/network_beacon_processor/mod.rs b/beacon_node/network/src/network_beacon_processor/mod.rs index 97673aa8b8..9f2acd73dc 100644 --- a/beacon_node/network/src/network_beacon_processor/mod.rs +++ b/beacon_node/network/src/network_beacon_processor/mod.rs @@ -37,7 +37,6 @@ use { }; pub use sync_methods::ChainSegmentProcessId; -use types::data::FixedBlobSidecarList; pub type Error = TrySendError>; @@ -534,31 +533,6 @@ impl NetworkBeaconProcessor { }) } - /// Create a new `Work` event for some blobs, where the result from computation (if any) is - /// sent to the other side of `result_tx`. - pub fn send_rpc_blobs( - self: &Arc, - block_root: Hash256, - blobs: FixedBlobSidecarList, - seen_timestamp: Duration, - process_type: BlockProcessType, - ) -> Result<(), Error> { - let blob_count = blobs.iter().filter(|b| b.is_some()).count(); - if blob_count == 0 { - return Ok(()); - } - let process_fn = self.clone().generate_rpc_blobs_process_fn( - block_root, - blobs, - seen_timestamp, - process_type, - ); - self.try_send(BeaconWorkEvent { - drop_during_sync: false, - work: Work::RpcBlobs { process_fn }, - }) - } - /// 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( 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 e3ba6fb3c4..d01795ee2c 100644 --- a/beacon_node/network/src/network_beacon_processor/sync_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/sync_methods.rs @@ -24,10 +24,7 @@ use lighthouse_network::service::api_types::CustodyBackfillBatchId; use logging::crit; use std::sync::Arc; use std::time::Duration; -use store::KzgCommitment; use tracing::{debug, debug_span, error, info, instrument, warn}; -use types::data::FixedBlobSidecarList; -use types::kzg_ext::format_kzg_commitments; use types::{BlockImportSource, DataColumnSidecarList, Epoch, Hash256}; /// Id associated to a batch processing request, either a sync batch or a parent lookup. @@ -241,114 +238,6 @@ impl NetworkBeaconProcessor { drop(handle); } - /// Returns an async closure which processes a list of blobs received via RPC. - /// - /// This separate function was required to prevent a cycle during compiler - /// type checking. - pub fn generate_rpc_blobs_process_fn( - self: Arc, - block_root: Hash256, - blobs: FixedBlobSidecarList, - seen_timestamp: Duration, - process_type: BlockProcessType, - ) -> AsyncFn { - let process_fn = async move { - self.clone() - .process_rpc_blobs(block_root, blobs, seen_timestamp, process_type) - .await; - }; - Box::pin(process_fn) - } - - /// Attempt to process a list of blobs received from a direct RPC request. - #[instrument( - name = "lh_process_rpc_blobs", - parent = None, - level = "debug", - skip_all, - fields(?block_root), - )] - pub async fn process_rpc_blobs( - self: Arc>, - block_root: Hash256, - blobs: FixedBlobSidecarList, - seen_timestamp: Duration, - process_type: BlockProcessType, - ) { - let Some(slot) = blobs - .iter() - .find_map(|blob| blob.as_ref().map(|blob| blob.slot())) - else { - return; - }; - - let (indices, commitments): (Vec, Vec) = blobs - .iter() - .filter_map(|blob_opt| { - blob_opt - .as_ref() - .map(|blob| (blob.index, blob.kzg_commitment)) - }) - .unzip(); - let commitments = format_kzg_commitments(&commitments); - - debug!( - ?indices, - %block_root, - %slot, - commitments, - "RPC blobs received" - ); - - if let Ok(current_slot) = self.chain.slot() - && current_slot == slot - { - // Note: this metric is useful to gauge how long it takes to receive blobs requested - // over rpc. Since we always send the request for block components at `get_unaggregated_attestation_due() / 2` - // we can use that as a baseline to measure against. - let delay = get_slot_delay_ms(seen_timestamp, slot, &self.chain.slot_clock); - - metrics::observe_duration(&metrics::BEACON_BLOB_RPC_SLOT_START_DELAY_TIME, delay); - } - - let result = self.chain.process_rpc_blobs(slot, block_root, blobs).await; - register_process_result_metrics(&result, metrics::BlockSource::Rpc, "blobs"); - - match &result { - Ok(AvailabilityProcessingStatus::Imported(hash)) => { - debug!( - result = "imported block and blobs", - %slot, - block_hash = %hash, - "Block components retrieved" - ); - self.chain.recompute_head_at_current_slot().await; - } - Ok(AvailabilityProcessingStatus::MissingComponents(_, _)) => { - debug!( - block_hash = %block_root, - %slot, - "Missing components over rpc" - ); - } - Err(BlockError::DuplicateFullyImported(_)) => { - debug!( - block_hash = %block_root, - %slot, - "Blobs have already been imported" - ); - } - // Errors are handled and logged in `block_lookups` - Err(_) => {} - } - - // Sync handles these results - self.send_sync_message(SyncMessage::BlockComponentProcessed { - process_type, - result: result.into(), - }); - } - #[instrument( name = "lh_process_rpc_custody_columns", parent = None, diff --git a/beacon_node/network/src/network_beacon_processor/tests.rs b/beacon_node/network/src/network_beacon_processor/tests.rs index 42d3b8f33d..c0b093e254 100644 --- a/beacon_node/network/src/network_beacon_processor/tests.rs +++ b/beacon_node/network/src/network_beacon_processor/tests.rs @@ -41,15 +41,12 @@ use std::iter::Iterator; use std::sync::Arc; use std::time::Duration; use tokio::sync::mpsc; +use types::data::BlobIdentifier; use types::{ - AttesterSlashing, BlobSidecar, ChainSpec, DataColumnSidecarList, DataColumnSubnetId, Epoch, - EthSpec, ExecutionPayloadEnvelope, ExecutionPayloadGloas, ExecutionRequests, Hash256, - MainnetEthSpec, ProposerSlashing, SignedAggregateAndProof, SignedBeaconBlock, - SignedExecutionPayloadEnvelope, SignedVoluntaryExit, SingleAttestation, Slot, SubnetId, -}; -use types::{ - BlobSidecarList, - data::{BlobIdentifier, FixedBlobSidecarList}, + AttesterSlashing, ChainSpec, DataColumnSidecarList, DataColumnSubnetId, Epoch, EthSpec, + ExecutionPayloadEnvelope, ExecutionPayloadGloas, ExecutionRequests, Hash256, MainnetEthSpec, + ProposerSlashing, SignedAggregateAndProof, SignedBeaconBlock, SignedExecutionPayloadEnvelope, + SignedVoluntaryExit, SingleAttestation, Slot, SubnetId, }; type E = MainnetEthSpec; @@ -69,7 +66,6 @@ const STANDARD_TIMEOUT: Duration = Duration::from_secs(10); struct TestRig { chain: Arc>, next_block: Arc>, - next_blobs: Option>, next_data_columns: Option>, attestations: Vec<(SingleAttestation, SubnetId)>, next_block_attestations: Vec<(SingleAttestation, SubnetId)>, @@ -341,7 +337,7 @@ impl TestRig { assert!(beacon_processor.is_ok()); let block = next_block_tuple.0; - let (blob_sidecars, data_columns) = if let Some((kzg_proofs, blobs)) = next_block_tuple.1 { + let data_columns = if let Some((kzg_proofs, blobs)) = next_block_tuple.1 { if chain.spec.is_peer_das_enabled_for_epoch(block.epoch()) { let kzg = get_kzg(&chain.spec); let epoch = block.slot().epoch(E::slots_per_epoch()); @@ -358,20 +354,17 @@ impl TestRig { .filter(|c| sampling_indices.contains(c.index())) .collect::>(); - (None, Some(custody_columns)) + Some(custody_columns) } else { - let blob_sidecars = - BlobSidecar::build_sidecars(blobs, &block, kzg_proofs, &chain.spec).unwrap(); - (Some(blob_sidecars), None) + None } } else { - (None, None) + None }; Self { chain, next_block: block, - next_blobs: blob_sidecars, next_data_columns: data_columns, attestations, next_block_attestations, @@ -448,20 +441,6 @@ impl TestRig { .unwrap(); } - pub fn enqueue_single_lookup_rpc_blobs(&self) { - if let Some(blobs) = self.next_blobs.clone() { - let blobs = FixedBlobSidecarList::new(blobs.into_iter().map(Some).collect::>()); - self.network_beacon_processor - .send_rpc_blobs( - self.next_block.canonical_root(), - blobs, - std::time::Duration::default(), - BlockProcessType::SingleBlob { id: 1 }, - ) - .unwrap(); - } - } - pub fn enqueue_single_lookup_rpc_data_columns(&self) { if let Some(data_columns) = self.next_data_columns.clone() { self.network_beacon_processor @@ -1278,7 +1257,6 @@ async fn attestation_to_unknown_block_processed(import_method: BlockImportMethod ); // Send the block and ensure that the attestation is received back and imported. - let num_blobs = rig.next_blobs.as_ref().map(|b| b.len()).unwrap_or(0); let num_data_columns = rig.next_data_columns.as_ref().map(|c| c.len()).unwrap_or(0); let mut events = vec![]; match import_method { @@ -1293,10 +1271,6 @@ async fn attestation_to_unknown_block_processed(import_method: BlockImportMethod BlockImportMethod::Rpc => { rig.enqueue_lookup_block(); events.push(WorkType::RpcBlock); - if num_blobs > 0 { - rig.enqueue_single_lookup_rpc_blobs(); - events.push(WorkType::RpcBlobs); - } if num_data_columns > 0 { rig.enqueue_single_lookup_rpc_data_columns(); events.push(WorkType::RpcCustodyColumn); @@ -1360,7 +1334,6 @@ async fn aggregate_attestation_to_unknown_block(import_method: BlockImportMethod ); // Send the block and ensure that the attestation is received back and imported. - let num_blobs = rig.next_blobs.as_ref().map(|b| b.len()).unwrap_or(0); let num_data_columns = rig.next_data_columns.as_ref().map(|c| c.len()).unwrap_or(0); let mut events = vec![]; match import_method { @@ -1375,10 +1348,6 @@ async fn aggregate_attestation_to_unknown_block(import_method: BlockImportMethod BlockImportMethod::Rpc => { rig.enqueue_lookup_block(); events.push(WorkType::RpcBlock); - if num_blobs > 0 { - rig.enqueue_single_lookup_rpc_blobs(); - events.push(WorkType::RpcBlobs); - } if num_data_columns > 0 { rig.enqueue_single_lookup_rpc_data_columns(); events.push(WorkType::RpcCustodyColumn); @@ -1565,19 +1534,13 @@ async fn import_misc_gossip_ops() { async fn test_rpc_block_reprocessing() { let mut rig = TestRig::new(SMALL_CHAIN).await; let next_block_root = rig.next_block.canonical_root(); + // Insert the next block into the duplicate cache manually let handle = rig.duplicate_cache.check_and_insert(next_block_root); rig.enqueue_single_lookup_block(); rig.assert_event_journal_completes(&[WorkType::RpcBlock]) .await; - let num_blobs = rig.next_blobs.as_ref().map(|b| b.len()).unwrap_or(0); - if num_blobs > 0 { - rig.enqueue_single_lookup_rpc_blobs(); - rig.assert_event_journal_completes(&[WorkType::RpcBlobs]) - .await; - } - let num_data_columns = rig.next_data_columns.as_ref().map(|c| c.len()).unwrap_or(0); if num_data_columns > 0 { rig.enqueue_single_lookup_rpc_data_columns(); diff --git a/beacon_node/network/src/router.rs b/beacon_node/network/src/router.rs index d2098d341e..a8e5c9ae4a 100644 --- a/beacon_node/network/src/router.rs +++ b/beacon_node/network/src/router.rs @@ -340,8 +340,8 @@ impl Router { Response::BlobsByRange(blob) => { self.on_blobs_by_range_response(peer_id, app_request_id, blob); } - Response::BlobsByRoot(blob) => { - self.on_blobs_by_root_response(peer_id, app_request_id, blob); + Response::BlobsByRoot(_) => { + crit!(%peer_id, "Unexpected BlobsByRoot response; lookup blob requests removed"); } Response::DataColumnsByRoot(data_column) => { self.on_data_columns_by_root_response(peer_id, app_request_id, data_column); @@ -721,40 +721,6 @@ impl Router { }); } - /// Handle a `BlobsByRoot` response from the peer. - pub fn on_blobs_by_root_response( - &mut self, - peer_id: PeerId, - app_request_id: AppRequestId, - blob_sidecar: Option>>, - ) { - let sync_request_id = match app_request_id { - AppRequestId::Sync(sync_id) => match sync_id { - id @ SyncRequestId::SingleBlob { .. } => id, - other => { - crit!(request = ?other, "BlobsByRoot response on incorrect request"); - return; - } - }, - AppRequestId::Router => { - crit!(%peer_id, "All BlobsByRoot requests belong to sync"); - return; - } - AppRequestId::Internal => unreachable!("Handled internally"), - }; - - trace!( - %peer_id, - "Received BlobsByRoot Response" - ); - self.send_to_sync(SyncMessage::RpcBlob { - sync_request_id, - peer_id, - blob_sidecar, - seen_timestamp: self.chain.slot_clock.now_duration().unwrap_or_default(), - }); - } - /// Handle a `DataColumnsByRoot` response from the peer. pub fn on_data_columns_by_root_response( &mut self, diff --git a/beacon_node/network/src/sync/block_lookups/common.rs b/beacon_node/network/src/sync/block_lookups/common.rs index bf11f0b658..4306458615 100644 --- a/beacon_node/network/src/sync/block_lookups/common.rs +++ b/beacon_node/network/src/sync/block_lookups/common.rs @@ -1,9 +1,7 @@ use crate::sync::block_lookups::single_block_lookup::{ LookupRequestError, SingleBlockLookup, SingleLookupRequestState, }; -use crate::sync::block_lookups::{ - BlobRequestState, BlockRequestState, CustodyRequestState, PeerId, -}; +use crate::sync::block_lookups::{BlockRequestState, CustodyRequestState, PeerId}; use crate::sync::manager::BlockProcessType; use crate::sync::network_context::{LookupRequestResult, SyncNetworkContext}; use beacon_chain::BeaconChainTypes; @@ -11,7 +9,6 @@ use lighthouse_network::service::api_types::Id; use parking_lot::RwLock; use std::collections::HashSet; use std::sync::Arc; -use types::data::FixedBlobSidecarList; use types::{DataColumnSidecarList, SignedBeaconBlock}; use super::SingleLookupId; @@ -20,17 +17,16 @@ use super::single_block_lookup::{ComponentRequests, DownloadResult}; #[derive(Debug, Copy, Clone)] pub enum ResponseType { Block, - Blob, CustodyColumn, } -/// This trait unifies common single block lookup functionality across blocks and blobs. This -/// includes making requests, verifying responses, and handling processing results. A -/// `SingleBlockLookup` includes both a `BlockRequestState` and a `BlobRequestState`, this trait is -/// implemented for each. +/// This trait unifies common single block lookup functionality across blocks and data columns. +/// This includes making requests, verifying responses, and handling processing results. A +/// `SingleBlockLookup` includes both a `BlockRequestState` and a `CustodyRequestState`, this trait +/// is implemented for each. /// /// The use of the `ResponseType` associated type gives us a degree of type -/// safety when handling a block/blob response ensuring we only mutate the correct corresponding +/// safety when handling a block/column response ensuring we only mutate the correct corresponding /// state. pub trait RequestState { /// The type created after validation. @@ -61,7 +57,7 @@ pub trait RequestState { /// Returns the `ResponseType` associated with this trait implementation. Useful in logging. fn response_type() -> ResponseType; - /// A getter for the `BlockRequestState` or `BlobRequestState` associated with this trait. + /// A getter for the `BlockRequestState` or `CustodyRequestState` associated with this trait. fn request_state_mut(request: &mut SingleBlockLookup) -> Result<&mut Self, &'static str>; /// A getter for a reference to the `SingleLookupRequestState` associated with this trait. @@ -114,54 +110,6 @@ impl RequestState for BlockRequestState { } } -impl RequestState for BlobRequestState { - type VerifiedResponseType = FixedBlobSidecarList; - - fn make_request( - &self, - id: Id, - lookup_peers: Arc>>, - expected_blobs: usize, - cx: &mut SyncNetworkContext, - ) -> Result { - cx.blob_lookup_request(id, lookup_peers, self.block_root, expected_blobs) - .map_err(LookupRequestError::SendFailedNetwork) - } - - fn send_for_processing( - id: Id, - download_result: DownloadResult, - cx: &SyncNetworkContext, - ) -> Result<(), LookupRequestError> { - let DownloadResult { - value, - block_root, - seen_timestamp, - .. - } = download_result; - cx.send_blobs_for_processing(id, block_root, value, seen_timestamp) - .map_err(LookupRequestError::SendFailedProcessor) - } - - fn response_type() -> ResponseType { - ResponseType::Blob - } - fn request_state_mut(request: &mut SingleBlockLookup) -> Result<&mut Self, &'static str> { - match &mut request.component_requests { - ComponentRequests::WaitingForBlock => Err("waiting for block"), - ComponentRequests::ActiveBlobRequest(request, _) => Ok(request), - ComponentRequests::ActiveCustodyRequest { .. } => Err("expecting custody request"), - ComponentRequests::NotNeeded { .. } => Err("not needed"), - } - } - fn get_state(&self) -> &SingleLookupRequestState { - &self.state - } - fn get_state_mut(&mut self) -> &mut SingleLookupRequestState { - &mut self.state - } -} - impl RequestState for CustodyRequestState { type VerifiedResponseType = DataColumnSidecarList; @@ -203,7 +151,6 @@ impl RequestState for CustodyRequestState { fn request_state_mut(request: &mut SingleBlockLookup) -> Result<&mut Self, &'static str> { match &mut request.component_requests { ComponentRequests::WaitingForBlock => Err("waiting for block"), - ComponentRequests::ActiveBlobRequest { .. } => Err("expecting blob request"), ComponentRequests::ActiveCustodyRequest(request) => Ok(request), ComponentRequests::NotNeeded { .. } => Err("not needed"), } diff --git a/beacon_node/network/src/sync/block_lookups/mod.rs b/beacon_node/network/src/sync/block_lookups/mod.rs index ff3bf6f998..51343cecdb 100644 --- a/beacon_node/network/src/sync/block_lookups/mod.rs +++ b/beacon_node/network/src/sync/block_lookups/mod.rs @@ -39,7 +39,7 @@ use fnv::FnvHashMap; use lighthouse_network::service::api_types::SingleLookupReqId; use lighthouse_network::{PeerAction, PeerId}; use lru_cache::LRUTimeCache; -pub use single_block_lookup::{BlobRequestState, BlockRequestState, CustodyRequestState}; +pub use single_block_lookup::{BlockRequestState, CustodyRequestState}; use std::collections::hash_map::Entry; use std::sync::Arc; use std::time::Duration; @@ -550,9 +550,6 @@ impl BlockLookups { BlockProcessType::SingleBlock { id } => { self.on_processing_result_inner::>(id, result, cx) } - BlockProcessType::SingleBlob { id } => { - self.on_processing_result_inner::>(id, result, cx) - } BlockProcessType::SingleCustodyColumn(id) => { self.on_processing_result_inner::>(id, result, cx) } @@ -696,7 +693,6 @@ impl BlockLookups { PeerAction::MidToleranceError, match R::response_type() { ResponseType::Block => "lookup_block_processing_failure", - ResponseType::Blob => "lookup_blobs_processing_failure", ResponseType::CustodyColumn => { "lookup_custody_column_processing_failure" } 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 536a8c5cb0..b712f6d86c 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 @@ -15,7 +15,6 @@ use std::time::{Duration, Instant}; use store::Hash256; use strum::IntoStaticStr; use tracing::{Span, debug_span}; -use types::data::FixedBlobSidecarList; use types::{DataColumnSidecarList, EthSpec, SignedBeaconBlock, Slot}; // Dedicated enum for LookupResult to force its usage @@ -77,7 +76,6 @@ pub struct SingleBlockLookup { #[derive(Debug)] pub(crate) enum ComponentRequests { WaitingForBlock, - ActiveBlobRequest(BlobRequestState, usize), ActiveCustodyRequest(CustodyRequestState), // When printing in debug this state display the reason why it's not needed #[allow(dead_code)] @@ -176,7 +174,6 @@ impl SingleBlockLookup { self.block_request_state.state.is_processed() && match &self.component_requests { ComponentRequests::WaitingForBlock => false, - ComponentRequests::ActiveBlobRequest(request, _) => request.state.is_processed(), ComponentRequests::ActiveCustodyRequest(request) => request.state.is_processed(), ComponentRequests::NotNeeded { .. } => true, } @@ -191,9 +188,6 @@ impl SingleBlockLookup { // check if the`block_request_state.state.is_awaiting_event(). However we already // checked that above, so `WaitingForBlock => false` is equivalent. ComponentRequests::WaitingForBlock => false, - ComponentRequests::ActiveBlobRequest(request, _) => { - request.state.is_awaiting_event() - } ComponentRequests::ActiveCustodyRequest(request) => { request.state.is_awaiting_event() } @@ -232,11 +226,6 @@ impl SingleBlockLookup { let block_epoch = block.slot().epoch(T::EthSpec::slots_per_epoch()); if expected_blobs == 0 { self.component_requests = ComponentRequests::NotNeeded("no data"); - } else if cx.chain.should_fetch_blobs(block_epoch) { - self.component_requests = ComponentRequests::ActiveBlobRequest( - BlobRequestState::new(self.block_root), - expected_blobs, - ); } else if cx.chain.should_fetch_custody_columns(block_epoch) { self.component_requests = ComponentRequests::ActiveCustodyRequest( CustodyRequestState::new(self.block_root, block.slot()), @@ -260,9 +249,6 @@ impl SingleBlockLookup { match &self.component_requests { ComponentRequests::WaitingForBlock => {} // do nothing - ComponentRequests::ActiveBlobRequest(_, expected_blobs) => { - self.continue_request::>(cx, *expected_blobs)? - } ComponentRequests::ActiveCustodyRequest(_) => { self.continue_request::>(cx, 0)? } @@ -373,24 +359,6 @@ impl SingleBlockLookup { } } -/// The state of the blob request component of a `SingleBlockLookup`. -#[derive(Educe)] -#[educe(Debug)] -pub struct BlobRequestState { - #[educe(Debug(ignore))] - pub block_root: Hash256, - pub state: SingleLookupRequestState>, -} - -impl BlobRequestState { - pub fn new(block_root: Hash256) -> Self { - Self { - block_root, - state: SingleLookupRequestState::new(), - } - } -} - /// The state of the custody request component of a `SingleBlockLookup`. #[derive(Educe)] #[educe(Debug)] diff --git a/beacon_node/network/src/sync/block_sidecar_coupling.rs b/beacon_node/network/src/sync/block_sidecar_coupling.rs index bb43396473..c1d85d0d3b 100644 --- a/beacon_node/network/src/sync/block_sidecar_coupling.rs +++ b/beacon_node/network/src/sync/block_sidecar_coupling.rs @@ -607,11 +607,14 @@ mod tests { let mut spec = test_spec::(); spec.deneb_fork_epoch = Some(Epoch::new(0)); + // Pin to pre-PeerDAS so this exercises the blob (not custody-column) path under any + // FORK_NAME. + spec.fulu_fork_epoch = None; let spec = Arc::new(spec); let da_checker = Arc::new(test_da_checker(spec.clone(), NodeCustodyType::Fullnode)); - // Assert response is finished and RpcBlocks cannot be constructed, because blobs weren't returned. + // Blobs are no longer required for availability, so the response succeeds without them. let result = info.responses(da_checker, spec).unwrap(); - assert!(result.is_err()) + assert!(result.is_ok()) } #[test] diff --git a/beacon_node/network/src/sync/manager.rs b/beacon_node/network/src/sync/manager.rs index 534e0bc7c8..c3869f00d5 100644 --- a/beacon_node/network/src/sync/manager.rs +++ b/beacon_node/network/src/sync/manager.rs @@ -44,7 +44,7 @@ use crate::network_beacon_processor::{ChainSegmentProcessId, NetworkBeaconProces use crate::service::NetworkMessage; use crate::status::ToStatusMessage; use crate::sync::block_lookups::{ - BlobRequestState, BlockComponent, BlockRequestState, CustodyRequestState, DownloadResult, + BlockComponent, BlockRequestState, CustodyRequestState, DownloadResult, }; use crate::sync::custody_backfill_sync::CustodyBackFillSync; use crate::sync::network_context::{PeerGroup, RpcResponseResult}; @@ -197,7 +197,6 @@ pub enum SyncMessage { #[derive(Debug, Clone)] pub enum BlockProcessType { SingleBlock { id: Id }, - SingleBlob { id: Id }, SingleCustodyColumn(Id), SinglePayloadEnvelope(Id), } @@ -206,7 +205,6 @@ impl BlockProcessType { pub fn id(&self) -> Id { match self { BlockProcessType::SingleBlock { id } - | BlockProcessType::SingleBlob { id } | BlockProcessType::SingleCustodyColumn(id) | BlockProcessType::SinglePayloadEnvelope(id) => *id, } @@ -507,9 +505,6 @@ impl SyncManager { SyncRequestId::SingleBlock { id } => { self.on_single_block_response(id, peer_id, RpcEvent::RPCError(error)) } - 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)) } @@ -1197,11 +1192,6 @@ impl SyncManager { seen_timestamp: Duration, ) { match sync_request_id { - SyncRequestId::SingleBlob { id } => self.on_single_blob_response( - id, - peer_id, - RpcEvent::from_chunk(blob, seen_timestamp), - ), SyncRequestId::BlobsByRange(id) => self.on_blobs_by_range_response( id, peer_id, @@ -1278,24 +1268,6 @@ impl SyncManager { } } - fn on_single_blob_response( - &mut self, - id: SingleLookupReqId, - peer_id: PeerId, - blob: RpcEvent>>, - ) { - if let Some(resp) = self.network.on_single_blob_response(id, peer_id, blob) { - self.block_lookups - .on_download_response::>( - id, - resp.map(|(value, seen_timestamp)| { - (value, PeerGroup::from_single(peer_id), seen_timestamp) - }), - &mut self.network, - ) - } - } - fn on_data_columns_by_root_response( &mut self, req_id: DataColumnsByRootRequestId, diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index 3aaf4dbaa0..95ae84755c 100644 --- a/beacon_node/network/src/sync/network_context.rs +++ b/beacon_node/network/src/sync/network_context.rs @@ -18,7 +18,6 @@ use crate::status::ToStatusMessage; use crate::sync::batch::ByRangeRequestType; use crate::sync::block_lookups::SingleLookupId; use crate::sync::block_sidecar_coupling::CouplingError; -use crate::sync::network_context::requests::BlobsByRootSingleBlockRequest; use crate::sync::range_data_column_batch_request::RangeDataColumnBatchRequest; use beacon_chain::block_verification_types::LookupBlock; use beacon_chain::block_verification_types::{AsBlock, RangeSyncBlock}; @@ -38,8 +37,8 @@ use lighthouse_network::{Client, NetworkGlobals, PeerAction, PeerId, ReportSourc use parking_lot::RwLock; pub use requests::LookupVerifyError; use requests::{ - ActiveRequests, BlobsByRangeRequestItems, BlobsByRootRequestItems, BlocksByRangeRequestItems, - BlocksByRootRequestItems, DataColumnsByRangeRequestItems, DataColumnsByRootRequestItems, + ActiveRequests, BlobsByRangeRequestItems, BlocksByRangeRequestItems, BlocksByRootRequestItems, + DataColumnsByRangeRequestItems, DataColumnsByRootRequestItems, PayloadEnvelopesByRootRequestItems, }; #[cfg(test)] @@ -53,7 +52,6 @@ use std::time::Duration; use task_executor::TaskExecutor; use tokio::sync::mpsc; use tracing::{Span, debug, debug_span, error, warn}; -use types::data::FixedBlobSidecarList; use types::{ BlobSidecar, BlockImportSource, ColumnIndex, DataColumnSidecar, DataColumnSidecarList, EthSpec, ForkContext, Hash256, SignedBeaconBlock, SignedExecutionPayloadEnvelope, Slot, @@ -203,8 +201,6 @@ pub struct SyncNetworkContext { /// A mapping of active BlocksByRoot requests, including both current slot and parent lookups. blocks_by_root_requests: 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>, @@ -300,7 +296,6 @@ impl SyncNetworkContext { execution_engine_state: EngineState::Online, // always assume `Online` at the start 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"), @@ -329,7 +324,6 @@ impl SyncNetworkContext { network_send: _, request_id: _, blocks_by_root_requests, - blobs_by_root_requests, payload_envelopes_by_root_requests, data_columns_by_root_requests, blocks_by_range_requests, @@ -350,10 +344,6 @@ impl SyncNetworkContext { .active_requests_of_peer(peer_id) .into_iter() .map(|id| SyncRequestId::SingleBlock { id: *id }); - let blobs_by_root_ids = blobs_by_root_requests - .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() @@ -375,7 +365,6 @@ impl SyncNetworkContext { .into_iter() .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) @@ -432,7 +421,6 @@ impl SyncNetworkContext { network_send: _, request_id: _, blocks_by_root_requests, - blobs_by_root_requests, payload_envelopes_by_root_requests, data_columns_by_root_requests, blocks_by_range_requests, @@ -455,7 +443,6 @@ 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()) @@ -1017,109 +1004,6 @@ impl SyncNetworkContext { 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 - /// - If the da_checker has pending blobs from gossip - /// - /// Returns false if no request was made, because we don't need to import (more) blobs. - pub fn blob_lookup_request( - &mut self, - lookup_id: SingleLookupId, - lookup_peers: Arc>>, - block_root: Hash256, - expected_blobs: usize, - ) -> Result { - let active_request_count_by_peer = self.active_request_count_by_peer(); - let Some(peer_id) = lookup_peers - .read() - .iter() - .map(|peer| { - ( - // Prefer peers with less overall requests - active_request_count_by_peer.get(peer).copied().unwrap_or(0), - // Random factor to break ties, otherwise the PeerID breaks ties - rand::random::(), - peer, - ) - }) - .min() - .map(|(_, _, peer)| *peer) - else { - // Allow lookup to not have any peers and do nothing. This is an optimization to not - // lose progress of lookups created from a block with unknown parent before we receive - // attestations for said block. - // Lookup sync event safety: If a lookup requires peers to make progress, and does - // not receive any new peers for some time it will be dropped. If it receives a new - // peer it must attempt to make progress. - return Ok(LookupRequestResult::Pending("no peers")); - }; - - let imported_blob_indexes = self - .chain - .data_availability_checker - .cached_blob_indexes(&block_root) - .unwrap_or_default(); - // Include only the blob indexes not yet imported (received through gossip) - let indices = (0..expected_blobs as u64) - .filter(|index| !imported_blob_indexes.contains(index)) - .collect::>(); - - if indices.is_empty() { - // No blobs required, do not issue any request - return Ok(LookupRequestResult::NoRequestNeeded("no indices to fetch")); - } - - let id = SingleLookupReqId { - lookup_id, - req_id: self.next_id(), - }; - - let request = BlobsByRootSingleBlockRequest { - block_root, - indices: indices.clone(), - }; - - // Lookup sync event safety: Refer to `Self::block_lookup_request` `network_send.send` call - let network_request = RequestType::BlobsByRoot( - 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::SingleBlob { id }), - }) - .map_err(|_| RpcRequestSendError::InternalError("network send error".to_owned()))?; - - debug!( - method = "BlobsByRoot", - ?block_root, - blob_indices = ?indices, - peer = %peer_id, - %id, - "Sync RPC request sent" - ); - - self.blobs_by_root_requests.insert( - id, - peer_id, - // true = enforce max_requests are returned for blobs_by_root. We only issue requests for - // blocks after we know the block has data, and only request peers after they claim to - // have imported the block+blobs. - true, - BlobsByRootRequestItems::new(request), - // Not implemented - Span::none(), - ); - - Ok(LookupRequestResult::RequestSent(id.req_id)) - } - /// Request to send a single `data_columns_by_root` request to the network. pub fn data_column_lookup_request( &mut self, @@ -1522,35 +1406,6 @@ impl SyncNetworkContext { self.on_rpc_response_result(resp, peer_id) } - pub(crate) fn on_single_blob_response( - &mut self, - id: SingleLookupReqId, - peer_id: PeerId, - rpc_event: RpcEvent>>, - ) -> Option>> { - let resp = self.blobs_by_root_requests.on_response(id, rpc_event); - let resp = resp.map(|res| { - res.and_then(|(blobs, seen_timestamp)| { - if let Some(max_len) = blobs - .first() - .map(|blob| self.chain.spec.max_blobs_per_block(blob.epoch()) as usize) - { - match to_fixed_blob_sidecar_list(blobs, max_len) { - Ok(blobs) => Ok((blobs, seen_timestamp)), - Err(e) => Err(e.into()), - } - } else { - Err(RpcResponseError::VerifyError( - LookupVerifyError::InternalError( - "Requested blobs for a block that has no blobs".to_string(), - ), - )) - } - }) - }); - self.on_rpc_response_result(resp, peer_id) - } - pub(crate) fn on_single_payload_envelope_response( &mut self, id: SingleLookupReqId, @@ -1718,36 +1573,6 @@ impl SyncNetworkContext { }) } - pub fn send_blobs_for_processing( - &self, - id: Id, - block_root: Hash256, - blobs: FixedBlobSidecarList, - seen_timestamp: Duration, - ) -> Result<(), SendErrorProcessor> { - let beacon_processor = self - .beacon_processor_if_enabled() - .ok_or(SendErrorProcessor::ProcessorNotAvailable)?; - - debug!(?block_root, ?id, "Sending blobs for processing"); - // Lookup sync event safety: If `beacon_processor.send_rpc_blobs` returns Ok() sync - // must receive a single `SyncMessage::BlockComponentProcessed` event with this process type - beacon_processor - .send_rpc_blobs( - block_root, - blobs, - seen_timestamp, - BlockProcessType::SingleBlob { id }, - ) - .map_err(|e| { - error!( - error = ?e, - "Failed to send sync blobs to processor" - ); - SendErrorProcessor::SendError - }) - } - #[allow(dead_code)] pub fn send_payload_for_processing( &self, @@ -1914,7 +1739,6 @@ impl SyncNetworkContext { pub(crate) fn register_metrics(&self) { for (id, count) in [ ("blocks_by_root", self.blocks_by_root_requests.len()), - ("blobs_by_root", self.blobs_by_root_requests.len()), ( "data_columns_by_root", self.data_columns_by_root_requests.len(), @@ -1935,17 +1759,3 @@ impl SyncNetworkContext { } } } - -fn to_fixed_blob_sidecar_list( - blobs: Vec>>, - max_len: usize, -) -> Result, LookupVerifyError> { - let mut fixed_list = FixedBlobSidecarList::new(vec![None; max_len]); - for blob in blobs.into_iter() { - let index = blob.index as usize; - *fixed_list - .get_mut(index) - .ok_or(LookupVerifyError::UnrequestedIndex(index as u64))? = Some(blob) - } - Ok(fixed_list) -} diff --git a/beacon_node/network/src/sync/network_context/requests.rs b/beacon_node/network/src/sync/network_context/requests.rs index 8c091eca80..72dd2c22d0 100644 --- a/beacon_node/network/src/sync/network_context/requests.rs +++ b/beacon_node/network/src/sync/network_context/requests.rs @@ -9,7 +9,6 @@ use tracing::{Span, debug}; use types::{Hash256, Slot}; pub use blobs_by_range::BlobsByRangeRequestItems; -pub use blobs_by_root::{BlobsByRootRequestItems, BlobsByRootSingleBlockRequest}; pub use blocks_by_range::BlocksByRangeRequestItems; pub use blocks_by_root::{BlocksByRootRequestItems, BlocksByRootSingleRequest}; pub use data_columns_by_range::DataColumnsByRangeRequestItems; @@ -25,7 +24,6 @@ use crate::metrics; use super::{RpcEvent, RpcResponseError, RpcResponseResult}; mod blobs_by_range; -mod blobs_by_root; mod blocks_by_range; mod blocks_by_root; mod data_columns_by_range; 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 deleted file mode 100644 index f0ff99867b..0000000000 --- a/beacon_node/network/src/sync/network_context/requests/blobs_by_root.rs +++ /dev/null @@ -1,73 +0,0 @@ -use lighthouse_network::rpc::methods::BlobsByRootRequest; -use std::sync::Arc; -use types::{BlobSidecar, EthSpec, ForkContext, Hash256, data::BlobIdentifier}; - -use super::{ActiveRequestItems, LookupVerifyError}; - -#[derive(Debug, Clone)] -pub struct BlobsByRootSingleBlockRequest { - pub block_root: Hash256, - pub indices: Vec, -} - -impl BlobsByRootSingleBlockRequest { - pub fn into_request(self, spec: &ForkContext) -> Result { - BlobsByRootRequest::new( - self.indices - .into_iter() - .map(|index| BlobIdentifier { - block_root: self.block_root, - index, - }) - .collect(), - spec, - ) - } -} - -pub struct BlobsByRootRequestItems { - request: BlobsByRootSingleBlockRequest, - items: Vec>>, -} - -impl BlobsByRootRequestItems { - pub fn new(request: BlobsByRootSingleBlockRequest) -> Self { - Self { - request, - items: vec![], - } - } -} - -impl ActiveRequestItems for BlobsByRootRequestItems { - type Item = Arc>; - - /// Appends a chunk to this multi-item request. If all expected chunks are received, this - /// method returns `Some`, resolving the request before the stream terminator. - /// The active request SHOULD be dropped after `add_response` returns an error - fn add(&mut self, blob: Self::Item) -> Result { - let block_root = blob.block_root(); - 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)); - } - if self.items.iter().any(|b| b.index == blob.index) { - return Err(LookupVerifyError::DuplicatedData(blob.slot(), blob.index)); - } - - self.items.push(blob); - - Ok(self.items.len() >= self.request.indices.len()) - } - - fn consume(&mut self) -> Vec { - std::mem::take(&mut self.items) - } -} diff --git a/beacon_node/network/src/sync/tests/lookups.rs b/beacon_node/network/src/sync/tests/lookups.rs index 5c9e18362c..6022c4796b 100644 --- a/beacon_node/network/src/sync/tests/lookups.rs +++ b/beacon_node/network/src/sync/tests/lookups.rs @@ -8,13 +8,11 @@ use crate::sync::{ SyncMessage, manager::{BatchProcessResult, BlockProcessType, BlockProcessingResult, SyncManager}, }; -use beacon_chain::blob_verification::KzgVerifiedBlob; use beacon_chain::block_verification_types::LookupBlock; use beacon_chain::custody_context::NodeCustodyType; use beacon_chain::{ AvailabilityProcessingStatus, BlockError, EngineState, NotifyExecutionLayer, block_verification_types::{AsBlock, AvailableBlockData}, - data_availability_checker::Availability, test_utils::{ AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType, NumBlobs, generate_rand_block_and_blobs, test_spec, @@ -36,8 +34,8 @@ use std::time::Duration; use tokio::sync::mpsc; use tracing::info; use types::{ - BlobSidecar, BlockImportSource, ColumnIndex, DataColumnSidecar, EthSpec, ForkContext, ForkName, - Hash256, MinimalEthSpec as E, SignedBeaconBlock, Slot, + BlobSidecar, BlockImportSource, ColumnIndex, DataColumnSidecar, ForkContext, ForkName, Hash256, + MinimalEthSpec as E, SignedBeaconBlock, Slot, }; const D: Duration = Duration::new(0, 0); @@ -549,52 +547,6 @@ impl TestRig { self.send_rpc_blocks_response(req_id, peer_id, &blocks); } - (RequestType::BlobsByRoot(req), AppRequestId::Sync(req_id)) => { - if self.complete_strategy.return_no_data_n_times > 0 { - self.complete_strategy.return_no_data_n_times -= 1; - return self.send_rpc_blobs_response(req_id, peer_id, &[]); - } - - let mut blobs = req - .blob_ids - .iter() - .map(|id| { - self.network_blocks_by_root - .get(&id.block_root) - .unwrap_or_else(|| { - panic!("Test consumer requested unknown block: {id:?}") - }) - .block_data() - .blobs() - .unwrap_or_else(|| panic!("Block {id:?} has no blobs")) - .iter() - .find(|blob| blob.index == id.index) - .unwrap_or_else(|| panic!("Blob id {id:?} not avail")) - .clone() - }) - .collect::>(); - - if self.complete_strategy.return_too_few_data_n_times > 0 { - self.complete_strategy.return_too_few_data_n_times -= 1; - blobs.pop(); - } - - if self - .complete_strategy - .return_wrong_sidecar_for_block_n_times - > 0 - { - self.complete_strategy - .return_wrong_sidecar_for_block_n_times -= 1; - let first = blobs.first_mut().expect("empty blobs"); - let mut blob = Arc::make_mut(first).clone(); - blob.signed_block_header.message.body_root = Hash256::ZERO; - *first = Arc::new(blob); - } - - self.send_rpc_blobs_response(req_id, peer_id, &blobs); - } - (RequestType::DataColumnsByRoot(req), AppRequestId::Sync(req_id)) => { if self.complete_strategy.return_no_data_n_times > 0 { self.complete_strategy.return_no_data_n_times -= 1; @@ -1006,48 +958,6 @@ impl TestRig { keypair.sk.sign(msg) } - fn corrupt_last_blob_proposer_signature(&mut self) { - let range_sync_block = self.get_last_block().clone(); - let block = range_sync_block.block_cloned(); - let mut blobs = range_sync_block - .block_data() - .blobs() - .expect("no blobs") - .into_iter() - .collect::>(); - let columns = range_sync_block.block_data().data_columns(); - let first = blobs.first_mut().expect("empty blobs"); - Arc::make_mut(first).signed_block_header.signature = self.valid_signature(); - let max_blobs = - self.harness - .spec - .max_blobs_per_block(block.slot().epoch(E::slots_per_epoch())) as usize; - let blobs = - types::BlobSidecarList::new(blobs, max_blobs).expect("invalid blob sidecar list"); - self.re_insert_block(block, Some(blobs), columns); - } - - fn corrupt_last_blob_kzg_proof(&mut self) { - let range_sync_block = self.get_last_block().clone(); - let block = range_sync_block.block_cloned(); - let mut blobs = range_sync_block - .block_data() - .blobs() - .expect("no blobs") - .into_iter() - .collect::>(); - let columns = range_sync_block.block_data().data_columns(); - let first = blobs.first_mut().expect("empty blobs"); - Arc::make_mut(first).kzg_proof = kzg::KzgProof::empty(); - let max_blobs = - self.harness - .spec - .max_blobs_per_block(block.slot().epoch(E::slots_per_epoch())) as usize; - let blobs = - types::BlobSidecarList::new(blobs, max_blobs).expect("invalid blob sidecar list"); - self.re_insert_block(block, Some(blobs), columns); - } - fn corrupt_last_column_proposer_signature(&mut self) { let range_sync_block = self.get_last_block().clone(); let block = range_sync_block.block_cloned(); @@ -1413,10 +1323,6 @@ impl TestRig { // Test setup - fn new_after_deneb() -> Option { - genesis_fork().deneb_enabled().then(Self::default) - } - fn new_after_fulu() -> Option { genesis_fork().fulu_enabled().then(Self::default) } @@ -1443,10 +1349,6 @@ impl TestRig { info!(msg, "TEST_RIG"); } - pub fn is_after_deneb(&self) -> bool { - self.fork_name.deneb_enabled() - } - pub fn is_after_fulu(&self) -> bool { self.fork_name.fulu_enabled() } @@ -1732,27 +1634,6 @@ impl TestRig { } } - fn insert_blob_to_da_checker(&mut self, blob: Arc>) { - match self - .harness - .chain - .data_availability_checker - .put_kzg_verified_blobs( - blob.block_root(), - std::iter::once( - KzgVerifiedBlob::new(blob, &self.harness.chain.kzg, Duration::new(0, 0)) - .expect("Invalid blob"), - ), - ) - .unwrap() - { - Availability::Available(_) => panic!("column removed from da_checker, available"), - Availability::MissingComponents(block_root) => { - self.log(&format!("inserted column to da_checker {block_root:?}")) - } - }; - } - fn insert_block_to_da_checker_as_pre_execution(&mut self, block: Arc>) { self.log(&format!( "Inserting block to availability_cache as pre_execution_block {:?}", @@ -1919,18 +1800,14 @@ async fn happy_path_unknown_block_parent(depth: usize) { r.build_chain(depth).await; r.trigger_with_last_unknown_block_parent(); r.simulate(SimulateConfig::happy_path()).await; - // All lookups should NOT complete on this test, however note the following for the tip lookup, - // it's the lookup for the tip block which has 0 peers and a block cached: + // Note the following for the tip lookup, it's the lookup for the tip block which has 0 peers + // and a block cached: // - before deneb the block is cached, so it's sent for processing, and success - // - before fulu the block is cached, but we can't fetch blobs so it's stuck + // - deneb/electra the block is cached, so it's sent for processing, and success // - after fulu the block is cached, we start a custody request and since we use the global pool // of peers we DO have 1 connected synced supernode peer, which gives us the columns and the // lookup succeeds - if r.is_after_deneb() && !r.is_after_fulu() { - r.assert_successful_lookup_sync_parent_trigger() - } else { - r.assert_successful_lookup_sync(); - } + r.assert_successful_lookup_sync(); } /// Assert that sync completes from an UnknownDataColumnParent @@ -1978,9 +1855,9 @@ async fn bad_peer_empty_block_response(depth: usize) { // TODO(tree-sync) Assert that a single lookup is created (no drops) } -/// Assert that if peer responds with no blobs / columns, we downscore, and retry the same lookup +/// Assert that if peer responds with no columns, we downscore, and retry the same lookup. async fn bad_peer_empty_data_response(depth: usize) { - let Some(mut r) = TestRig::new_after_deneb() else { + let Some(mut r) = TestRig::new_after_fulu() else { return; }; r.build_chain_and_trigger_last_block(depth).await; @@ -1992,10 +1869,10 @@ async fn bad_peer_empty_data_response(depth: usize) { // TODO(tree-sync) Assert that a single lookup is created (no drops) } -/// Assert that if peer responds with not enough blobs / columns, we downscore, and retry the same -/// lookup +/// Assert that if peer responds with not enough columns, we downscore, and retry the same +/// lookup. async fn bad_peer_too_few_data_response(depth: usize) { - let Some(mut r) = TestRig::new_after_deneb() else { + let Some(mut r) = TestRig::new_after_fulu() else { return; }; r.build_chain_and_trigger_last_block(depth).await; @@ -2019,9 +1896,9 @@ async fn bad_peer_wrong_block_response(depth: usize) { // TODO(tree-sync) Assert that a single lookup is created (no drops) } -/// Assert that if peer responds with bad blobs / columns, we downscore, and retry the same lookup +/// Assert that if peer responds with bad columns, we downscore, and retry the same lookup. async fn bad_peer_wrong_data_response(depth: usize) { - let Some(mut r) = TestRig::new_after_deneb() else { + let Some(mut r) = TestRig::new_after_fulu() else { return; }; r.build_chain_and_trigger_last_block(depth).await; @@ -2342,8 +2219,8 @@ async fn test_same_chain_race_condition() { #[tokio::test] /// Assert that if the lookup's block is in the da_checker we don't download it again async fn block_in_da_checker_skips_download() { - // Only in Deneb, as the block needs blobs to remain in the da_checker - let Some(mut r) = TestRig::new_after_deneb_before_fulu() else { + // Only post-Fulu, as the block needs custody columns to remain in the da_checker + let Some(mut r) = TestRig::new_after_fulu() else { return; }; // Add block to da_checker @@ -2407,32 +2284,6 @@ async fn block_in_processing_cache_becomes_valid_imported() { r.assert_no_active_lookups(); } -// IGNORE: wait for change that delays blob fetching to knowing the block -#[tokio::test] -async fn blobs_in_da_checker_skip_download() { - let Some(mut r) = TestRig::new_after_deneb_before_fulu() else { - return; - }; - r.build_chain(1).await; - let block = r.get_last_block().clone(); - let blobs = block.block_data().blobs().expect("block with no blobs"); - for blob in &blobs { - r.insert_blob_to_da_checker(blob.clone()); - } - r.trigger_with_last_block(); - r.simulate(SimulateConfig::happy_path()).await; - - r.assert_successful_lookup_sync(); - assert_eq!( - r.requests - .iter() - .filter(|(request, _)| matches!(request, RequestType::BlobsByRoot(_))) - .collect::>(), - Vec::<&(RequestType, AppRequestId)>::new(), - "There should be no blob requests" - ); -} - macro_rules! fulu_peer_matrix_tests { ( [$($name:ident => $variant:expr),+ $(,)?] @@ -2545,42 +2396,6 @@ async fn crypto_on_fail_with_invalid_block_signature() { } } -#[tokio::test] -async fn crypto_on_fail_with_bad_blob_proposer_signature() { - let Some(mut r) = TestRig::new_after_deneb_before_fulu() else { - return; - }; - r.build_chain(1).await; - r.corrupt_last_blob_proposer_signature(); - r.trigger_with_last_block(); - r.simulate(SimulateConfig::happy_path()).await; - if cfg!(feature = "fake_crypto") { - r.assert_successful_lookup_sync(); - r.assert_no_penalties(); - } else { - r.assert_failed_lookup_sync(); - r.assert_penalties_of_type("lookup_blobs_processing_failure"); - } -} - -#[tokio::test] -async fn crypto_on_fail_with_bad_blob_kzg_proof() { - let Some(mut r) = TestRig::new_after_deneb_before_fulu() else { - return; - }; - r.build_chain(1).await; - r.corrupt_last_blob_kzg_proof(); - r.trigger_with_last_block(); - r.simulate(SimulateConfig::happy_path()).await; - if cfg!(feature = "fake_crypto") { - r.assert_successful_lookup_sync(); - r.assert_no_penalties(); - } else { - r.assert_failed_lookup_sync(); - r.assert_penalties_of_type("lookup_blobs_processing_failure"); - } -} - #[tokio::test] async fn crypto_on_fail_with_bad_column_proposer_signature() { let Some(mut r) = TestRig::new_fulu_peer_test(FuluTestType::WeSupernodeThemSupernode) else { diff --git a/testing/ef_tests/src/cases/fork_choice.rs b/testing/ef_tests/src/cases/fork_choice.rs index 1736cd951f..f640583189 100644 --- a/testing/ef_tests/src/cases/fork_choice.rs +++ b/testing/ef_tests/src/cases/fork_choice.rs @@ -767,7 +767,8 @@ impl Tester { ))? .map(|avail: AvailabilityProcessingStatus| avail.try_into()); let success = blob_success && result.as_ref().is_ok_and(|inner| inner.is_ok()); - if success != valid { + // Only assert valid blocks import; blob-DA failure cases are expected to import now. + if valid && !success { return Err(Error::DidntFail(format!( "block with root {} was valid={} whilst test expects valid={}. result: {:?}", block_root, From bbe7ead81335d4e6f75dfdc8d7a56c740d5b9b68 Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Tue, 2 Jun 2026 04:50:56 +0200 Subject: [PATCH 06/26] Move BlockProcessingResult match out of block lookups (#9327) - https://github.com/sigp/lighthouse/pull/9155 remove the trait abstraction for processing block / blobs / columns / payloads As a result we would have to duplicate x3 the big match on `BlockProcessingResult` we currently have in block lookups mod.rs This PR moves the match of `BlockProcessingResult` to `sync_methods` to reduce the diff of https://github.com/sigp/lighthouse/pull/9155. There are some subtle changes that deserve dedicated attention, and may be drowned in the bigger diff of https://github.com/sigp/lighthouse/pull/9155 otherwise: | Unstable | This PR / #9115 | | - | - | | Some error conditions immediately `Drop` the lookup (no retries). For example for "internal" errors like the BeaconChainError | Retries ALL errors 4 times. I believe assuming some errors are internal is risky as dropping a lookup drops all its children potentially forcing the node to resync a lot of blocks because of an internal timeout Co-Authored-By: dapplion <35266934+dapplion@users.noreply.github.com> --- .../beacon_chain/src/block_verification.rs | 6 +- .../src/data_availability_checker/error.rs | 3 +- .../src/network_beacon_processor/mod.rs | 4 +- .../network_beacon_processor/sync_methods.rs | 143 +++++++++++++++- .../network/src/sync/block_lookups/mod.rs | 152 ++++-------------- .../sync/block_lookups/single_block_lookup.rs | 3 - .../src/sync/block_sidecar_coupling.rs | 2 +- beacon_node/network/src/sync/manager.rs | 30 +--- beacon_node/network/src/sync/mod.rs | 1 + beacon_node/network/src/sync/tests/lookups.rs | 37 +++-- 10 files changed, 214 insertions(+), 167 deletions(-) diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 22e50e4185..33317cbda7 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -92,7 +92,7 @@ use std::fs; use std::io::Write; use std::sync::Arc; use store::{Error as DBError, KeyValueStore}; -use strum::AsRefStr; +use strum::{AsRefStr, IntoStaticStr}; use task_executor::JoinHandle; use tracing::{Instrument, Span, debug, debug_span, error, info_span, instrument}; use types::{ @@ -114,7 +114,7 @@ const WRITE_BLOCK_PROCESSING_SSZ: bool = cfg!(feature = "write_ssz_files"); /// /// - The block is malformed/invalid (indicated by all results other than `BeaconChainError`. /// - We encountered an error whilst trying to verify the block (a `BeaconChainError`). -#[derive(Debug, AsRefStr)] +#[derive(Debug, AsRefStr, IntoStaticStr)] pub enum BlockError { /// The parent block was unknown. /// @@ -336,7 +336,7 @@ impl From for BlockError { /// Returned when block validation failed due to some issue verifying /// the execution payload. -#[derive(Debug)] +#[derive(Debug, IntoStaticStr)] pub enum ExecutionPayloadError { /// There's no eth1 connection (mandatory after merge) /// diff --git a/beacon_node/beacon_chain/src/data_availability_checker/error.rs b/beacon_node/beacon_chain/src/data_availability_checker/error.rs index ab69a62985..2653c84860 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker/error.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker/error.rs @@ -1,7 +1,8 @@ use kzg::{Error as KzgError, KzgCommitment}; +use strum::IntoStaticStr; use types::{BeaconStateError, ColumnIndex, Hash256}; -#[derive(Debug)] +#[derive(Debug, IntoStaticStr)] pub enum Error { InvalidBlobs(KzgError), MissingBid(Hash256), diff --git a/beacon_node/network/src/network_beacon_processor/mod.rs b/beacon_node/network/src/network_beacon_processor/mod.rs index 9f2acd73dc..c2c8577046 100644 --- a/beacon_node/network/src/network_beacon_processor/mod.rs +++ b/beacon_node/network/src/network_beacon_processor/mod.rs @@ -36,13 +36,13 @@ use { slot_clock::ManualSlotClock, store::MemoryStore, tokio::sync::mpsc::UnboundedSender, }; -pub use sync_methods::ChainSegmentProcessId; +pub use sync_methods::{BlockProcessingResult, ChainSegmentProcessId}; pub type Error = TrySendError>; mod gossip_methods; mod rpc_methods; -mod sync_methods; +pub(crate) mod sync_methods; mod tests; pub(crate) const FUTURE_SLOT_TOLERANCE: u64 = 1; 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 d01795ee2c..87d11946bd 100644 --- a/beacon_node/network/src/network_beacon_processor/sync_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/sync_methods.rs @@ -3,12 +3,14 @@ use crate::network_beacon_processor::{FUTURE_SLOT_TOLERANCE, NetworkBeaconProces use crate::sync::BatchProcessResult; use crate::sync::manager::CustodyBatchProcessResult; use crate::sync::{ - ChainId, + ChainId, PeerGroup, SyncNetworkContext, manager::{BlockProcessType, SyncMessage}, }; use beacon_chain::block_verification_types::LookupBlock; use beacon_chain::block_verification_types::{AsBlock, RangeSyncBlock}; -use beacon_chain::data_availability_checker::AvailabilityCheckError; +use beacon_chain::data_availability_checker::{ + AvailabilityCheckError, AvailabilityCheckErrorCategory, +}; use beacon_chain::historical_data_columns::HistoricalDataColumnError; use beacon_chain::{ AvailabilityProcessingStatus, BeaconChainTypes, BlockError, ChainSegmentResult, @@ -20,6 +22,7 @@ use beacon_processor::{ }; use beacon_processor::{Work, WorkEvent}; use lighthouse_network::PeerAction; +use lighthouse_network::PeerId; use lighthouse_network::service::api_types::CustodyBackfillBatchId; use logging::crit; use std::sync::Arc; @@ -87,10 +90,17 @@ impl NetworkBeaconProcessor { ); // A closure which will ignore the block. let ignore_fn = move || { + warn!( + ?process_type, + "Block processing task dropped, cpu might be overloaded" + ); // Sync handles these results self.send_sync_message(SyncMessage::BlockComponentProcessed { process_type, - result: crate::sync::manager::BlockProcessingResult::Ignored, + result: BlockProcessingResult::Error { + penalty: None, + reason: "ignored_processor_overloaded".to_string(), + }, }); }; (process_fn, Box::new(ignore_fn)) @@ -949,3 +959,130 @@ impl NetworkBeaconProcessor { } } } + +/// The classified outcome of submitting a block / blob / column for processing, ready for the +/// lookup state machine to act on without re-inspecting `BlockError`. +#[derive(Debug)] +pub enum BlockProcessingResult { + /// `fully_imported` is true if the lookup is complete; false if `MissingComponents` (the + /// lookup must keep fetching). `info` is a stable label for logs / metrics. + Imported(bool, &'static str), + ParentUnknown { + parent_root: Hash256, + }, + /// Processing failed. `penalty` is `Some` when an attributable peer should be downscored; + /// the third tuple element is the `report_peer` telemetry msg. `reason` is for logs only. + Error { + penalty: Option<(PeerAction, WhichPeerToPenalize, &'static str)>, + reason: String, + }, +} + +impl From> for BlockProcessingResult { + fn from(result: Result) -> Self { + fn block_peer_penalty>( + err: E, + ) -> Option<(PeerAction, WhichPeerToPenalize, &'static str)> { + Some(( + PeerAction::MidToleranceError, + WhichPeerToPenalize::BlockPeer, + err.into(), + )) + } + match result { + Ok(AvailabilityProcessingStatus::Imported(_)) => Self::Imported(true, "imported"), + Ok(AvailabilityProcessingStatus::MissingComponents(_, _)) => { + Self::Imported(false, "missing_components") + } + Err(e) => { + let penalty = match &e { + BlockError::DuplicateFullyImported(_) => { + return Self::Imported(true, "duplicate"); + } + BlockError::GenesisBlock => return Self::Imported(true, "genesis"), + BlockError::ParentUnknown { parent_root, .. } => { + return Self::ParentUnknown { + parent_root: *parent_root, + }; + } + BlockError::BeaconChainError(_) | BlockError::InternalError(_) => None, + BlockError::DuplicateImportStatusUnknown(_) => None, + BlockError::AvailabilityCheck(inner) => match inner { + AvailabilityCheckError::InvalidColumn((Some(idx), _)) => Some(( + PeerAction::MidToleranceError, + WhichPeerToPenalize::CustodyPeerForColumn(*idx), + (&e).into(), + )), + inner => match inner.category() { + AvailabilityCheckErrorCategory::Internal => None, + AvailabilityCheckErrorCategory::Malicious => block_peer_penalty(inner), + }, + }, + BlockError::ExecutionPayloadError(epe) => { + if epe.penalize_peer() { + block_peer_penalty(epe) + } else { + None + } + } + // Remaining invalid blocks: penalize the block peer. Listed explicitly so a + // new `BlockError` variant forces a compile error here. + BlockError::FutureSlot { .. } + | BlockError::StateRootMismatch { .. } + | BlockError::WouldRevertFinalizedSlot { .. } + | BlockError::NotFinalizedDescendant { .. } + | BlockError::BlockSlotLimitReached + | BlockError::IncorrectBlockProposer { .. } + | BlockError::UnknownValidator(_) + | BlockError::InvalidSignature(_) + | BlockError::BlockIsNotLaterThanParent { .. } + | BlockError::NonLinearParentRoots + | BlockError::NonLinearSlots + | BlockError::PerBlockProcessingError(_) + | BlockError::WeakSubjectivityConflict + | BlockError::InconsistentFork(_) + | BlockError::ParentExecutionPayloadInvalid { .. } + | BlockError::KnownInvalidExecutionPayload(_) + | BlockError::Slashable + | BlockError::EnvelopeBlockRootUnknown(_) + | BlockError::OptimisticSyncNotSupported { .. } + | BlockError::InvalidBlobCount { .. } + | BlockError::BidParentRootMismatch { .. } => block_peer_penalty(&e), + }; + Self::Error { + penalty, + reason: format!("{e:?}"), + } + } + } + } +} + +/// Selector for which peer(s) in a `PeerGroup` to downscore. +#[derive(Debug, Clone, Copy)] +pub enum WhichPeerToPenalize { + /// All peers in the group (block peer, or all data peers). + BlockPeer, + /// Only the peer(s) that served the given column index. + CustodyPeerForColumn(u64), +} + +impl WhichPeerToPenalize { + pub fn apply( + self, + action: PeerAction, + peer_group: &PeerGroup, + msg: &'static str, + cx: &mut SyncNetworkContext, + ) { + let peers: Vec = match self { + WhichPeerToPenalize::BlockPeer => peer_group.all().copied().collect(), + WhichPeerToPenalize::CustodyPeerForColumn(idx) => { + peer_group.of_index(idx as usize).copied().collect() + } + }; + for peer in peers { + cx.report_peer(peer, action, msg); + } + } +} diff --git a/beacon_node/network/src/sync/block_lookups/mod.rs b/beacon_node/network/src/sync/block_lookups/mod.rs index 51343cecdb..ecaee7c0ec 100644 --- a/beacon_node/network/src/sync/block_lookups/mod.rs +++ b/beacon_node/network/src/sync/block_lookups/mod.rs @@ -23,21 +23,18 @@ use self::parent_chain::{NodeChain, compute_parent_chains}; pub use self::single_block_lookup::DownloadResult; use self::single_block_lookup::{LookupRequestError, LookupResult, SingleBlockLookup}; -use super::manager::{BlockProcessType, BlockProcessingResult, SLOT_IMPORT_TOLERANCE}; +use super::manager::{BlockProcessType, SLOT_IMPORT_TOLERANCE}; use super::network_context::{PeerGroup, RpcResponseError, SyncNetworkContext}; use crate::metrics; +use crate::network_beacon_processor::BlockProcessingResult; use crate::sync::SyncMessage; -use crate::sync::block_lookups::common::ResponseType; use crate::sync::block_lookups::parent_chain::find_oldest_fork_ancestor; +use beacon_chain::BeaconChainTypes; use beacon_chain::block_verification_types::AsBlock; -use beacon_chain::data_availability_checker::{ - AvailabilityCheckError, AvailabilityCheckErrorCategory, -}; -use beacon_chain::{AvailabilityProcessingStatus, BeaconChainTypes, BlockError}; pub use common::RequestState; use fnv::FnvHashMap; +use lighthouse_network::PeerId; use lighthouse_network::service::api_types::SingleLookupReqId; -use lighthouse_network::{PeerAction, PeerId}; use lru_cache::LRUTimeCache; pub use single_block_lookup::{BlockRequestState, CustodyRequestState}; use std::collections::hash_map::Entry; @@ -106,7 +103,6 @@ pub type SingleLookupId = u32; enum Action { Retry, ParentUnknown { parent_root: Hash256 }, - Drop(/* reason: */ String), Continue, } @@ -584,125 +580,51 @@ impl BlockLookups { ); let action = match result { - BlockProcessingResult::Ok(AvailabilityProcessingStatus::Imported(_)) - | BlockProcessingResult::Err(BlockError::DuplicateFullyImported(..)) - | BlockProcessingResult::Err(BlockError::GenesisBlock) => { - // Successfully imported - request_state.on_processing_success()?; - Action::Continue - } - - BlockProcessingResult::Ok(AvailabilityProcessingStatus::MissingComponents { - .. - }) => { - // `on_processing_success` is called here to ensure the request state is updated prior to checking - // if both components have been processed. + BlockProcessingResult::Imported(fully_imported, _info) => { + // `on_processing_success` is called here to ensure the request state is updated + // prior to checking if all components have been processed (relevant for + // MissingComponents). request_state.on_processing_success()?; - if lookup.all_components_processed() { + if fully_imported { + Action::Continue + } else if lookup.all_components_processed() { // We don't request for other block components until being sure that the block has // data. If we request blobs / columns to a peer we are sure those must exist. // Therefore if all components are processed and we still receive `MissingComponents` // it indicates an internal bug. - return Err(LookupRequestError::MissingComponentsAfterAllProcessed); + return Err(LookupRequestError::Failed( + "missing components after all processed".to_owned(), + )); } else { - // Continue request, potentially request blobs Action::Retry } } - BlockProcessingResult::Err(BlockError::DuplicateImportStatusUnknown(..)) => { - // This is unreachable because RPC blocks do not undergo gossip verification, and - // this error can *only* come from gossip verification. - error!(?block_root, "Single block lookup hit unreachable condition"); - Action::Drop("DuplicateImportStatusUnknown".to_owned()) + BlockProcessingResult::ParentUnknown { parent_root } => { + // `BlockError::ParentUnknown` is only returned when processing blocks. Reverts + // the status of this request to `AwaitingProcessing` holding the downloaded + // data. A future call to `continue_requests` will re-submit it once there are + // no pending parent requests. + request_state.revert_to_awaiting_processing()?; + Action::ParentUnknown { parent_root } } - BlockProcessingResult::Ignored => { - // Beacon processor signalled to ignore the block processing result. - // This implies that the cpu is overloaded. Drop the request. - warn!( + BlockProcessingResult::Error { penalty, reason } => { + // Retry on every processing error: `on_processing_failure` increments the + // per-component failure counter, so `SINGLE_BLOCK_LOOKUP_MAX_ATTEMPTS` bounds the + // retry loop and eventually drops the lookup if the failure persists. Whether the + // peer should be downscored is the producer's call (encoded in `penalty`). + debug!( + ?block_root, component = ?R::response_type(), - "Lookup component processing ignored, cpu might be overloaded" + reason, + ?penalty, + "Lookup component processing failed; retrying" ); - Action::Drop("Block processing ignored".to_owned()) - } - BlockProcessingResult::Err(e) => { - match e { - BlockError::BeaconChainError(e) => { - // Internal error - error!(%block_root, error = ?e, "Beacon chain error processing lookup component"); - Action::Drop(format!("{e:?}")) - } - BlockError::ParentUnknown { parent_root, .. } => { - // Reverts the status of this request to `AwaitingProcessing` holding the - // downloaded data. A future call to `continue_requests` will re-submit it - // once there are no pending parent requests. - // Note: `BlockError::ParentUnknown` is only returned when processing - // blocks, not blobs. - request_state.revert_to_awaiting_processing()?; - Action::ParentUnknown { parent_root } - } - ref e @ BlockError::ExecutionPayloadError(ref epe) if !epe.penalize_peer() => { - // These errors indicate that the execution layer is offline - // and failed to validate the execution payload. Do not downscore peer. - debug!( - ?block_root, - error = ?e, - "Single block lookup failed. Execution layer is offline / unsynced / misconfigured" - ); - Action::Drop(format!("{e:?}")) - } - BlockError::AvailabilityCheck(e) - if e.category() == AvailabilityCheckErrorCategory::Internal => - { - // There errors indicate internal problems and should not downscore the peer - warn!(?block_root, error = ?e, "Internal availability check failure"); - - // Here we choose *not* to call `on_processing_failure` because this could result in a bad - // lookup state transition. This error invalidates both blob and block requests, and we don't know the - // state of both requests. Blobs may have already successfullly processed for example. - // We opt to drop the lookup instead. - Action::Drop(format!("{e:?}")) - } - other => { - debug!( - ?block_root, - component = ?R::response_type(), - error = ?other, - "Invalid lookup component" - ); - let peer_group = request_state.on_processing_failure()?; - let peers_to_penalize: Vec<_> = match other { - // Note: currenlty only InvalidColumn errors have index granularity, - // but future errors may follow the same pattern. Generalize this - // pattern with https://github.com/sigp/lighthouse/pull/6321 - BlockError::AvailabilityCheck( - AvailabilityCheckError::InvalidColumn((index_opt, _)), - ) => { - match index_opt { - Some(index) => peer_group.of_index(index as usize).collect(), - // If no index supplied this is an un-attributable fault. In practice - // this should never happen. - None => vec![], - } - } - _ => peer_group.all().collect(), - }; - for peer in peers_to_penalize { - cx.report_peer( - *peer, - PeerAction::MidToleranceError, - match R::response_type() { - ResponseType::Block => "lookup_block_processing_failure", - ResponseType::CustodyColumn => { - "lookup_custody_column_processing_failure" - } - }, - ); - } - - Action::Retry - } + let peer_group = request_state.on_processing_failure()?; + if let Some((action_kind, whom, msg)) = penalty { + whom.apply(action_kind, &peer_group, msg, cx); } + Action::Retry } }; @@ -737,10 +659,6 @@ impl BlockLookups { ))) } } - Action::Drop(reason) => { - // Drop with noop - Err(LookupRequestError::Failed(reason)) - } Action::Continue => { // Drop this completed lookup only Ok(LookupResult::Completed) 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 b712f6d86c..dda58023be 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 @@ -41,9 +41,6 @@ pub enum LookupRequestError { BadState(String), /// Lookup failed for some other reason and should be dropped Failed(/* reason: */ String), - /// Received MissingComponents when all components have been processed. This should never - /// happen, and indicates some internal bug - MissingComponentsAfterAllProcessed, /// Attempted to retrieve a not known lookup id UnknownLookup, /// Received a download result for a different request id than the in-flight request. diff --git a/beacon_node/network/src/sync/block_sidecar_coupling.rs b/beacon_node/network/src/sync/block_sidecar_coupling.rs index c1d85d0d3b..5ec45c8fea 100644 --- a/beacon_node/network/src/sync/block_sidecar_coupling.rs +++ b/beacon_node/network/src/sync/block_sidecar_coupling.rs @@ -62,7 +62,7 @@ enum RangeBlockDataRequest { } #[derive(Debug)] -pub(crate) enum CouplingError { +pub enum CouplingError { InternalError(String), /// The peer we requested the columns from was faulty/malicious DataColumnPeerFailure { diff --git a/beacon_node/network/src/sync/manager.rs b/beacon_node/network/src/sync/manager.rs index c3869f00d5..ecbe6227cc 100644 --- a/beacon_node/network/src/sync/manager.rs +++ b/beacon_node/network/src/sync/manager.rs @@ -40,7 +40,9 @@ use super::network_context::{ }; use super::peer_sync_info::{PeerSyncType, remote_sync_type}; use super::range_sync::{EPOCHS_PER_BATCH, RangeSync, RangeSyncType}; -use crate::network_beacon_processor::{ChainSegmentProcessId, NetworkBeaconProcessor}; +use crate::network_beacon_processor::{ + BlockProcessingResult, ChainSegmentProcessId, NetworkBeaconProcessor, +}; use crate::service::NetworkMessage; use crate::status::ToStatusMessage; use crate::sync::block_lookups::{ @@ -49,9 +51,7 @@ use crate::sync::block_lookups::{ use crate::sync::custody_backfill_sync::CustodyBackFillSync; use crate::sync::network_context::{PeerGroup, RpcResponseResult}; use beacon_chain::block_verification_types::AsBlock; -use beacon_chain::{ - AvailabilityProcessingStatus, BeaconChain, BeaconChainTypes, BlockError, EngineState, -}; +use beacon_chain::{BeaconChain, BeaconChainTypes, EngineState}; use futures::StreamExt; use lighthouse_network::SyncInfo; use lighthouse_network::rpc::RPCError; @@ -211,13 +211,6 @@ impl BlockProcessType { } } -#[derive(Debug)] -pub enum BlockProcessingResult { - Ok(AvailabilityProcessingStatus), - Err(BlockError), - Ignored, -} - /// The result of processing multiple blocks (a chain segment). #[derive(Debug)] pub enum BatchProcessResult { @@ -1467,18 +1460,3 @@ impl SyncManager { } } } - -impl From> for BlockProcessingResult { - fn from(result: Result) -> Self { - match result { - Ok(status) => BlockProcessingResult::Ok(status), - Err(e) => BlockProcessingResult::Err(e), - } - } -} - -impl From for BlockProcessingResult { - fn from(e: BlockError) -> Self { - BlockProcessingResult::Err(e) - } -} diff --git a/beacon_node/network/src/sync/mod.rs b/beacon_node/network/src/sync/mod.rs index 054bab654c..f121c1f1b7 100644 --- a/beacon_node/network/src/sync/mod.rs +++ b/beacon_node/network/src/sync/mod.rs @@ -15,4 +15,5 @@ mod range_sync; mod tests; pub use manager::{BatchProcessResult, SyncMessage}; +pub use network_context::{PeerGroup, SyncNetworkContext}; pub use range_sync::ChainId; diff --git a/beacon_node/network/src/sync/tests/lookups.rs b/beacon_node/network/src/sync/tests/lookups.rs index 6022c4796b..1a60e4f243 100644 --- a/beacon_node/network/src/sync/tests/lookups.rs +++ b/beacon_node/network/src/sync/tests/lookups.rs @@ -1,17 +1,19 @@ use super::*; use crate::NetworkMessage; +use crate::network_beacon_processor::BlockProcessingResult; +use crate::network_beacon_processor::sync_methods::WhichPeerToPenalize; use crate::network_beacon_processor::{ ChainSegmentProcessId, InvalidBlockStorage, NetworkBeaconProcessor, }; use crate::sync::block_lookups::{BlockLookupSummary, PARENT_DEPTH_TOLERANCE}; use crate::sync::{ SyncMessage, - manager::{BatchProcessResult, BlockProcessType, BlockProcessingResult, SyncManager}, + manager::{BatchProcessResult, BlockProcessType, SyncManager}, }; use beacon_chain::block_verification_types::LookupBlock; use beacon_chain::custody_context::NodeCustodyType; use beacon_chain::{ - AvailabilityProcessingStatus, BlockError, EngineState, NotifyExecutionLayer, + AvailabilityProcessingStatus, EngineState, NotifyExecutionLayer, block_verification_types::{AsBlock, AvailableBlockData}, test_utils::{ AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType, NumBlobs, @@ -1947,7 +1949,14 @@ async fn too_many_processing_failures(depth: usize) { r.build_chain_and_trigger_last_block(depth).await; // Simulate that a peer always returns empty r.simulate( - SimulateConfig::new().with_process_result(|| BlockError::BlockSlotLimitReached.into()), + SimulateConfig::new().with_process_result(|| BlockProcessingResult::Error { + penalty: Some(( + PeerAction::MidToleranceError, + WhichPeerToPenalize::BlockPeer, + "lookup_block_processing_failure", + )), + reason: "lookup_block_processing_failure".to_string(), + }), ) .await; // We register multiple penalties, the lookup fails and sync does not progress @@ -1991,15 +2000,21 @@ async fn unknown_parent_does_not_add_peers_to_itself() { } #[tokio::test] -/// Assert that if the beacon processor returns Ignored, the lookup is dropped +/// Assert that a non-attributable processing error (e.g. processor overloaded) is retried up to +/// `SINGLE_BLOCK_LOOKUP_MAX_ATTEMPTS`, no peer is penalized, and the lookup is then dropped. async fn test_single_block_lookup_ignored_response() { let mut r = TestRig::default(); r.build_chain_and_trigger_last_block(1).await; - // Send an Ignored response, the request should be dropped - r.simulate(SimulateConfig::new().with_process_result(|| BlockProcessingResult::Ignored)) - .await; + r.simulate( + SimulateConfig::new().with_process_result(|| BlockProcessingResult::Error { + penalty: None, + reason: "processor_overloaded".to_string(), + }), + ) + .await; // The block was not actually imported r.assert_head_slot(0); + r.assert_no_penalties(); assert_eq!(r.created_lookups(), 1, "no created lookups"); assert_eq!(r.dropped_lookups(), 1, "no dropped lookups"); assert_eq!(r.completed_lookups(), 0, "some completed lookups"); @@ -2013,7 +2028,7 @@ async fn test_single_block_lookup_duplicate_response() { // Send a DuplicateFullyImported response, the lookup should complete successfully r.simulate( SimulateConfig::new() - .with_process_result(|| BlockError::DuplicateFullyImported(Hash256::ZERO).into()), + .with_process_result(|| BlockProcessingResult::Imported(true, "duplicate")), ) .await; // The block was not actually imported @@ -2392,7 +2407,7 @@ async fn crypto_on_fail_with_invalid_block_signature() { r.assert_no_penalties(); } else { r.assert_failed_lookup_sync(); - r.assert_penalties_of_type("lookup_block_processing_failure"); + r.assert_penalties_of_type("InvalidSignature"); } } @@ -2410,7 +2425,7 @@ async fn crypto_on_fail_with_bad_column_proposer_signature() { r.assert_no_penalties(); } else { r.assert_failed_lookup_sync(); - r.assert_penalties_of_type("lookup_custody_column_processing_failure"); + r.assert_penalties_of_type("InvalidSignature"); } } @@ -2428,6 +2443,6 @@ async fn crypto_on_fail_with_bad_column_kzg_proof() { r.assert_no_penalties(); } else { r.assert_failed_lookup_sync(); - r.assert_penalties_of_type("lookup_custody_column_processing_failure"); + r.assert_penalties_of_type("AvailabilityCheck"); } } From d7d56e6312e3e4d2fe6e3988853dd08f21c6a888 Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Tue, 2 Jun 2026 16:57:03 +0200 Subject: [PATCH 07/26] Delete unnecessary SyncMessage variants (#9379) - Simplification from https://github.com/sigp/lighthouse/pull/9155 Lookup sync does not cache sidecars, so sending the full network object adds unnecessary complexity. Sync only needs to know: We have received a header that has an unknown parent. Replace `UnknownParentDataColumn` and `UnknownParentPartialDataColumn` for `UnknownParentSidecarHeader` Co-Authored-By: dapplion <35266934+dapplion@users.noreply.github.com> Co-Authored-By: Eitan Seri-Levi --- .../gossip_methods.rs | 12 +++-- .../network/src/sync/block_lookups/mod.rs | 13 ++--- .../sync/block_lookups/single_block_lookup.rs | 2 +- beacon_node/network/src/sync/manager.rs | 53 +++---------------- beacon_node/network/src/sync/tests/lookups.rs | 13 ++++- 5 files changed, 31 insertions(+), 62 deletions(-) diff --git a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs index 65c95eff35..df94b473a8 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -719,17 +719,19 @@ impl NetworkBeaconProcessor { MessageAcceptance::Accept, ); } - GossipDataColumnError::ParentUnknown { parent_root, .. } => { + GossipDataColumnError::ParentUnknown { parent_root, slot } => { debug!( action = "requesting parent", %block_root, %parent_root, "Unknown parent hash for column" ); - self.send_sync_message(SyncMessage::UnknownParentDataColumn( + self.send_sync_message(SyncMessage::UnknownParentSidecarHeader { peer_id, - column_sidecar, - )); + block_root, + parent_root, + slot, + }); } GossipDataColumnError::BlockRootUnknown { block_root: unknown_block_root, @@ -1047,7 +1049,7 @@ impl NetworkBeaconProcessor { %parent_root, "Unknown parent hash for partial column" ); - self.send_sync_message(SyncMessage::UnknownParentPartialDataColumn { + self.send_sync_message(SyncMessage::UnknownParentSidecarHeader { peer_id, block_root, parent_root, diff --git a/beacon_node/network/src/sync/block_lookups/mod.rs b/beacon_node/network/src/sync/block_lookups/mod.rs index ecaee7c0ec..0c3cb988a9 100644 --- a/beacon_node/network/src/sync/block_lookups/mod.rs +++ b/beacon_node/network/src/sync/block_lookups/mod.rs @@ -74,26 +74,23 @@ const LOOKUP_MAX_DURATION_NO_PEERS_SECS: u64 = 10; /// take at most 2 GB. 200 lookups allow 3 parallel chains of depth 64 (current maximum). const MAX_LOOKUPS: usize = 200; -/// The values for `Blob`, `DataColumn` and `PartialDataColumn` is the parent root of the column. +/// The value for `Sidecar` is the parent root of the sidecar. pub enum BlockComponent { Block(DownloadResult>>), - DataColumn(DownloadResult), - PartialDataColumn(DownloadResult), + Sidecar { parent_root: Hash256 }, } impl BlockComponent { fn parent_root(&self) -> Hash256 { match self { BlockComponent::Block(block) => block.value.parent_root(), - BlockComponent::DataColumn(parent_root) - | BlockComponent::PartialDataColumn(parent_root) => parent_root.value, + BlockComponent::Sidecar { parent_root } => *parent_root, } } fn get_type(&self) -> &'static str { match self { BlockComponent::Block(_) => "block", - BlockComponent::DataColumn(_) => "data_column", - BlockComponent::PartialDataColumn(_) => "partial_data_column", + BlockComponent::Sidecar { .. } => "sidecar", } } } @@ -207,7 +204,7 @@ impl BlockLookups { block_root, Some(block_component), Some(parent_root), - // On a `UnknownParentBlock` or `UnknownParentDataColumn` event the peer is not + // On a `UnknownParentBlock` or `UnknownParentSidecarHeader` 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. &[], 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 dda58023be..38d6b2216d 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 @@ -151,7 +151,7 @@ impl SingleBlockLookup { .block_request_state .state .insert_verified_response(block), - BlockComponent::DataColumn(_) | BlockComponent::PartialDataColumn(_) => { + BlockComponent::Sidecar { .. } => { // 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 ecbe6227cc..37d13c2eae 100644 --- a/beacon_node/network/src/sync/manager.rs +++ b/beacon_node/network/src/sync/manager.rs @@ -144,11 +144,9 @@ pub enum SyncMessage { /// A block with an unknown parent has been received. UnknownParentBlock(PeerId, Arc>, Hash256), - /// A data column with an unknown parent has been received. - UnknownParentDataColumn(PeerId, Arc>), - - /// A partial data column with an unknown parent has been received. - UnknownParentPartialDataColumn { + /// A sidecar (full/partial data column) with an unknown parent has been received. Carries only the header + /// info needed to trigger a parent lookup, decoupled from the concrete sidecar type. + UnknownParentSidecarHeader { peer_id: PeerId, block_root: Hash256, parent_root: Hash256, @@ -875,58 +873,19 @@ impl SyncManager { }), ); } - SyncMessage::UnknownParentDataColumn(peer_id, data_column) => { - let data_column_slot = data_column.slot(); - let block_root = data_column.block_root(); - match data_column.as_ref() { - DataColumnSidecar::Fulu(column) => { - let parent_root = column.block_parent_root(); - debug!(%block_root, %parent_root, "Received unknown parent data column message"); - self.handle_unknown_parent( - peer_id, - block_root, - parent_root, - data_column_slot, - BlockComponent::DataColumn(DownloadResult { - value: parent_root, - block_root, - seen_timestamp: self - .chain - .slot_clock - .now_duration() - .unwrap_or_default(), - peer_group: PeerGroup::from_single(peer_id), - }), - ); - } - DataColumnSidecar::Gloas(_) => { - // TODO(gloas): proper lookup sync for Gloas. Routing into - // `handle_unknown_block_root` here mixes column processing with the - // single-block-lookup path; the Gloas column-arrives-before-block - // case wants its own queue/wakeup. - debug!(%block_root, "Received unknown block data column message"); - self.handle_unknown_block_root(peer_id, block_root); - } - } - } - SyncMessage::UnknownParentPartialDataColumn { + SyncMessage::UnknownParentSidecarHeader { peer_id, block_root, parent_root, slot, } => { - debug!(%block_root, %parent_root, "Received unknown parent partial column message"); + debug!(%block_root, %parent_root, "Received unknown parent sidecar header message"); self.handle_unknown_parent( peer_id, block_root, parent_root, slot, - BlockComponent::PartialDataColumn(DownloadResult { - value: parent_root, - block_root, - seen_timestamp: self.chain.slot_clock.now_duration().unwrap_or_default(), - peer_group: PeerGroup::from_single(peer_id), - }), + BlockComponent::Sidecar { parent_root }, ); } SyncMessage::UnknownBlockHashFromAttestation(peer_id, block_root) => { diff --git a/beacon_node/network/src/sync/tests/lookups.rs b/beacon_node/network/src/sync/tests/lookups.rs index 1a60e4f243..3ec4d11da2 100644 --- a/beacon_node/network/src/sync/tests/lookups.rs +++ b/beacon_node/network/src/sync/tests/lookups.rs @@ -1365,7 +1365,18 @@ impl TestRig { peer_id: PeerId, data_column: Arc>, ) { - self.send_sync_message(SyncMessage::UnknownParentDataColumn(peer_id, data_column)); + let block_root = data_column.block_root(); + let slot = data_column.slot(); + let parent_root = match data_column.as_ref() { + DataColumnSidecar::Fulu(column) => column.block_parent_root(), + DataColumnSidecar::Gloas(_) => panic!("Gloas data column not supported in this test"), + }; + self.send_sync_message(SyncMessage::UnknownParentSidecarHeader { + peer_id, + block_root, + parent_root, + slot, + }); } fn trigger_unknown_block_from_attestation(&mut self, block_root: Hash256, peer_id: PeerId) { From c2ac519c69311527a23ea161b7bde5117755506e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Oliveira?= Date: Wed, 3 Jun 2026 09:05:31 +0100 Subject: [PATCH 08/26] Disable Mplex by default (#9365) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: João Oliveira --- beacon_node/lighthouse_network/src/config.rs | 4 ++ .../lighthouse_network/src/service/mod.rs | 10 +++-- .../lighthouse_network/src/service/utils.rs | 40 ++++++++++++------- beacon_node/src/cli.rs | 8 ++++ beacon_node/src/config.rs | 4 ++ book/src/help_bn.md | 3 ++ 6 files changed, 52 insertions(+), 17 deletions(-) diff --git a/beacon_node/lighthouse_network/src/config.rs b/beacon_node/lighthouse_network/src/config.rs index db42d0cfa8..4d4d91a456 100644 --- a/beacon_node/lighthouse_network/src/config.rs +++ b/beacon_node/lighthouse_network/src/config.rs @@ -125,6 +125,9 @@ pub struct Config { /// Whether light client protocols should be enabled. pub enable_light_client_server: bool, + /// Whether to enable the deprecated mplex multiplexer alongside yamux. + pub enable_mplex: bool, + /// Configuration for the outbound rate limiter (requests made by this node). pub outbound_rate_limiter_config: Option, @@ -362,6 +365,7 @@ impl Default for Config { proposer_only: false, metrics_enabled: false, enable_light_client_server: true, + enable_mplex: false, outbound_rate_limiter_config: None, invalid_block_storage: None, inbound_rate_limiter_config: None, diff --git a/beacon_node/lighthouse_network/src/service/mod.rs b/beacon_node/lighthouse_network/src/service/mod.rs index 41d937e324..f5e2442f86 100644 --- a/beacon_node/lighthouse_network/src/service/mod.rs +++ b/beacon_node/lighthouse_network/src/service/mod.rs @@ -466,9 +466,13 @@ impl Network { } }; - // Set up the transport - tcp/quic with noise and mplex - let transport = build_transport(local_keypair.clone(), !config.disable_quic_support) - .map_err(|e| format!("Failed to build transport: {:?}", e))?; + // Set up the transport - tcp/quic with noise and yamux (mplex optional) + let transport = build_transport( + local_keypair.clone(), + !config.disable_quic_support, + config.enable_mplex, + ) + .map_err(|e| format!("Failed to build transport: {:?}", e))?; // use the executor for libp2p struct Executor(task_executor::TaskExecutor); diff --git a/beacon_node/lighthouse_network/src/service/utils.rs b/beacon_node/lighthouse_network/src/service/utils.rs index c7dabcb391..47629f4fd3 100644 --- a/beacon_node/lighthouse_network/src/service/utils.rs +++ b/beacon_node/lighthouse_network/src/service/utils.rs @@ -34,27 +34,39 @@ pub struct Context<'a> { type BoxedTransport = Boxed<(PeerId, StreamMuxerBox)>; /// The implementation supports TCP/IP, QUIC (experimental) over UDP, noise as the encryption layer, and -/// mplex/yamux as the multiplexing layer (when using TCP). +/// yamux as the multiplexing layer (when using TCP). Mplex can be optionally enabled. pub fn build_transport( local_private_key: Keypair, quic_support: bool, + enable_mplex: bool, ) -> std::io::Result { - // mplex config - let mut mplex_config = libp2p_mplex::Config::new(); - mplex_config.set_max_buffer_size(256); - mplex_config.set_max_buffer_behaviour(libp2p_mplex::MaxBufferBehaviour::Block); - // yamux config let yamux_config = yamux::Config::default(); + // Creates the TCP transport layer - let tcp = libp2p::tcp::tokio::Transport::new(libp2p::tcp::Config::default().nodelay(true)) - .upgrade(core::upgrade::Version::V1) - .authenticate(generate_noise_config(&local_private_key)) - .multiplex(core::upgrade::SelectUpgrade::new( - yamux_config, - mplex_config, - )) - .timeout(Duration::from_secs(10)); + let tcp: BoxedTransport = if enable_mplex { + // Enable both yamux and mplex. + let mut mplex_config = libp2p_mplex::Config::new(); + mplex_config.set_max_num_streams(32); + mplex_config.set_max_buffer_behaviour(libp2p_mplex::MaxBufferBehaviour::ResetStream); + libp2p::tcp::tokio::Transport::new(libp2p::tcp::Config::default().nodelay(true)) + .upgrade(core::upgrade::Version::V1) + .authenticate(generate_noise_config(&local_private_key)) + .multiplex(core::upgrade::SelectUpgrade::new( + yamux_config, + mplex_config, + )) + .timeout(Duration::from_secs(10)) + .boxed() + } else { + // Yamux only + libp2p::tcp::tokio::Transport::new(libp2p::tcp::Config::default().nodelay(true)) + .upgrade(core::upgrade::Version::V1) + .authenticate(generate_noise_config(&local_private_key)) + .multiplex(yamux_config) + .timeout(Duration::from_secs(10)) + .boxed() + }; let transport = if quic_support { // Enables Quic // The default quic configuration suits us for now. diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index 647b5858cb..988e2d1fc5 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -387,6 +387,14 @@ pub fn cli_app() -> Command { .help("Disables the quic transport. The node will rely solely on the TCP transport for libp2p connections.") .display_order(0) ) + .arg( + Arg::new("enable-mplex") + .long("enable-mplex") + .action(ArgAction::SetTrue) + .help_heading(FLAG_HEADER) + .help("Enables mplex multiplexer alongside yamux. Yamux is preferred when both are available.") + .display_order(0) + ) .arg( Arg::new("disable-peer-scoring") .long("disable-peer-scoring") diff --git a/beacon_node/src/config.rs b/beacon_node/src/config.rs index 045b432dc9..ddf8d07c4e 100644 --- a/beacon_node/src/config.rs +++ b/beacon_node/src/config.rs @@ -1443,6 +1443,10 @@ pub fn set_network_config( config.disable_quic_support = true; } + if parse_flag(cli_args, "enable-mplex") { + config.enable_mplex = true; + } + if parse_flag(cli_args, "disable-upnp") { config.upnp_enabled = false; } diff --git a/book/src/help_bn.md b/book/src/help_bn.md index 30163f1f0c..1f57db1b59 100644 --- a/book/src/help_bn.md +++ b/book/src/help_bn.md @@ -494,6 +494,9 @@ Flags: Sets the local ENR IP address and port to match those set for lighthouse. Specifically, the IP address will be the value of --listen-address and the UDP port will be --discovery-port. + --enable-mplex + Enables mplex multiplexer alongside yamux. Yamux is preferred when + both are available. --enable-partial-columns Enable partial messages for data columns. This can reduce the amount of data sent over the network. Enabled by default on Hoodi and From eab5163d68f2c1e88b43627956dcd496a321ab41 Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Wed, 3 Jun 2026 17:29:04 +0200 Subject: [PATCH 09/26] Remove RequestState trait from lookup sync (#9391) Co-Authored-By: dapplion <35266934+dapplion@users.noreply.github.com> --- .../network/src/sync/block_lookups/common.rs | 164 ------ .../network/src/sync/block_lookups/mod.rs | 275 ++-------- .../sync/block_lookups/single_block_lookup.rs | 510 ++++++++++-------- beacon_node/network/src/sync/manager.rs | 29 +- .../network/src/sync/network_context.rs | 29 +- .../src/sync/network_context/custody.rs | 12 +- 6 files changed, 369 insertions(+), 650 deletions(-) delete mode 100644 beacon_node/network/src/sync/block_lookups/common.rs diff --git a/beacon_node/network/src/sync/block_lookups/common.rs b/beacon_node/network/src/sync/block_lookups/common.rs deleted file mode 100644 index 4306458615..0000000000 --- a/beacon_node/network/src/sync/block_lookups/common.rs +++ /dev/null @@ -1,164 +0,0 @@ -use crate::sync::block_lookups::single_block_lookup::{ - LookupRequestError, SingleBlockLookup, SingleLookupRequestState, -}; -use crate::sync::block_lookups::{BlockRequestState, CustodyRequestState, PeerId}; -use crate::sync::manager::BlockProcessType; -use crate::sync::network_context::{LookupRequestResult, SyncNetworkContext}; -use beacon_chain::BeaconChainTypes; -use lighthouse_network::service::api_types::Id; -use parking_lot::RwLock; -use std::collections::HashSet; -use std::sync::Arc; -use types::{DataColumnSidecarList, SignedBeaconBlock}; - -use super::SingleLookupId; -use super::single_block_lookup::{ComponentRequests, DownloadResult}; - -#[derive(Debug, Copy, Clone)] -pub enum ResponseType { - Block, - CustodyColumn, -} - -/// This trait unifies common single block lookup functionality across blocks and data columns. -/// This includes making requests, verifying responses, and handling processing results. A -/// `SingleBlockLookup` includes both a `BlockRequestState` and a `CustodyRequestState`, this trait -/// is implemented for each. -/// -/// The use of the `ResponseType` associated type gives us a degree of type -/// safety when handling a block/column response ensuring we only mutate the correct corresponding -/// state. -pub trait RequestState { - /// The type created after validation. - type VerifiedResponseType: Clone; - - /// Request the network context to prepare a request of a component of `block_root`. If the - /// request is not necessary because the component is already known / processed, return false. - /// Return true if it sent a request and we can expect an event back from the network. - fn make_request( - &self, - id: Id, - lookup_peers: Arc>>, - expected_blobs: usize, - cx: &mut SyncNetworkContext, - ) -> Result; - - /* Response handling methods */ - - /// Send the response to the beacon processor. - fn send_for_processing( - id: Id, - result: DownloadResult, - cx: &SyncNetworkContext, - ) -> Result<(), LookupRequestError>; - - /* Utility methods */ - - /// Returns the `ResponseType` associated with this trait implementation. Useful in logging. - fn response_type() -> ResponseType; - - /// A getter for the `BlockRequestState` or `CustodyRequestState` associated with this trait. - fn request_state_mut(request: &mut SingleBlockLookup) -> Result<&mut Self, &'static str>; - - /// A getter for a reference to the `SingleLookupRequestState` associated with this trait. - fn get_state(&self) -> &SingleLookupRequestState; - - /// A getter for a mutable reference to the SingleLookupRequestState associated with this trait. - fn get_state_mut(&mut self) -> &mut SingleLookupRequestState; -} - -impl RequestState for BlockRequestState { - type VerifiedResponseType = Arc>; - - fn make_request( - &self, - id: SingleLookupId, - lookup_peers: Arc>>, - _: usize, - cx: &mut SyncNetworkContext, - ) -> Result { - cx.block_lookup_request(id, lookup_peers, self.requested_block_root) - .map_err(LookupRequestError::SendFailedNetwork) - } - - fn send_for_processing( - id: SingleLookupId, - download_result: DownloadResult, - cx: &SyncNetworkContext, - ) -> Result<(), LookupRequestError> { - let DownloadResult { - value, - block_root, - seen_timestamp, - .. - } = download_result; - cx.send_block_for_processing(id, block_root, value, seen_timestamp) - .map_err(LookupRequestError::SendFailedProcessor) - } - - fn response_type() -> ResponseType { - ResponseType::Block - } - fn request_state_mut(request: &mut SingleBlockLookup) -> Result<&mut Self, &'static str> { - Ok(&mut request.block_request_state) - } - fn get_state(&self) -> &SingleLookupRequestState { - &self.state - } - fn get_state_mut(&mut self) -> &mut SingleLookupRequestState { - &mut self.state - } -} - -impl RequestState for CustodyRequestState { - type VerifiedResponseType = DataColumnSidecarList; - - fn make_request( - &self, - id: Id, - lookup_peers: Arc>>, - _: usize, - cx: &mut SyncNetworkContext, - ) -> Result { - cx.custody_lookup_request(id, self.block_root, self.slot, lookup_peers) - .map_err(LookupRequestError::SendFailedNetwork) - } - - fn send_for_processing( - id: Id, - download_result: DownloadResult, - cx: &SyncNetworkContext, - ) -> Result<(), LookupRequestError> { - let DownloadResult { - value, - block_root, - seen_timestamp, - .. - } = download_result; - cx.send_custody_columns_for_processing( - id, - block_root, - value, - seen_timestamp, - BlockProcessType::SingleCustodyColumn(id), - ) - .map_err(LookupRequestError::SendFailedProcessor) - } - - fn response_type() -> ResponseType { - ResponseType::CustodyColumn - } - fn request_state_mut(request: &mut SingleBlockLookup) -> Result<&mut Self, &'static str> { - match &mut request.component_requests { - ComponentRequests::WaitingForBlock => Err("waiting for block"), - ComponentRequests::ActiveCustodyRequest(request) => Ok(request), - ComponentRequests::NotNeeded { .. } => Err("not needed"), - } - } - fn get_state(&self) -> &SingleLookupRequestState { - &self.state - } - fn get_state_mut(&mut self) -> &mut SingleLookupRequestState { - &mut self.state - } -} diff --git a/beacon_node/network/src/sync/block_lookups/mod.rs b/beacon_node/network/src/sync/block_lookups/mod.rs index 0c3cb988a9..a265373e3f 100644 --- a/beacon_node/network/src/sync/block_lookups/mod.rs +++ b/beacon_node/network/src/sync/block_lookups/mod.rs @@ -24,27 +24,23 @@ use self::parent_chain::{NodeChain, compute_parent_chains}; pub use self::single_block_lookup::DownloadResult; use self::single_block_lookup::{LookupRequestError, LookupResult, SingleBlockLookup}; use super::manager::{BlockProcessType, SLOT_IMPORT_TOLERANCE}; -use super::network_context::{PeerGroup, RpcResponseError, SyncNetworkContext}; +use super::network_context::{RpcResponseError, SyncNetworkContext}; use crate::metrics; use crate::network_beacon_processor::BlockProcessingResult; use crate::sync::SyncMessage; use crate::sync::block_lookups::parent_chain::find_oldest_fork_ancestor; use beacon_chain::BeaconChainTypes; -use beacon_chain::block_verification_types::AsBlock; -pub use common::RequestState; use fnv::FnvHashMap; use lighthouse_network::PeerId; use lighthouse_network::service::api_types::SingleLookupReqId; use lru_cache::LRUTimeCache; -pub use single_block_lookup::{BlockRequestState, CustodyRequestState}; use std::collections::hash_map::Entry; use std::sync::Arc; use std::time::Duration; use store::Hash256; use tracing::{debug, error, warn}; -use types::{EthSpec, SignedBeaconBlock}; +use types::{DataColumnSidecarList, EthSpec, SignedBeaconBlock}; -pub mod common; pub mod parent_chain; mod single_block_lookup; @@ -74,35 +70,17 @@ const LOOKUP_MAX_DURATION_NO_PEERS_SECS: u64 = 10; /// take at most 2 GB. 200 lookups allow 3 parallel chains of depth 64 (current maximum). const MAX_LOOKUPS: usize = 200; -/// The value for `Sidecar` is the parent root of the sidecar. +type BlockDownloadResponse = Result>>, RpcResponseError>; +type CustodyDownloadResponse = + Result>, RpcResponseError>; + pub enum BlockComponent { Block(DownloadResult>>), - Sidecar { parent_root: Hash256 }, -} - -impl BlockComponent { - fn parent_root(&self) -> Hash256 { - match self { - BlockComponent::Block(block) => block.value.parent_root(), - BlockComponent::Sidecar { parent_root } => *parent_root, - } - } - fn get_type(&self) -> &'static str { - match self { - BlockComponent::Block(_) => "block", - BlockComponent::Sidecar { .. } => "sidecar", - } - } + Sidecar, } pub type SingleLookupId = u32; -enum Action { - Retry, - ParentUnknown { parent_root: Hash256 }, - Continue, -} - pub struct BlockLookups { /// A cache of block roots that must be ignored for some time to prevent useless searches. For /// example if a chain is too long, its lookup chain is dropped, and range sync is expected to @@ -190,11 +168,10 @@ impl BlockLookups { &mut self, block_root: Hash256, block_component: BlockComponent, + parent_root: Hash256, peer_id: PeerId, cx: &mut SyncNetworkContext, ) -> bool { - let parent_root = block_component.parent_root(); - let parent_lookup_exists = self.search_parent_of_child(parent_root, block_root, &[peer_id], cx); // Only create the child lookup if the parent exists @@ -215,7 +192,7 @@ impl BlockLookups { } } - /// Seach a block whose parent root is unknown. + /// Search a block whose parent root is unknown. /// /// Returns true if the lookup is created or already exists #[must_use = "only reference the new lookup if returns true"] @@ -358,13 +335,9 @@ impl BlockLookups { .find(|(_id, lookup)| lookup.is_for_block(block_root)) { if let Some(block_component) = block_component { - let component_type = block_component.get_type(); let imported = lookup.add_child_components(block_component); if !imported { - debug!( - ?block_root, - component_type, "Lookup child component ignored" - ); + debug!(?block_root, "Lookup child component ignored"); } } @@ -436,88 +409,33 @@ impl BlockLookups { /* Lookup responses */ - /// Process a block or blob response received from a single lookup request. - pub fn on_download_response>( + /// Process a block response received from a single lookup request. + pub fn on_block_download_response( &mut self, id: SingleLookupReqId, - response: Result<(R::VerifiedResponseType, PeerGroup, Duration), RpcResponseError>, + response: BlockDownloadResponse, cx: &mut SyncNetworkContext, ) { - let result = self.on_download_response_inner::(id, response, cx); - self.on_lookup_result(id.lookup_id, result, "download_response", cx); + let Some(lookup) = self.single_block_lookups.get_mut(&id.lookup_id) else { + debug!(?id, "Block returned for single block lookup not present"); + return; + }; + let result = lookup.on_block_download_response(id.req_id, response, cx); + self.on_lookup_result(id.lookup_id, result, "block_download_response", cx); } - /// Process a block or blob response received from a single lookup request. - pub fn on_download_response_inner>( + pub fn on_custody_download_response( &mut self, id: SingleLookupReqId, - response: Result<(R::VerifiedResponseType, PeerGroup, Duration), RpcResponseError>, + response: CustodyDownloadResponse, cx: &mut SyncNetworkContext, - ) -> Result { - // Note: no need to downscore peers here, already downscored on network context - - let response_type = R::response_type(); + ) { let Some(lookup) = self.single_block_lookups.get_mut(&id.lookup_id) else { - // We don't have the ability to cancel in-flight RPC requests. So this can happen - // if we started this RPC request, and later saw the block/blobs via gossip. - debug!(?id, "Block returned for single block lookup not present"); - return Err(LookupRequestError::UnknownLookup); + debug!(?id, "Custody returned for single block lookup not present"); + return; }; - - let block_root = lookup.block_root(); - let request_state = R::request_state_mut(lookup) - .map_err(|e| LookupRequestError::BadState(e.to_owned()))? - .get_state_mut(); - - match response { - Ok((response, peer_group, seen_timestamp)) => { - debug!( - ?block_root, - ?id, - ?peer_group, - ?response_type, - "Received lookup download success" - ); - - // Here we could check if response extends a parent chain beyond its max length. - // However we defer that check to the handling of a processing error ParentUnknown. - // - // Here we could check if there's already a lookup for parent_root of `response`. In - // that case we know that sending the response for processing will likely result in - // a `ParentUnknown` error. However, for simplicity we choose to not implement this - // optimization. - - // Register the download peer here. Once we have received some data over the wire we - // attribute it to this peer for scoring latter regardless of how the request was - // done. - request_state.on_download_success( - id.req_id, - DownloadResult { - value: response, - block_root, - seen_timestamp, - peer_group, - }, - )?; - // continue_request will send for processing as the request state is AwaitingProcessing - } - Err(e) => { - // No need to log peer source here. When sending a DataColumnsByRoot request we log - // the peer and the request ID which is linked to this `id` value here. - debug!( - ?block_root, - ?id, - ?response_type, - error = ?e, - "Received lookup download failure" - ); - - request_state.on_download_failure(id.req_id)?; - // continue_request will retry a download as the request state is AwaitingDownload - } - } - - lookup.continue_requests(cx) + let result = lookup.on_custody_download_response(id.req_id, response, cx); + self.on_lookup_result(id.lookup_id, result, "custody_download_response", cx); } /* Error responses */ @@ -539,128 +457,29 @@ impl BlockLookups { result: BlockProcessingResult, cx: &mut SyncNetworkContext, ) { - let lookup_result = match process_type { - BlockProcessType::SingleBlock { id } => { - self.on_processing_result_inner::>(id, result, cx) - } - 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); - } - - pub fn on_processing_result_inner>( - &mut self, - lookup_id: SingleLookupId, - result: BlockProcessingResult, - cx: &mut SyncNetworkContext, - ) -> Result { + let lookup_id = process_type.id(); let Some(lookup) = self.single_block_lookups.get_mut(&lookup_id) else { debug!(id = lookup_id, "Unknown single block lookup"); - return Err(LookupRequestError::UnknownLookup); + return; }; - let block_root = lookup.block_root(); - let request_state = R::request_state_mut(lookup) - .map_err(|e| LookupRequestError::BadState(e.to_owned()))? - .get_state_mut(); - debug!( - component = ?R::response_type(), - ?block_root, + block_root = ?lookup.block_root(), id = lookup_id, + ?process_type, ?result, "Received lookup processing result" ); - let action = match result { - BlockProcessingResult::Imported(fully_imported, _info) => { - // `on_processing_success` is called here to ensure the request state is updated - // prior to checking if all components have been processed (relevant for - // MissingComponents). - request_state.on_processing_success()?; - - if fully_imported { - Action::Continue - } else if lookup.all_components_processed() { - // We don't request for other block components until being sure that the block has - // data. If we request blobs / columns to a peer we are sure those must exist. - // Therefore if all components are processed and we still receive `MissingComponents` - // it indicates an internal bug. - return Err(LookupRequestError::Failed( - "missing components after all processed".to_owned(), - )); - } else { - Action::Retry - } - } - BlockProcessingResult::ParentUnknown { parent_root } => { - // `BlockError::ParentUnknown` is only returned when processing blocks. Reverts - // the status of this request to `AwaitingProcessing` holding the downloaded - // data. A future call to `continue_requests` will re-submit it once there are - // no pending parent requests. - request_state.revert_to_awaiting_processing()?; - Action::ParentUnknown { parent_root } - } - BlockProcessingResult::Error { penalty, reason } => { - // Retry on every processing error: `on_processing_failure` increments the - // per-component failure counter, so `SINGLE_BLOCK_LOOKUP_MAX_ATTEMPTS` bounds the - // retry loop and eventually drops the lookup if the failure persists. Whether the - // peer should be downscored is the producer's call (encoded in `penalty`). - debug!( - ?block_root, - component = ?R::response_type(), - reason, - ?penalty, - "Lookup component processing failed; retrying" - ); - let peer_group = request_state.on_processing_failure()?; - if let Some((action_kind, whom, msg)) = penalty { - whom.apply(action_kind, &peer_group, msg, cx); - } - Action::Retry + let lookup_result = match process_type { + BlockProcessType::SingleBlock { .. } => lookup.on_block_processing_result(result, cx), + BlockProcessType::SingleCustodyColumn(_) => { + lookup.on_data_processing_result(result, cx) } + // TODO(gloas): route into the payload envelope lookup state machine. + BlockProcessType::SinglePayloadEnvelope(_) => Ok(LookupResult::Pending), }; - - match action { - Action::Retry => { - // Trigger download for all components in case `MissingComponents` failed the blob - // request. Also if blobs are `AwaitingProcessing` and need to be progressed - lookup.continue_requests(cx) - } - Action::ParentUnknown { parent_root } => { - let peers = lookup.all_peers(); - // Mark lookup as awaiting **before** creating the parent lookup. At this point the - // lookup maybe inconsistent. - lookup.set_awaiting_parent(parent_root); - let parent_lookup_exists = - self.search_parent_of_child(parent_root, block_root, &peers, cx); - if parent_lookup_exists { - // The parent lookup exist or has been created. It's safe for `lookup` to - // reference the parent as awaiting. - debug!( - id = lookup_id, - ?block_root, - ?parent_root, - "Marking lookup as awaiting parent" - ); - Ok(LookupResult::Pending) - } else { - // The parent lookup is faulty and was not created, we must drop the `lookup` as - // it's in an inconsistent state. We must drop all of its children too. - Err(LookupRequestError::Failed(format!( - "Parent lookup is faulty {parent_root:?}" - ))) - } - } - Action::Continue => { - // Drop this completed lookup only - Ok(LookupResult::Completed) - } - } + self.on_lookup_result(lookup_id, lookup_result, "processing_result", cx); } pub fn on_external_processing_result( @@ -757,7 +576,20 @@ impl BlockLookups { cx: &mut SyncNetworkContext, ) -> bool { match result { - Ok(LookupResult::Pending) => true, // no action + Ok(LookupResult::Pending) => true, + Ok(LookupResult::ParentUnknown { + parent_root, + block_root, + peers, + }) => { + if self.search_parent_of_child(parent_root, block_root, &peers, cx) { + true + } else { + self.drop_lookup_and_children(id, "Failed"); + self.update_metrics(); + false + } + } Ok(LookupResult::Completed) => { if let Some(lookup) = self.single_block_lookups.remove(&id) { debug!( @@ -923,6 +755,7 @@ impl BlockLookups { } /// Adds peers to a lookup and its ancestors recursively. + /// /// Note: Takes a `lookup_id` as argument to allow recursion on mutable lookups, without having /// to duplicate the code to add peers to a lookup fn add_peers_to_lookup_and_ancestors( @@ -949,12 +782,12 @@ impl BlockLookups { } if let Some(parent_root) = lookup.awaiting_parent() { - if let Some((&child_id, _)) = self + if let Some((&parent_id, _)) = self .single_block_lookups .iter() .find(|(_, l)| l.block_root() == parent_root) { - self.add_peers_to_lookup_and_ancestors(child_id, peers, cx) + self.add_peers_to_lookup_and_ancestors(parent_id, peers, cx) } else { Err(format!("Lookup references unknown parent {parent_root:?}")) } 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 38d6b2216d..8eb58da4e6 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 @@ -1,15 +1,17 @@ use super::{BlockComponent, PeerId, SINGLE_BLOCK_LOOKUP_MAX_ATTEMPTS}; -use crate::sync::block_lookups::common::RequestState; +use crate::network_beacon_processor::BlockProcessingResult; +use crate::sync::block_lookups::{BlockDownloadResponse, CustodyDownloadResponse}; +use crate::sync::manager::BlockProcessType; use crate::sync::network_context::{ - LookupRequestResult, PeerGroup, ReqId, RpcRequestSendError, SendErrorProcessor, - SyncNetworkContext, + LookupRequestResult, PeerGroup, ReqId, RpcRequestSendError, RpcResponseError, + SendErrorProcessor, SyncNetworkContext, }; -use beacon_chain::{BeaconChainTypes, BlockProcessStatus}; +use beacon_chain::BeaconChainTypes; +use beacon_chain::block_verification_types::AsBlock; use educe::Educe; use lighthouse_network::service::api_types::Id; use parking_lot::RwLock; use std::collections::HashSet; -use std::fmt::Debug; use std::sync::Arc; use std::time::{Duration, Instant}; use store::Hash256; @@ -24,15 +26,18 @@ pub enum LookupResult { Completed, /// Lookup is expecting some future event from the network Pending, + /// Block's parent is not known to fork-choice, a parent lookup is needed + ParentUnknown { + parent_root: Hash256, + block_root: Hash256, + peers: Vec, + }, } #[derive(Debug, PartialEq, Eq, IntoStaticStr)] pub enum LookupRequestError { /// Too many failed attempts - TooManyAttempts { - /// The failed attempts were primarily due to processing failures. - cannot_process: bool, - }, + TooManyAttempts, /// Error sending event to network SendFailedNetwork(RpcRequestSendError), /// Error sending event to processor @@ -52,33 +57,63 @@ pub enum LookupRequestError { }, } +#[derive(Debug)] +struct BlockRequest { + state: SingleLookupRequestState>>, +} + +impl BlockRequest { + fn new() -> Self { + Self { + state: SingleLookupRequestState::new(), + } + } + + fn is_complete(&self) -> bool { + self.state.is_processed() + } +} + +#[derive(Debug)] +enum DataRequest { + WaitingForBlock, + Request { + slot: Slot, + state: SingleLookupRequestState>, + }, + NoData, +} + +impl DataRequest { + fn is_complete(&self) -> bool { + match &self { + DataRequest::WaitingForBlock => false, + DataRequest::Request { state, .. } => state.is_processed(), + DataRequest::NoData => true, + } + } +} + +type PeerSet = Arc>>; + #[derive(Educe)] #[educe(Debug(bound(T: BeaconChainTypes)))] pub struct SingleBlockLookup { pub id: Id, - pub block_request_state: BlockRequestState, - pub component_requests: ComponentRequests, + block_root: Hash256, + block_request: BlockRequest, + data_request: DataRequest, /// Peers that claim to have imported this set of block components. This state is shared with /// the custody request to have an updated view of the peers that claim to have imported the /// block associated with this lookup. The peer set of a lookup can change rapidly, and faster /// than the lifetime of a custody request. #[educe(Debug(method(fmt_peer_set_as_len)))] - peers: Arc>>, - block_root: Hash256, + peers: PeerSet, awaiting_parent: Option, created: Instant, pub(crate) span: Span, } -#[derive(Debug)] -pub(crate) enum ComponentRequests { - WaitingForBlock, - ActiveCustodyRequest(CustodyRequestState), - // When printing in debug this state display the reason why it's not needed - #[allow(dead_code)] - NotNeeded(&'static str), -} - impl SingleBlockLookup { pub fn new( requested_block_root: Hash256, @@ -94,25 +129,25 @@ impl SingleBlockLookup { Self { id, - block_request_state: BlockRequestState::new(requested_block_root), - component_requests: ComponentRequests::WaitingForBlock, - peers: Arc::new(RwLock::new(HashSet::from_iter(peers.iter().copied()))), block_root: requested_block_root, + block_request: BlockRequest::new(), + data_request: DataRequest::WaitingForBlock, + peers: Arc::new(RwLock::new(peers.iter().copied().collect())), awaiting_parent, created: Instant::now(), span: lookup_span, } } - /// Reset the status of all internal requests + /// Reset the status of all requests (used on block processing failure) pub fn reset_requests(&mut self) { - self.block_request_state = BlockRequestState::new(self.block_root); - self.component_requests = ComponentRequests::WaitingForBlock; + self.block_request = BlockRequest::new(); + self.data_request = DataRequest::WaitingForBlock; } - /// Return the slot of this lookup's block if it's currently cached as `AwaitingProcessing` + /// Return the slot of this lookup's block if it's currently cached pub fn peek_downloaded_block_slot(&self) -> Option { - self.block_request_state + self.block_request .state .peek_downloaded_data() .map(|block| block.slot()) @@ -147,15 +182,12 @@ impl SingleBlockLookup { /// Maybe insert a verified response into this lookup. Returns true if imported pub fn add_child_components(&mut self, block_component: BlockComponent) -> bool { match block_component { - BlockComponent::Block(block) => self - .block_request_state - .state - .insert_verified_response(block), - BlockComponent::Sidecar { .. } => { - // For now ignore single blobs and columns, as the blob request state assumes all blobs are - // attributed to the same peer = the peer serving the remaining blobs. Ignoring this - // block component has a minor effect, causing the node to re-request this blob - // once the parent chain is successfully resolved + BlockComponent::Block(block) => { + self.block_request.state.insert_verified_response(block) + } + BlockComponent::Sidecar => { + // There's nothing to do here, there's no component to insert. The lookup downloads + // its required data columns itself once it has the block. false } } @@ -166,29 +198,14 @@ impl SingleBlockLookup { self.block_root() == block_root } - /// Returns true if the block has already been downloaded. - pub fn all_components_processed(&self) -> bool { - self.block_request_state.state.is_processed() - && match &self.component_requests { - ComponentRequests::WaitingForBlock => false, - ComponentRequests::ActiveCustodyRequest(request) => request.state.is_processed(), - ComponentRequests::NotNeeded { .. } => true, - } - } - /// Returns true if this request is expecting some event to make progress pub fn is_awaiting_event(&self) -> bool { self.awaiting_parent.is_some() - || self.block_request_state.state.is_awaiting_event() - || match &self.component_requests { - // If components are waiting for the block request to complete, here we should - // check if the`block_request_state.state.is_awaiting_event(). However we already - // checked that above, so `WaitingForBlock => false` is equivalent. - ComponentRequests::WaitingForBlock => false, - ComponentRequests::ActiveCustodyRequest(request) => { - request.state.is_awaiting_event() - } - ComponentRequests::NotNeeded { .. } => false, + || self.block_request.state.is_awaiting_event() + || match &self.data_request { + DataRequest::WaitingForBlock => true, + DataRequest::Request { state, .. } => state.is_awaiting_event(), + DataRequest::NoData => false, } } @@ -199,139 +216,167 @@ impl SingleBlockLookup { cx: &mut SyncNetworkContext, ) -> Result { let _guard = self.span.clone().entered(); - // TODO: Check what's necessary to download, specially for blobs - self.continue_request::>(cx, 0)?; - if let ComponentRequests::WaitingForBlock = self.component_requests { - let downloaded_block = self - .block_request_state - .state - .peek_downloaded_data() - .cloned(); - - if let Some(block) = downloaded_block.or_else(|| { - // If the block is already being processed or fully validated, retrieve how many blobs - // it expects. Consider any stage of the block. If the block root has been validated, we - // can assert that this is the correct value of `blob_kzg_commitments_count`. - match cx.chain.get_block_process_status(&self.block_root) { - BlockProcessStatus::Unknown => None, - BlockProcessStatus::NotValidated(block, _) - | BlockProcessStatus::ExecutionValidated(block) => Some(block.clone()), - } - }) { - let expected_blobs = block.num_expected_blobs(); - let block_epoch = block.slot().epoch(T::EthSpec::slots_per_epoch()); - if expected_blobs == 0 { - self.component_requests = ComponentRequests::NotNeeded("no data"); - } else if cx.chain.should_fetch_custody_columns(block_epoch) { - self.component_requests = ComponentRequests::ActiveCustodyRequest( - CustodyRequestState::new(self.block_root, block.slot()), - ); - } else { - self.component_requests = ComponentRequests::NotNeeded("outside da window"); - } - } else { - // Wait to download the block before downloading blobs. Then we can be sure that the - // block has data, so there's no need to do "blind" requests for all possible blobs and - // latter handle the case where if the peer sent no blobs, penalize. - // - // Lookup sync event safety: Reaching this code means that a block is not in any pre-import - // cache nor in the request state of this lookup. Therefore, the block must either: (1) not - // be downloaded yet or (2) the block is already imported into the fork-choice. - // In case (1) the lookup must either successfully download the block or get dropped. - // In case (2) the block will be downloaded, processed, reach `DuplicateFullyImported` - // and get dropped as completed. - } + // === Block request === + self.block_request.state.maybe_start_downloading(|| { + cx.block_lookup_request(self.id, self.peers.clone(), self.block_root) + })?; + if self.awaiting_parent.is_none() + && let Some(data) = self.block_request.state.maybe_start_processing() + { + cx.send_block_for_processing(self.id, self.block_root, data.value, data.seen_timestamp) + .map_err(LookupRequestError::SendFailedProcessor)?; } - match &self.component_requests { - ComponentRequests::WaitingForBlock => {} // do nothing - ComponentRequests::ActiveCustodyRequest(_) => { - self.continue_request::>(cx, 0)? + // === Data request === + loop { + match &mut self.data_request { + DataRequest::WaitingForBlock => { + if let Some(block) = self.block_request.state.peek_downloaded_data() { + let block_epoch = block + .slot() + .epoch(::EthSpec::slots_per_epoch()); + self.data_request = if block.num_expected_blobs() == 0 { + DataRequest::NoData + } else if cx.chain.should_fetch_custody_columns(block_epoch) { + DataRequest::Request { + slot: block.slot(), + state: SingleLookupRequestState::new(), + } + } else { + DataRequest::NoData + }; + } else { + break; + } + } + DataRequest::Request { slot, state } => { + state.maybe_start_downloading(|| { + cx.custody_lookup_request( + self.id, + self.block_root, + *slot, + self.peers.clone(), + ) + })?; + // Wait for the parent to be imported, data column processing result handle does + // not support `ParentUnknown`. + if self.awaiting_parent.is_none() + && let Some(data) = state.maybe_start_processing() + { + cx.send_custody_columns_for_processing( + self.id, + self.block_root, + data.value, + data.seen_timestamp, + BlockProcessType::SingleCustodyColumn(self.id), + ) + .map_err(LookupRequestError::SendFailedProcessor)?; + } + break; + } + DataRequest::NoData => break, } - ComponentRequests::NotNeeded { .. } => {} // do nothing } // If all components of this lookup are already processed, there will be no future events // that can make progress so it must be dropped. Consider the lookup completed. // This case can happen if we receive the components from gossip during a retry. - if self.all_components_processed() { - self.span = Span::none(); - Ok(LookupResult::Completed) - } else { - Ok(LookupResult::Pending) + if self.block_request.is_complete() && self.data_request.is_complete() { + return Ok(LookupResult::Completed); } + + Ok(LookupResult::Pending) } - /// Potentially makes progress on this request if it's in a progress-able state - fn continue_request>( + /// Handle block processing result. Advances the lookup state machine. + pub fn on_block_processing_result( &mut self, + result: BlockProcessingResult, cx: &mut SyncNetworkContext, - expected_blobs: usize, - ) -> Result<(), LookupRequestError> { - let id = self.id; - let awaiting_parent = self.awaiting_parent.is_some(); - let request = - R::request_state_mut(self).map_err(|e| LookupRequestError::BadState(e.to_owned()))?; - - // Attempt to progress awaiting downloads - if request.get_state().is_awaiting_download() { - // Verify the current request has not exceeded the maximum number of attempts. - let request_state = request.get_state(); - if request_state.failed_attempts() >= SINGLE_BLOCK_LOOKUP_MAX_ATTEMPTS { - let cannot_process = request_state.more_failed_processing_attempts(); - return Err(LookupRequestError::TooManyAttempts { cannot_process }); + ) -> Result { + match result { + BlockProcessingResult::Imported(_fully_imported, _info) => { + self.block_request.state.on_processing_success()?; } - - let peers = self.peers.clone(); - let request = R::request_state_mut(self) - .map_err(|e| LookupRequestError::BadState(e.to_owned()))?; - - match request.make_request(id, peers, expected_blobs, cx)? { - LookupRequestResult::RequestSent(req_id) => { - // Lookup sync event safety: If make_request returns `RequestSent`, we are - // guaranteed that `BlockLookups::on_download_response` will be called exactly - // with this `req_id`. - request.get_state_mut().on_download_start(req_id)? - } - LookupRequestResult::NoRequestNeeded(reason) => { - // Lookup sync event safety: Advances this request to the terminal `Processed` - // state. If all requests reach this state, the request is marked as completed - // in `Self::continue_requests`. - request.get_state_mut().on_completed_request(reason)? - } - // Sync will receive a future event to make progress on the request, do nothing now - LookupRequestResult::Pending(reason) => { - // Lookup sync event safety: Refer to the code paths constructing - // `LookupRequestResult::Pending` - request - .get_state_mut() - .update_awaiting_download_status(reason); - return Ok(()); + BlockProcessingResult::ParentUnknown { parent_root } => { + // `BlockError::ParentUnknown` is only returned when processing blocks. Revert the + // block request to `Downloaded` and park this lookup until the parent resolves; a + // future call to `continue_requests` will re-submit the block for processing once + // the parent lookup completes. + self.block_request.state.revert_to_awaiting_processing()?; + self.set_awaiting_parent(parent_root); + return Ok(LookupResult::ParentUnknown { + parent_root, + block_root: self.block_root, + peers: self.all_peers(), + }); + } + BlockProcessingResult::Error { penalty, .. } => { + let peers = self.block_request.state.on_processing_failure()?; + if let Some((action, whom, msg)) = penalty { + whom.apply(action, &peers, msg, cx); } } - - // Otherwise, attempt to progress awaiting processing - // If this request is awaiting a parent lookup to be processed, do not send for processing. - // The request will be rejected with unknown parent error. - } else if !awaiting_parent { - // maybe_start_processing returns Some if state == AwaitingProcess. This pattern is - // useful to conditionally access the result data. - if let Some(result) = request.get_state_mut().maybe_start_processing() { - // Lookup sync event safety: If `send_for_processing` returns Ok() we are guaranteed - // that `BlockLookups::on_processing_result` will be called exactly once with this - // lookup_id - return R::send_for_processing(id, result, cx); - } - // Lookup sync event safety: If the request is not in `AwaitingDownload` or - // `AwaitingProcessing` state it is guaranteed to receive some event to make progress. } + self.continue_requests(cx) + } - // Lookup sync event safety: If a lookup is awaiting a parent we are guaranteed to either: - // (1) attempt to make progress with `BlockLookups::continue_child_lookups` if the parent - // lookup completes, or (2) get dropped if the parent fails and is dropped. + /// Handle data processing result + pub fn on_data_processing_result( + &mut self, + result: BlockProcessingResult, + cx: &mut SyncNetworkContext, + ) -> Result { + let DataRequest::Request { state, .. } = &mut self.data_request else { + return Err(LookupRequestError::BadState("no data_request".to_owned())); + }; - Ok(()) + match result { + BlockProcessingResult::Imported(_fully_imported, _info) => { + state.on_processing_success()?; + } + BlockProcessingResult::ParentUnknown { .. } => { + return Err(LookupRequestError::BadState( + "data processing returned ParentUnknown".to_owned(), + )); + } + BlockProcessingResult::Error { penalty, .. } => { + let peers = state.on_processing_failure()?; + if let Some((action, whom, msg)) = penalty { + whom.apply(action, &peers, msg, cx); + } + } + } + self.continue_requests(cx) + } + + /// Handle a block download response. Updates download state and advances the lookup. + pub fn on_block_download_response( + &mut self, + req_id: ReqId, + result: BlockDownloadResponse, + cx: &mut SyncNetworkContext, + ) -> Result { + self.block_request + .state + .on_download_response(req_id, result)?; + self.continue_requests(cx) + } + + /// Handle a custody columns download response. Updates download state and advances the lookup. + pub fn on_custody_download_response( + &mut self, + req_id: ReqId, + result: CustodyDownloadResponse, + cx: &mut SyncNetworkContext, + ) -> Result { + let DataRequest::Request { state, .. } = &mut self.data_request else { + return Err(LookupRequestError::BadState("no data_request".to_owned())); + }; + + state.on_download_response(req_id, result)?; + self.continue_requests(cx) } /// Get all unique peers that claim to have imported this set of block components @@ -340,7 +385,7 @@ impl SingleBlockLookup { } /// Add peer to all request states. The peer must be able to serve this request. - /// Returns true if the peer was newly inserted into some request state. + /// Returns true if the peer was newly inserted into any peer set. pub fn add_peer(&mut self, peer_id: PeerId) -> bool { self.peers.write().insert(peer_id) } @@ -356,52 +401,23 @@ impl SingleBlockLookup { } } -/// The state of the custody request component of a `SingleBlockLookup`. -#[derive(Educe)] -#[educe(Debug)] -pub struct CustodyRequestState { - #[educe(Debug(ignore))] - pub block_root: Hash256, - pub slot: Slot, - pub state: SingleLookupRequestState>, -} - -impl CustodyRequestState { - pub fn new(block_root: Hash256, slot: Slot) -> Self { - Self { - block_root, - slot, - state: SingleLookupRequestState::new(), - } - } -} - -/// The state of the block request component of a `SingleBlockLookup`. -#[derive(Educe)] -#[educe(Debug)] -pub struct BlockRequestState { - #[educe(Debug(ignore))] - pub requested_block_root: Hash256, - pub state: SingleLookupRequestState>>, -} - -impl BlockRequestState { - pub fn new(block_root: Hash256) -> Self { - Self { - requested_block_root: block_root, - state: SingleLookupRequestState::new(), - } - } -} - #[derive(Debug, Clone)] pub struct DownloadResult { pub value: T, - pub block_root: Hash256, pub seen_timestamp: Duration, pub peer_group: PeerGroup, } +impl DownloadResult { + pub fn new(value: T, peer_group: PeerGroup, seen_timestamp: Duration) -> Self { + Self { + value, + seen_timestamp, + peer_group, + } + } +} + #[derive(IntoStaticStr)] pub enum State { AwaitingDownload(/* reason */ &'static str), @@ -410,7 +426,7 @@ pub enum State { /// Request is processing, sent by lookup sync Processing(DownloadResult), /// Request is processed - Processed(/* reason */ &'static str), + Processed(/* reason */ &'static str, T), } /// Object representing the state of a single block or blob lookup request. @@ -477,10 +493,29 @@ impl SingleLookupRequestState { State::Downloading { .. } => None, State::AwaitingProcess(result) => Some(&result.value), State::Processing(result) => Some(&result.value), - State::Processed { .. } => None, + State::Processed(_, value) => Some(value), } } + /// Drive download: check max attempts, issue request, handle result. + fn maybe_start_downloading( + &mut self, + request_fn: impl FnOnce() -> Result, RpcRequestSendError>, + ) -> Result<(), LookupRequestError> { + if self.is_awaiting_download() { + match request_fn().map_err(LookupRequestError::SendFailedNetwork)? { + LookupRequestResult::RequestSent(req_id) => self.on_download_start(req_id)?, + LookupRequestResult::NoRequestNeeded(reason, value) => { + self.on_completed_request(reason, value)? + } + LookupRequestResult::Pending(reason) => { + self.update_awaiting_download_status(reason) + } + } + } + Ok(()) + } + /// Switch to `AwaitingProcessing` if the request is in `AwaitingDownload` state, otherwise /// ignore. pub fn insert_verified_response(&mut self, result: DownloadResult) -> bool { @@ -513,6 +548,17 @@ impl SingleLookupRequestState { } } + pub fn on_download_response( + &mut self, + req_id: ReqId, + result: Result, RpcResponseError>, + ) -> Result<(), LookupRequestError> { + match result { + Ok(result) => self.on_download_success(req_id, result), + Err(_) => self.on_download_failure(req_id), + } + } + /// Registers a failure in downloading a block. This might be a peer disconnection or a wrong /// block. pub fn on_download_failure(&mut self, req_id: ReqId) -> Result<(), LookupRequestError> { @@ -525,6 +571,10 @@ impl SingleLookupRequestState { }); } self.failed_downloading = self.failed_downloading.saturating_add(1); + if self.failed_downloading >= SINGLE_BLOCK_LOOKUP_MAX_ATTEMPTS { + return Err(LookupRequestError::TooManyAttempts); + } + self.state = State::AwaitingDownload("not started"); Ok(()) } @@ -589,6 +639,9 @@ impl SingleLookupRequestState { State::Processing(result) => { let peers_source = result.peer_group.clone(); self.failed_processing = self.failed_processing.saturating_add(1); + if self.failed_processing >= SINGLE_BLOCK_LOOKUP_MAX_ATTEMPTS { + return Err(LookupRequestError::TooManyAttempts); + } self.state = State::AwaitingDownload("not started"); Ok(peers_source) } @@ -600,8 +653,8 @@ impl SingleLookupRequestState { pub fn on_processing_success(&mut self) -> Result<(), LookupRequestError> { match &self.state { - State::Processing(_) => { - self.state = State::Processed("processing success"); + State::Processing(data) => { + self.state = State::Processed("processing success", data.value.clone()); Ok(()) } other => Err(LookupRequestError::BadState(format!( @@ -611,10 +664,14 @@ impl SingleLookupRequestState { } /// Mark a request as complete without any download or processing - pub fn on_completed_request(&mut self, reason: &'static str) -> Result<(), LookupRequestError> { + pub fn on_completed_request( + &mut self, + reason: &'static str, + value: T, + ) -> Result<(), LookupRequestError> { match &self.state { State::AwaitingDownload { .. } => { - self.state = State::Processed(reason); + self.state = State::Processed(reason, value); Ok(()) } other => Err(LookupRequestError::BadState(format!( @@ -622,15 +679,6 @@ impl SingleLookupRequestState { ))), } } - - /// The total number of failures, whether it be processing or downloading. - pub fn failed_attempts(&self) -> u8 { - self.failed_processing + self.failed_downloading - } - - pub fn more_failed_processing_attempts(&self) -> bool { - self.failed_processing >= self.failed_downloading - } } // Display is used in the BadState assertions above @@ -647,15 +695,15 @@ impl std::fmt::Debug for State { match self { Self::AwaitingDownload(reason) => write!(f, "AwaitingDownload({})", reason), Self::Downloading(req_id) => write!(f, "Downloading({:?})", req_id), - Self::AwaitingProcess(d) => write!(f, "AwaitingProcess({:?})", d.peer_group), - Self::Processing(d) => write!(f, "Processing({:?})", d.peer_group), - Self::Processed(reason) => write!(f, "Processed({})", reason), + Self::AwaitingProcess(_) => write!(f, "AwaitingProcess"), + Self::Processing(_) => write!(f, "Processing"), + Self::Processed(reason, _) => write!(f, "Processed({})", reason), } } } fn fmt_peer_set_as_len( - peer_set: &Arc>>, + peer_set: &PeerSet, f: &mut std::fmt::Formatter, ) -> Result<(), std::fmt::Error> { write!(f, "{}", peer_set.read().len()) diff --git a/beacon_node/network/src/sync/manager.rs b/beacon_node/network/src/sync/manager.rs index 37d13c2eae..166c65b6e1 100644 --- a/beacon_node/network/src/sync/manager.rs +++ b/beacon_node/network/src/sync/manager.rs @@ -45,9 +45,7 @@ use crate::network_beacon_processor::{ }; use crate::service::NetworkMessage; use crate::status::ToStatusMessage; -use crate::sync::block_lookups::{ - BlockComponent, BlockRequestState, CustodyRequestState, DownloadResult, -}; +use crate::sync::block_lookups::{BlockComponent, DownloadResult}; use crate::sync::custody_backfill_sync::CustodyBackFillSync; use crate::sync::network_context::{PeerGroup, RpcResponseResult}; use beacon_chain::block_verification_types::AsBlock; @@ -867,7 +865,6 @@ impl SyncManager { block_slot, BlockComponent::Block(DownloadResult { value: block.block_cloned(), - block_root, seen_timestamp: self.chain.slot_clock.now_duration().unwrap_or_default(), peer_group: PeerGroup::from_single(peer_id), }), @@ -885,7 +882,7 @@ impl SyncManager { block_root, parent_root, slot, - BlockComponent::Sidecar { parent_root }, + BlockComponent::Sidecar, ); } SyncMessage::UnknownBlockHashFromAttestation(peer_id, block_root) => { @@ -975,6 +972,7 @@ impl SyncManager { if self.block_lookups.search_child_and_parent( block_root, block_component, + parent_root, peer_id, &mut self.network, ) { @@ -1125,14 +1123,13 @@ impl SyncManager { block: RpcEvent>>, ) { if let Some(resp) = self.network.on_single_block_response(id, peer_id, block) { - self.block_lookups - .on_download_response::>( - id, - resp.map(|(value, seen_timestamp)| { - (value, PeerGroup::from_single(peer_id), seen_timestamp) - }), - &mut self.network, - ) + self.block_lookups.on_block_download_response( + id, + resp.map(|(value, seen_timestamp)| { + DownloadResult::new(value, PeerGroup::from_single(peer_id), seen_timestamp) + }), + &mut self.network, + ) } } @@ -1308,11 +1305,7 @@ impl SyncManager { response: CustodyByRootResult, ) { self.block_lookups - .on_download_response::>( - requester.0, - response, - &mut self.network, - ); + .on_custody_download_response(requester.0, response, &mut self.network); } /// Handles receiving a response for a range sync request that should have both blocks and diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index 95ae84755c..1e35c0a72f 100644 --- a/beacon_node/network/src/sync/network_context.rs +++ b/beacon_node/network/src/sync/network_context.rs @@ -16,7 +16,7 @@ use crate::network_beacon_processor::TestBeaconChainType; use crate::service::NetworkMessage; use crate::status::ToStatusMessage; use crate::sync::batch::ByRangeRequestType; -use crate::sync::block_lookups::SingleLookupId; +use crate::sync::block_lookups::{DownloadResult, SingleLookupId}; use crate::sync::block_sidecar_coupling::CouplingError; use crate::sync::range_data_column_batch_request::RangeDataColumnBatchRequest; use beacon_chain::block_verification_types::LookupBlock; @@ -95,7 +95,7 @@ pub type RpcResponseResult = Result<(T, Duration), RpcResponseError>; /// Duration = latest seen timestamp of all received data columns pub type CustodyByRootResult = - Result<(DataColumnSidecarList, PeerGroup, Duration), RpcResponseError>; + Result>, RpcResponseError>; #[derive(Debug)] pub enum RpcResponseError { @@ -176,13 +176,13 @@ impl PeerGroup { /// Sequential ID that uniquely identifies ReqResp outgoing requests pub type ReqId = u32; -pub enum LookupRequestResult { +pub enum LookupRequestResult { /// A request is sent. Sync MUST receive an event from the network in the future for either: /// completed response or failed request RequestSent(I), /// No request is sent, and no further action is necessary to consider this request completed. /// Includes a reason why this request is not needed. - NoRequestNeeded(&'static str), + NoRequestNeeded(&'static str, T), /// No request is sent, but the request is not completed. Sync MUST receive some future event /// that makes progress on the request. For example: request is processing from a different /// source (i.e. block received from gossip) and sync MUST receive an event with that processing @@ -820,7 +820,7 @@ impl SyncNetworkContext { lookup_id: SingleLookupId, lookup_peers: Arc>>, block_root: Hash256, - ) -> Result { + ) -> Result>>, RpcRequestSendError> { let active_request_count_by_peer = self.active_request_count_by_peer(); let Some(peer_id) = lookup_peers .read() @@ -871,9 +871,10 @@ impl SyncNetworkContext { }, // Block is fully validated. If it's not yet imported it's waiting for missing block // components. Consider this request completed and do nothing. - BlockProcessStatus::ExecutionValidated { .. } => { + BlockProcessStatus::ExecutionValidated(block) => { return Ok(LookupRequestResult::NoRequestNeeded( "block execution validated", + block, )); } } @@ -937,12 +938,13 @@ impl SyncNetworkContext { lookup_id: SingleLookupId, lookup_peers: Arc>>, block_root: Hash256, - ) -> Result { + ) -> Result, RpcRequestSendError> { // 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", + (), )); } @@ -1011,7 +1013,7 @@ impl SyncNetworkContext { peer_id: PeerId, request: DataColumnsByRootSingleBlockRequest, expect_max_responses: bool, - ) -> Result, &'static str> { + ) -> Result, &'static str> { let id = DataColumnsByRootRequestId { id: self.next_id(), requester, @@ -1060,7 +1062,7 @@ impl SyncNetworkContext { block_root: Hash256, block_slot: Slot, lookup_peers: Arc>>, - ) -> Result { + ) -> Result>, RpcRequestSendError> { let custody_indexes_imported = self .chain .cached_data_column_indexes(&block_root, block_slot) @@ -1078,7 +1080,10 @@ impl SyncNetworkContext { if custody_indexes_to_fetch.is_empty() { // No indexes required, do not issue any request - return Ok(LookupRequestResult::NoRequestNeeded("no indices to fetch")); + return Ok(LookupRequestResult::NoRequestNeeded( + "no indices to fetch", + vec![], + )); } let id = SingleLookupReqId { @@ -1528,8 +1533,8 @@ impl SyncNetworkContext { // Convert a result from internal format of `ActiveCustodyRequest` (error first to use ?) to // an Option first to use in an `if let Some() { act on result }` block. match result.as_ref() { - Some(Ok((columns, peer_group, _))) => { - debug!(?id, count = columns.len(), peers = ?peer_group, "Custody request success, removing") + Some(Ok(data)) => { + debug!(?id, count = data.value.len(), peers = ?data.peer_group, "Custody request success, removing") } Some(Err(e)) => { debug!(?id, error = ?e, "Custody request failure, removing" ) diff --git a/beacon_node/network/src/sync/network_context/custody.rs b/beacon_node/network/src/sync/network_context/custody.rs index 2b96800e37..e74b74ec08 100644 --- a/beacon_node/network/src/sync/network_context/custody.rs +++ b/beacon_node/network/src/sync/network_context/custody.rs @@ -1,3 +1,4 @@ +use crate::sync::block_lookups::DownloadResult; use crate::sync::network_context::{ DataColumnsByRootRequestId, DataColumnsByRootSingleBlockRequest, }; @@ -56,8 +57,7 @@ struct ActiveBatchColumnsRequest { span: Span, } -pub type CustodyRequestResult = - Result, PeerGroup, Duration)>, Error>; +pub type CustodyRequestResult = Result>>, Error>; impl ActiveCustodyRequest { pub(crate) fn new( @@ -227,7 +227,11 @@ impl ActiveCustodyRequest { .into_iter() .max() .unwrap_or_else(|| cx.chain.slot_clock.now_duration().unwrap_or_default()); - return Ok(Some((columns, peer_group, max_seen_timestamp))); + return Ok(Some(DownloadResult::new( + columns, + peer_group, + max_seen_timestamp, + ))); } let active_request_count_by_peer = cx.active_request_count_by_peer(); @@ -343,7 +347,7 @@ impl ActiveCustodyRequest { }, ); } - LookupRequestResult::NoRequestNeeded(_) => unreachable!(), + LookupRequestResult::NoRequestNeeded(..) => unreachable!(), LookupRequestResult::Pending(_) => unreachable!(), } } From d617c826fe6c4983cd883d50b7f4df5bce31304f Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Wed, 3 Jun 2026 09:07:41 -0700 Subject: [PATCH 10/26] Gloas data column reprocess queue (#9339) When debugging ePBS with columns, we noticed that columns arriving before their block dont pass gossip verification checks and are dropped. This PR ensures that columns arriving before the block are sent to the reprocess queue. Once their block arrives, they are reprocessed. This isn't an issue pre-gloas because we don't make block root checks for fulu data columns. This allows us to gossip verify the column and send it to the DA cache before the block arrives. I think we also need to handle this edge case for partial data columns. Theres an existing TODO for that already. Co-Authored-By: Eitan Seri-Levi --- beacon_node/beacon_processor/src/lib.rs | 23 +- .../src/scheduler/work_queue.rs | 7 + .../src/scheduler/work_reprocessing_queue.rs | 216 +++++++++++++++--- .../gossip_methods.rs | 46 +++- .../src/network_beacon_processor/mod.rs | 2 + .../src/network_beacon_processor/tests.rs | 1 + beacon_node/network/src/router.rs | 1 + 7 files changed, 248 insertions(+), 48 deletions(-) diff --git a/beacon_node/beacon_processor/src/lib.rs b/beacon_node/beacon_processor/src/lib.rs index af3ff09c8a..d6233ebaf9 100644 --- a/beacon_node/beacon_processor/src/lib.rs +++ b/beacon_node/beacon_processor/src/lib.rs @@ -41,8 +41,8 @@ pub use crate::scheduler::BeaconProcessorQueueLengths; use crate::scheduler::work_queue::WorkQueues; use crate::work_reprocessing_queue::{ - QueuedBackfillBatch, QueuedColumnReconstruction, QueuedGossipBlock, QueuedGossipEnvelope, - ReprocessQueueMessage, + QueuedBackfillBatch, QueuedColumnReconstruction, QueuedGossipBlock, QueuedGossipDataColumn, + QueuedGossipEnvelope, ReprocessQueueMessage, }; use futures::stream::{Stream, StreamExt}; use futures::task::Poll; @@ -304,6 +304,10 @@ impl From for WorkEvent { work: Work::ColumnReconstruction(process_fn), } } + ReadyWork::DataColumn(QueuedGossipDataColumn { process_fn, .. }) => Self { + drop_during_sync: true, + work: Work::UnknownBlockDataColumn { process_fn }, + }, } } } @@ -369,6 +373,9 @@ pub enum Work { UnknownBlockAttestation { process_fn: BlockingFn, }, + UnknownBlockDataColumn { + process_fn: BlockingFn, + }, GossipAttestationBatch { attestations: GossipAttestationBatch, process_batch: Box, @@ -464,6 +471,7 @@ pub enum WorkType { GossipAttestation, GossipAttestationToConvert, UnknownBlockAttestation, + UnknownBlockDataColumn, GossipAttestationBatch, GossipAggregate, UnknownBlockAggregate, @@ -569,6 +577,7 @@ impl Work { Work::LightClientFinalityUpdateRequest(_) => WorkType::LightClientFinalityUpdateRequest, Work::LightClientUpdatesByRangeRequest(_) => WorkType::LightClientUpdatesByRangeRequest, Work::UnknownBlockAttestation { .. } => WorkType::UnknownBlockAttestation, + Work::UnknownBlockDataColumn { .. } => WorkType::UnknownBlockDataColumn, Work::UnknownBlockAggregate { .. } => WorkType::UnknownBlockAggregate, Work::UnknownLightClientOptimisticUpdate { .. } => { WorkType::UnknownLightClientOptimisticUpdate @@ -842,6 +851,9 @@ impl BeaconProcessor { Some(item) } else if let Some(item) = work_queues.gossip_data_column_queue.pop() { Some(item) + } else if let Some(item) = work_queues.unknown_block_data_column_queue.pop() + { + Some(item) } else if let Some(item) = work_queues.gossip_partial_data_column_queue.pop() { @@ -1238,6 +1250,9 @@ impl BeaconProcessor { Work::UnknownBlockAttestation { .. } => { work_queues.unknown_block_attestation_queue.push(work) } + Work::UnknownBlockDataColumn { .. } => work_queues + .unknown_block_data_column_queue + .push(work, work_id), Work::UnknownBlockAggregate { .. } => { work_queues.unknown_block_aggregate_queue.push(work) } @@ -1288,6 +1303,9 @@ impl BeaconProcessor { WorkType::UnknownBlockAttestation => { work_queues.unknown_block_attestation_queue.len() } + WorkType::UnknownBlockDataColumn => { + work_queues.unknown_block_data_column_queue.len() + } WorkType::GossipAttestationBatch => 0, // No queue WorkType::GossipAggregate => work_queues.aggregate_queue.len(), WorkType::UnknownBlockAggregate => { @@ -1504,6 +1522,7 @@ impl BeaconProcessor { }), Work::UnknownBlockAttestation { process_fn } | Work::UnknownBlockAggregate { process_fn } + | Work::UnknownBlockDataColumn { process_fn } | Work::UnknownLightClientOptimisticUpdate { process_fn, .. } => { task_spawner.spawn_blocking(process_fn) } diff --git a/beacon_node/beacon_processor/src/scheduler/work_queue.rs b/beacon_node/beacon_processor/src/scheduler/work_queue.rs index ebd66e743d..cc03feac51 100644 --- a/beacon_node/beacon_processor/src/scheduler/work_queue.rs +++ b/beacon_node/beacon_processor/src/scheduler/work_queue.rs @@ -111,6 +111,7 @@ pub struct BeaconProcessorQueueLengths { attestation_queue: usize, unknown_block_aggregate_queue: usize, unknown_block_attestation_queue: usize, + unknown_block_data_column_queue: usize, sync_message_queue: usize, sync_contribution_queue: usize, gossip_voluntary_exit_queue: usize, @@ -174,6 +175,8 @@ impl BeaconProcessorQueueLengths { Ok(Self { aggregate_queue: 4096, unknown_block_aggregate_queue: 1024, + // Capacity for two slot's worth of data columns for a supernode. + unknown_block_data_column_queue: 256, // Capacity for a full slot's worth of attestations if subscribed to all subnets attestation_queue: std::cmp::max( active_validator_count / slots_per_epoch, @@ -245,6 +248,7 @@ pub struct WorkQueues { pub attestation_debounce: TimeLatch, pub unknown_block_aggregate_queue: LifoQueue>, pub unknown_block_attestation_queue: LifoQueue>, + pub unknown_block_data_column_queue: FifoQueue>, pub sync_message_queue: LifoQueue>, pub sync_contribution_queue: LifoQueue>, pub gossip_voluntary_exit_queue: FifoQueue>, @@ -302,6 +306,8 @@ impl WorkQueues { LifoQueue::new(queue_lengths.unknown_block_aggregate_queue); let unknown_block_attestation_queue = LifoQueue::new(queue_lengths.unknown_block_attestation_queue); + let unknown_block_data_column_queue = + FifoQueue::new(queue_lengths.unknown_block_data_column_queue); let sync_message_queue = LifoQueue::new(queue_lengths.sync_message_queue); let sync_contribution_queue = LifoQueue::new(queue_lengths.sync_contribution_queue); @@ -383,6 +389,7 @@ impl WorkQueues { attestation_debounce, unknown_block_aggregate_queue, unknown_block_attestation_queue, + unknown_block_data_column_queue, sync_message_queue, sync_contribution_queue, gossip_voluntary_exit_queue, diff --git a/beacon_node/beacon_processor/src/scheduler/work_reprocessing_queue.rs b/beacon_node/beacon_processor/src/scheduler/work_reprocessing_queue.rs index b1fa56af01..62ed86fbad 100644 --- a/beacon_node/beacon_processor/src/scheduler/work_reprocessing_queue.rs +++ b/beacon_node/beacon_processor/src/scheduler/work_reprocessing_queue.rs @@ -52,6 +52,10 @@ pub const QUEUED_ATTESTATION_DELAY: Duration = Duration::from_secs(12); /// For how long to queue light client updates for re-processing. pub const QUEUED_LIGHT_CLIENT_UPDATE_DELAY: Duration = Duration::from_secs(12); +/// Data column timeout as a multiplier of slot duration. Columns waiting for their block will be +/// sent for processing after this many slots worth of time, even if the block hasn't arrived. +const QUEUED_DATA_COLUMN_DELAY_SLOTS: u32 = 1; + /// Envelope timeout as a multiplier of slot duration. Envelopes waiting for their block will be /// sent for processing after this many slots worth of time, even if the block hasn't arrived. const QUEUED_ENVELOPE_DELAY_SLOTS: u32 = 1; @@ -76,6 +80,9 @@ const MAXIMUM_QUEUED_ENVELOPES: usize = 16; /// How many attestations we keep before new ones get dropped. const MAXIMUM_QUEUED_ATTESTATIONS: usize = 16_384; +/// How many columns we keep before new ones get dropped. +const MAXIMUM_QUEUED_DATA_COLUMNS: usize = 256; + /// How many light client updates we keep before new ones get dropped. const MAXIMUM_QUEUED_LIGHT_CLIENT_UPDATES: usize = 128; @@ -123,6 +130,8 @@ pub enum ReprocessQueueMessage { UnknownLightClientOptimisticUpdate(QueuedLightClientUpdate), /// A new backfill batch that needs to be scheduled for processing. BackfillSync(QueuedBackfillBatch), + /// A gossip data column that references an unknown block. + UnknownBlockDataColumn(QueuedGossipDataColumn), /// A delayed column reconstruction that needs checking DelayColumnReconstruction(QueuedColumnReconstruction), } @@ -138,6 +147,7 @@ pub enum ReadyWork { LightClientUpdate(QueuedLightClientUpdate), BackfillSync(QueuedBackfillBatch), ColumnReconstruction(QueuedColumnReconstruction), + DataColumn(QueuedGossipDataColumn), } /// An Attestation for which the corresponding block was not seen while processing, queued for @@ -200,6 +210,12 @@ pub struct QueuedColumnReconstruction { pub process_fn: AsyncFn, } +/// A gossip data column that references an unknown block, queued for later reprocessing. +pub struct QueuedGossipDataColumn { + pub beacon_block_root: Hash256, + pub process_fn: BlockingFn, +} + impl TryFrom> for QueuedBackfillBatch { type Error = WorkEvent; @@ -240,6 +256,8 @@ enum InboundEvent { ReadyBackfillSync(QueuedBackfillBatch), /// A column reconstruction that was queued is ready for processing. ReadyColumnReconstruction(QueuedColumnReconstruction), + /// A gossip data column that is ready for re-processing. + ReadyDataColumn(Hash256), /// A message sent to the `ReprocessQueue` Msg(ReprocessQueueMessage), } @@ -264,6 +282,8 @@ struct ReprocessQueue { lc_updates_delay_queue: DelayQueue, /// Queue to manage scheduled column reconstructions. column_reconstructions_delay_queue: DelayQueue, + /// Queue to manage gossip data column timeouts. + data_columns_delay_queue: DelayQueue, /* Queued items */ /// Queued blocks. @@ -284,6 +304,10 @@ struct ReprocessQueue { queued_column_reconstructions: HashMap>, /// Queued backfill batches queued_backfill_batches: Vec, + /// Queued gossip data columns awaiting their block, keyed by block root. + awaiting_data_columns_per_root: HashMap, DelayKey)>, + /// Total number of queued gossip data columns across all roots. + queued_data_columns_count: usize, /* Aux */ /// Next attestation id, used for both aggregated and unaggregated attestations @@ -294,6 +318,7 @@ struct ReprocessQueue { rpc_block_debounce: TimeLatch, attestation_delay_debounce: TimeLatch, lc_update_delay_debounce: TimeLatch, + data_column_delay_debounce: TimeLatch, next_backfill_batch_event: Option>>, slot_clock: Arc, } @@ -387,6 +412,13 @@ impl Stream for ReprocessQueue { Poll::Ready(None) | Poll::Pending => (), } + match self.data_columns_delay_queue.poll_expired(cx) { + Poll::Ready(Some(block_root)) => { + return Poll::Ready(Some(InboundEvent::ReadyDataColumn(block_root.into_inner()))); + } + Poll::Ready(None) | Poll::Pending => (), + } + if let Some(next_backfill_batch_event) = self.next_backfill_batch_event.as_mut() { match next_backfill_batch_event.as_mut().poll(cx) { Poll::Ready(_) => { @@ -455,6 +487,7 @@ impl ReprocessQueue { attestations_delay_queue: DelayQueue::new(), lc_updates_delay_queue: DelayQueue::new(), column_reconstructions_delay_queue: DelayQueue::new(), + data_columns_delay_queue: DelayQueue::new(), queued_gossip_block_roots: HashSet::new(), awaiting_envelopes_per_root: HashMap::new(), queued_lc_updates: FnvHashMap::default(), @@ -464,6 +497,8 @@ impl ReprocessQueue { awaiting_lc_updates_per_parent_root: HashMap::new(), queued_backfill_batches: Vec::new(), queued_column_reconstructions: HashMap::new(), + awaiting_data_columns_per_root: HashMap::new(), + queued_data_columns_count: 0, next_attestation: 0, next_lc_update: 0, early_block_debounce: TimeLatch::default(), @@ -471,6 +506,7 @@ impl ReprocessQueue { rpc_block_debounce: TimeLatch::default(), attestation_delay_debounce: TimeLatch::default(), lc_update_delay_debounce: TimeLatch::default(), + data_column_delay_debounce: TimeLatch::default(), next_backfill_batch_event: None, slot_clock, } @@ -551,22 +587,16 @@ impl ReprocessQueue { return; } - // When the queue is full, evict the oldest entry to make room for newer envelopes. + // When the queue is full, drop the new envelope. if self.awaiting_envelopes_per_root.len() >= MAXIMUM_QUEUED_ENVELOPES { if self.envelope_delay_debounce.elapsed() { warn!( queue_size = MAXIMUM_QUEUED_ENVELOPES, msg = "system resources may be saturated", - "Envelope delay queue is full, evicting oldest entry" + "Envelope delay queue is full, dropping envelope" ); } - if let Some(oldest_root) = - self.awaiting_envelopes_per_root.keys().next().copied() - && let Some((_envelope, delay_key)) = - self.awaiting_envelopes_per_root.remove(&oldest_root) - { - self.envelope_delay_queue.remove(&delay_key); - } + return; } // Register the timeout. @@ -688,6 +718,37 @@ impl ReprocessQueue { self.next_attestation += 1; } + InboundEvent::Msg(UnknownBlockDataColumn(queued_data_column)) => { + let block_root = queued_data_column.beacon_block_root; + + if self.queued_data_columns_count >= MAXIMUM_QUEUED_DATA_COLUMNS { + if self.data_column_delay_debounce.elapsed() { + warn!( + queue_size = MAXIMUM_QUEUED_DATA_COLUMNS, + msg = "system resources may be saturated", + "Data column delay queue is full, dropping column" + ); + } + return; + } + + if let Some((columns, _delay_key)) = + self.awaiting_data_columns_per_root.get_mut(&block_root) + { + // Append to existing entry; the timer for this root is already running. + columns.push(queued_data_column); + } else { + let delay_key = self.data_columns_delay_queue.insert( + block_root, + self.slot_clock.slot_duration() * QUEUED_DATA_COLUMN_DELAY_SLOTS, + ); + + self.awaiting_data_columns_per_root + .insert(block_root, (vec![queued_data_column], delay_key)); + } + + self.queued_data_columns_count += 1; + } InboundEvent::Msg(UnknownLightClientOptimisticUpdate( queued_light_client_optimistic_update, )) => { @@ -800,6 +861,25 @@ impl ReprocessQueue { ); } } + + // Unqueue the data columns we have for this root, if any. + if let Some((data_columns, delay_key)) = + self.awaiting_data_columns_per_root.remove(&block_root) + { + self.data_columns_delay_queue.remove(&delay_key); + self.queued_data_columns_count = self + .queued_data_columns_count + .saturating_sub(data_columns.len()); + for data_column in data_columns { + if self + .ready_work_tx + .try_send(ReadyWork::DataColumn(data_column)) + .is_err() + { + error!(?block_root, "Failed to send data column for reprocessing"); + } + } + } } InboundEvent::Msg(NewLightClientOptimisticUpdate { parent_root }) => { // Unqueue the light client optimistic updates we have for this root, if any. @@ -1053,6 +1133,27 @@ impl ReprocessQueue { ); } } + InboundEvent::ReadyDataColumn(block_root) => { + if let Some((data_columns, _)) = + self.awaiting_data_columns_per_root.remove(&block_root) + { + self.queued_data_columns_count = self + .queued_data_columns_count + .saturating_sub(data_columns.len()); + for data_column in data_columns { + if self + .ready_work_tx + .try_send(ReadyWork::DataColumn(data_column)) + .is_err() + { + error!( + hint = "system may be overloaded", + "Ignored expired gossip data column" + ); + } + } + } + } } metrics::set_gauge_vec( @@ -1581,48 +1682,87 @@ mod tests { assert_eq!(queue.envelope_delay_queue.len(), 1); } + /// Tests that a queued gossip data column is released when its block is imported. #[tokio::test] - async fn envelope_capacity_evicts_oldest() { + async fn data_column_released_on_block_imported() { + create_test_tracing_subscriber(); + + let config = BeaconProcessorConfig::default(); + let (ready_work_tx, mut ready_work_rx) = + mpsc::channel::(config.max_scheduled_work_queue_len); + let (_, reprocess_work_rx) = + mpsc::channel::(config.max_scheduled_work_queue_len); + let slot_clock = Arc::new(testing_slot_clock(12)); + let mut queue = ReprocessQueue::new(ready_work_tx, reprocess_work_rx, slot_clock); + + tokio::time::pause(); + + let beacon_block_root = Hash256::repeat_byte(0xbb); + + let msg = ReprocessQueueMessage::UnknownBlockDataColumn(QueuedGossipDataColumn { + beacon_block_root, + process_fn: Box::new(|| {}), + }); + queue.handle_message(InboundEvent::Msg(msg)); + + assert_eq!(queue.awaiting_data_columns_per_root.len(), 1); + assert!( + queue + .awaiting_data_columns_per_root + .contains_key(&beacon_block_root) + ); + assert_eq!(queue.data_columns_delay_queue.len(), 1); + + // Simulate block import. + queue.handle_message(InboundEvent::Msg(ReprocessQueueMessage::BlockImported { + block_root: beacon_block_root, + parent_root: Hash256::repeat_byte(0x00), + })); + + // Internal state should be cleaned up. + assert!(queue.awaiting_data_columns_per_root.is_empty()); + assert_eq!(queue.data_columns_delay_queue.len(), 0); + + // The column should have been sent to the ready_work channel. + let ready = ready_work_rx.try_recv().expect("column should be ready"); + assert!(matches!(ready, ReadyWork::DataColumn(_))); + } + + /// Tests that an expired gossip data column is pruned cleanly from all internal state. + #[tokio::test] + async fn prune_awaiting_data_columns_per_root() { create_test_tracing_subscriber(); let mut queue = test_queue(); - // Pause time so it only advances manually tokio::time::pause(); - // Fill the queue to capacity. - for i in 0..MAXIMUM_QUEUED_ENVELOPES { - let block_root = Hash256::repeat_byte(i as u8); - let msg = ReprocessQueueMessage::UnknownBlockForEnvelope(QueuedGossipEnvelope { - beacon_block_slot: Slot::new(1), - beacon_block_root: block_root, - process_fn: Box::pin(async {}), - }); - queue.handle_message(InboundEvent::Msg(msg)); - } - assert_eq!( - queue.awaiting_envelopes_per_root.len(), - MAXIMUM_QUEUED_ENVELOPES - ); + let beacon_block_root = Hash256::repeat_byte(0xcd); - // One more should evict the oldest and insert the new one. - let overflow_root = Hash256::repeat_byte(0xff); - let msg = ReprocessQueueMessage::UnknownBlockForEnvelope(QueuedGossipEnvelope { - beacon_block_slot: Slot::new(1), - beacon_block_root: overflow_root, - process_fn: Box::pin(async {}), + let msg = ReprocessQueueMessage::UnknownBlockDataColumn(QueuedGossipDataColumn { + beacon_block_root, + process_fn: Box::new(|| {}), }); queue.handle_message(InboundEvent::Msg(msg)); - // Queue should still be at capacity, with the new root present. - assert_eq!( - queue.awaiting_envelopes_per_root.len(), - MAXIMUM_QUEUED_ENVELOPES - ); + assert_eq!(queue.awaiting_data_columns_per_root.len(), 1); assert!( queue - .awaiting_envelopes_per_root - .contains_key(&overflow_root) + .awaiting_data_columns_per_root + .contains_key(&beacon_block_root) ); + + // Advance time past the delay so the entry expires. + advance_time( + &queue.slot_clock, + 2 * queue.slot_clock.slot_duration() * QUEUED_DATA_COLUMN_DELAY_SLOTS, + ) + .await; + let ready_msg = queue.next().await.unwrap(); + assert!(matches!(ready_msg, InboundEvent::ReadyDataColumn(_))); + queue.handle_message(ready_msg); + + // All internal state should be cleaned up. + assert!(queue.awaiting_data_columns_per_root.is_empty()); } } 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 df94b473a8..9becfd4d59 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -61,8 +61,8 @@ use beacon_processor::work_reprocessing_queue::QueuedColumnReconstruction; use beacon_processor::{ DuplicateCache, GossipAggregatePackage, GossipAttestationBatch, work_reprocessing_queue::{ - QueuedAggregate, QueuedGossipBlock, QueuedGossipEnvelope, QueuedLightClientUpdate, - QueuedUnaggregate, ReprocessQueueMessage, + QueuedAggregate, QueuedGossipBlock, QueuedGossipDataColumn, QueuedGossipEnvelope, + QueuedLightClientUpdate, QueuedUnaggregate, ReprocessQueueMessage, }, }; @@ -657,6 +657,7 @@ impl NetworkBeaconProcessor { subnet_id: DataColumnSubnetId, column_sidecar: Arc>, seen_duration: Duration, + allow_reprocess: bool, ) { let slot = column_sidecar.slot(); let block_root = column_sidecar.block_root(); @@ -738,19 +739,48 @@ impl NetworkBeaconProcessor { .. } => { debug!( - action = "ignoring", + action = "queuing for reprocessing", %unknown_block_root, "Unknown block root for column" ); - // TODO(gloas): wire this into proper lookup sync. Sending - // `UnknownBlockHashFromAttestation` here is a Fulu-shaped fallback that - // mixes column processing with the attestation lookup path and is not - // the right primitive for Gloas column lookups. self.propagate_validation_result( - message_id, + message_id.clone(), peer_id, MessageAcceptance::Ignore, ); + + if allow_reprocess { + // Queue the column for reprocessing when the block arrives. + let processor = self.clone(); + let reprocess_msg = ReprocessQueueMessage::UnknownBlockDataColumn( + QueuedGossipDataColumn { + beacon_block_root: unknown_block_root, + process_fn: Box::new(move || { + let _ = processor.send_gossip_data_column_sidecar( + message_id, + peer_id, + subnet_id, + column_sidecar, + seen_duration, + false, // Do not reprocess this message again. + ); + }), + }, + ); + if self + .beacon_processor_send + .try_send(WorkEvent { + drop_during_sync: false, + work: Work::Reprocess(reprocess_msg), + }) + .is_err() + { + debug!( + %unknown_block_root, + "Failed to queue data column for reprocessing" + ); + } + } } GossipDataColumnError::InvalidVariant | GossipDataColumnError::PubkeyCacheTimeout diff --git a/beacon_node/network/src/network_beacon_processor/mod.rs b/beacon_node/network/src/network_beacon_processor/mod.rs index c2c8577046..f3c773eb25 100644 --- a/beacon_node/network/src/network_beacon_processor/mod.rs +++ b/beacon_node/network/src/network_beacon_processor/mod.rs @@ -201,6 +201,7 @@ impl NetworkBeaconProcessor { subnet_id: DataColumnSubnetId, column_sidecar: Arc>, seen_timestamp: Duration, + allow_reprocess: bool, ) -> Result<(), Error> { let processor = self.clone(); let process_fn = async move { @@ -211,6 +212,7 @@ impl NetworkBeaconProcessor { subnet_id, column_sidecar, seen_timestamp, + allow_reprocess, ) .await }; diff --git a/beacon_node/network/src/network_beacon_processor/tests.rs b/beacon_node/network/src/network_beacon_processor/tests.rs index c0b093e254..ad98851532 100644 --- a/beacon_node/network/src/network_beacon_processor/tests.rs +++ b/beacon_node/network/src/network_beacon_processor/tests.rs @@ -412,6 +412,7 @@ impl TestRig { DataColumnSubnetId::from_column_index(*data_column.index(), &self.chain.spec), data_column.clone(), Duration::from_secs(0), + true, ) .unwrap(); } diff --git a/beacon_node/network/src/router.rs b/beacon_node/network/src/router.rs index a8e5c9ae4a..277ece0aa8 100644 --- a/beacon_node/network/src/router.rs +++ b/beacon_node/network/src/router.rs @@ -422,6 +422,7 @@ impl Router { subnet_id, column_sidecar, seen_timestamp, + true, ), ) } From 91456fb2186b2557937d6db88ca244dd5586998a Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Thu, 4 Jun 2026 17:24:27 +1000 Subject: [PATCH 11/26] Regression test for range sync CGC race condition (#8039) Co-Authored-By: Jimmy Chen --- .../src/peer_manager/peerdb.rs | 58 +++++++++----- .../network/src/sync/backfill_sync/mod.rs | 2 +- beacon_node/network/src/sync/tests/lookups.rs | 34 ++++++++- beacon_node/network/src/sync/tests/range.rs | 76 ++++++++++++++++++- 4 files changed, 147 insertions(+), 23 deletions(-) diff --git a/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs b/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs index 11ce785350..23f47c67a7 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs @@ -793,12 +793,39 @@ impl PeerDB { ); } - /// Updates the connection state. MUST ONLY BE USED IN TESTS. - pub fn __add_connected_peer_testing_only( + /// Adds a connected peer to the PeerDB and sets the custody subnets. + /// WARNING: This updates the connection state. MUST ONLY BE USED IN TESTS. + pub fn __add_connected_peer_with_custody_subnets( &mut self, supernode: bool, spec: &ChainSpec, enr_key: CombinedKey, + ) -> PeerId { + let peer_id = self.__add_connected_peer(supernode, enr_key, spec); + + let subnets = if supernode { + (0..spec.data_column_sidecar_subnet_count) + .map(|subnet_id| subnet_id.into()) + .collect() + } else { + let node_id = peer_id_to_node_id(&peer_id).expect("convert peer_id to node_id"); + compute_subnets_for_node::(node_id.raw(), spec.custody_requirement, spec) + .expect("should compute custody subnets") + }; + + let peer_info = self.peers.get_mut(&peer_id).expect("peer exists"); + peer_info.set_custody_subnets(subnets); + + peer_id + } + + /// Adds a connected peer to the PeerDB and updates the connection state. + /// MUST ONLY BE USED IN TESTS. + pub fn __add_connected_peer( + &mut self, + supernode: bool, + enr_key: CombinedKey, + spec: &ChainSpec, ) -> PeerId { let mut enr = Enr::builder().build(&enr_key).unwrap(); let peer_id = enr.peer_id(); @@ -835,24 +862,21 @@ impl PeerDB { }, ); - if supernode { - let peer_info = self.peers.get_mut(&peer_id).expect("peer exists"); - let all_subnets = (0..spec.data_column_sidecar_subnet_count) - .map(|subnet_id| subnet_id.into()) - .collect(); - peer_info.set_custody_subnets(all_subnets); - } else { - let peer_info = self.peers.get_mut(&peer_id).expect("peer exists"); - let node_id = peer_id_to_node_id(&peer_id).expect("convert peer_id to node_id"); - let subnets = - compute_subnets_for_node::(node_id.raw(), spec.custody_requirement, spec) - .expect("should compute custody subnets"); - peer_info.set_custody_subnets(subnets); - } - peer_id } + /// MUST ONLY BE USED IN TESTS. + pub fn __set_custody_subnets( + &mut self, + peer_id: &PeerId, + custody_subnets: HashSet, + ) -> Result<(), String> { + self.peers + .get_mut(peer_id) + .map(|info| info.set_custody_subnets(custody_subnets)) + .ok_or_else(|| "Cannot set custody subnets, peer not found".to_string()) + } + /// The connection state of the peer has been changed. Modify the peer in the db to ensure all /// variables are in sync with libp2p. /// Updating the state can lead to a `BanOperation` which needs to be processed via the peer diff --git a/beacon_node/network/src/sync/backfill_sync/mod.rs b/beacon_node/network/src/sync/backfill_sync/mod.rs index 0f80138d24..f3dab7f395 100644 --- a/beacon_node/network/src/sync/backfill_sync/mod.rs +++ b/beacon_node/network/src/sync/backfill_sync/mod.rs @@ -1247,7 +1247,7 @@ mod tests { let peer_id = network_globals .peers .write() - .__add_connected_peer_testing_only( + .__add_connected_peer_with_custody_subnets( true, &beacon_chain.spec, k256::ecdsa::SigningKey::random(&mut rng).into(), diff --git a/beacon_node/network/src/sync/tests/lookups.rs b/beacon_node/network/src/sync/tests/lookups.rs index 3ec4d11da2..5642f7846a 100644 --- a/beacon_node/network/src/sync/tests/lookups.rs +++ b/beacon_node/network/src/sync/tests/lookups.rs @@ -31,13 +31,14 @@ use lighthouse_network::{ types::SyncState, }; use slot_clock::{SlotClock, TestingSlotClock}; +use std::collections::HashSet; use std::sync::Arc; use std::time::Duration; use tokio::sync::mpsc; use tracing::info; use types::{ - BlobSidecar, BlockImportSource, ColumnIndex, DataColumnSidecar, ForkContext, ForkName, Hash256, - MinimalEthSpec as E, SignedBeaconBlock, Slot, + BlobSidecar, BlockImportSource, ColumnIndex, DataColumnSidecar, DataColumnSubnetId, + ForkContext, ForkName, Hash256, MinimalEthSpec as E, SignedBeaconBlock, Slot, }; const D: Duration = Duration::new(0, 0); @@ -1454,7 +1455,7 @@ impl TestRig { .network_globals .peers .write() - .__add_connected_peer_testing_only(false, &self.harness.spec, key); + .__add_connected_peer_with_custody_subnets(false, &self.harness.spec, key); // Assumes custody subnet count == column count let custody_subnets = self @@ -1485,13 +1486,38 @@ impl TestRig { .network_globals .peers .write() - .__add_connected_peer_testing_only(true, &self.harness.spec, key); + .__add_connected_peer_with_custody_subnets(true, &self.harness.spec, key); self.log(&format!( "Added new peer for testing {peer_id:?}, custody: supernode" )); peer_id } + /// Add a connected supernode peer, but without setting the peers' custody subnet. + /// This is to simulate the real behaviour where metadata is only received some time after + /// a connection is established. + pub fn new_connected_supernode_peer_no_metadata_custody_subnet(&mut self) -> PeerId { + let key = self.determinstic_key(); + self.network_globals + .peers + .write() + .__add_connected_peer(true, key, &self.harness.spec) + } + + /// Update the peer's custody subnet in PeerDB and send a `UpdatedPeerCgc` message to sync. + pub fn send_peer_cgc_update_to_sync( + &mut self, + peer_id: &PeerId, + subnets: HashSet, + ) { + self.network_globals + .peers + .write() + .__set_custody_subnets(peer_id, subnets) + .unwrap(); + self.send_sync_message(SyncMessage::UpdatedPeerCgc(*peer_id)) + } + fn determinstic_key(&mut self) -> CombinedKey { k256::ecdsa::SigningKey::random(&mut self.rng_08).into() } diff --git a/beacon_node/network/src/sync/tests/range.rs b/beacon_node/network/src/sync/tests/range.rs index 891d9d1e97..1499ae5016 100644 --- a/beacon_node/network/src/sync/tests/range.rs +++ b/beacon_node/network/src/sync/tests/range.rs @@ -27,6 +27,7 @@ use crate::sync::range_sync::RangeSyncType; use lighthouse_network::rpc::RPCError; use lighthouse_network::rpc::methods::StatusMessageV2; use lighthouse_network::{PeerId, SyncInfo}; +use std::collections::HashSet; use types::{Epoch, EthSpec, Hash256, MinimalEthSpec as E, Slot}; /// MinimalEthSpec has 8 slots per epoch @@ -50,7 +51,7 @@ impl TestRig { finalized_root: Hash256::random(), head_slot: finalized_epoch.start_slot(E::slots_per_epoch()), head_root: Hash256::random(), - earliest_available_slot: None, + earliest_available_slot: Some(Slot::new(0)), } } @@ -476,3 +477,76 @@ async fn not_enough_custody_peers_then_peers_arrive() { r.simulate(SimulateConfig::happy_path()).await; r.assert_range_sync_completed(); } + +/// This is a regression test for the following race condition scenario: +/// 1. A node is connected to 3 supernode peers: peer 1 is synced, & peer 2 and 3 are advanced. +/// 2. No metadata has been received yet (i.e. no custody info), so the node cannot start data +/// column range sync yet. +/// 3. Now peer 1 sends the CGC via metadata response, we now have one peer on all custody subnets, +/// BUT not on the finalized syncing chain. +/// 4. The node tries to `send_batch` but fails repeatedly with `NoPeers`, as there's no peer +/// that is able to serve columns for the advanced epochs. The chain is removed after 5 failed attempts. +/// 5. Now peer 2 & 3 send CGC updates, BUT because there's no syncing chain, nothing happens - +/// sync is stuck until finding new peers. +/// +/// The expected behaviour in this scenario should be: +/// 4. not finding suitable peers, chain is kept and batch remains in AwaitingDownload +/// 5. finalized sync should resume as soon as CGC updates are received from peer 2 or 3. +#[tokio::test] +async fn finalized_sync_not_enough_custody_peers_resume_after_peer_cgc_update() { + let mut r = TestRig::default(); + if !r.fork_name.fulu_enabled() { + return; + } + + // GIVEN: the node is connected to 3 supernode peers: + let advanced_epochs: usize = 2; + let sync_epochs = advanced_epochs + 3; + let sync_slots = sync_epochs * SLOTS_PER_EPOCH - 1; + r.build_chain(sync_slots).await; + r.harness.set_current_slot(Slot::new(sync_slots as u64 + 1)); + + // Peer 1 is synced (same finalized epoch), but its earliest available slot means it + // cannot serve the batches needed for this sync. + let peer_1 = r.new_connected_supernode_peer_no_metadata_custody_subnet(); + let mut remote_info = r.local_info().clone(); + remote_info.earliest_available_slot = Some(Slot::new(sync_slots as u64)); + r.send_sync_message(SyncMessage::AddPeer(peer_1, remote_info)); + + // Peer 2 is advanced (local finalized epoch + 2) + let peer_2 = r.new_connected_supernode_peer_no_metadata_custody_subnet(); + let remote_info = r.finalized_remote_info_advanced_by((advanced_epochs as u64).into()); + r.send_sync_message(SyncMessage::AddPeer(peer_2, remote_info.clone())); + // We expect a finalized chain to be created with peer 2, but no requests sent out yet due to missing custody info. + r.assert_state(RangeSyncType::Finalized); + r.assert_empty_network(); + + // Peer 3 is connected and advanced + let peer_3 = r.new_connected_supernode_peer_no_metadata_custody_subnet(); + r.send_sync_message(SyncMessage::AddPeer(peer_3, remote_info)); + // We are still in finalized sync state (now with peer 3 added) + r.assert_state(RangeSyncType::Finalized); + + for (i, p) in [peer_1, peer_2, peer_3].iter().enumerate() { + let peer_idx = i + 1; + r.log(&format!("Peer {peer_idx}: {p:?}")); + } + + // WHEN: peer 1 sends its CGC via metadata response + let all_custody_subnets = (0..r.harness.spec.data_column_sidecar_subnet_count) + .map(|i| i.into()) + .collect::>(); + r.send_peer_cgc_update_to_sync(&peer_1, all_custody_subnets.clone()); + + // We still don't have any peers on the syncing chain with custody columns (only peer 1) + // The node won't send the batch and will remain in the finalized sync state (this was failing before!) + r.assert_state(RangeSyncType::Finalized); + r.assert_empty_network(); + + // Now we receive peer 2 & 3's CGC updates, the node will resume syncing from these two peers + r.send_peer_cgc_update_to_sync(&peer_2, all_custody_subnets.clone()); + r.send_peer_cgc_update_to_sync(&peer_3, all_custody_subnets); + + r.simulate(SimulateConfig::happy_path()).await; + r.assert_range_sync_completed(); +} From d98de9f8dd757ddde73bf1e48f55d362306fc94f Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Thu, 4 Jun 2026 17:53:05 +0200 Subject: [PATCH 12/26] Reject importing Gloas block until parent's payload is imported (#9382) Co-Authored-By: dapplion <35266934+dapplion@users.noreply.github.com> Co-Authored-By: Michael Sproul --- .../beacon_chain/src/block_verification.rs | 63 +++++++------------ beacon_node/beacon_chain/src/lib.rs | 2 +- .../beacon_chain/tests/block_verification.rs | 38 ++++++++++- beacon_node/beacon_chain/tests/store_tests.rs | 8 +++ consensus/fork_choice/src/fork_choice.rs | 43 +++++++++++++ consensus/fork_choice/src/lib.rs | 6 +- 6 files changed, 116 insertions(+), 44 deletions(-) diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 33317cbda7..de592e8dae 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -70,7 +70,7 @@ use bls::{PublicKey, PublicKeyBytes}; use educe::Educe; use eth2::types::{BlockGossip, EventKind}; use execution_layer::PayloadStatus; -pub use fork_choice::{AttestationFromBlock, PayloadVerificationStatus}; +pub use fork_choice::{AttestationFromBlock, ParentImportStatus, PayloadVerificationStatus}; use metrics::TryExt; use parking_lot::RwLockReadGuard; use proto_array::Block as ProtoBlock; @@ -870,7 +870,7 @@ impl GossipVerifiedBlock { let block_epoch = block.slot().epoch(T::EthSpec::slots_per_epoch()); let (parent_block, block) = - verify_parent_block_is_known::(&fork_choice_read_lock, block)?; + verify_parent_block_and_envelope_are_known::(&fork_choice_read_lock, block)?; // [New in Gloas]: Verify bid.parent_block_root matches block.parent_root. if let Ok(bid) = block.message().body().signed_execution_payload_bid() @@ -882,13 +882,6 @@ impl GossipVerifiedBlock { }); } - // TODO(gloas) The following validation can only be completed once fork choice has been implemented: - // The block's parent execution payload (defined by bid.parent_block_hash) has been seen - // (via gossip or non-gossip sources) (a client MAY queue blocks for processing - // once the parent payload is retrieved). If execution_payload verification of block's execution - // payload parent by an execution node is complete, verify the block's execution payload - // parent (defined by bid.parent_block_hash) passes all validation. - drop(fork_choice_read_lock); // Track the number of skip slots between the block and its parent. @@ -1381,32 +1374,23 @@ impl ExecutionPendingBlock { .observe_proposal(block_root, block.message()) .map_err(|e| BlockError::BeaconChainError(Box::new(e.into())))?; - if let Some(parent) = chain + match chain .canonical_head .fork_choice_read_lock() - .get_block(&block.parent_root()) + .get_parent_import_status(block.as_block()) { - // Reject any block where the parent has an invalid payload. It's impossible for a valid - // block to descend from an invalid parent. - if parent.execution_status.is_invalid() { - return Err(BlockError::ParentExecutionPayloadInvalid { + ParentImportStatus::Imported(parent) => { + if parent.execution_status.is_invalid() { + return Err(BlockError::ParentExecutionPayloadInvalid { + parent_root: block.parent_root(), + }); + } + } + ParentImportStatus::UnknownBlock | ParentImportStatus::UnknownPayload => { + return Err(BlockError::ParentUnknown { parent_root: block.parent_root(), }); } - } else { - // Reject any block if its parent is not known to fork choice. - // - // A block that is not in fork choice is either: - // - // - Not yet imported: we should reject this block because we should only import a child - // after its parent has been fully imported. - // - Pre-finalized: if the parent block is _prior_ to finalization, we should ignore it - // because it will revert finalization. Note that the finalized block is stored in fork - // choice, so we will not reject any child of the finalized block (this is relevant during - // genesis). - return Err(BlockError::ParentUnknown { - parent_root: block.parent_root(), - }); } /* @@ -1862,19 +1846,20 @@ pub fn get_block_header_root(block_header: &SignedBeaconBlockHeader) -> Hash256 block_root } -/// Verify the parent of `block` is known, returning some information about the parent block from -/// fork choice. +/// Verify the parent block — and, for a post-Gloas FULL child, the parent payload — are known to +/// fork choice; both missing cases return `ParentUnknown`. #[allow(clippy::type_complexity)] -fn verify_parent_block_is_known( +fn verify_parent_block_and_envelope_are_known( fork_choice_read_lock: &RwLockReadGuard>, block: Arc>, ) -> Result<(ProtoBlock, Arc>), BlockError> { - if let Some(proto_block) = fork_choice_read_lock.get_block(&block.parent_root()) { - Ok((proto_block, block)) - } else { - Err(BlockError::ParentUnknown { - parent_root: block.parent_root(), - }) + match fork_choice_read_lock.get_parent_import_status(&block) { + ParentImportStatus::Imported(parent) => Ok((parent, block)), + ParentImportStatus::UnknownBlock | ParentImportStatus::UnknownPayload => { + Err(BlockError::ParentUnknown { + parent_root: block.parent_root(), + }) + } } } @@ -1901,7 +1886,7 @@ fn load_parent>( if !chain .canonical_head .fork_choice_read_lock() - .contains_block(&block.parent_root()) + .is_parent_imported(block.as_block()) { return Err(BlockError::ParentUnknown { parent_root: block.parent_root(), diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index 804268a613..774920fa45 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -85,7 +85,7 @@ pub use beacon_fork_choice_store::{ }; pub use block_verification::{ BlockError, ExecutionPayloadError, ExecutionPendingBlock, GossipVerifiedBlock, - IntoExecutionPendingBlock, IntoGossipVerifiedBlock, InvalidSignature, + IntoExecutionPendingBlock, IntoGossipVerifiedBlock, InvalidSignature, ParentImportStatus, PayloadVerificationOutcome, PayloadVerificationStatus, build_blob_data_column_sidecars, get_block_root, signature_verify_chain_segment, }; diff --git a/beacon_node/beacon_chain/tests/block_verification.rs b/beacon_node/beacon_chain/tests/block_verification.rs index e0c39c350b..deadafac36 100644 --- a/beacon_node/beacon_chain/tests/block_verification.rs +++ b/beacon_node/beacon_chain/tests/block_verification.rs @@ -9,7 +9,7 @@ use beacon_chain::{ custody_context::NodeCustodyType, test_utils::{ AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType, - MakeAttestationOptions, test_spec, + MakeAttestationOptions, fork_name_from_env, test_spec, }, }; use beacon_chain::{ @@ -359,6 +359,10 @@ fn update_data_column_signed_header( #[tokio::test] async fn chain_segment_full_segment() { + // TODO(gloas): re-enable for Gloas once range sync imports payload envelopes. + if fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } let harness = get_harness(VALIDATOR_COUNT, NodeCustodyType::Fullnode); let (chain_segment, chain_segment_blobs) = get_chain_segment().await; store_envelopes_for_chain_segment(&chain_segment, &harness); @@ -399,6 +403,10 @@ async fn chain_segment_full_segment() { #[tokio::test] async fn chain_segment_varying_chunk_size() { + // TODO(gloas): re-enable for Gloas once range sync imports payload envelopes. + if fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } let (chain_segment, chain_segment_blobs) = get_chain_segment().await; let harness = get_harness(VALIDATOR_COUNT, NodeCustodyType::Fullnode); let blocks: Vec> = @@ -679,6 +687,10 @@ async fn get_invalid_sigs_harness( } #[tokio::test] async fn invalid_signature_gossip_block() { + // TODO(gloas): re-enable for Gloas once range sync imports payload envelopes. + if fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } let (chain_segment, chain_segment_blobs) = get_chain_segment().await; for &block_index in BLOCK_INDICES { // Ensure the block will be rejected if imported on its own (without gossip checking). @@ -735,6 +747,10 @@ async fn invalid_signature_gossip_block() { #[tokio::test] async fn invalid_signature_block_proposal() { + // TODO(gloas): re-enable for Gloas once range sync imports payload envelopes. + if fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } let (chain_segment, chain_segment_blobs) = get_chain_segment().await; for &block_index in BLOCK_INDICES { let harness = get_invalid_sigs_harness(&chain_segment).await; @@ -774,6 +790,10 @@ async fn invalid_signature_block_proposal() { #[tokio::test] async fn invalid_signature_randao_reveal() { + // TODO(gloas): re-enable for Gloas once range sync imports payload envelopes. + if fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } let (chain_segment, mut chain_segment_blobs) = get_chain_segment().await; for &block_index in BLOCK_INDICES { let harness = get_invalid_sigs_harness(&chain_segment).await; @@ -802,6 +822,10 @@ async fn invalid_signature_randao_reveal() { #[tokio::test] async fn invalid_signature_proposer_slashing() { + // TODO(gloas): re-enable for Gloas once range sync imports payload envelopes. + if fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } let (chain_segment, mut chain_segment_blobs) = get_chain_segment().await; for &block_index in BLOCK_INDICES { let harness = get_invalid_sigs_harness(&chain_segment).await; @@ -844,6 +868,10 @@ async fn invalid_signature_proposer_slashing() { #[tokio::test] async fn invalid_signature_attester_slashing() { + // TODO(gloas): re-enable for Gloas once range sync imports payload envelopes. + if fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } let (chain_segment, mut chain_segment_blobs) = get_chain_segment().await; for &block_index in BLOCK_INDICES { let harness = get_invalid_sigs_harness(&chain_segment).await; @@ -965,6 +993,10 @@ async fn invalid_signature_attester_slashing() { #[tokio::test] async fn invalid_signature_attestation() { + // TODO(gloas): re-enable for Gloas once range sync imports payload envelopes. + if fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } let (chain_segment, mut chain_segment_blobs) = get_chain_segment().await; let mut checked_attestation = false; @@ -1090,6 +1122,10 @@ async fn invalid_signature_deposit() { #[tokio::test] async fn invalid_signature_exit() { + // TODO(gloas): re-enable for Gloas once range sync imports payload envelopes. + if fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } let (chain_segment, mut chain_segment_blobs) = get_chain_segment().await; for &block_index in BLOCK_INDICES { let harness = get_invalid_sigs_harness(&chain_segment).await; diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index 0ac77dcfaa..b70961c499 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -3148,6 +3148,14 @@ async fn weak_subjectivity_sync_test( .store .put_payload_envelope(&wss_block_root, &envelope) .unwrap(); + + // `from_anchor` doesn't mark the anchor's payload received, so do it here; otherwise the + // first forward block (a FULL child of the anchor) would be rejected with `ParentUnknown`. + beacon_chain + .canonical_head + .fork_choice_write_lock() + .on_valid_payload_envelope_received(wss_block_root) + .unwrap(); } // Apply blocks forward to reach head. diff --git a/consensus/fork_choice/src/fork_choice.rs b/consensus/fork_choice/src/fork_choice.rs index 2de8ce7d81..edced9b246 100644 --- a/consensus/fork_choice/src/fork_choice.rs +++ b/consensus/fork_choice/src/fork_choice.rs @@ -207,6 +207,18 @@ pub enum InvalidPayloadAttestation { }, } +/// The import status of a block's parent, as seen by fork choice. +#[allow(clippy::large_enum_variant)] +pub enum ParentImportStatus { + /// The parent block is imported and the child's bid commits to a parent payload known to fork + /// choice. + Imported(ProtoBlock), + /// The parent block is not known to fork choice. + UnknownBlock, + /// The parent block is known, but the child's bid commits to a payload not known to fork choice. + UnknownPayload, +} + impl From for Error { fn from(e: String) -> Self { Error::ProtoArrayStringError(e) @@ -1537,6 +1549,37 @@ where && self.is_finalized_checkpoint_or_descendant(*block_root) } + /// Returns `true` if the block's parent is imported (and, for a post-Gloas FULL child, its + /// parent's payload is imported too). See [`Self::get_parent_import_status`]. + pub fn is_parent_imported(&self, block: &SignedBeaconBlock) -> bool { + matches!( + self.get_parent_import_status(block), + ParentImportStatus::Imported(_) + ) + } + + /// Returns the import status of the parent of `block`. + /// + /// A post-Gloas FULL child also requires the parent's payload (committed to by the child's bid) + /// to have been received by fork choice. + pub fn get_parent_import_status(&self, block: &SignedBeaconBlock) -> ParentImportStatus { + if let Some(parent_block) = self.get_block(&block.parent_root()) { + let Some(parent_block_hash) = parent_block.execution_payload_block_hash else { + // Pre-Gloas parent: payload is embedded in the block, so treat as imported. + return ParentImportStatus::Imported(parent_block); + }; + if block.is_parent_block_full(parent_block_hash) + && !self.is_payload_received(&block.parent_root()) + { + ParentImportStatus::UnknownPayload + } else { + ParentImportStatus::Imported(parent_block) + } + } else { + ParentImportStatus::UnknownBlock + } + } + /// Called by the proposer to decide whether to build on the full or empty parent. pub fn should_build_on_full( &self, diff --git a/consensus/fork_choice/src/lib.rs b/consensus/fork_choice/src/lib.rs index 159eab0ec0..dcc499547b 100644 --- a/consensus/fork_choice/src/lib.rs +++ b/consensus/fork_choice/src/lib.rs @@ -4,9 +4,9 @@ mod metrics; pub use crate::fork_choice::{ AttestationFromBlock, Error, ForkChoice, ForkChoiceView, ForkchoiceUpdateParameters, - InvalidAttestation, InvalidBlock, InvalidPayloadAttestation, PayloadVerificationStatus, - PersistedForkChoice, PersistedForkChoiceV28, PersistedForkChoiceV29, QueuedAttestation, - ResetPayloadStatuses, + InvalidAttestation, InvalidBlock, InvalidPayloadAttestation, ParentImportStatus, + PayloadVerificationStatus, PersistedForkChoice, PersistedForkChoiceV28, PersistedForkChoiceV29, + QueuedAttestation, ResetPayloadStatuses, }; pub use fork_choice_store::ForkChoiceStore; pub use proto_array::{ From eeae8514b1ddb35980e8b39c1113677c0193cf59 Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Thu, 4 Jun 2026 21:14:32 +0200 Subject: [PATCH 13/26] Remove unused spec field from AvailableBlock (#9411) N/A Remove unused spec field from AvailableBlock Co-Authored-By: dapplion <35266934+dapplion@users.noreply.github.com> --- beacon_node/beacon_chain/src/data_availability_checker.rs | 4 ---- .../src/data_availability_checker/overflow_lru_cache.rs | 4 +--- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index 4dfb476686..9829db0f1d 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -861,8 +861,6 @@ pub struct AvailableBlock { #[educe(Hash(ignore))] /// Timestamp at which this block first became available (UNIX timestamp, time since 1970). blobs_available_timestamp: Option, - #[educe(Hash(ignore))] - pub spec: Arc, } impl AvailableBlock { @@ -952,7 +950,6 @@ impl AvailableBlock { block, blob_data: block_data, blobs_available_timestamp: None, - spec: spec.clone(), }) } @@ -1007,7 +1004,6 @@ impl AvailableBlock { } }, blobs_available_timestamp: self.blobs_available_timestamp, - spec: self.spec.clone(), }) } } 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 3e325cec02..2254728850 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 @@ -200,7 +200,6 @@ impl PendingComponents { /// must be persisted in the DB along with the block. pub fn make_available( &self, - spec: &Arc, num_expected_columns_opt: Option, ) -> Result>, AvailabilityCheckError> { let Some(CachedBlock::Executed(block)) = &self.block else { @@ -271,7 +270,6 @@ impl PendingComponents { block: block.clone(), blob_data, blobs_available_timestamp, - spec: spec.clone(), }; self.span.in_scope(|| { @@ -529,7 +527,7 @@ impl DataAvailabilityCheckerInner { num_expected_columns_opt: Option, ) -> Result, AvailabilityCheckError> { if let Some(available_block) = - pending_components.make_available(&self.spec, num_expected_columns_opt)? + pending_components.make_available(num_expected_columns_opt)? { // Explicitly drop read lock before acquiring write lock drop(pending_components); From da42d37456b556d97fb888ab5a8773457c185751 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Thu, 4 Jun 2026 17:01:20 -0700 Subject: [PATCH 14/26] Ensure PTC votes accurately reflect data availability (#9412) Co-Authored-By: Eitan Seri-Levi --- beacon_node/beacon_chain/src/beacon_chain.rs | 7 +- .../tests/attestation_production.rs | 67 ++++++++++++++++ beacon_node/http_api/tests/tests.rs | 77 +++++++++++++++++++ 3 files changed, 149 insertions(+), 2 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index d826895a25..73f1cd43d3 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -2197,8 +2197,11 @@ impl BeaconChain { 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; + // A payload is only imported into fork choice if its data was available. + let blob_data_available = self + .canonical_head + .fork_choice_read_lock() + .is_payload_received(&beacon_block_root); Ok(PayloadAttestationData { beacon_block_root, diff --git a/beacon_node/beacon_chain/tests/attestation_production.rs b/beacon_node/beacon_chain/tests/attestation_production.rs index 1b87fc041a..862c2a9fe8 100644 --- a/beacon_node/beacon_chain/tests/attestation_production.rs +++ b/beacon_node/beacon_chain/tests/attestation_production.rs @@ -8,6 +8,7 @@ use beacon_chain::test_utils::{ use beacon_chain::validator_monitor::UNAGGREGATED_ATTESTATION_LAG_SLOTS; use beacon_chain::{StateSkipConfig, WhenSlotSkipped, metrics}; use bls::{AggregateSignature, Keypair}; +use slot_clock::SlotClock; use std::sync::{Arc, LazyLock}; use tree_hash::TreeHash; use types::{Attestation, EthSpec, MainnetEthSpec, RelativeEpoch, Slot}; @@ -448,3 +449,69 @@ async fn gloas_attestation_index_payload_absent() { "gloas attestation to prior slot without payload should have index=0 (payload_absent)" ); } + +/// Verify that `produce_payload_attestation_data` reports `payload_present = true` but +/// `blob_data_available = false` when the envelope was observed on but not imported +/// because its data was unavailable. +/// +/// Setup: build a chain through slot 2, then at slot 3 import only the beacon block (no +/// envelope) and mark the envelope as observed on time. +#[tokio::test] +async fn gloas_payload_attestation_seen_but_data_unavailable() { + if fork_name_from_env().is_some_and(|f| !f.gloas_enabled()) { + return; + } + + let harness = BeaconChainHarness::builder(MainnetEthSpec) + .default_spec() + .keypairs(KEYPAIRS[..].to_vec()) + .fresh_ephemeral_store() + .mock_execution_layer() + .build(); + + let chain = &harness.chain; + + harness.advance_slot(); + harness + .extend_chain( + 2, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + // Slot 3: import the beacon block but withhold its envelope. + harness.advance_slot(); + let state = harness.get_current_state(); + let (block_contents, _envelope, _new_state) = + harness.make_block_with_envelope(state, Slot::new(3)).await; + let block_root = block_contents.0.canonical_root(); + harness + .process_block(Slot::new(3), block_root, block_contents) + .await + .expect("block should import without envelope"); + + assert_eq!(chain.head_snapshot().beacon_block.slot(), Slot::new(3)); + + // Mark the envelope as observed at the start of the slot, before its deadline. + let slot_start = chain.slot_clock.start_of(Slot::new(3)).unwrap(); + chain.envelope_times_cache.write().set_time_observed( + block_root, + Slot::new(3), + slot_start, + None, + ); + + let pa_data = chain + .produce_payload_attestation_data(Slot::new(3)) + .expect("should produce payload attestation data"); + + assert!( + pa_data.payload_present, + "envelope observed before the deadline should vote payload_present=true" + ); + assert!( + !pa_data.blob_data_available, + "unimported envelope data should vote blob_data_available=false" + ); +} diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 40cb2e592f..319229d5f1 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -4970,6 +4970,10 @@ impl ApiTester { "payload attestation should report payload_present=true after publishing \ the envelope via the HTTP API (slot {slot})" ); + assert!( + pa_data.blob_data_available, + "blob_data_available should be true once the envelope is imported (slot {slot})" + ); self.chain.slot_clock.set_slot(slot.as_u64() + 1); } @@ -4977,6 +4981,71 @@ impl ApiTester { self } + /// When a payload hasn't been seen, the payload attestation data + /// must report `payload_present = false` and `blob_data_available = false`. + pub async fn test_payload_attestation_unavailable_without_envelope(self) -> Self { + if !self.chain.spec.is_gloas_scheduled() { + return self; + } + + let fork = self.chain.canonical_head.cached_head().head_fork(); + let genesis_validators_root = self.chain.genesis_validators_root; + + for _ in 0..E::slots_per_epoch() * 3 { + let slot = self.chain.slot().unwrap(); + let epoch = self.chain.epoch().unwrap(); + let fork_name = self.chain.spec.fork_name_at_slot::(slot); + + if !fork_name.gloas_enabled() { + self.chain.slot_clock.set_slot(slot.as_u64() + 1); + continue; + } + + let (sk, randao_reveal) = self + .proposer_setup(slot, epoch, &fork, genesis_validators_root) + .await; + + // Produce and publish a block, but withhold its envelope. + let (response, _metadata) = self + .client + .get_validator_blocks_v4::(slot, &randao_reveal, None, None, None, None) + .await + .unwrap(); + let block = response.data; + let block_root = block.tree_hash_root(); + + let signed_block = block.sign(&sk, &fork, genesis_validators_root, &self.chain.spec); + let signed_block_request = + PublishBlockRequest::try_from(Arc::new(signed_block)).unwrap(); + self.client + .post_beacon_blocks_v2(&signed_block_request, None) + .await + .unwrap(); + + let pa_data = self + .client + .get_validator_payload_attestation_data(slot) + .await + .unwrap() + .expect("expected payload attestation data for slot with block") + .into_data(); + + assert_eq!(pa_data.beacon_block_root, block_root); + assert!( + !pa_data.payload_present, + "payload_present should be false when the envelope is withheld (slot {slot})" + ); + assert!( + !pa_data.blob_data_available, + "blob_data_available should be false when the envelope is not imported (slot {slot})" + ); + + return self; + } + + self + } + pub async fn test_get_validator_payload_attestation_data_pre_gloas(self) -> Self { let slot = self.chain.slot().unwrap(); @@ -8703,6 +8772,14 @@ async fn payload_attestation_present_after_envelope_publish() { .await; } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn payload_attestation_unavailable_without_envelope() { + ApiTester::new_with_hard_forks() + .await + .test_payload_attestation_unavailable_without_envelope() + .await; +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn post_beacon_pool_payload_attestations_valid() { if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { From 494b00a3491e2c5e281f6972aa00694b17f16722 Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Fri, 5 Jun 2026 03:24:49 +0200 Subject: [PATCH 15/26] =?UTF-8?q?Fix=20O(n=C2=B2)=20find=5Fhead=20and=20st?= =?UTF-8?q?ack=20overflow=20in=20filter=5Fblock=5Ftree=20(#9090)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: dapplion <35266934+dapplion@users.noreply.github.com> Co-Authored-By: Michael Sproul --- Cargo.lock | 1 + consensus/proto_array/Cargo.toml | 8 + consensus/proto_array/benches/find_head.rs | 118 ++++++++++ consensus/proto_array/src/proto_array.rs | 212 ++++++++++++------ .../src/proto_array_fork_choice.rs | 1 + consensus/proto_array/src/ssz_container.rs | 4 +- 6 files changed, 272 insertions(+), 72 deletions(-) create mode 100644 consensus/proto_array/benches/find_head.rs diff --git a/Cargo.lock b/Cargo.lock index a9fdfe70bd..40db8876cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7020,6 +7020,7 @@ dependencies = [ name = "proto_array" version = "0.2.0" dependencies = [ + "criterion", "ethereum_ssz", "ethereum_ssz_derive", "fixed_bytes", diff --git a/consensus/proto_array/Cargo.toml b/consensus/proto_array/Cargo.toml index ee86277f9c..c424c01f6c 100644 --- a/consensus/proto_array/Cargo.toml +++ b/consensus/proto_array/Cargo.toml @@ -19,3 +19,11 @@ superstruct = { workspace = true } typenum = { workspace = true } types = { workspace = true } yaml_serde = { workspace = true } + +[dev-dependencies] +criterion = { workspace = true } +fixed_bytes = { workspace = true } + +[[bench]] +name = "find_head" +harness = false diff --git a/consensus/proto_array/benches/find_head.rs b/consensus/proto_array/benches/find_head.rs new file mode 100644 index 0000000000..98077a7f97 --- /dev/null +++ b/consensus/proto_array/benches/find_head.rs @@ -0,0 +1,118 @@ +use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main}; +use fixed_bytes::FixedBytesExtended; +use proto_array::{Block, ExecutionStatus, JustifiedBalances, ProtoArrayForkChoice}; +use std::collections::BTreeSet; +use std::time::Duration; +use types::{ + AttestationShufflingId, Checkpoint, Epoch, EthSpec, ExecutionBlockHash, Hash256, + MainnetEthSpec, Slot, +}; + +fn get_root(i: u64) -> Hash256 { + Hash256::from_low_u64_be(i) +} + +fn get_hash(i: u64) -> ExecutionBlockHash { + ExecutionBlockHash::from_root(get_root(i)) +} + +/// Build a linear chain of `num_blocks` blocks. +fn build_chain(num_blocks: u64, gloas: bool) -> (ProtoArrayForkChoice, types::ChainSpec) { + let mut spec = MainnetEthSpec::default_spec(); + let gloas_fork_slot = 32; + if gloas { + spec.gloas_fork_epoch = Some(Epoch::new(1)); + } + + let finalized_checkpoint = Checkpoint { + epoch: Epoch::new(0), + root: get_root(0), + }; + let junk_shuffling_id = AttestationShufflingId::from_components(Epoch::new(0), Hash256::zero()); + + let mut fork_choice = ProtoArrayForkChoice::new::( + Slot::new(0), + Slot::new(0), + Hash256::zero(), + finalized_checkpoint, + finalized_checkpoint, + junk_shuffling_id.clone(), + junk_shuffling_id.clone(), + ExecutionStatus::Optimistic(ExecutionBlockHash::zero()), + None, + None, + 0, + &spec, + ) + .expect("should create fork choice"); + + for i in 1..=num_blocks { + let is_gloas = gloas && i >= gloas_fork_slot; + let block = Block { + slot: Slot::new(i), + root: get_root(i), + parent_root: Some(get_root(i - 1)), + state_root: Hash256::zero(), + target_root: get_root(0), + current_epoch_shuffling_id: junk_shuffling_id.clone(), + next_epoch_shuffling_id: junk_shuffling_id.clone(), + justified_checkpoint: finalized_checkpoint, + finalized_checkpoint, + execution_status: ExecutionStatus::Optimistic(ExecutionBlockHash::zero()), + unrealized_justified_checkpoint: Some(finalized_checkpoint), + unrealized_finalized_checkpoint: Some(finalized_checkpoint), + execution_payload_parent_hash: if is_gloas { + Some(get_hash(i - 1)) + } else { + None + }, + execution_payload_block_hash: if is_gloas { Some(get_hash(i)) } else { None }, + proposer_index: Some(0), + }; + + fork_choice + .process_block::(block, Slot::new(i), &spec, Duration::ZERO) + .expect("should process block"); + } + + (fork_choice, spec) +} + +fn bench_find_head(c: &mut Criterion) { + let mut group = c.benchmark_group("find_head"); + let equivocating_indices = BTreeSet::new(); + let finalized_checkpoint = Checkpoint { + epoch: Epoch::new(0), + root: get_root(0), + }; + let balances = JustifiedBalances::from_effective_balances(vec![1; 64]).unwrap(); + + // 216k = ~1 month non-finality mainnet, 518k = ~1 month non-finality Gnosis. + // Must survive extended non-finality (500k+ blocks). + for (label, gloas) in [("pre_gloas", false), ("gloas", true)] { + for &num_blocks in &[100, 1_000, 10_000, 50_000, 216_000, 518_000] { + let (mut fork_choice, spec) = build_chain(num_blocks, gloas); + + group.bench_function(BenchmarkId::new(label, num_blocks), |b| { + b.iter(|| { + fork_choice + .find_head::( + finalized_checkpoint, + finalized_checkpoint, + &balances, + Hash256::zero(), + &equivocating_indices, + Slot::new(num_blocks), + &spec, + ) + .expect("should find head") + }); + }); + } + } + + group.finish(); +} + +criterion_group!(benches, bench_find_head); +criterion_main!(benches); diff --git a/consensus/proto_array/src/proto_array.rs b/consensus/proto_array/src/proto_array.rs index 1e3303afbb..bd15bb4599 100644 --- a/consensus/proto_array/src/proto_array.rs +++ b/consensus/proto_array/src/proto_array.rs @@ -391,6 +391,10 @@ pub struct ProtoArray { pub prune_threshold: usize, pub nodes: Vec, pub indices: HashMap, + /// Cached parent→children index. `children[i]` holds the node indices of all children of + /// node `i`. Maintained incrementally by `on_block` and `maybe_prune`. + #[serde(skip)] + pub children: Vec>, } impl ProtoArray { @@ -673,6 +677,16 @@ impl ProtoArray { self.indices.insert(node.root(), node_index); self.nodes.push(node.clone()); + // Maintain cached children index. `parent_index` is already bounds-checked above + // against `self.nodes`, and `self.children` is kept in lockstep with `self.nodes`. + self.children.push(Vec::new()); + if let Some(parent_index) = node.parent() { + self.children + .get_mut(parent_index) + .ok_or(Error::InvalidNodeIndex(parent_index))? + .push(node_index); + } + if let Some(parent_index) = node.parent() && matches!(block.execution_status, ExecutionStatus::Valid(_)) { @@ -1095,6 +1109,22 @@ impl ProtoArray { Ok((best_fc_node.root, best_fc_node.payload_status)) } + /// Rebuild the cached `self.children` index from `self.nodes`. Called once after + /// deserialization to populate the transient field. + pub fn rebuild_children_index(&mut self) -> Result<(), Error> { + let mut children = vec![Vec::new(); self.nodes.len()]; + for (i, node) in self.nodes.iter().enumerate() { + if let Some(parent_idx) = node.parent() { + children + .get_mut(parent_idx) + .ok_or(Error::InvalidNodeIndex(parent_idx))? + .push(i); + } + } + self.children = children; + Ok(()) + } + /// Spec: `get_filtered_block_tree`. /// /// Returns the set of node indices on viable branches — those with at least @@ -1105,7 +1135,7 @@ impl ProtoArray { current_slot: Slot, best_justified_checkpoint: Checkpoint, best_finalized_checkpoint: Checkpoint, - ) -> HashSet { + ) -> Result, Error> { let mut viable = HashSet::new(); self.filter_block_tree::( start_index, @@ -1113,71 +1143,88 @@ impl ProtoArray { best_justified_checkpoint, best_finalized_checkpoint, &mut viable, - ); - viable + )?; + Ok(viable) } /// Spec: `filter_block_tree`. + /// + /// Proto_array stores nodes in insertion order — children always have higher + /// indices than their parents. A single reverse pass therefore processes every + /// child before its parent, matching the spec's recursive post-order semantics + /// without recursion (required to survive 500k+ blocks of non-finality). + /// + /// The spec removes execution-invalid blocks (and their entire subtrees) from + /// `store.blocks` before running. We replicate that here with a forward pass + /// propagating `excluded` from parent to child — V29 children of an invalidated + /// V17 ancestor are excluded transitively, since V29 nodes carry no + /// `execution_status` of their own. fn filter_block_tree( &self, - node_index: usize, + start_index: usize, current_slot: Slot, best_justified_checkpoint: Checkpoint, best_finalized_checkpoint: Checkpoint, viable: &mut HashSet, - ) -> bool { - let Some(node) = self.nodes.get(node_index) else { - return false; - }; + ) -> Result<(), Error> { + // Forward pass: a node is "excluded" if it (or any ancestor down to + // `start_index`) has an invalid execution status. + let mut excluded = vec![false; self.nodes.len()]; + for i in (start_index + 1)..self.nodes.len() { + let node = self.nodes.get(i).ok_or(Error::InvalidNodeIndex(i))?; + let parent_excluded = match node.parent() { + Some(p) => *excluded.get(p).ok_or(Error::InvalidNodeIndex(p))?, + None => false, + }; + let self_invalid = node.execution_status().is_ok_and(|s| s.is_invalid()); + excluded[i] = parent_excluded || self_invalid; + } - // Skip invalid children — they aren't in store.blocks in the spec. - let children: Vec = self - .nodes - .iter() - .enumerate() - .filter(|(_, child)| { - child.parent() == Some(node_index) - && !child - .execution_status() - .is_ok_and(|status| status.is_invalid()) - }) - .map(|(i, _)| i) - .collect(); - - if !children.is_empty() { - // Evaluate ALL children (no short-circuit) to mark all viable branches. - let any_viable = children - .iter() - .map(|&child_index| { - self.filter_block_tree::( - child_index, - current_slot, - best_justified_checkpoint, - best_finalized_checkpoint, - viable, - ) - }) - .collect::>() - .into_iter() - .any(|v| v); - if any_viable { - viable.insert(node_index); - return true; + for node_index in (start_index..self.nodes.len()).rev() { + // Spec: invalid subtree removed from `store.blocks` — skip entirely. + if *excluded + .get(node_index) + .ok_or(Error::InvalidNodeIndex(node_index))? + { + continue; } - return false; - } + let node = self + .nodes + .get(node_index) + .ok_or(Error::InvalidNodeIndex(node_index))?; - // Leaf node: check viability. - if self.node_is_viable_for_head::( - node, - current_slot, - best_justified_checkpoint, - best_finalized_checkpoint, - ) { - viable.insert(node_index); - return true; + // Spec: children = [root for root in blocks if blocks[root].parent_root == block_root] + let valid_children: Vec = self + .children + .get(node_index) + .ok_or(Error::InvalidNodeIndex(node_index))? + .iter() + .copied() + .filter_map(|i| match excluded.get(i) { + Some(false) => Some(Ok(i)), + Some(true) => None, + None => Some(Err(Error::InvalidNodeIndex(i))), + }) + .collect::>()?; + + if !valid_children.is_empty() { + // Spec: if any(children): if any(filter_block_tree_result): blocks[block_root] = block + if valid_children.iter().any(|c| viable.contains(c)) { + viable.insert(node_index); + } + } else { + // Spec: leaf — check correct_justified and correct_finalized + if self.node_is_viable_for_head::( + node, + current_slot, + best_justified_checkpoint, + best_finalized_checkpoint, + ) { + viable.insert(node_index); + } + } } - false + Ok(()) } /// Spec: `get_head`. @@ -1204,7 +1251,7 @@ impl ProtoArray { current_slot, best_justified_checkpoint, best_finalized_checkpoint, - ); + )?; // Compute once rather than per-child per-level. let apply_proposer_boost = @@ -1468,25 +1515,35 @@ impl ProtoArray { } Ok(children) } else { - Ok(self - .nodes + // Spec: [root for root in blocks.keys() if blocks[root].parent_root == node.root ...] + // (cached `self.children[i]` is the same set as the spec's filtered scan). + let indices = self + .children + .get(node.proto_node_index) + .ok_or(Error::InvalidNodeIndex(node.proto_node_index))?; + indices .iter() - .enumerate() - .filter(|(_, child_node)| { - child_node.parent() == Some(node.proto_node_index) - && child_node.get_parent_payload_status() == node.payload_status + .copied() + .filter_map(|i| { + self.nodes + .get(i) + .ok_or(Error::InvalidNodeIndex(i)) + .map(|child| { + // Spec: node.payload_status == get_parent_payload_status(store, blocks[root]) + (child.get_parent_payload_status() == node.payload_status).then(|| { + ( + IndexedForkChoiceNode { + root: child.root(), + proto_node_index: i, + payload_status: PayloadStatus::Pending, + }, + child.clone(), + ) + }) + }) + .transpose() }) - .map(|(child_index, child_node)| { - ( - IndexedForkChoiceNode { - root: child_node.root(), - proto_node_index: child_index, - payload_status: PayloadStatus::Pending, - }, - child_node.clone(), - ) - }) - .collect()) + .collect() } } @@ -1617,6 +1674,19 @@ impl ProtoArray { // Drop all the nodes prior to finalization. self.nodes = self.nodes.split_off(finalized_index); + // Drop pruned entries from children index and shift all remaining indices down. + // Invariant: child_index > parent_index, and all parents we kept have + // index >= finalized_index, so every remaining child_index is also + // >= finalized_index. + self.children = self.children.split_off(finalized_index); + for children in self.children.iter_mut() { + for child_index in children.iter_mut() { + *child_index = child_index + .checked_sub(finalized_index) + .ok_or(Error::IndexOverflow("children"))?; + } + } + // Adjust the indices map. for (_root, index) in self.indices.iter_mut() { *index = index diff --git a/consensus/proto_array/src/proto_array_fork_choice.rs b/consensus/proto_array/src/proto_array_fork_choice.rs index 96d2302266..2c1195b491 100644 --- a/consensus/proto_array/src/proto_array_fork_choice.rs +++ b/consensus/proto_array/src/proto_array_fork_choice.rs @@ -514,6 +514,7 @@ impl ProtoArrayForkChoice { prune_threshold: DEFAULT_PRUNE_THRESHOLD, nodes: Vec::with_capacity(1), indices: HashMap::with_capacity(1), + children: Vec::with_capacity(1), }; let block = Block { diff --git a/consensus/proto_array/src/ssz_container.rs b/consensus/proto_array/src/ssz_container.rs index 69efb35027..ec70e88a73 100644 --- a/consensus/proto_array/src/ssz_container.rs +++ b/consensus/proto_array/src/ssz_container.rs @@ -59,11 +59,13 @@ impl TryFrom<(SszContainerV29, JustifiedBalances)> for ProtoArrayForkChoice { type Error = Error; fn try_from((from, balances): (SszContainerV29, JustifiedBalances)) -> Result { - let proto_array = ProtoArray { + let mut proto_array = ProtoArray { prune_threshold: from.prune_threshold, nodes: from.nodes, indices: from.indices.into_iter().collect::>(), + children: Vec::new(), }; + proto_array.rebuild_children_index()?; Ok(Self { proto_array, From 6698872f8a4748fd7bbe936d06ddab1cd63484fe Mon Sep 17 00:00:00 2001 From: Pawan Dhananjay Date: Fri, 5 Jun 2026 19:27:28 +0530 Subject: [PATCH 16/26] Improve error types for envelopes (#9414) N/A Currently, we have `EnvelopeError` having a `ImportError` wrapping a `BlockError`. I feel this is extremely unintuitive because most of the envelope processing functions can simply return an `EnvelopeError` that makes sense in the function's context. It revealed further ugliness when implementing range sync in #9362 This PR does 2 main things: 1. Removes `ImportError(BlockError)` variant 2. Adds `EnvelopeError(EnvelopeError)` variant to a `BlockError`. I feel this is more natural as there can be envelope errors when we try importing a Block but envelope errors can be contained to just envelope related errors. The main blocker to doing this was `PayloadVerificationHandle` returning a `BlockError`. It uses a very small subset of `BlockError` which I extracted to its own error type which can be converted into both a BlockError and EnvelopeError. This allows us to keep most of the pure envelope processing functions to just return EnvelopeErrors while we convert it to a `BlockError` only in import paths where we need to return a consolidated `BlockError`. Co-Authored-By: Pawan Dhananjay --- beacon_node/beacon_chain/src/beacon_chain.rs | 1 + .../beacon_chain/src/block_verification.rs | 79 ++++++++++++++++--- .../beacon_chain/src/execution_payload.rs | 8 +- beacon_node/beacon_chain/src/lib.rs | 4 +- .../payload_envelope_verification/import.rs | 52 +++++------- .../src/payload_envelope_verification/mod.rs | 37 +++++++-- .../payload_notifier.rs | 13 +-- .../src/beacon/execution_payload_envelope.rs | 2 +- .../gossip_methods.rs | 17 +++- .../network_beacon_processor/sync_methods.rs | 8 +- 10 files changed, 151 insertions(+), 70 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 73f1cd43d3..dc786fb7fb 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -4104,6 +4104,7 @@ impl BeaconChain { publish_fn()?; self.import_available_execution_payload_envelope(available_envelope) .await + .map_err(Into::into) } PayloadAvailability::MissingComponents(block_root) => Ok( AvailabilityProcessingStatus::MissingComponents(slot, block_root), diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index de592e8dae..49ab4a06d2 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -59,6 +59,7 @@ use crate::execution_payload::{ }; use crate::kzg_utils::blobs_to_data_column_sidecars; use crate::observed_block_producers::SeenBlock; +use crate::payload_envelope_verification::EnvelopeError; use crate::validator_monitor::HISTORIC_EPOCHS as VALIDATOR_MONITOR_HISTORIC_EPOCHS; use crate::validator_pubkey_cache::ValidatorPubkeyCache; use crate::{ @@ -122,7 +123,9 @@ pub enum BlockError { /// /// It's unclear if this block is valid, but it cannot be processed without already knowing /// its parent. - ParentUnknown { parent_root: Hash256 }, + ParentUnknown { + parent_root: Hash256, + }, /// The block slot is greater than the present slot. /// /// ## Peer scoring @@ -137,7 +140,10 @@ pub enum BlockError { /// ## Peer scoring /// /// The peer has incompatible state transition logic and is faulty. - StateRootMismatch { block: Hash256, local: Hash256 }, + StateRootMismatch { + block: Hash256, + local: Hash256, + }, /// The block was a genesis block, these blocks cannot be re-imported. GenesisBlock, /// The slot is finalized, no need to import. @@ -156,7 +162,9 @@ pub enum BlockError { /// /// It's unclear if this block is valid, but it conflicts with finality and shouldn't be /// imported. - NotFinalizedDescendant { block_parent_root: Hash256 }, + NotFinalizedDescendant { + block_parent_root: Hash256, + }, /// Block is already known and valid, no need to re-import. /// /// ## Peer scoring @@ -183,7 +191,10 @@ pub enum BlockError { /// ## Peer scoring /// /// The block is invalid and the peer is faulty. - IncorrectBlockProposer { block: u64, local_shuffling: u64 }, + IncorrectBlockProposer { + block: u64, + local_shuffling: u64, + }, /// The `block.proposal_index` is not known. /// /// ## Peer scoring @@ -201,7 +212,10 @@ pub enum BlockError { /// ## Peer scoring /// /// The block is invalid and the peer is faulty. - BlockIsNotLaterThanParent { block_slot: Slot, parent_slot: Slot }, + BlockIsNotLaterThanParent { + block_slot: Slot, + parent_slot: Slot, + }, /// At least one block in the chain segment did not have it's parent root set to the root of /// the prior block. /// @@ -257,7 +271,9 @@ pub enum BlockError { /// If it's actually our fault (e.g. our execution node database is corrupt) we have bigger /// problems to worry about than losing peers, and we're doing the network a favour by /// disconnecting. - ParentExecutionPayloadInvalid { parent_root: Hash256 }, + ParentExecutionPayloadInvalid { + parent_root: Hash256, + }, /// This is a known invalid block that was listed in Lighthouses configuration. /// At the moment this error is only relevant as part of the Holesky network recovery efforts. KnownInvalidExecutionPayload(Hash256), @@ -285,10 +301,6 @@ pub enum BlockError { /// TODO: We may need to penalize the peer that gave us a potentially invalid rpc blob. /// https://github.com/sigp/lighthouse/issues/4546 AvailabilityCheck(AvailabilityCheckError), - /// The payload envelope's block root is unknown. - EnvelopeBlockRootUnknown(Hash256), - /// Optimistic sync is not supported for Gloas payload envelopes. - OptimisticSyncNotSupported { block_root: Hash256 }, /// An internal error has occurred when processing the block or sidecars. /// /// ## Peer scoring @@ -315,6 +327,7 @@ pub enum BlockError { bid_parent_root: Hash256, block_parent_root: Hash256, }, + EnvelopeError(Box), } /// Which specific signature(s) are invalid in a SignedBeaconBlock @@ -487,6 +500,50 @@ pub struct PayloadVerificationOutcome { pub payload_verification_status: PayloadVerificationStatus, } +/// The set of errors that can occur while notifying the execution layer of a new payload. +/// +/// This is deliberately narrow: notifying the EL can only fail in these two ways. The type is +/// shared by both the pre-Gloas block import path and the Gloas payload envelope path so that +/// neither pipeline has to borrow the other's error enum. It converts cleanly into both +/// [`BlockError`] and [`EnvelopeError`](crate::payload_envelope_verification::EnvelopeError) at the +/// point where the verification handle is consumed. +#[derive(Debug)] +pub enum PayloadVerificationError { + /// The execution payload was rejected by, or could not be sent to, the execution engine. + ExecutionPayloadError(ExecutionPayloadError), + /// An internal error occurred while notifying the execution layer. + BeaconChainError(Box), +} + +impl From for PayloadVerificationError { + fn from(e: ExecutionPayloadError) -> Self { + PayloadVerificationError::ExecutionPayloadError(e) + } +} + +impl From for PayloadVerificationError { + fn from(e: BeaconChainError) -> Self { + PayloadVerificationError::BeaconChainError(Box::new(e)) + } +} + +impl From for PayloadVerificationError { + fn from(e: BeaconStateError) -> Self { + PayloadVerificationError::BeaconChainError(Box::new(BeaconChainError::BeaconStateError(e))) + } +} + +impl From for BlockError { + fn from(e: PayloadVerificationError) -> Self { + match e { + PayloadVerificationError::ExecutionPayloadError(e) => { + BlockError::ExecutionPayloadError(e) + } + PayloadVerificationError::BeaconChainError(e) => BlockError::BeaconChainError(e), + } + } +} + /// Information about invalid blocks which might still be slashable despite being invalid. #[allow(clippy::enum_variant_names)] pub enum BlockSlashInfo { @@ -657,7 +714,7 @@ pub struct SignatureVerifiedBlock { /// Used to await the result of executing payload with an EE. pub type PayloadVerificationHandle = - JoinHandle>>; + JoinHandle>>; /// A wrapper around a `SignedBeaconBlock` that indicates that this block is fully verified and /// ready to import into the `BeaconChain`. The validation includes: diff --git a/beacon_node/beacon_chain/src/execution_payload.rs b/beacon_node/beacon_chain/src/execution_payload.rs index c8976fc6a8..d8cd3e0287 100644 --- a/beacon_node/beacon_chain/src/execution_payload.rs +++ b/beacon_node/beacon_chain/src/execution_payload.rs @@ -9,7 +9,7 @@ use crate::{ BeaconChain, BeaconChainError, BeaconChainTypes, BlockError, BlockProductionError, - ExecutionPayloadError, + ExecutionPayloadError, PayloadVerificationError, }; use execution_layer::{ BlockProposalContentsType, BuilderParams, NewPayloadRequest, PayloadAttributes, @@ -104,7 +104,9 @@ impl PayloadNotifier { }) } - pub async fn notify_new_payload(self) -> Result { + pub async fn notify_new_payload( + self, + ) -> Result { if let Some(precomputed_status) = self.payload_verification_status { Ok(precomputed_status) } else { @@ -133,7 +135,7 @@ pub async fn notify_new_payload( slot: Slot, parent_beacon_block_root: Hash256, new_payload_request: NewPayloadRequest<'_, T::EthSpec>, -) -> Result { +) -> Result { let execution_layer = chain .execution_layer .as_ref() diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index 774920fa45..9795d360ca 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -86,8 +86,8 @@ pub use beacon_fork_choice_store::{ pub use block_verification::{ BlockError, ExecutionPayloadError, ExecutionPendingBlock, GossipVerifiedBlock, IntoExecutionPendingBlock, IntoGossipVerifiedBlock, InvalidSignature, ParentImportStatus, - PayloadVerificationOutcome, PayloadVerificationStatus, build_blob_data_column_sidecars, - get_block_root, signature_verify_chain_segment, + PayloadVerificationError, PayloadVerificationOutcome, PayloadVerificationStatus, + build_blob_data_column_sidecars, get_block_root, signature_verify_chain_segment, }; pub use block_verification_types::AvailabilityPendingExecutedBlock; pub use block_verification_types::ExecutedBlock; diff --git a/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs b/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs index 73ddb43273..a10372b933 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs @@ -33,6 +33,13 @@ impl BeaconChain { /// /// Returns an `Err` if the given payload envelope was invalid, or an error was encountered during /// verification. + /// + /// Note: Returns a `BlockError` even though its an envelope processing function. + /// The reason is that this function actually imports the envelope in `check_envelope_availability_and_import` + /// which is coupled tightly with the block and data column import functions. + /// These functions return one error type for consistency across function signatures. + /// In the future, we could make the import error types more generic and then + /// this function could return an `EnvelopeError` as well. #[instrument(skip_all, fields(block_root = ?block_root, envelope_source = %envelope_source))] pub async fn process_execution_payload_envelope( self: &Arc, @@ -41,7 +48,7 @@ impl BeaconChain { notify_execution_layer: NotifyExecutionLayer, envelope_source: BlockImportSource, publish_fn: impl FnOnce() -> Result<(), EnvelopeError>, - ) -> Result { + ) -> Result { let block_slot = unverified_envelope.signed_envelope.slot(); // Set observed time if not already set. Usually this should be set by gossip or RPC, @@ -83,13 +90,7 @@ impl BeaconChain { // about what the function actually does. let executed_envelope = chain .into_executed_payload_envelope(execution_pending) - .await - .map_err(|error| match error { - BlockError::ExecutionPayloadError(error) => { - EnvelopeError::ExecutionPayloadError(error) - } - error => EnvelopeError::ImportError(error), - })?; + .await?; // Record the time it took to wait for execution layer verification. if let Some(timestamp) = slot_clock.now_duration() { @@ -100,7 +101,6 @@ impl BeaconChain { self.check_envelope_availability_and_import(executed_envelope) .await - .map_err(EnvelopeError::ImportError) }; // Verify and import the payload envelope. @@ -128,28 +128,12 @@ impl BeaconChain { Ok(status) } - Err(EnvelopeError::BeaconChainError(e)) => { - if matches!(e.as_ref(), BeaconChainError::TokioJoin(_)) { - debug!(error = ?e, "Envelope processing cancelled"); - } else { - warn!(error = ?e, "Execution payload envelope rejected"); - } - Err(EnvelopeError::BeaconChainError(e)) - } - Err(EnvelopeError::ImportError(BlockError::BeaconChainError(e))) => { - if matches!(e.as_ref(), BeaconChainError::TokioJoin(_)) { - debug!(error = ?e, "Envelope processing cancelled"); - } else { - warn!(error = ?e, "Execution payload envelope rejected"); - } - Err(EnvelopeError::ImportError(BlockError::BeaconChainError(e))) - } - Err(other) => { + Err(err) => { warn!( - reason = other.to_string(), + reason = err.to_string(), "Execution payload envelope rejected" ); - Err(other) + Err(err) } } } @@ -175,7 +159,7 @@ impl BeaconChain { async fn into_executed_payload_envelope( self: Arc, pending_envelope: ExecutionPendingEnvelope, - ) -> Result, BlockError> { + ) -> Result, EnvelopeError> { let ExecutionPendingEnvelope { signed_envelope, block_root, @@ -192,7 +176,7 @@ impl BeaconChain { .payload_verification_status .is_optimistic() { - return Err(BlockError::OptimisticSyncNotSupported { block_root }); + return Err(EnvelopeError::OptimisticSyncNotSupported { block_root }); } Ok(AvailabilityPendingExecutedEnvelope::new( @@ -206,7 +190,7 @@ impl BeaconChain { pub async fn import_available_execution_payload_envelope( self: &Arc, envelope: Box>, - ) -> Result { + ) -> Result { let AvailableExecutedEnvelope { envelope, block_root, @@ -243,13 +227,13 @@ impl BeaconChain { signed_envelope: AvailableEnvelope, block_root: Hash256, payload_verification_status: PayloadVerificationStatus, - ) -> Result { + ) -> Result { // Everything in this initial section is on the hot path for processing the envelope. // Take an upgradable read lock on fork choice so we can check if this block has already // been imported. We don't want to repeat work importing a block that is already imported. let fork_choice_reader = self.canonical_head.fork_choice_upgradable_read_lock(); if !fork_choice_reader.contains_block(&block_root) { - return Err(BlockError::EnvelopeBlockRootUnknown(block_root)); + return Err(EnvelopeError::BlockRootNotInForkChoice(block_root)); } // TODO(gloas) add defensive check to see if payload envelope is already in fork choice @@ -264,7 +248,7 @@ impl BeaconChain { // node which can be eligible for head. fork_choice .on_valid_payload_envelope_received(block_root) - .map_err(|e| BlockError::InternalError(format!("{e:?}")))?; + .map_err(|e| EnvelopeError::InternalError(format!("{e:?}")))?; // TODO(gloas) emit SSE event if the payload became the new head payload diff --git a/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs b/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs index a1e4e34eb6..a1cbac35b3 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs @@ -30,7 +30,7 @@ use types::{ use crate::{ BeaconChainError, BeaconChainTypes, BeaconStore, BlockError, ExecutionPayloadError, - PayloadVerificationOutcome, + PayloadVerificationError, PayloadVerificationOutcome, }; pub mod execution_pending_envelope; @@ -155,15 +155,23 @@ pub enum EnvelopeError { latest_finalized_slot: Slot, }, /// Some Beacon Chain Error - BeaconChainError(Arc), + BeaconChainError(Box), /// Some Beacon State error BeaconStateError(BeaconStateError), /// Some EnvelopeProcessingError EnvelopeProcessingError(EnvelopeProcessingError), /// Error verifying the execution payload ExecutionPayloadError(ExecutionPayloadError), - /// An error from importing the envelope. - ImportError(BlockError), + /// Optimistic sync is not supported for Gloas payload envelopes. + OptimisticSyncNotSupported { block_root: Hash256 }, + /// The envelope's beacon block was not present in fork choice at import time. + /// + /// Unlike [`EnvelopeError::BlockRootUnknown`] (raised during gossip verification, where the + /// block may simply not have arrived yet), this is raised during import where the block is + /// expected to already be present, so it indicates an internal inconsistency. + BlockRootNotInForkChoice(Hash256), + /// An internal error occurred while importing the envelope (e.g. updating fork choice). + InternalError(String), } impl std::fmt::Display for EnvelopeError { @@ -174,7 +182,7 @@ impl std::fmt::Display for EnvelopeError { impl From for EnvelopeError { fn from(e: BeaconChainError) -> Self { - EnvelopeError::BeaconChainError(Arc::new(e)) + EnvelopeError::BeaconChainError(Box::new(e)) } } @@ -192,7 +200,24 @@ impl From for EnvelopeError { impl From for EnvelopeError { fn from(e: DBError) -> Self { - EnvelopeError::BeaconChainError(Arc::new(BeaconChainError::DBError(e))) + EnvelopeError::BeaconChainError(Box::new(BeaconChainError::DBError(e))) + } +} + +impl From for BlockError { + fn from(e: EnvelopeError) -> Self { + BlockError::EnvelopeError(Box::new(e)) + } +} + +impl From for EnvelopeError { + fn from(e: PayloadVerificationError) -> Self { + match e { + PayloadVerificationError::ExecutionPayloadError(e) => { + EnvelopeError::ExecutionPayloadError(e) + } + PayloadVerificationError::BeaconChainError(e) => EnvelopeError::BeaconChainError(e), + } } } diff --git a/beacon_node/beacon_chain/src/payload_envelope_verification/payload_notifier.rs b/beacon_node/beacon_chain/src/payload_envelope_verification/payload_notifier.rs index 0bbe32525a..8a47e4689a 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/payload_notifier.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/payload_notifier.rs @@ -7,7 +7,7 @@ use tracing::warn; use types::{SignedBeaconBlock, SignedExecutionPayloadEnvelope}; use crate::{ - BeaconChain, BeaconChainTypes, BlockError, NotifyExecutionLayer, + BeaconChain, BeaconChainTypes, NotifyExecutionLayer, PayloadVerificationError, execution_payload::notify_new_payload, payload_envelope_verification::EnvelopeError, }; @@ -31,8 +31,7 @@ impl PayloadNotifier { match notify_execution_layer { NotifyExecutionLayer::No if chain.config.optimistic_finalized_sync => { - let new_payload_request = Self::build_new_payload_request(&envelope, &block) - .map_err(EnvelopeError::ImportError)?; + let new_payload_request = Self::build_new_payload_request(&envelope, &block)?; // TODO(gloas): check and test RLP block hash calculation post-Gloas if let Err(e) = new_payload_request.perform_optimistic_sync_verifications() { warn!( @@ -58,7 +57,9 @@ impl PayloadNotifier { }) } - pub async fn notify_new_payload(self) -> Result { + pub async fn notify_new_payload( + self, + ) -> Result { if let Some(precomputed_status) = self.payload_verification_status { Ok(precomputed_status) } else { @@ -71,12 +72,12 @@ impl PayloadNotifier { fn build_new_payload_request<'a>( envelope: &'a SignedExecutionPayloadEnvelope, block: &'a SignedBeaconBlock, - ) -> Result, BlockError> { + ) -> Result, PayloadVerificationError> { let bid = &block .message() .body() .signed_execution_payload_bid() - .map_err(|e| BlockError::BeaconChainError(Box::new(e.into())))? + .map_err(|e| PayloadVerificationError::BeaconChainError(Box::new(e.into())))? .message; let versioned_hashes = bid diff --git a/beacon_node/http_api/src/beacon/execution_payload_envelope.rs b/beacon_node/http_api/src/beacon/execution_payload_envelope.rs index d8813b0db5..d12e4371a4 100644 --- a/beacon_node/http_api/src/beacon/execution_payload_envelope.rs +++ b/beacon_node/http_api/src/beacon/execution_payload_envelope.rs @@ -144,7 +144,7 @@ pub async fn publish_execution_payload_envelope( PubsubMessage::ExecutionPayload(Box::new(envelope_for_gossip)), ) .map_err(|_| { - EnvelopeError::BeaconChainError(Arc::new( + EnvelopeError::BeaconChainError(Box::new( beacon_chain::BeaconChainError::UnableToPublish, )) }) diff --git a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs index 9becfd4d59..17e569206b 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -1654,9 +1654,14 @@ impl NetworkBeaconProcessor { crit!(error = %e, "Internal block gossip validation error. Availability check during gossip validation"); return None; } - Err(e @ BlockError::InternalError(_)) - | Err(e @ BlockError::EnvelopeBlockRootUnknown(_)) - | Err(e @ BlockError::OptimisticSyncNotSupported { .. }) => { + // This error variant cannot be reached when doing gossip block validation: a block has + // no envelope to verify, and `BlockError::EnvelopeError` is only ever produced by the + // envelope import pipeline. + Err(e @ BlockError::EnvelopeError(_)) => { + crit!(error = %e, "Internal block gossip validation error. Envelope error during gossip validation"); + return None; + } + Err(e @ BlockError::InternalError(_)) => { error!(error = %e, "Internal block gossip validation error"); return None; } @@ -3748,7 +3753,11 @@ impl NetworkBeaconProcessor { EnvelopeError::PriorToFinalization { .. } | EnvelopeError::BeaconChainError(_) | EnvelopeError::BeaconStateError(_) - | EnvelopeError::ImportError(_) => { + // The following variants are produced during envelope import, not gossip + // verification, so they cannot be reached here. Ignore them to be safe. + | EnvelopeError::OptimisticSyncNotSupported { .. } + | EnvelopeError::BlockRootNotInForkChoice(_) + | EnvelopeError::InternalError(_) => { self.propagate_validation_result( message_id, peer_id, diff --git a/beacon_node/network/src/network_beacon_processor/sync_methods.rs b/beacon_node/network/src/network_beacon_processor/sync_methods.rs index 87d11946bd..8245b5dc0c 100644 --- a/beacon_node/network/src/network_beacon_processor/sync_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/sync_methods.rs @@ -367,7 +367,7 @@ impl NetworkBeaconProcessor { ) .await } - Err(e) => Err(e), + Err(e) => Err(e.into()), }; // TODO(gloas): structured penalty classification arrives with the envelope lookup state @@ -1025,6 +1025,10 @@ impl From> for BlockProcessingR None } } + BlockError::EnvelopeError(_) => { + // TODO(gloas): penalize correctly in range sync PR + None + } // Remaining invalid blocks: penalize the block peer. Listed explicitly so a // new `BlockError` variant forces a compile error here. BlockError::FutureSlot { .. } @@ -1044,8 +1048,6 @@ impl From> for BlockProcessingR | BlockError::ParentExecutionPayloadInvalid { .. } | BlockError::KnownInvalidExecutionPayload(_) | BlockError::Slashable - | BlockError::EnvelopeBlockRootUnknown(_) - | BlockError::OptimisticSyncNotSupported { .. } | BlockError::InvalidBlobCount { .. } | BlockError::BidParentRootMismatch { .. } => block_peer_penalty(&e), }; From e78e1d38babab042771868f7a36451dcc2cd9e8d Mon Sep 17 00:00:00 2001 From: Daniel Knopik <107140945+dknopik@users.noreply.github.com> Date: Fri, 5 Jun 2026 16:50:19 +0200 Subject: [PATCH 17/26] Update libp2p (#9331) Update libp2p to benefit from recent improvements, including partial messages bugfixes. Co-Authored-By: Daniel Knopik --- Cargo.lock | 686 +++++++++++++----- Cargo.toml | 5 +- beacon_node/lighthouse_network/Cargo.toml | 2 - beacon_node/lighthouse_network/src/config.rs | 4 +- .../lighthouse_network/src/service/mod.rs | 41 +- .../lighthouse_network/src/types/pubsub.rs | 11 +- .../gossip_methods.rs | 23 +- 7 files changed, 549 insertions(+), 223 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 40db8876cf..1bfc32a7a0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -73,7 +73,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -1758,7 +1758,18 @@ checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" dependencies = [ "cfg-if", "cipher", - "cpufeatures", + "cpufeatures 0.2.17", +] + +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", ] [[package]] @@ -1768,7 +1779,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" dependencies = [ "aead", - "chacha20", + "chacha20 0.9.1", "cipher", "poly1305", "zeroize", @@ -1973,6 +1984,16 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "compare_fields" version = "0.1.1" @@ -2009,7 +2030,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8030735ecb0d128428b64cd379809817e620a40e5001c54465b99ec5feec2857" dependencies = [ "futures-core", - "prost", + "prost 0.13.5", "prost-types", "tonic 0.12.3", "tracing-core", @@ -2028,7 +2049,7 @@ dependencies = [ "hdrhistogram", "humantime", "hyper-util", - "prost", + "prost 0.13.5", "prost-types", "serde", "serde_json", @@ -2048,7 +2069,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3bb320cac8a0750d7f25280aa97b09c26edfe161164238ecbbb31092b079e735" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "proptest", "serde_core", ] @@ -2146,15 +2167,6 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" -[[package]] -name = "core2" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" -dependencies = [ - "memchr", -] - [[package]] name = "cpufeatures" version = "0.2.17" @@ -2164,6 +2176,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc" version = "3.4.0" @@ -2319,7 +2340,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "curve25519-dalek-derive", "digest 0.10.7", "fiat-crypto", @@ -2910,9 +2931,9 @@ dependencies = [ [[package]] name = "either" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" dependencies = [ "serde", ] @@ -3051,18 +3072,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "enum-as-inner" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "enum-ordinalize" version = "4.3.2" @@ -3118,7 +3127,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -3265,7 +3274,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5aa93f58bb1eb3d1e556e4f408ef1dac130bad01ac37db4e7ade45de40d1c86a" dependencies = [ - "cpufeatures", + "cpufeatures 0.2.17", "ring", "sha2", ] @@ -3724,7 +3733,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f2f12607f92c69b12ed746fabf9ca4f5c482cba46679c1a75b874ed7c26adb" dependencies = [ "futures-io", - "rustls 0.23.35", + "rustls 0.23.40", "rustls-pki-types", ] @@ -3823,11 +3832,25 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasip2", + "wasip3", +] + [[package]] name = "ghash" version = "0.5.1" @@ -4081,24 +4104,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b07f60793ff0a4d9cef0f18e63b5357e06209987153a64648c972c1e5aff336f" [[package]] -name = "hickory-proto" -version = "0.25.2" +name = "hickory-net" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" +checksum = "e2295ed2f9c31e471e1428a8f88a3f0e1f4b27c15049592138d1eebe9c35b183" dependencies = [ "async-trait", "cfg-if", "data-encoding", - "enum-as-inner", "futures-channel", "futures-io", "futures-util", + "hickory-proto", "idna", "ipnet", - "once_cell", - "rand 0.9.2", - "ring", - "socket2 0.5.10", + "jni", + "rand 0.10.1", "thiserror 2.0.17", "tinyvec", "tokio", @@ -4107,21 +4128,46 @@ dependencies = [ ] [[package]] -name = "hickory-resolver" -version = "0.25.2" +name = "hickory-proto" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" +checksum = "0bab31817bfb44672a252e97fe81cd0c18d1b2cf892108922f6818820df8c643" +dependencies = [ + "data-encoding", + "idna", + "ipnet", + "jni", + "once_cell", + "prefix-trie", + "rand 0.10.1", + "ring", + "thiserror 2.0.17", + "tinyvec", + "tracing", + "url", +] + +[[package]] +name = "hickory-resolver" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d58d28879ceecde6607729660c2667a081ccdc082e082675042793960f178c" dependencies = [ "cfg-if", "futures-util", + "hickory-net", "hickory-proto", "ipconfig", + "ipnet", + "jni", "moka", + "ndk-context", "once_cell", "parking_lot", - "rand 0.9.2", + "rand 0.10.1", "resolv-conf", "smallvec", + "system-configuration", "thiserror 2.0.17", "tokio", "tracing", @@ -4349,7 +4395,7 @@ dependencies = [ "http 1.4.0", "hyper 1.8.1", "hyper-util", - "rustls 0.23.35", + "rustls 0.23.40", "rustls-pki-types", "tokio", "tokio-rustls 0.26.4", @@ -4388,7 +4434,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.3", + "socket2 0.5.10", "tokio", "tower-service", "tracing", @@ -4499,6 +4545,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -4590,6 +4642,26 @@ dependencies = [ "xmltree", ] +[[package]] +name = "igd-next" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de7238d487a9aff61f81b5ab41c0a841532a115a398b5fa92a2fadd0885e2581" +dependencies = [ + "attohttpc", + "bytes", + "futures", + "http 1.4.0", + "http-body-util", + "hyper 1.8.1", + "hyper-util", + "log", + "rand 0.10.1", + "tokio", + "url", + "xmltree", +] + [[package]] name = "impl-codec" version = "0.6.0" @@ -4707,6 +4779,9 @@ name = "ipnet" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +dependencies = [ + "serde", +] [[package]] name = "iri-string" @@ -4766,6 +4841,55 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror 2.0.17", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version 0.4.1", + "simd_cesu8", + "syn 2.0.117", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.117", +] + [[package]] name = "jobserver" version = "0.1.34" @@ -4778,9 +4902,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.83" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" dependencies = [ "once_cell", "wasm-bindgen", @@ -4822,7 +4946,7 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" dependencies = [ - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -4918,6 +5042,12 @@ dependencies = [ "yaml_serde", ] +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "leveldb" version = "0.8.6" @@ -4943,9 +5073,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.185" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libloading" @@ -4981,7 +5111,7 @@ dependencies = [ [[package]] name = "libp2p" version = "0.57.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#f4cf4bf79b710c7502969eeab8343191ec63c956" +source = "git+https://github.com/libp2p/rust-libp2p.git#3e72d4c071d5ec8815d2f6f7ee3602600ff51798" dependencies = [ "bytes", "either", @@ -5012,7 +5142,7 @@ dependencies = [ [[package]] name = "libp2p-allow-block-list" version = "0.7.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#f4cf4bf79b710c7502969eeab8343191ec63c956" +source = "git+https://github.com/libp2p/rust-libp2p.git#3e72d4c071d5ec8815d2f6f7ee3602600ff51798" dependencies = [ "libp2p-core", "libp2p-identity", @@ -5022,7 +5152,7 @@ dependencies = [ [[package]] name = "libp2p-connection-limits" version = "0.7.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#f4cf4bf79b710c7502969eeab8343191ec63c956" +source = "git+https://github.com/libp2p/rust-libp2p.git#3e72d4c071d5ec8815d2f6f7ee3602600ff51798" dependencies = [ "libp2p-core", "libp2p-identity", @@ -5032,7 +5162,7 @@ dependencies = [ [[package]] name = "libp2p-core" version = "0.44.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#f4cf4bf79b710c7502969eeab8343191ec63c956" +source = "git+https://github.com/libp2p/rust-libp2p.git#3e72d4c071d5ec8815d2f6f7ee3602600ff51798" dependencies = [ "either", "fnv", @@ -5044,7 +5174,7 @@ dependencies = [ "multistream-select", "parking_lot", "pin-project", - "quick-protobuf", + "prost 0.14.3", "rand 0.8.5", "rw-stream-sink", "thiserror 2.0.17", @@ -5056,7 +5186,7 @@ dependencies = [ [[package]] name = "libp2p-dns" version = "0.45.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#f4cf4bf79b710c7502969eeab8343191ec63c956" +source = "git+https://github.com/libp2p/rust-libp2p.git#3e72d4c071d5ec8815d2f6f7ee3602600ff51798" dependencies = [ "futures", "hickory-resolver", @@ -5070,7 +5200,7 @@ dependencies = [ [[package]] name = "libp2p-gossipsub" version = "0.50.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#f4cf4bf79b710c7502969eeab8343191ec63c956" +source = "git+https://github.com/libp2p/rust-libp2p.git#3e72d4c071d5ec8815d2f6f7ee3602600ff51798" dependencies = [ "async-channel 2.5.0", "asynchronous-codec", @@ -5088,8 +5218,8 @@ dependencies = [ "libp2p-identity", "libp2p-swarm", "prometheus-client", - "quick-protobuf", - "quick-protobuf-codec", + "prost 0.14.3", + "prost-codec", "rand 0.8.5", "regex", "sha2", @@ -5100,7 +5230,7 @@ dependencies = [ [[package]] name = "libp2p-identify" version = "0.48.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#f4cf4bf79b710c7502969eeab8343191ec63c956" +source = "git+https://github.com/libp2p/rust-libp2p.git#3e72d4c071d5ec8815d2f6f7ee3602600ff51798" dependencies = [ "asynchronous-codec", "either", @@ -5110,8 +5240,8 @@ dependencies = [ "libp2p-core", "libp2p-identity", "libp2p-swarm", - "quick-protobuf", - "quick-protobuf-codec", + "prost 0.14.3", + "prost-codec", "smallvec", "thiserror 2.0.17", "tracing", @@ -5119,9 +5249,9 @@ dependencies = [ [[package]] name = "libp2p-identity" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c7892c221730ba55f7196e98b0b8ba5e04b4155651736036628e9f73ed6fc3" +checksum = "9525f3831544f7ae497bde79adf114ef127b0fbbb97edbbf692a80408636421c" dependencies = [ "asn1_der", "bs58 0.5.1", @@ -5129,7 +5259,7 @@ dependencies = [ "hkdf", "k256", "multihash", - "quick-protobuf", + "prost 0.14.3", "rand 0.8.5", "sha2", "thiserror 2.0.17", @@ -5140,7 +5270,7 @@ dependencies = [ [[package]] name = "libp2p-mdns" version = "0.49.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#f4cf4bf79b710c7502969eeab8343191ec63c956" +source = "git+https://github.com/libp2p/rust-libp2p.git#3e72d4c071d5ec8815d2f6f7ee3602600ff51798" dependencies = [ "futures", "hickory-proto", @@ -5150,7 +5280,7 @@ dependencies = [ "libp2p-swarm", "rand 0.8.5", "smallvec", - "socket2 0.6.3", + "socket2 0.6.4", "tokio", "tracing", ] @@ -5158,7 +5288,7 @@ dependencies = [ [[package]] name = "libp2p-metrics" version = "0.18.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#f4cf4bf79b710c7502969eeab8343191ec63c956" +source = "git+https://github.com/libp2p/rust-libp2p.git#3e72d4c071d5ec8815d2f6f7ee3602600ff51798" dependencies = [ "futures", "libp2p-core", @@ -5174,7 +5304,7 @@ dependencies = [ [[package]] name = "libp2p-mplex" version = "0.44.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#f4cf4bf79b710c7502969eeab8343191ec63c956" +source = "git+https://github.com/libp2p/rust-libp2p.git#3e72d4c071d5ec8815d2f6f7ee3602600ff51798" dependencies = [ "asynchronous-codec", "bytes", @@ -5192,7 +5322,7 @@ dependencies = [ [[package]] name = "libp2p-noise" version = "0.47.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#f4cf4bf79b710c7502969eeab8343191ec63c956" +source = "git+https://github.com/libp2p/rust-libp2p.git#3e72d4c071d5ec8815d2f6f7ee3602600ff51798" dependencies = [ "asynchronous-codec", "bytes", @@ -5201,7 +5331,7 @@ dependencies = [ "libp2p-identity", "multiaddr", "multihash", - "quick-protobuf", + "prost 0.14.3", "rand 0.8.5", "snow", "static_assertions", @@ -5214,7 +5344,7 @@ dependencies = [ [[package]] name = "libp2p-quic" version = "0.14.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#f4cf4bf79b710c7502969eeab8343191ec63c956" +source = "git+https://github.com/libp2p/rust-libp2p.git#3e72d4c071d5ec8815d2f6f7ee3602600ff51798" dependencies = [ "futures", "futures-timer", @@ -5225,8 +5355,8 @@ dependencies = [ "quinn", "rand 0.8.5", "ring", - "rustls 0.23.35", - "socket2 0.6.3", + "rustls 0.23.40", + "socket2 0.6.4", "thiserror 2.0.17", "tokio", "tracing", @@ -5235,7 +5365,7 @@ dependencies = [ [[package]] name = "libp2p-swarm" version = "0.48.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#f4cf4bf79b710c7502969eeab8343191ec63c956" +source = "git+https://github.com/libp2p/rust-libp2p.git#3e72d4c071d5ec8815d2f6f7ee3602600ff51798" dependencies = [ "either", "fnv", @@ -5258,7 +5388,7 @@ dependencies = [ [[package]] name = "libp2p-swarm-derive" version = "0.36.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#f4cf4bf79b710c7502969eeab8343191ec63c956" +source = "git+https://github.com/libp2p/rust-libp2p.git#3e72d4c071d5ec8815d2f6f7ee3602600ff51798" dependencies = [ "heck", "quote", @@ -5268,14 +5398,14 @@ dependencies = [ [[package]] name = "libp2p-tcp" version = "0.45.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#f4cf4bf79b710c7502969eeab8343191ec63c956" +source = "git+https://github.com/libp2p/rust-libp2p.git#3e72d4c071d5ec8815d2f6f7ee3602600ff51798" dependencies = [ "futures", "futures-timer", "if-watch", "libc", "libp2p-core", - "socket2 0.6.3", + "socket2 0.6.4", "tokio", "tracing", ] @@ -5283,7 +5413,7 @@ dependencies = [ [[package]] name = "libp2p-tls" version = "0.7.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#f4cf4bf79b710c7502969eeab8343191ec63c956" +source = "git+https://github.com/libp2p/rust-libp2p.git#3e72d4c071d5ec8815d2f6f7ee3602600ff51798" dependencies = [ "futures", "futures-rustls", @@ -5291,21 +5421,21 @@ dependencies = [ "libp2p-identity", "rcgen", "ring", - "rustls 0.23.35", + "rustls 0.23.40", "rustls-webpki 0.103.13", "thiserror 2.0.17", - "x509-parser", - "yasna", + "x509-parser 0.18.1", + "yasna 0.6.0", ] [[package]] name = "libp2p-upnp" version = "0.7.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#f4cf4bf79b710c7502969eeab8343191ec63c956" +source = "git+https://github.com/libp2p/rust-libp2p.git#3e72d4c071d5ec8815d2f6f7ee3602600ff51798" dependencies = [ "futures", "futures-timer", - "igd-next", + "igd-next 0.17.1", "libp2p-core", "libp2p-swarm", "tokio", @@ -5315,7 +5445,7 @@ dependencies = [ [[package]] name = "libp2p-yamux" version = "0.48.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#f4cf4bf79b710c7502969eeab8343191ec63c956" +source = "git+https://github.com/libp2p/rust-libp2p.git#3e72d4c071d5ec8815d2f6f7ee3602600ff51798" dependencies = [ "either", "futures", @@ -5448,7 +5578,6 @@ dependencies = [ "if-addrs 0.14.0", "itertools 0.14.0", "libp2p", - "libp2p-gossipsub", "libp2p-mplex", "lighthouse_version", "logging", @@ -5831,9 +5960,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.1.1" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" dependencies = [ "libc", "wasi", @@ -5985,18 +6114,17 @@ dependencies = [ [[package]] name = "multihash" -version = "0.19.3" +version = "0.19.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b430e7953c29dd6a09afc29ff0bb69c6e306329ee6794700aee27b76a1aea8d" +checksum = "577c63b00ad74d57e8c9aa870b5fccebf2fd64a308a5aee9f1bb88e4aea19447" dependencies = [ - "core2", "unsigned-varint", ] [[package]] name = "multistream-select" version = "0.14.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#f4cf4bf79b710c7502969eeab8343191ec63c956" +source = "git+https://github.com/libp2p/rust-libp2p.git#3e72d4c071d5ec8815d2f6f7ee3602600ff51798" dependencies = [ "bytes", "futures", @@ -6006,6 +6134,12 @@ dependencies = [ "unsigned-varint", ] +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + [[package]] name = "netlink-packet-core" version = "0.8.1" @@ -6077,7 +6211,7 @@ dependencies = [ "futures", "genesis", "hex", - "igd-next", + "igd-next 0.16.2", "itertools 0.14.0", "k256", "kzg", @@ -6196,7 +6330,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -6410,7 +6544,7 @@ dependencies = [ "opentelemetry-http", "opentelemetry-proto", "opentelemetry_sdk", - "prost", + "prost 0.13.5", "reqwest", "thiserror 2.0.17", "tokio", @@ -6426,7 +6560,7 @@ checksum = "2e046fd7660710fe5a05e8748e70d9058dc15c94ba914e7c4faa7c728f0e8ddc" dependencies = [ "opentelemetry", "opentelemetry_sdk", - "prost", + "prost 0.13.5", "tonic 0.13.1", ] @@ -6492,7 +6626,7 @@ dependencies = [ "sha1", "sha2", "thiserror 2.0.17", - "x509-parser", + "x509-parser 0.17.0", ] [[package]] @@ -6624,18 +6758,18 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.11" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.11" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" dependencies = [ "proc-macro2", "quote", @@ -6754,7 +6888,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" dependencies = [ - "cpufeatures", + "cpufeatures 0.2.17", "opaque-debug", "universal-hash", ] @@ -6766,7 +6900,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "opaque-debug", "universal-hash", ] @@ -6827,6 +6961,17 @@ dependencies = [ "termtree", ] +[[package]] +name = "prefix-trie" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cf6e3177f0684016a5c209b00882e15f8bdd3f3bb48f0491df10cd102d0c6e7" +dependencies = [ + "either", + "ipnet", + "num-traits", +] + [[package]] name = "pretty_reqwest_error" version = "0.1.0" @@ -6991,7 +7136,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" dependencies = [ "bytes", - "prost-derive", + "prost-derive 0.13.5", +] + +[[package]] +name = "prost" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" +dependencies = [ + "bytes", + "prost-derive 0.14.3", +] + +[[package]] +name = "prost-codec" +version = "0.4.0" +source = "git+https://github.com/libp2p/rust-libp2p.git#3e72d4c071d5ec8815d2f6f7ee3602600ff51798" +dependencies = [ + "asynchronous-codec", + "bytes", + "prost 0.14.3", + "thiserror 2.0.17", + "unsigned-varint", ] [[package]] @@ -7007,13 +7174,26 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "prost-derive" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" +dependencies = [ + "anyhow", + "itertools 0.10.5", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "prost-types" version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" dependencies = [ - "prost", + "prost 0.13.5", ] [[package]] @@ -7058,26 +7238,6 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" -[[package]] -name = "quick-protobuf" -version = "0.8.1" -source = "git+https://github.com/sigp/quick-protobuf.git?rev=87c4ccb9bb2af494de375f5f6c62850badd26304#87c4ccb9bb2af494de375f5f6c62850badd26304" -dependencies = [ - "byteorder", -] - -[[package]] -name = "quick-protobuf-codec" -version = "0.4.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#f4cf4bf79b710c7502969eeab8343191ec63c956" -dependencies = [ - "asynchronous-codec", - "bytes", - "quick-protobuf", - "thiserror 2.0.17", - "unsigned-varint", -] - [[package]] name = "quinn" version = "0.11.9" @@ -7091,8 +7251,8 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash 2.1.1", - "rustls 0.23.35", - "socket2 0.6.3", + "rustls 0.23.40", + "socket2 0.5.10", "thiserror 2.0.17", "tokio", "tracing", @@ -7111,7 +7271,7 @@ dependencies = [ "rand 0.9.2", "ring", "rustc-hash 2.1.1", - "rustls 0.23.35", + "rustls 0.23.40", "rustls-pki-types", "slab", "thiserror 2.0.17", @@ -7129,9 +7289,9 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.3", + "socket2 0.5.10", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -7149,6 +7309,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "r2d2" version = "0.8.10" @@ -7200,6 +7366,17 @@ dependencies = [ "serde", ] +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20 0.10.0", + "getrandom 0.4.2", + "rand_core 0.10.1", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -7239,6 +7416,12 @@ dependencies = [ "serde", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "rand_xorshift" version = "0.3.0" @@ -7305,7 +7488,7 @@ dependencies = [ "ring", "rustls-pki-types", "time", - "yasna", + "yasna 0.5.2", ] [[package]] @@ -7408,7 +7591,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.35", + "rustls 0.23.40", "rustls-pki-types", "serde", "serde_json", @@ -7653,7 +7836,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -7672,9 +7855,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.35" +version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ "log", "once_cell", @@ -7759,7 +7942,7 @@ dependencies = [ [[package]] name = "rw-stream-sink" version = "0.5.0" -source = "git+https://github.com/libp2p/rust-libp2p.git#f4cf4bf79b710c7502969eeab8343191ec63c956" +source = "git+https://github.com/libp2p/rust-libp2p.git#3e72d4c071d5ec8815d2f6f7ee3602600ff51798" dependencies = [ "futures", "pin-project", @@ -8088,7 +8271,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest 0.10.7", ] @@ -8099,7 +8282,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest 0.10.7", ] @@ -8181,6 +8364,22 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version 0.4.1", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "similar" version = "2.7.0" @@ -8355,9 +8554,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", "windows-sys 0.60.2", @@ -8705,7 +8904,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -8918,9 +9117,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.50.0" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", @@ -8928,7 +9127,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.3", + "socket2 0.6.4", "tokio-macros", "tracing", "windows-sys 0.61.2", @@ -8936,9 +9135,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", @@ -8962,7 +9161,7 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls 0.23.35", + "rustls 0.23.40", "tokio", ] @@ -9043,7 +9242,7 @@ dependencies = [ "hyper-util", "percent-encoding", "pin-project", - "prost", + "prost 0.13.5", "socket2 0.5.10", "tokio", "tokio-stream", @@ -9070,7 +9269,7 @@ dependencies = [ "hyper-util", "percent-encoding", "pin-project", - "prost", + "prost 0.13.5", "rustls-native-certs", "tokio", "tokio-rustls 0.26.4", @@ -9832,14 +10031,23 @@ version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.46.0", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", ] [[package]] name = "wasm-bindgen" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" dependencies = [ "cfg-if", "once_cell", @@ -9850,11 +10058,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.56" +version = "0.4.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" dependencies = [ "cfg-if", + "futures-util", "js-sys", "once_cell", "wasm-bindgen", @@ -9863,9 +10072,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -9873,9 +10082,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" dependencies = [ "bumpalo", "proc-macro2", @@ -9886,13 +10095,35 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.12.1", + "wasm-encoder", + "wasmparser", +] + [[package]] name = "wasm-streams" version = "0.4.2" @@ -9906,6 +10137,18 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.10.0", + "hashbrown 0.15.5", + "indexmap 2.12.1", + "semver 1.0.27", +] + [[package]] name = "wasmtimer" version = "0.4.3" @@ -9922,9 +10165,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.83" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" dependencies = [ "js-sys", "wasm-bindgen", @@ -10016,7 +10259,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -10403,6 +10646,94 @@ version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap 2.12.1", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.10.0", + "indexmap 2.12.1", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.12.1", + "log", + "semver 1.0.27", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + [[package]] name = "workspace_members" version = "0.1.0" @@ -10466,6 +10797,23 @@ dependencies = [ "time", ] +[[package]] +name = "x509-parser" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "rusticata-macros", + "thiserror 2.0.17", + "time", +] + [[package]] name = "xdelta3" version = "0.1.5" @@ -10559,6 +10907,12 @@ dependencies = [ "time", ] +[[package]] +name = "yasna" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5f6765e852b9b4dc8e2a76843e4d64d1cea8e79bcde0b6901aea8e7c7f08282" + [[package]] name = "yoke" version = "0.8.1" diff --git a/Cargo.toml b/Cargo.toml index 71398530fe..50b1733232 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -161,7 +161,7 @@ initialized_validators = { path = "validator_client/initialized_validators" } int_to_bytes = { path = "consensus/int_to_bytes" } itertools = "0.14" kzg = { path = "crypto/kzg" } -libp2p = { git = "https://github.com/libp2p/rust-libp2p.git", default-features = false, features = ["identify", "yamux", "noise", "dns", "tcp", "tokio", "secp256k1", "macros", "metrics", "quic", "upnp", "gossipsub"] } +libp2p = { git = "https://github.com/libp2p/rust-libp2p.git", default-features = false, features = ["identify", "yamux", "noise", "dns", "tcp", "tokio", "secp256k1", "macros", "metrics", "quic", "upnp", "gossipsub", "gossipsub-partial-messages"] } libsecp256k1 = "0.7" lighthouse_network = { path = "beacon_node/lighthouse_network" } lighthouse_validator_store = { path = "validator_client/lighthouse_validator_store" } @@ -273,6 +273,3 @@ incremental = false inherits = "release" debug = true -[patch.crates-io] -quick-protobuf = { git = "https://github.com/sigp/quick-protobuf.git", rev = "87c4ccb9bb2af494de375f5f6c62850badd26304" } - diff --git a/beacon_node/lighthouse_network/Cargo.toml b/beacon_node/lighthouse_network/Cargo.toml index 44af8d7006..659886f0f1 100644 --- a/beacon_node/lighthouse_network/Cargo.toml +++ b/beacon_node/lighthouse_network/Cargo.toml @@ -21,8 +21,6 @@ ethereum_ssz_derive = { workspace = true } fixed_bytes = { workspace = true } fnv = { workspace = true } futures = { workspace = true } -# Enable partial messages feature -gossipsub = { package = "libp2p-gossipsub", git = "https://github.com/libp2p/rust-libp2p.git", features = ["partial_messages"] } hex = { workspace = true } if-addrs = "0.14" itertools = { workspace = true } diff --git a/beacon_node/lighthouse_network/src/config.rs b/beacon_node/lighthouse_network/src/config.rs index 4d4d91a456..8f7c1dd8de 100644 --- a/beacon_node/lighthouse_network/src/config.rs +++ b/beacon_node/lighthouse_network/src/config.rs @@ -508,7 +508,9 @@ pub fn gossipsub_config( .fanout_ttl(Duration::from_secs(60)) .history_length(12) .flood_publish(false) - .max_messages_per_rpc(Some(500)) // Responses to IWANT can be quite large + .max_publish_messages(500) // Responses to IWANT can be quite large + .max_control_messages_sent(500) + .max_control_message_size(128 << 10) // 128KB .history_gossip(load.history_gossip) .validate_messages() // require validation before propagation .validation_mode(gossipsub::ValidationMode::Anonymous) diff --git a/beacon_node/lighthouse_network/src/service/mod.rs b/beacon_node/lighthouse_network/src/service/mod.rs index f5e2442f86..097736a010 100644 --- a/beacon_node/lighthouse_network/src/service/mod.rs +++ b/beacon_node/lighthouse_network/src/service/mod.rs @@ -311,11 +311,8 @@ impl Network { let fork = ctx.chain_spec.fork_name_at_epoch(epoch); all_topics_at_fork::(fork, &ctx.chain_spec) .into_iter() - .map(|topic| { - Topic::new(GossipTopic::new(topic, GossipEncoding::default(), digest)) - .into() - }) - .collect::>() + .map(|topic| GossipTopic::new(topic, GossipEncoding::default(), digest)) + .collect::>() }) .collect::>(); @@ -368,11 +365,20 @@ impl Network { gossipsub.add_explicit_peer(&PeerId::from(explicit_peer.clone())); } + // Register topics with enabled partial messages + for topic in all_topics_for_digests.iter().flatten() { + if topic.kind().use_partial_messages(&config) { + gossipsub.enable_partials_for_topic(Topic::new(topic.clone()).hash(), true); + } + } + // If we are using metrics, then register which topics we want to make sure to keep // track of if ctx.libp2p_registry.is_some() { for topics in all_topics_for_digests { - gossipsub.register_topics_for_metrics(topics); + gossipsub.register_topics_for_metrics( + topics.into_iter().map(|t| Topic::new(t).hash()).collect(), + ); } } @@ -823,18 +829,9 @@ impl Network { .write() .insert(topic.clone()); - let partial = topic - .kind() - .use_partial_messages(self.network_globals.config.as_ref()); let topic: Topic = topic.into(); - let subscribe_result = if partial { - self.gossipsub_mut().subscribe_partial(&topic, true) - } else { - self.gossipsub_mut().subscribe(&topic) - }; - - match subscribe_result { + match self.gossipsub_mut().subscribe(&topic) { Err(e) => { warn!(%topic, error = ?e, "Failed to subscribe to topic"); false @@ -1381,9 +1378,9 @@ impl Network { /* Sub-behaviour event handling functions */ /// Handle a gossipsub event. - fn inject_gs_event(&mut self, event: gossipsub::Event) -> Option> { + fn inject_gs_event(&mut self, event: Event) -> Option> { match event { - gossipsub::Event::Message { + Event::Message { propagation_source, message_id: id, message: gs_msg, @@ -1461,7 +1458,7 @@ impl Network { } } } - gossipsub::Event::Subscribed { peer_id, topic } => { + Event::Subscribed { peer_id, topic, .. } => { if let Ok(topic) = GossipTopic::decode(topic.as_str()) { if let Some(subnet_id) = topic.subnet_id() { self.network_globals @@ -1513,7 +1510,7 @@ impl Network { } } } - gossipsub::Event::Unsubscribed { peer_id, topic } => { + Event::Unsubscribed { peer_id, topic } => { if let Some(subnet_id) = subnet_from_topic_hash(&topic) { self.network_globals .peers @@ -1521,7 +1518,7 @@ impl Network { .remove_subscription(&peer_id, &subnet_id); } } - gossipsub::Event::GossipsubNotSupported { peer_id } => { + Event::GossipsubNotSupported { peer_id } => { debug!(%peer_id, "Peer does not support gossipsub"); self.peer_manager_mut().report_peer( &peer_id, @@ -1531,7 +1528,7 @@ impl Network { "does_not_support_gossipsub", ); } - gossipsub::Event::SlowPeer { + Event::SlowPeer { peer_id, failed_messages, } => { diff --git a/beacon_node/lighthouse_network/src/types/pubsub.rs b/beacon_node/lighthouse_network/src/types/pubsub.rs index 043d1cfb88..d486ca5129 100644 --- a/beacon_node/lighthouse_network/src/types/pubsub.rs +++ b/beacon_node/lighthouse_network/src/types/pubsub.rs @@ -1,7 +1,7 @@ //! Handles the encoding and decoding of pubsub messages. use crate::types::{GossipEncoding, GossipKind, GossipTopic}; -use gossipsub::TopicHash; +use libp2p::gossipsub::{DataTransform, Message, RawMessage, TopicHash}; use snap::raw::{Decoder, Encoder, decompress_len}; use ssz::{Decode, Encode}; use std::io::{Error, ErrorKind}; @@ -73,12 +73,9 @@ impl SnappyTransform { } } -impl gossipsub::DataTransform for SnappyTransform { +impl DataTransform for SnappyTransform { // Provides the snappy decompression from RawGossipsubMessages - fn inbound_transform( - &self, - raw_message: gossipsub::RawMessage, - ) -> Result { + fn inbound_transform(&self, raw_message: RawMessage) -> Result { // first check the size of the compressed payload if raw_message.data.len() > self.max_compressed_len { return Err(Error::new( @@ -99,7 +96,7 @@ impl gossipsub::DataTransform for SnappyTransform { let decompressed_data = decoder.decompress_vec(&raw_message.data)?; // Build the GossipsubMessage struct - Ok(gossipsub::Message { + Ok(Message { source: raw_message.source, data: decompressed_data, sequence_number: raw_message.sequence_number, 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 17e569206b..68b41cab5e 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -171,17 +171,6 @@ 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 */ @@ -2018,11 +2007,7 @@ impl NetworkBeaconProcessor { } }; - self.propagate_validation_result( - message_id, - peer_id, - clone_message_acceptance(&validation_result), - ); + self.propagate_validation_result(message_id, peer_id, validation_result); if let Some(slashing) = verified_slashing_opt { metrics::inc_counter(&metrics::BEACON_PROCESSOR_PROPOSER_SLASHING_VERIFIED_TOTAL); @@ -2084,11 +2069,7 @@ impl NetworkBeaconProcessor { } }; - self.propagate_validation_result( - message_id, - peer_id, - clone_message_acceptance(&validation_result), - ); + self.propagate_validation_result(message_id, peer_id, validation_result); if let Some(slashing) = verified_slashing_opt { metrics::inc_counter(&metrics::BEACON_PROCESSOR_ATTESTER_SLASHING_VERIFIED_TOTAL); From 42e678189ce75bcd34b8b74d40e54a3f87ed001b Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Fri, 5 Jun 2026 13:16:06 -0700 Subject: [PATCH 18/26] Update gloas api routes to match updated spec (#9418) tldr the routes got pluralized https://github.com/ethereum/beacon-APIs/pull/613 Co-Authored-By: Eitan Seri-Levi --- ...yload_bid.rs => execution_payload_bids.rs} | 12 ++--- ...lope.rs => execution_payload_envelopes.rs} | 20 +++---- beacon_node/http_api/src/beacon/mod.rs | 4 +- beacon_node/http_api/src/lib.rs | 48 ++++++++--------- ...lope.rs => execution_payload_envelopes.rs} | 6 +-- beacon_node/http_api/src/validator/mod.rs | 2 +- beacon_node/http_api/tests/tests.rs | 26 ++++----- common/eth2/src/lib.rs | 54 +++++++++---------- .../validator_services/src/block_service.rs | 4 +- 9 files changed, 88 insertions(+), 88 deletions(-) rename beacon_node/http_api/src/beacon/{execution_payload_bid.rs => execution_payload_bids.rs} (91%) rename beacon_node/http_api/src/beacon/{execution_payload_envelope.rs => execution_payload_envelopes.rs} (95%) rename beacon_node/http_api/src/validator/{execution_payload_envelope.rs => execution_payload_envelopes.rs} (95%) diff --git a/beacon_node/http_api/src/beacon/execution_payload_bid.rs b/beacon_node/http_api/src/beacon/execution_payload_bids.rs similarity index 91% rename from beacon_node/http_api/src/beacon/execution_payload_bid.rs rename to beacon_node/http_api/src/beacon/execution_payload_bids.rs index f6041b55c8..856670aa94 100644 --- a/beacon_node/http_api/src/beacon/execution_payload_bid.rs +++ b/beacon_node/http_api/src/beacon/execution_payload_bids.rs @@ -14,8 +14,8 @@ use tracing::{debug, warn}; use types::SignedExecutionPayloadBid; use warp::{Filter, Rejection, Reply, hyper::Body, hyper::Response}; -// POST /eth/v1/beacon/execution_payload_bid (SSZ) -pub(crate) fn post_beacon_execution_payload_bid_ssz( +// POST /eth/v1/beacon/execution_payload_bids (SSZ) +pub(crate) fn post_beacon_execution_payload_bids_ssz( eth_v1: EthV1Filter, task_spawner_filter: TaskSpawnerFilter, chain_filter: ChainFilter, @@ -23,7 +23,7 @@ pub(crate) fn post_beacon_execution_payload_bid_ssz( ) -> ResponseFilter { eth_v1 .and(warp::path("beacon")) - .and(warp::path("execution_payload_bid")) + .and(warp::path("execution_payload_bids")) .and(warp::path::end()) .and(warp::body::bytes()) .and(task_spawner_filter) @@ -46,8 +46,8 @@ pub(crate) fn post_beacon_execution_payload_bid_ssz( .boxed() } -// POST /eth/v1/beacon/execution_payload_bid -pub(crate) fn post_beacon_execution_payload_bid( +// POST /eth/v1/beacon/execution_payload_bids +pub(crate) fn post_beacon_execution_payload_bids( eth_v1: EthV1Filter, task_spawner_filter: TaskSpawnerFilter, chain_filter: ChainFilter, @@ -55,7 +55,7 @@ pub(crate) fn post_beacon_execution_payload_bid( ) -> ResponseFilter { eth_v1 .and(warp::path("beacon")) - .and(warp::path("execution_payload_bid")) + .and(warp::path("execution_payload_bids")) .and(warp::path::end()) .and(warp::body::json()) .and(task_spawner_filter) diff --git a/beacon_node/http_api/src/beacon/execution_payload_envelope.rs b/beacon_node/http_api/src/beacon/execution_payload_envelopes.rs similarity index 95% rename from beacon_node/http_api/src/beacon/execution_payload_envelope.rs rename to beacon_node/http_api/src/beacon/execution_payload_envelopes.rs index d12e4371a4..f8ab8cddc8 100644 --- a/beacon_node/http_api/src/beacon/execution_payload_envelope.rs +++ b/beacon_node/http_api/src/beacon/execution_payload_envelopes.rs @@ -24,8 +24,8 @@ use warp::{ hyper::{Body, Response}, }; -// POST beacon/execution_payload_envelope (SSZ) -pub(crate) fn post_beacon_execution_payload_envelope_ssz( +// POST beacon/execution_payload_envelopes (SSZ) +pub(crate) fn post_beacon_execution_payload_envelopes_ssz( eth_v1: EthV1Filter, task_spawner_filter: TaskSpawnerFilter, chain_filter: ChainFilter, @@ -33,7 +33,7 @@ pub(crate) fn post_beacon_execution_payload_envelope_ssz( ) -> ResponseFilter { eth_v1 .and(warp::path("beacon")) - .and(warp::path("execution_payload_envelope")) + .and(warp::path("execution_payload_envelopes")) .and(warp::path::end()) .and(warp::body::bytes()) .and(task_spawner_filter) @@ -57,8 +57,8 @@ pub(crate) fn post_beacon_execution_payload_envelope_ssz( .boxed() } -// POST beacon/execution_payload_envelope -pub(crate) fn post_beacon_execution_payload_envelope( +// POST beacon/execution_payload_envelopes +pub(crate) fn post_beacon_execution_payload_envelopes( eth_v1: EthV1Filter, task_spawner_filter: TaskSpawnerFilter, chain_filter: ChainFilter, @@ -66,7 +66,7 @@ pub(crate) fn post_beacon_execution_payload_envelope( ) -> ResponseFilter { eth_v1 .and(warp::path("beacon")) - .and(warp::path("execution_payload_envelope")) + .and(warp::path("execution_payload_envelopes")) .and(warp::path::end()) .and(warp::body::json()) .and(task_spawner_filter.clone()) @@ -85,7 +85,7 @@ pub(crate) fn post_beacon_execution_payload_envelope( .boxed() } /// Publishes a signed execution payload envelope to the network. Implements -/// `POST /eth/v1/beacon/execution_payload_envelope` per the in-flight beacon-APIs PR +/// `POST /eth/v1/beacon/execution_payload_envelopes` per the in-flight beacon-APIs PR /// . pub async fn publish_execution_payload_envelope( envelope: SignedExecutionPayloadEnvelope, @@ -292,8 +292,8 @@ fn build_gloas_data_columns( } // TODO(gloas): add tests for this endpoint once we support importing payloads into the db -// GET beacon/execution_payload_envelope/{block_id} -pub(crate) fn get_beacon_execution_payload_envelope( +// GET beacon/execution_payload_envelopes/{block_id} +pub(crate) fn get_beacon_execution_payload_envelopes( eth_v1: EthV1Filter, block_id_or_err: impl Filter + Clone @@ -305,7 +305,7 @@ pub(crate) fn get_beacon_execution_payload_envelope( ) -> ResponseFilter { eth_v1 .and(warp::path("beacon")) - .and(warp::path("execution_payload_envelope")) + .and(warp::path("execution_payload_envelopes")) .and(block_id_or_err) .and(warp::path::end()) .and(task_spawner_filter) diff --git a/beacon_node/http_api/src/beacon/mod.rs b/beacon_node/http_api/src/beacon/mod.rs index db0062c14f..31c4077540 100644 --- a/beacon_node/http_api/src/beacon/mod.rs +++ b/beacon_node/http_api/src/beacon/mod.rs @@ -1,4 +1,4 @@ -pub mod execution_payload_bid; -pub mod execution_payload_envelope; +pub mod execution_payload_bids; +pub mod execution_payload_envelopes; pub mod pool; pub mod states; diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index ff88c12925..94f2e3f1df 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -36,12 +36,12 @@ mod validator_inclusion; mod validators; mod version; -use crate::beacon::execution_payload_bid::{ - post_beacon_execution_payload_bid, post_beacon_execution_payload_bid_ssz, +use crate::beacon::execution_payload_bids::{ + post_beacon_execution_payload_bids, post_beacon_execution_payload_bids_ssz, }; -use crate::beacon::execution_payload_envelope::{ - get_beacon_execution_payload_envelope, post_beacon_execution_payload_envelope, - post_beacon_execution_payload_envelope_ssz, +use crate::beacon::execution_payload_envelopes::{ + get_beacon_execution_payload_envelopes, post_beacon_execution_payload_envelopes, + post_beacon_execution_payload_envelopes_ssz, }; use crate::beacon::pool::*; use crate::caches::DEFAULT_HISTORICAL_COMMITTEE_CACHE_SIZE; @@ -101,7 +101,7 @@ use types::{ BeaconStateError, Checkpoint, ConfigAndPreset, Epoch, EthSpec, ForkName, Hash256, SignedBlindedBeaconBlock, }; -use validator::execution_payload_envelope::get_validator_execution_payload_envelope; +use validator::execution_payload_envelopes::get_validator_execution_payload_envelopes; use version::{ ResponseIncludesVersion, V1, V2, add_consensus_version_header, add_ssz_content_type_header, execution_optimistic_finalized_beacon_response, inconsistent_fork_rejection, @@ -1542,40 +1542,40 @@ pub fn serve( network_tx_filter.clone(), ); - // POST beacon/execution_payload_envelope - let post_beacon_execution_payload_envelope = post_beacon_execution_payload_envelope( + // POST beacon/execution_payload_envelopes + let post_beacon_execution_payload_envelopes = post_beacon_execution_payload_envelopes( eth_v1.clone(), task_spawner_filter.clone(), chain_filter.clone(), network_tx_filter.clone(), ); - // POST beacon/execution_payload_envelope (SSZ) - let post_beacon_execution_payload_envelope_ssz = post_beacon_execution_payload_envelope_ssz( + // POST beacon/execution_payload_envelopes (SSZ) + let post_beacon_execution_payload_envelopes_ssz = post_beacon_execution_payload_envelopes_ssz( eth_v1.clone(), task_spawner_filter.clone(), chain_filter.clone(), network_tx_filter.clone(), ); - // POST beacon/execution_payload_bid - let post_beacon_execution_payload_bid = post_beacon_execution_payload_bid( + // POST beacon/execution_payload_bids + let post_beacon_execution_payload_bids = post_beacon_execution_payload_bids( eth_v1.clone(), task_spawner_filter.clone(), chain_filter.clone(), network_tx_filter.clone(), ); - // POST beacon/execution_payload_bid (SSZ) - let post_beacon_execution_payload_bid_ssz = post_beacon_execution_payload_bid_ssz( + // POST beacon/execution_payload_bids (SSZ) + let post_beacon_execution_payload_bids_ssz = post_beacon_execution_payload_bids_ssz( eth_v1.clone(), task_spawner_filter.clone(), chain_filter.clone(), network_tx_filter.clone(), ); - // GET beacon/execution_payload_envelope/{block_id} - let get_beacon_execution_payload_envelope = get_beacon_execution_payload_envelope( + // GET beacon/execution_payload_envelopes/{block_id} + let get_beacon_execution_payload_envelopes = get_beacon_execution_payload_envelopes( eth_v1.clone(), block_id_or_err, task_spawner_filter.clone(), @@ -2584,8 +2584,8 @@ pub fn serve( task_spawner_filter.clone(), ); - // GET validator/execution_payload_envelope/{slot}/{builder_index} - let get_validator_execution_payload_envelope = get_validator_execution_payload_envelope( + // GET validator/execution_payload_envelopes/{slot}/{builder_index} + let get_validator_execution_payload_envelopes = get_validator_execution_payload_envelopes( eth_v1.clone(), chain_filter.clone(), not_while_syncing_filter.clone(), @@ -3401,7 +3401,7 @@ pub fn serve( .uor(get_beacon_block_root) .uor(get_blob_sidecars) .uor(get_blobs) - .uor(get_beacon_execution_payload_envelope) + .uor(get_beacon_execution_payload_envelopes) .uor(get_beacon_pool_attestations) .uor(get_beacon_pool_attester_slashings) .uor(get_beacon_pool_proposer_slashings) @@ -3425,7 +3425,7 @@ pub fn serve( .uor(get_validator_duties_proposer) .uor(get_validator_blocks) .uor(get_validator_blinded_blocks) - .uor(get_validator_execution_payload_envelope) + .uor(get_validator_execution_payload_envelopes) .uor(get_validator_attestation_data) .uor(get_validator_payload_attestation_data) .uor(get_validator_aggregate_attestation) @@ -3463,8 +3463,8 @@ pub fn serve( .uor(post_beacon_blocks_v2_ssz) .uor(post_beacon_blinded_blocks_ssz) .uor(post_beacon_blinded_blocks_v2_ssz) - .uor(post_beacon_execution_payload_envelope_ssz) - .uor(post_beacon_execution_payload_bid_ssz) + .uor(post_beacon_execution_payload_envelopes_ssz) + .uor(post_beacon_execution_payload_bids_ssz) .uor(post_beacon_pool_payload_attestations_ssz) .uor(post_validator_proposer_preferences_ssz), ) @@ -3480,8 +3480,8 @@ pub fn serve( .uor(post_beacon_pool_payload_attestations) .uor(post_beacon_pool_bls_to_execution_changes) .uor(post_validator_proposer_preferences) - .uor(post_beacon_execution_payload_envelope) - .uor(post_beacon_execution_payload_bid) + .uor(post_beacon_execution_payload_envelopes) + .uor(post_beacon_execution_payload_bids) .uor(post_beacon_state_validators) .uor(post_beacon_state_validator_balances) .uor(post_beacon_state_validator_identities) diff --git a/beacon_node/http_api/src/validator/execution_payload_envelope.rs b/beacon_node/http_api/src/validator/execution_payload_envelopes.rs similarity index 95% rename from beacon_node/http_api/src/validator/execution_payload_envelope.rs rename to beacon_node/http_api/src/validator/execution_payload_envelopes.rs index 7a7a430414..3a20b37c9b 100644 --- a/beacon_node/http_api/src/validator/execution_payload_envelope.rs +++ b/beacon_node/http_api/src/validator/execution_payload_envelopes.rs @@ -12,8 +12,8 @@ use types::Slot; use warp::http::Response; use warp::{Filter, Rejection}; -// GET validator/execution_payload_envelope/{slot} -pub fn get_validator_execution_payload_envelope( +// GET validator/execution_payload_envelopes/{slot} +pub fn get_validator_execution_payload_envelopes( eth_v1: EthV1Filter, chain_filter: ChainFilter, not_while_syncing_filter: NotWhileSyncingFilter, @@ -21,7 +21,7 @@ pub fn get_validator_execution_payload_envelope( ) -> ResponseFilter { eth_v1 .and(warp::path("validator")) - .and(warp::path("execution_payload_envelope")) + .and(warp::path("execution_payload_envelopes")) .and(warp::path::param::().or_else(|_| async { Err(warp_utils::reject::custom_bad_request( "Invalid slot".to_string(), diff --git a/beacon_node/http_api/src/validator/mod.rs b/beacon_node/http_api/src/validator/mod.rs index 77df94bc36..8639914774 100644 --- a/beacon_node/http_api/src/validator/mod.rs +++ b/beacon_node/http_api/src/validator/mod.rs @@ -36,7 +36,7 @@ use types::{ use warp::{Filter, Rejection, Reply}; use warp_utils::reject::convert_rejection; -pub mod execution_payload_envelope; +pub mod execution_payload_envelopes; /// Uses the `chain.validator_pubkey_cache` to resolve a pubkey to a validator /// index and then ensures that the validator exists in the given `state`. diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 319229d5f1..455a957337 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -3085,12 +3085,12 @@ impl ApiTester { } /// JSON bid with a valid structure reaches gossip verification and is rejected with 400. - pub async fn test_post_beacon_execution_payload_bid_json(self) -> Self { + pub async fn test_post_beacon_execution_payload_bids_json(self) -> Self { let (bid, fork_name) = self.make_signed_execution_payload_bid(); let result = self .client - .post_beacon_execution_payload_bid(&bid, fork_name) + .post_beacon_execution_payload_bids(&bid, fork_name) .await; assert!( @@ -3102,12 +3102,12 @@ impl ApiTester { } /// SSZ bid with a valid structure reaches gossip verification and is rejected with 400. - pub async fn test_post_beacon_execution_payload_bid_ssz(self) -> Self { + pub async fn test_post_beacon_execution_payload_bids_ssz(self) -> Self { let (bid, fork_name) = self.make_signed_execution_payload_bid(); let result = self .client - .post_beacon_execution_payload_bid_ssz(&bid, fork_name) + .post_beacon_execution_payload_bids_ssz(&bid, fork_name) .await; assert!( @@ -4433,7 +4433,7 @@ impl ApiTester { let envelope = self .client - .get_validator_execution_payload_envelope::(slot) + .get_validator_execution_payload_envelopes::(slot) .await .unwrap() .data; @@ -4452,7 +4452,7 @@ impl ApiTester { let signed_envelope = self.sign_envelope(envelope, &sk, epoch, &fork, genesis_validators_root); self.client - .post_beacon_execution_payload_envelope(&signed_envelope, fork_name) + .post_beacon_execution_payload_envelopes(&signed_envelope, fork_name) .await .unwrap(); @@ -4495,7 +4495,7 @@ impl ApiTester { let envelope = self .client - .get_validator_execution_payload_envelope_ssz::(slot) + .get_validator_execution_payload_envelopes_ssz::(slot) .await .unwrap(); @@ -4513,7 +4513,7 @@ impl ApiTester { let signed_envelope = self.sign_envelope(envelope, &sk, epoch, &fork, genesis_validators_root); self.client - .post_beacon_execution_payload_envelope_ssz(&signed_envelope, fork_name) + .post_beacon_execution_payload_envelopes_ssz(&signed_envelope, fork_name) .await .unwrap(); @@ -4942,7 +4942,7 @@ impl ApiTester { // Retrieve and publish the envelope. let envelope = self .client - .get_validator_execution_payload_envelope::(slot) + .get_validator_execution_payload_envelopes::(slot) .await .unwrap() .data; @@ -4950,7 +4950,7 @@ impl ApiTester { let signed_envelope = self.sign_envelope(envelope, &sk, epoch, &fork, genesis_validators_root); self.client - .post_beacon_execution_payload_envelope(&signed_envelope, fork_name) + .post_beacon_execution_payload_envelopes(&signed_envelope, fork_name) .await .unwrap(); @@ -9558,14 +9558,14 @@ async fn post_validator_proposer_preferences() { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn post_beacon_execution_payload_bid() { +async fn post_beacon_execution_payload_bids() { if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { return; } ApiTester::new_with_hard_forks() .await - .test_post_beacon_execution_payload_bid_json() + .test_post_beacon_execution_payload_bids_json() .await - .test_post_beacon_execution_payload_bid_ssz() + .test_post_beacon_execution_payload_bids_ssz() .await; } diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index e5c66ab5ff..572f9522ee 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -2755,8 +2755,8 @@ impl BeaconNodeHttpClient { opt_response.ok_or(Error::StatusCode(StatusCode::NOT_FOUND)) } - /// `GET v1/validator/execution_payload_envelope/{slot}` - pub async fn get_validator_execution_payload_envelope( + /// `GET v1/validator/execution_payload_envelopes/{slot}` + pub async fn get_validator_execution_payload_envelopes( &self, slot: Slot, ) -> Result>, Error> { @@ -2765,14 +2765,14 @@ impl BeaconNodeHttpClient { path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("validator") - .push("execution_payload_envelope") + .push("execution_payload_envelopes") .push(&slot.to_string()); self.get(path).await } - /// `GET v1/validator/execution_payload_envelope/{slot}` in SSZ format - pub async fn get_validator_execution_payload_envelope_ssz( + /// `GET v1/validator/execution_payload_envelopes/{slot}` in SSZ format + pub async fn get_validator_execution_payload_envelopes_ssz( &self, slot: Slot, ) -> Result, Error> { @@ -2781,7 +2781,7 @@ impl BeaconNodeHttpClient { path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("validator") - .push("execution_payload_envelope") + .push("execution_payload_envelopes") .push(&slot.to_string()); let opt_response = self @@ -2793,8 +2793,8 @@ impl BeaconNodeHttpClient { ExecutionPayloadEnvelope::from_ssz_bytes(&response_bytes).map_err(Error::InvalidSsz) } - /// `POST v1/beacon/execution_payload_envelope` - pub async fn post_beacon_execution_payload_envelope( + /// `POST v1/beacon/execution_payload_envelopes` + pub async fn post_beacon_execution_payload_envelopes( &self, envelope: &SignedExecutionPayloadEnvelope, fork_name: ForkName, @@ -2804,7 +2804,7 @@ impl BeaconNodeHttpClient { path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("beacon") - .push("execution_payload_envelope"); + .push("execution_payload_envelopes"); self.post_generic_with_consensus_version( path, @@ -2817,8 +2817,8 @@ impl BeaconNodeHttpClient { Ok(()) } - /// `POST v1/beacon/execution_payload_envelope` in SSZ format - pub async fn post_beacon_execution_payload_envelope_ssz( + /// `POST v1/beacon/execution_payload_envelopes` in SSZ format + pub async fn post_beacon_execution_payload_envelopes_ssz( &self, envelope: &SignedExecutionPayloadEnvelope, fork_name: ForkName, @@ -2828,7 +2828,7 @@ impl BeaconNodeHttpClient { path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("beacon") - .push("execution_payload_envelope"); + .push("execution_payload_envelopes"); self.post_generic_with_consensus_version_and_ssz_body( path, @@ -2841,8 +2841,8 @@ impl BeaconNodeHttpClient { Ok(()) } - /// `POST v1/beacon/execution_payload_bid` - pub async fn post_beacon_execution_payload_bid( + /// `POST v1/beacon/execution_payload_bids` + pub async fn post_beacon_execution_payload_bids( &self, bid: &SignedExecutionPayloadBid, fork_name: ForkName, @@ -2852,7 +2852,7 @@ impl BeaconNodeHttpClient { path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("beacon") - .push("execution_payload_bid"); + .push("execution_payload_bids"); self.post_generic_with_consensus_version( path, @@ -2865,8 +2865,8 @@ impl BeaconNodeHttpClient { Ok(()) } - /// `POST v1/beacon/execution_payload_bid` in SSZ format - pub async fn post_beacon_execution_payload_bid_ssz( + /// `POST v1/beacon/execution_payload_bids` in SSZ format + pub async fn post_beacon_execution_payload_bids_ssz( &self, bid: &SignedExecutionPayloadBid, fork_name: ForkName, @@ -2876,7 +2876,7 @@ impl BeaconNodeHttpClient { path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("beacon") - .push("execution_payload_bid"); + .push("execution_payload_bids"); self.post_generic_with_consensus_version_and_ssz_body( path, @@ -2889,8 +2889,8 @@ impl BeaconNodeHttpClient { Ok(()) } - /// Path for `v1/beacon/execution_payload_envelope/{block_id}` - pub fn get_beacon_execution_payload_envelope_path( + /// Path for `v1/beacon/execution_payload_envelopes/{block_id}` + pub fn get_beacon_execution_payload_envelopes_path( &self, block_id: BlockId, ) -> Result { @@ -2898,35 +2898,35 @@ impl BeaconNodeHttpClient { path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("beacon") - .push("execution_payload_envelope") + .push("execution_payload_envelopes") .push(&block_id.to_string()); Ok(path) } - /// `GET v1/beacon/execution_payload_envelope/{block_id}` + /// `GET v1/beacon/execution_payload_envelopes/{block_id}` /// /// Returns `Ok(None)` on a 404 error. - pub async fn get_beacon_execution_payload_envelope( + pub async fn get_beacon_execution_payload_envelopes( &self, block_id: BlockId, ) -> Result< Option>>, Error, > { - let path = self.get_beacon_execution_payload_envelope_path(block_id)?; + let path = self.get_beacon_execution_payload_envelopes_path(block_id)?; self.get_opt(path) .await .map(|opt| opt.map(BeaconResponse::ForkVersioned)) } - /// `GET v1/beacon/execution_payload_envelope/{block_id}` in SSZ format + /// `GET v1/beacon/execution_payload_envelopes/{block_id}` in SSZ format /// /// Returns `Ok(None)` on a 404 error. - pub async fn get_beacon_execution_payload_envelope_ssz( + pub async fn get_beacon_execution_payload_envelopes_ssz( &self, block_id: BlockId, ) -> Result>, Error> { - let path = self.get_beacon_execution_payload_envelope_path(block_id)?; + let path = self.get_beacon_execution_payload_envelopes_path(block_id)?; let opt_response = self .get_bytes_opt_accept_header(path, Accept::Ssz, self.timeouts.get_beacon_blocks_ssz) .await?; diff --git a/validator_client/validator_services/src/block_service.rs b/validator_client/validator_services/src/block_service.rs index 1dd1878f4c..06fd14360a 100644 --- a/validator_client/validator_services/src/block_service.rs +++ b/validator_client/validator_services/src/block_service.rs @@ -659,7 +659,7 @@ impl BlockService { .beacon_nodes .first_success(|beacon_node| async move { beacon_node - .get_validator_execution_payload_envelope_ssz::(slot) + .get_validator_execution_payload_envelopes_ssz::(slot) .await .map_err(|e| { BlockError::Recoverable(format!( @@ -702,7 +702,7 @@ impl BlockService { let signed_envelope = signed_envelope.clone(); async move { beacon_node - .post_beacon_execution_payload_envelope_ssz(&signed_envelope, fork_name) + .post_beacon_execution_payload_envelopes_ssz(&signed_envelope, fork_name) .await .map_err(|e| { BlockError::Recoverable(format!( From abe7ca20a9becd341ef226fb040be53eb7876dec Mon Sep 17 00:00:00 2001 From: Alleysira <56925051+Alleysira@users.noreply.github.com> Date: Sat, 6 Jun 2026 04:16:28 +0800 Subject: [PATCH 19/26] fix(network): clear ENR `nfd` field when no next fork is scheduled during runtime transitions (#9131) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit No. But related to #9009 and #8996 - Change the `ForkContext::next_fork_digest()` to return `[u8; 4]` (returning `[0u8; 4]` for "no next fork"). - Update the initialization path and runtime fork transition path accordingly. Added tests: - [x] `test_next_fork_digest` — existing test passes with non-Option return type - [x] `test_next_fork_digest_returns_zero_when_no_next_fork` — init at last BPO fork returns `[0u8; 4]` - [x] `test_next_fork_digest_zero_after_runtime_transition_to_last_fork` — simulates `update_current_fork` to last fork, then verifies zero Co-Authored-By: alleysira <1367108378@qq.com> Co-Authored-By: Alleysira <56925051+Alleysira@users.noreply.github.com> Co-Authored-By: chonghe <44791194+chong-he@users.noreply.github.com> --- .../lighthouse_network/src/discovery/enr.rs | 5 ++- .../lighthouse_network/src/service/mod.rs | 4 +- beacon_node/network/src/service.rs | 5 +-- consensus/types/src/fork/fork_context.rs | 45 +++++++++++++++++-- 4 files changed, 46 insertions(+), 13 deletions(-) diff --git a/beacon_node/lighthouse_network/src/discovery/enr.rs b/beacon_node/lighthouse_network/src/discovery/enr.rs index 01a01d55ab..0735cbb37a 100644 --- a/beacon_node/lighthouse_network/src/discovery/enr.rs +++ b/beacon_node/lighthouse_network/src/discovery/enr.rs @@ -320,11 +320,12 @@ fn compare_enr(local_enr: &Enr, disk_enr: &Enr) -> bool { && (local_enr.udp4().is_none() || local_enr.udp4() == disk_enr.udp4()) && (local_enr.udp6().is_none() || local_enr.udp6() == disk_enr.udp6()) // we need the ATTESTATION_BITFIELD_ENR_KEY and SYNC_COMMITTEE_BITFIELD_ENR_KEY and - // PEERDAS_CUSTODY_GROUP_COUNT_ENR_KEY key to match, otherwise we use a new ENR. This will - // likely only be true for non-validating nodes. + // PEERDAS_CUSTODY_GROUP_COUNT_ENR_KEY and NEXT_FORK_DIGEST_ENR_KEY keys to match, + // otherwise we use a new ENR. This will likely only be true for non-validating nodes. && local_enr.get_decodable::(ATTESTATION_BITFIELD_ENR_KEY) == disk_enr.get_decodable(ATTESTATION_BITFIELD_ENR_KEY) && local_enr.get_decodable::(SYNC_COMMITTEE_BITFIELD_ENR_KEY) == disk_enr.get_decodable(SYNC_COMMITTEE_BITFIELD_ENR_KEY) && local_enr.get_decodable::(PEERDAS_CUSTODY_GROUP_COUNT_ENR_KEY) == disk_enr.get_decodable(PEERDAS_CUSTODY_GROUP_COUNT_ENR_KEY) + && local_enr.get_decodable::(NEXT_FORK_DIGEST_ENR_KEY) == disk_enr.get_decodable(NEXT_FORK_DIGEST_ENR_KEY) } /// Loads enr from the given directory diff --git a/beacon_node/lighthouse_network/src/service/mod.rs b/beacon_node/lighthouse_network/src/service/mod.rs index 097736a010..93c8410490 100644 --- a/beacon_node/lighthouse_network/src/service/mod.rs +++ b/beacon_node/lighthouse_network/src/service/mod.rs @@ -201,9 +201,7 @@ impl Network { // set up a collection of variables accessible outside of the network crate // Create an ENR or load from disk if appropriate - // Per [spec](https://github.com/ethereum/consensus-specs/blob/1baa05e71148b0975e28918ac6022d2256b56f4a/specs/fulu/p2p-interface.md?plain=1#L636-L637) - // `nfd` must be zero-valued when no next fork is scheduled. - let next_fork_digest = ctx.fork_context.next_fork_digest().unwrap_or_default(); + let next_fork_digest = ctx.fork_context.next_fork_digest(); let advertised_cgc = config .advertise_false_custody_group_count diff --git a/beacon_node/network/src/service.rs b/beacon_node/network/src/service.rs index ce54ffc38f..c2e79fe9e8 100644 --- a/beacon_node/network/src/service.rs +++ b/beacon_node/network/src/service.rs @@ -883,10 +883,7 @@ impl NetworkService { fork_context.update_current_fork(*new_fork_name, new_fork_digest, current_epoch); if self.beacon_chain.spec.is_peer_das_scheduled() { - let next_fork_digest = fork_context - .next_fork_digest() - .unwrap_or_else(|| fork_context.current_fork_digest()); - self.libp2p.update_nfd(next_fork_digest); + self.libp2p.update_nfd(fork_context.next_fork_digest()); } self.libp2p.update_fork_version(new_enr_fork_id); diff --git a/consensus/types/src/fork/fork_context.rs b/consensus/types/src/fork/fork_context.rs index 3407689e79..f563578237 100644 --- a/consensus/types/src/fork/fork_context.rs +++ b/consensus/types/src/fork/fork_context.rs @@ -93,14 +93,16 @@ impl ForkContext { pub fn current_fork_digest(&self) -> [u8; 4] { self.current_fork.read().fork_digest } - - /// Returns the next fork digest. If there's no future fork, returns the current fork digest. - pub fn next_fork_digest(&self) -> Option<[u8; 4]> { + /// Per [spec](https://github.com/ethereum/consensus-specs/blob/1baa05e71148b0975e28918ac6022d2256b56f4a/specs/fulu/p2p-interface.md?plain=1#L636-L637) + /// `nfd` must be zero-valued when no next fork is scheduled. + /// Returns the next fork digest. If there's no future fork, returns zero-valued bytes. + pub fn next_fork_digest(&self) -> [u8; 4] { let current_fork_epoch = self.current_fork_epoch(); self.epoch_to_forks .range(current_fork_epoch..) .nth(1) .map(|(_, fork)| fork.fork_digest) + .unwrap_or_default() } /// Updates the `digest_epoch` field to a new digest epoch. @@ -222,11 +224,46 @@ mod tests { let context = ForkContext::new::(electra_slot, genesis_root, &spec); - let next_digest = context.next_fork_digest().unwrap(); + let next_digest = context.next_fork_digest(); let expected_digest = spec.compute_fork_digest(genesis_root, spec.fulu_fork_epoch.unwrap()); assert_eq!(next_digest, expected_digest); } + #[test] + fn test_next_fork_digest_returns_zero_when_no_next_fork() { + let spec = make_chain_spec(); + let genesis_root = Hash256::ZERO; + // Epoch 100 is the last BPO fork in make_chain_spec + let last_bpo_slot = Epoch::new(100).end_slot(E::slots_per_epoch()); + + let context = ForkContext::new::(last_bpo_slot, genesis_root, &spec); + + // No next fork after the last BPO epoch — must return zero bytes per spec + assert_eq!(context.next_fork_digest(), [0u8; 4]); + } + + #[test] + fn test_next_fork_digest_zero_after_runtime_transition_to_last_fork() { + let spec = make_chain_spec(); + let genesis_root = Hash256::ZERO; + // Start at Gloas (epoch 7) + let gloas_epoch = spec.gloas_fork_epoch.unwrap(); + let gloas_slot = gloas_epoch.end_slot(E::slots_per_epoch()); + + let context = ForkContext::new::(gloas_slot, genesis_root, &spec); + + // Before: next fork exists (BPO at epoch 50) + let bpo_50_digest = spec.compute_fork_digest(genesis_root, Epoch::new(50)); + assert_eq!(context.next_fork_digest(), bpo_50_digest); + + // Simulate runtime transition to the last BPO fork (epoch 100) + let last_digest = spec.compute_fork_digest(genesis_root, Epoch::new(100)); + context.update_current_fork(ForkName::Gloas, last_digest, Epoch::new(100)); + + // After: no next fork — must return zero bytes per spec + assert_eq!(context.next_fork_digest(), [0u8; 4]); + } + #[test] fn test_get_fork_from_context_bytes() { let spec = make_chain_spec(); From 65f1a832e44c29afaa1fb0d1e3d87a196ffe3b90 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Sat, 6 Jun 2026 06:51:33 +1000 Subject: [PATCH 20/26] Optimise slow block verification tests (#9274) Reduce CI time for `block_verification` tests that exceed 120s on Fulu/Gloas forks. - Cache the chain segment across tests using `LazyLock` (was rebuilt 10+ times independently) - Reduce chain length from 320 to 192 slots (10 epochs to 6 epochs) - Reduce `BLOCK_INDICES` from 7 to 3 - Reduce `chain_segment_varying_chunk_size` from 5 to 3 AI assisted, self reviewed. Before vs After comparison (1 sample only): * **Before**: 19 tests exceeded the 120s slow threshold. * **After**: zero exceed it. Overall: 1,890s down to 797s (-58%). ``` | Test | Before | After | Change | |------------------------------------------------------|---------|---------|---------| | chain_segment_varying_chunk_size | 239s | 98s | -59% | | invalid_signature_attester_slashing | 175s | 64s | -64% | | invalid_signature_exit | 173s | 62s | -64% | | invalid_signature_deposit | 170s | 60s | -65% | | invalid_signature_attestation | 165s | 62s | -63% | | invalid_signature_proposer_slashing | 161s | 56s | -66% | | block_gossip_verification | 154s | 91s | -41% | | invalid_signature_block_proposal | 151s | 58s | -61% | | invalid_signature_randao_reveal | 149s | 54s | -64% | | invalid_signature_gossip_block | 135s | 46s | -66% | |------------------------------------------------------|---------|---------|---------| | TOTAL | 1890s | 797s | -58% | ``` Co-Authored-By: Jimmy Chen Co-Authored-By: Michael Sproul --- .../beacon_chain/tests/block_verification.rs | 85 +++++++++++-------- 1 file changed, 51 insertions(+), 34 deletions(-) diff --git a/beacon_node/beacon_chain/tests/block_verification.rs b/beacon_node/beacon_chain/tests/block_verification.rs index deadafac36..74a521a79c 100644 --- a/beacon_node/beacon_chain/tests/block_verification.rs +++ b/beacon_node/beacon_chain/tests/block_verification.rs @@ -35,20 +35,30 @@ type E = MainnetEthSpec; // Gloas requires >= 1 validator per slot for PTC committee computation, so >= 32 for MainnetEthSpec. const VALIDATOR_COUNT: usize = 32; -const CHAIN_SEGMENT_LENGTH: usize = 64 * 5; -const BLOCK_INDICES: &[usize] = &[0, 1, 32, 64, 68 + 1, 129, CHAIN_SEGMENT_LENGTH - 1]; +const CHAIN_SEGMENT_LENGTH: usize = 32 * 6; +const BLOCK_INDICES: &[usize] = &[1, 32, 64]; /// A cached set of keys. static KEYPAIRS: LazyLock> = LazyLock::new(|| types::test_utils::generate_deterministic_keypairs(VALIDATOR_COUNT)); // TODO(#8633): Delete this unnecessary enum and refactor this file to use `AvailableBlockData` instead. +#[derive(Clone)] enum DataSidecars { Blobs(BlobSidecarList), DataColumns(Vec>), } -async fn get_chain_segment() -> (Vec>, Vec>>) { +type ChainSegmentData = (Vec>, Vec>>); + +static CHAIN_SEGMENT: LazyLock> = + LazyLock::new(tokio::sync::OnceCell::new); + +async fn get_chain_segment() -> &'static ChainSegmentData { + CHAIN_SEGMENT.get_or_init(build_chain_segment).await +} + +async fn build_chain_segment() -> ChainSegmentData { // The assumption that you can re-import a block based on what you have in your DB // is no longer true, as fullnodes stores less than what they sample. // We use a supernode here to build a chain segment. @@ -365,9 +375,9 @@ async fn chain_segment_full_segment() { } let harness = get_harness(VALIDATOR_COUNT, NodeCustodyType::Fullnode); let (chain_segment, chain_segment_blobs) = get_chain_segment().await; - store_envelopes_for_chain_segment(&chain_segment, &harness); + store_envelopes_for_chain_segment(chain_segment, &harness); let blocks: Vec> = - chain_segment_blocks(&chain_segment, &chain_segment_blobs, harness.chain.clone()) + chain_segment_blocks(chain_segment, chain_segment_blobs, harness.chain.clone()) .into_iter() .collect(); @@ -391,7 +401,7 @@ async fn chain_segment_full_segment() { .into_block_error() .expect("should import chain segment"); - update_fork_choice_with_envelopes(&chain_segment, &harness); + update_fork_choice_with_envelopes(chain_segment, &harness); harness.chain.recompute_head_at_current_slot().await; assert_eq!( @@ -410,13 +420,13 @@ async fn chain_segment_varying_chunk_size() { let (chain_segment, chain_segment_blobs) = get_chain_segment().await; let harness = get_harness(VALIDATOR_COUNT, NodeCustodyType::Fullnode); let blocks: Vec> = - chain_segment_blocks(&chain_segment, &chain_segment_blobs, harness.chain.clone()) + chain_segment_blocks(chain_segment, chain_segment_blobs, harness.chain.clone()) .into_iter() .collect(); - for chunk_size in &[1, 2, 31, 32, 33] { + for chunk_size in &[1, 32, 33] { let harness = get_harness(VALIDATOR_COUNT, NodeCustodyType::Fullnode); - store_envelopes_for_chain_segment(&chain_segment, &harness); + store_envelopes_for_chain_segment(chain_segment, &harness); harness .chain @@ -432,7 +442,7 @@ async fn chain_segment_varying_chunk_size() { .unwrap_or_else(|_| panic!("should import chain segment of len {}", chunk_size)); } - update_fork_choice_with_envelopes(&chain_segment, &harness); + update_fork_choice_with_envelopes(chain_segment, &harness); harness.chain.recompute_head_at_current_slot().await; assert_eq!( @@ -457,7 +467,7 @@ async fn chain_segment_non_linear_parent_roots() { * Test with a block removed. */ let mut blocks: Vec> = - chain_segment_blocks(&chain_segment, &chain_segment_blobs, harness.chain.clone()) + chain_segment_blocks(chain_segment, chain_segment_blobs, harness.chain.clone()) .into_iter() .collect(); blocks.remove(2); @@ -478,7 +488,7 @@ async fn chain_segment_non_linear_parent_roots() { * Test with a modified parent root. */ let mut blocks: Vec> = - chain_segment_blocks(&chain_segment, &chain_segment_blobs, harness.chain.clone()) + chain_segment_blocks(chain_segment, chain_segment_blobs, harness.chain.clone()) .into_iter() .collect(); @@ -520,7 +530,7 @@ async fn chain_segment_non_linear_slots() { */ let mut blocks: Vec> = - chain_segment_blocks(&chain_segment, &chain_segment_blobs, harness.chain.clone()) + chain_segment_blocks(chain_segment, chain_segment_blobs, harness.chain.clone()) .into_iter() .collect(); let (mut block, signature) = blocks[3].as_block().clone().deconstruct(); @@ -550,7 +560,7 @@ async fn chain_segment_non_linear_slots() { */ let mut blocks: Vec> = - chain_segment_blocks(&chain_segment, &chain_segment_blobs, harness.chain.clone()) + chain_segment_blocks(chain_segment, chain_segment_blobs, harness.chain.clone()) .into_iter() .collect(); let (mut block, signature) = blocks[3].as_block().clone().deconstruct(); @@ -694,7 +704,7 @@ async fn invalid_signature_gossip_block() { let (chain_segment, chain_segment_blobs) = get_chain_segment().await; for &block_index in BLOCK_INDICES { // Ensure the block will be rejected if imported on its own (without gossip checking). - let harness = get_invalid_sigs_harness(&chain_segment).await; + let harness = get_invalid_sigs_harness(chain_segment).await; let mut snapshots = chain_segment.clone(); let (block, _) = snapshots[block_index] .beacon_block @@ -753,7 +763,7 @@ async fn invalid_signature_block_proposal() { } let (chain_segment, chain_segment_blobs) = get_chain_segment().await; for &block_index in BLOCK_INDICES { - let harness = get_invalid_sigs_harness(&chain_segment).await; + let harness = get_invalid_sigs_harness(chain_segment).await; let mut snapshots = chain_segment.clone(); let (block, _) = snapshots[block_index] .beacon_block @@ -794,9 +804,10 @@ async fn invalid_signature_randao_reveal() { if fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { return; } - let (chain_segment, mut chain_segment_blobs) = get_chain_segment().await; + let (chain_segment, ref_blobs) = get_chain_segment().await; + let mut chain_segment_blobs = ref_blobs.clone(); for &block_index in BLOCK_INDICES { - let harness = get_invalid_sigs_harness(&chain_segment).await; + let harness = get_invalid_sigs_harness(chain_segment).await; let mut snapshots = chain_segment.clone(); let (mut block, signature) = snapshots[block_index] .beacon_block @@ -809,7 +820,7 @@ async fn invalid_signature_randao_reveal() { update_parent_roots(&mut snapshots, &mut chain_segment_blobs); update_proposal_signatures(&mut snapshots, &harness); assert_invalid_signature( - &chain_segment, + chain_segment, &chain_segment_blobs, &harness, block_index, @@ -826,9 +837,10 @@ async fn invalid_signature_proposer_slashing() { if fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { return; } - let (chain_segment, mut chain_segment_blobs) = get_chain_segment().await; + let (chain_segment, ref_blobs) = get_chain_segment().await; + let mut chain_segment_blobs = ref_blobs.clone(); for &block_index in BLOCK_INDICES { - let harness = get_invalid_sigs_harness(&chain_segment).await; + let harness = get_invalid_sigs_harness(chain_segment).await; let mut snapshots = chain_segment.clone(); let (mut block, signature) = snapshots[block_index] .beacon_block @@ -855,7 +867,7 @@ async fn invalid_signature_proposer_slashing() { update_parent_roots(&mut snapshots, &mut chain_segment_blobs); update_proposal_signatures(&mut snapshots, &harness); assert_invalid_signature( - &chain_segment, + chain_segment, &chain_segment_blobs, &harness, block_index, @@ -872,9 +884,10 @@ async fn invalid_signature_attester_slashing() { if fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { return; } - let (chain_segment, mut chain_segment_blobs) = get_chain_segment().await; + let (chain_segment, ref_blobs) = get_chain_segment().await; + let mut chain_segment_blobs = ref_blobs.clone(); for &block_index in BLOCK_INDICES { - let harness = get_invalid_sigs_harness(&chain_segment).await; + let harness = get_invalid_sigs_harness(chain_segment).await; let mut snapshots = chain_segment.clone(); let fork_name = harness.chain.spec.fork_name_at_slot::(Slot::new(0)); @@ -980,7 +993,7 @@ async fn invalid_signature_attester_slashing() { update_parent_roots(&mut snapshots, &mut chain_segment_blobs); update_proposal_signatures(&mut snapshots, &harness); assert_invalid_signature( - &chain_segment, + chain_segment, &chain_segment_blobs, &harness, block_index, @@ -997,11 +1010,12 @@ async fn invalid_signature_attestation() { if fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { return; } - let (chain_segment, mut chain_segment_blobs) = get_chain_segment().await; + let (chain_segment, ref_blobs) = get_chain_segment().await; + let mut chain_segment_blobs = ref_blobs.clone(); let mut checked_attestation = false; for &block_index in BLOCK_INDICES { - let harness = get_invalid_sigs_harness(&chain_segment).await; + let harness = get_invalid_sigs_harness(chain_segment).await; let mut snapshots = chain_segment.clone(); let (mut block, signature) = snapshots[block_index] .beacon_block @@ -1049,7 +1063,7 @@ async fn invalid_signature_attestation() { update_parent_roots(&mut snapshots, &mut chain_segment_blobs); update_proposal_signatures(&mut snapshots, &harness); assert_invalid_signature( - &chain_segment, + chain_segment, &chain_segment_blobs, &harness, block_index, @@ -1069,10 +1083,11 @@ async fn invalid_signature_attestation() { #[tokio::test] async fn invalid_signature_deposit() { - let (chain_segment, mut chain_segment_blobs) = get_chain_segment().await; + let (chain_segment, ref_blobs) = get_chain_segment().await; + let mut chain_segment_blobs = ref_blobs.clone(); for &block_index in BLOCK_INDICES { // Note: an invalid deposit signature is permitted! - let harness = get_invalid_sigs_harness(&chain_segment).await; + let harness = get_invalid_sigs_harness(chain_segment).await; let mut snapshots = chain_segment.clone(); let deposit = Deposit { proof: vec![Hash256::zero(); DEPOSIT_TREE_DEPTH + 1] @@ -1126,9 +1141,10 @@ async fn invalid_signature_exit() { if fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { return; } - let (chain_segment, mut chain_segment_blobs) = get_chain_segment().await; + let (chain_segment, ref_blobs) = get_chain_segment().await; + let mut chain_segment_blobs = ref_blobs.clone(); for &block_index in BLOCK_INDICES { - let harness = get_invalid_sigs_harness(&chain_segment).await; + let harness = get_invalid_sigs_harness(chain_segment).await; let mut snapshots = chain_segment.clone(); let epoch = snapshots[block_index].beacon_state.current_epoch(); let (mut block, signature) = snapshots[block_index] @@ -1152,7 +1168,7 @@ async fn invalid_signature_exit() { update_parent_roots(&mut snapshots, &mut chain_segment_blobs); update_proposal_signatures(&mut snapshots, &harness); assert_invalid_signature( - &chain_segment, + chain_segment, &chain_segment_blobs, &harness, block_index, @@ -1173,7 +1189,8 @@ fn unwrap_err(result: Result) -> U { #[tokio::test] async fn block_gossip_verification() { let harness = get_harness(VALIDATOR_COUNT, NodeCustodyType::Fullnode); - let (chain_segment, chain_segment_blobs) = get_chain_segment().await; + let (chain_segment, ref_blobs) = get_chain_segment().await; + let chain_segment_blobs = ref_blobs.clone(); let block_index = CHAIN_SEGMENT_LENGTH - 2; From 8e4df4ababf68d7c58973177013d78447b9a208d Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Sat, 6 Jun 2026 01:52:45 +0200 Subject: [PATCH 21/26] Simplify lookup sync da_checker oracle (#9428) Implementing gloas lookup sync is currently incompatible with the `GossipBlockProcessResult` mechanism. Today it's implemented such that if we receive a sucessful `GossipBlockProcessResult` we directly mark the lookup as Complete and delete it. In Gloas we can't delete a lookup after block import, as we may still have FULL child awaiting the payload. IMO this `GossipBlockProcessResult` brings a lot of headache and edge cases that we can just live without. Also the `reset_request` business is nasty and can easily leave the lookup in a bad state. If we get rid of `GossipBlockProcessResult` we only pay the following performance penalty: - Lookup is created exactly while the block's payload is being execution validated - (new degradation) we download the block again - send the block for processing but the duplicate cache prevents double execution So in the worst case we spend a few KBs of extra download bandwidth. Remember each block is downloaded 8x times through gossip in the happy case. Co-Authored-By: dapplion <35266934+dapplion@users.noreply.github.com> Co-Authored-By: Pawan Dhananjay --- .../gossip_methods.rs | 23 ---- .../src/network_beacon_processor/tests.rs | 104 +---------------- .../network/src/sync/block_lookups/mod.rs | 33 ------ .../sync/block_lookups/single_block_lookup.rs | 6 - beacon_node/network/src/sync/manager.rs | 13 --- .../network/src/sync/network_context.rs | 33 ++---- beacon_node/network/src/sync/tests/lookups.rs | 107 ------------------ 7 files changed, 13 insertions(+), 306 deletions(-) diff --git a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs index 68b41cab5e..29e43b18c2 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -922,14 +922,6 @@ impl NetworkBeaconProcessor { &metrics::BEACON_BLOB_DELAY_FULL_VERIFICATION, processing_start_time.elapsed().as_millis() as i64, ); - - // If a block is in the da_checker, sync maybe awaiting for an event when block is finally - // imported. A block can become imported both after processing a block or data column. If - // 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!( @@ -1354,16 +1346,6 @@ impl NetworkBeaconProcessor { // contributing to the partial. } } - - // If a block is in the da_checker, sync maybe awaiting for an event when block is finally - // imported. A block can become imported both after processing a block or data column. If a - // importing a block results in `Imported`, notify. Do not notify of data column errors. - if matches!(result, Ok(AvailabilityProcessingStatus::Imported(_))) { - self.send_sync_message(SyncMessage::GossipBlockProcessResult { - block_root, - imported: true, - }); - } } async fn check_reconstruction_trigger(self: &Arc, slot: Slot, block_root: &Hash256) { @@ -1898,11 +1880,6 @@ impl NetworkBeaconProcessor { if let Err(e) = &result { self.maybe_store_invalid_block(&invalid_block_storage, block_root, &block, e); } - - self.send_sync_message(SyncMessage::GossipBlockProcessResult { - block_root, - imported: matches!(result, Ok(AvailabilityProcessingStatus::Imported(_))), - }); } pub fn process_gossip_voluntary_exit( diff --git a/beacon_node/network/src/network_beacon_processor/tests.rs b/beacon_node/network/src/network_beacon_processor/tests.rs index ad98851532..8ccfe38fa3 100644 --- a/beacon_node/network/src/network_beacon_processor/tests.rs +++ b/beacon_node/network/src/network_beacon_processor/tests.rs @@ -6,7 +6,7 @@ use crate::{ ChainSegmentProcessId, DuplicateCache, InvalidBlockStorage, NetworkBeaconProcessor, }, service::NetworkMessage, - sync::{SyncMessage, manager::BlockProcessType}, + sync::manager::BlockProcessType, }; use beacon_chain::block_verification_types::LookupBlock; use beacon_chain::custody_context::NodeCustodyType; @@ -76,7 +76,6 @@ struct TestRig { beacon_processor_tx: BeaconProcessorSend, work_journal_rx: mpsc::Receiver<&'static str>, network_rx: mpsc::UnboundedReceiver>, - sync_rx: mpsc::UnboundedReceiver>, duplicate_cache: DuplicateCache, network_beacon_processor: Arc>, _harness: BeaconChainHarness, @@ -270,7 +269,7 @@ impl TestRig { beacon_processor_rx, } = BeaconProcessorChannels::new(&beacon_processor_config); - let (sync_tx, sync_rx) = mpsc::unbounded_channel(); + let (sync_tx, _sync_rx) = mpsc::unbounded_channel(); // Default metadata let meta_data = if spec.is_peer_das_scheduled() { @@ -375,7 +374,6 @@ impl TestRig { beacon_processor_tx, work_journal_rx, network_rx, - sync_rx, duplicate_cache, network_beacon_processor, _harness: harness, @@ -844,45 +842,6 @@ impl TestRig { Some(events) } } - - /// Listen for sync messages and collect them for a specified duration or until reaching a count. - /// - /// Returns None if no messages were received, or Some(Vec) containing the received messages. - pub async fn receive_sync_messages_with_timeout( - &mut self, - timeout: Duration, - count: Option, - ) -> Option>> { - let mut events = vec![]; - - let timeout_future = tokio::time::sleep(timeout); - tokio::pin!(timeout_future); - - loop { - // Break if we've received the requested count of messages - if let Some(target_count) = count - && events.len() >= target_count - { - break; - } - - tokio::select! { - _ = &mut timeout_future => break, - maybe_msg = self.sync_rx.recv() => { - match maybe_msg { - Some(msg) => events.push(msg), - None => break, // Channel closed - } - } - } - } - - if events.is_empty() { - None - } else { - Some(events) - } - } } fn junk_peer_id() -> PeerId { @@ -1862,65 +1821,6 @@ async fn test_blobs_by_root_post_fulu_should_return_empty() { assert_eq!(0, actual_count); } -/// Ensure that data column processing that results in block import sends a sync notification -#[tokio::test] -async fn test_data_column_import_notifies_sync() { - if test_spec::().fulu_fork_epoch.is_none() { - return; - } - - let mut rig = TestRig::new(SMALL_CHAIN).await; - let block_root = rig.next_block.canonical_root(); - - // Enqueue the block first to prepare for data column processing - rig.enqueue_gossip_block(); - rig.assert_event_journal_completes(&[WorkType::GossipBlock]) - .await; - rig.receive_sync_messages_with_timeout(Duration::from_millis(100), Some(1)) - .await - .expect("should receive sync message"); - - // Enqueue data columns which should trigger block import when complete - let num_data_columns = rig.next_data_columns.as_ref().map(|c| c.len()).unwrap_or(0); - if num_data_columns > 0 { - for i in 0..num_data_columns { - rig.enqueue_gossip_data_columns(i); - rig.assert_event_journal_completes(&[WorkType::GossipDataColumnSidecar]) - .await; - } - - // Verify block import succeeded - assert_eq!( - rig.head_root(), - block_root, - "block should be imported and become head" - ); - - // Check that sync was notified of the successful import - let sync_messages = rig - .receive_sync_messages_with_timeout(Duration::from_millis(100), Some(1)) - .await - .expect("should receive sync message"); - - // Verify we received the expected GossipBlockProcessResult message - assert_eq!( - sync_messages.len(), - 1, - "should receive exactly one sync message" - ); - match &sync_messages[0] { - SyncMessage::GossipBlockProcessResult { - block_root: msg_block_root, - imported, - } => { - assert_eq!(*msg_block_root, block_root, "block root should match"); - assert!(*imported, "block should be marked as imported"); - } - other => panic!("expected GossipBlockProcessResult, got {:?}", other), - } - } -} - #[tokio::test] async fn test_data_columns_by_range_request_only_returns_requested_columns() { if test_spec::().fulu_fork_epoch.is_none() { diff --git a/beacon_node/network/src/sync/block_lookups/mod.rs b/beacon_node/network/src/sync/block_lookups/mod.rs index a265373e3f..0cbeb5ee4e 100644 --- a/beacon_node/network/src/sync/block_lookups/mod.rs +++ b/beacon_node/network/src/sync/block_lookups/mod.rs @@ -482,39 +482,6 @@ impl BlockLookups { self.on_lookup_result(lookup_id, lookup_result, "processing_result", cx); } - pub fn on_external_processing_result( - &mut self, - block_root: Hash256, - imported: bool, - cx: &mut SyncNetworkContext, - ) { - let Some((id, lookup)) = self - .single_block_lookups - .iter_mut() - .find(|(_, lookup)| lookup.is_for_block(block_root)) - else { - // Ok to ignore gossip process events - return; - }; - - let lookup_result = if imported { - Ok(LookupResult::Completed) - } else { - // A lookup may be in the following state: - // - Block awaiting processing from a different source - // - Blobs downloaded processed, and inserted into the da_checker - // - // At this point the block fails processing (e.g. execution engine offline) and it is - // removed from the da_checker. Note that ALL components are removed from the da_checker - // so when we re-download and process the block we get the error - // MissingComponentsAfterAllProcessed and get stuck. - lookup.reset_requests(); - lookup.continue_requests(cx) - }; - let id = *id; - self.on_lookup_result(id, lookup_result, "external_processing_result", cx); - } - /// Makes progress on the immediate children of `block_root` pub fn continue_child_lookups(&mut self, block_root: Hash256, cx: &mut SyncNetworkContext) { let mut lookup_results = vec![]; // < need to buffer lookup results to not re-borrow &mut self diff --git a/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs b/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs index 8eb58da4e6..157da5d806 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 @@ -139,12 +139,6 @@ impl SingleBlockLookup { } } - /// Reset the status of all requests (used on block processing failure) - pub fn reset_requests(&mut self) { - self.block_request = BlockRequest::new(); - self.data_request = DataRequest::WaitingForBlock; - } - /// Return the slot of this lookup's block if it's currently cached pub fn peek_downloaded_block_slot(&self) -> Option { self.block_request diff --git a/beacon_node/network/src/sync/manager.rs b/beacon_node/network/src/sync/manager.rs index 166c65b6e1..66bb13ae98 100644 --- a/beacon_node/network/src/sync/manager.rs +++ b/beacon_node/network/src/sync/manager.rs @@ -182,11 +182,6 @@ pub enum SyncMessage { process_type: BlockProcessType, result: BlockProcessingResult, }, - - /// A gossip-received component has completed processing and the block may now be imported. - /// In Fulu this is sent after block or blob processing. In Gloas this is also sent after - /// data column or payload envelope processing triggers availability. - GossipBlockProcessResult { block_root: Hash256, imported: bool }, } /// The type of processing specified for a received block. @@ -907,14 +902,6 @@ impl SyncManager { } => self .block_lookups .on_processing_result(process_type, result, &mut self.network), - SyncMessage::GossipBlockProcessResult { - block_root, - imported, - } => self.block_lookups.on_external_processing_result( - block_root, - imported, - &mut self.network, - ), SyncMessage::BatchProcessed { sync_type, result } => match sync_type { ChainSegmentProcessId::RangeBatchId(chain_id, epoch) => { self.range_sync.handle_block_process_result( diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index 1e35c0a72f..dfeb8d8f12 100644 --- a/beacon_node/network/src/sync/network_context.rs +++ b/beacon_node/network/src/sync/network_context.rs @@ -53,8 +53,8 @@ use task_executor::TaskExecutor; use tokio::sync::mpsc; use tracing::{Span, debug, debug_span, error, warn}; use types::{ - BlobSidecar, BlockImportSource, ColumnIndex, DataColumnSidecar, DataColumnSidecarList, EthSpec, - ForkContext, Hash256, SignedBeaconBlock, SignedExecutionPayloadEnvelope, Slot, + BlobSidecar, ColumnIndex, DataColumnSidecar, DataColumnSidecarList, EthSpec, ForkContext, + Hash256, SignedBeaconBlock, SignedExecutionPayloadEnvelope, Slot, }; pub mod custody; @@ -849,26 +849,15 @@ impl SyncNetworkContext { match self.chain.get_block_process_status(&block_root) { // Unknown block, continue request to download BlockProcessStatus::Unknown => {} - // Block is known and currently processing. Imports from gossip and HTTP API insert the - // block in the da_cache. However, HTTP API is unable to notify sync when it completes - // block import. Returning `Pending` here will result in stuck lookups if the block is - // importing from sync. - BlockProcessStatus::NotValidated(_, source) => match source { - BlockImportSource::Gossip => { - // Lookup sync event safety: If the block is currently in the processing cache, we - // are guaranteed to receive a `SyncMessage::GossipBlockProcessResult` that will - // make progress on this lookup - return Ok(LookupRequestResult::Pending("block in processing cache")); - } - BlockImportSource::Lookup - | BlockImportSource::RangeSync - | BlockImportSource::HttpApi => { - // Lookup, RangeSync or HttpApi block import don't emit the GossipBlockProcessResult - // event. If a lookup happens to be created during block import from one of - // those sources just import the block twice. Otherwise the lookup will get - // stuck. Double imports are fine, they just waste resources. - } - }, + // Block is known but processing. The block may turn out to be invalid, so we want sync to + // NOT mark the request as complete yet. The ideal flow would be: + // - Wait for processing to complete + // - Only if there is an error re-download and re-process + // But implementing this introduces complexity and the risk for the lookup to get stuck. + // Instead we always re-download the block eagerly and de-duplicate the processing. So in + // the happy case we just download the block again if the lookup is created while execution + // processing the block. + BlockProcessStatus::NotValidated(..) => {} // Block is fully validated. If it's not yet imported it's waiting for missing block // components. Consider this request completed and do nothing. BlockProcessStatus::ExecutionValidated(block) => { diff --git a/beacon_node/network/src/sync/tests/lookups.rs b/beacon_node/network/src/sync/tests/lookups.rs index 5642f7846a..91227d77f8 100644 --- a/beacon_node/network/src/sync/tests/lookups.rs +++ b/beacon_node/network/src/sync/tests/lookups.rs @@ -1235,12 +1235,6 @@ impl TestRig { self.assert_empty_network(); } - fn assert_pending_lookup_sync(&self) { - assert!(self.created_lookups() > 0, "no created lookups"); - assert_eq!(self.dropped_lookups(), 0, "some dropped lookups"); - assert_eq!(self.completed_lookups(), 0, "some completed lookups"); - } - /// Assert there is at least one range sync chain created and that all sync chains completed pub(super) fn assert_successful_range_sync(&self) { assert!( @@ -1330,15 +1324,6 @@ impl TestRig { 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() { - Some(Self::default()) - } else { - None - } - } - pub fn new_fulu_peer_test(fulu_test_type: FuluTestType) -> Option { genesis_fork().fulu_enabled().then(|| { Self::new(TestRigConfig { @@ -1673,56 +1658,6 @@ impl TestRig { } } - fn insert_block_to_da_checker_as_pre_execution(&mut self, block: Arc>) { - self.log(&format!( - "Inserting block to availability_cache as pre_execution_block {:?}", - block.canonical_root() - )); - self.harness - .chain - .data_availability_checker - .put_pre_execution_block(block.canonical_root(), block, BlockImportSource::Gossip) - .unwrap(); - } - - fn simulate_block_gossip_processing_becomes_invalid(&mut self, block_root: Hash256) { - self.log(&format!( - "Marking block {block_root:?} in da_checker as execution error" - )); - self.harness - .chain - .data_availability_checker - .remove_block_on_execution_error(&block_root); - - self.send_sync_message(SyncMessage::GossipBlockProcessResult { - block_root, - imported: false, - }); - } - - async fn simulate_block_gossip_processing_becomes_valid( - &mut self, - block: Arc>, - ) { - let block_root = block.canonical_root(); - - match self.import_block_to_da_checker(block).await { - AvailabilityProcessingStatus::Imported(block_root) => { - self.log(&format!( - "insert block to da_checker and it imported {block_root:?}" - )); - } - AvailabilityProcessingStatus::MissingComponents(_, _) => { - panic!("block not imported after adding to da_checker"); - } - } - - self.send_sync_message(SyncMessage::GossipBlockProcessResult { - block_root, - imported: false, - }); - } - fn requests_count(&self) -> HashMap<&'static str, usize> { let mut requests_count = HashMap::new(); for (request, _) in &self.requests { @@ -2294,48 +2229,6 @@ async fn block_in_da_checker_skips_download() { ); } -#[tokio::test] -async fn block_in_processing_cache_becomes_invalid() { - let Some(mut r) = TestRig::new_after_deneb_before_fulu() else { - return; - }; - r.build_chain(1).await; - let block = r.block_at_slot(1); - r.insert_block_to_da_checker_as_pre_execution(block.clone()); - r.trigger_with_last_block(); - r.simulate(SimulateConfig::happy_path()).await; - r.assert_pending_lookup_sync(); - // Here the only active lookup is waiting for the block to finish processing - - // Simulate invalid block, removing it from processing cache - r.simulate_block_gossip_processing_becomes_invalid(block.canonical_root()); - // Should download block, then issue blobs request - r.simulate(SimulateConfig::happy_path()).await; - r.assert_successful_lookup_sync(); -} - -#[tokio::test] -async fn block_in_processing_cache_becomes_valid_imported() { - let Some(mut r) = TestRig::new_after_deneb_before_fulu() else { - return; - }; - r.build_chain(1).await; - let block = r.block_at_slot(1); - r.insert_block_to_da_checker_as_pre_execution(block.clone()); - r.trigger_with_last_block(); - r.simulate(SimulateConfig::happy_path()).await; - r.assert_pending_lookup_sync(); - // Here the only active lookup is waiting for the block to finish processing - - // Resolve the block from processing step - r.simulate_block_gossip_processing_becomes_valid(block) - .await; - // Should not trigger block or blob request - r.assert_empty_network(); - // Resolve blob and expect lookup completed - r.assert_no_active_lookups(); -} - macro_rules! fulu_peer_matrix_tests { ( [$($name:ident => $variant:expr),+ $(,)?] From ebe5ded2fa91fc5e9e4fdcfbb39bebaa794d6dba Mon Sep 17 00:00:00 2001 From: Daniel Knopik <107140945+dknopik@users.noreply.github.com> Date: Tue, 9 Jun 2026 19:38:55 +0200 Subject: [PATCH 22/26] Fix sending partials from immediately complete columns (#9433) The initial partial send after getBlobs only sends incomplete partial columns, causing issues as we do not propagate these columns. Return complete and incomplete columns from `get_columns_and_mark_as_local_fetched`, and convert any complete columns to partials if necessary. Send all columns retrieved this way after getBlobs Co-Authored-By: Daniel Knopik --- .../src/partial_data_column_assembler.rs | 55 +++++++++++++------ .../src/network_beacon_processor/mod.rs | 32 +++++++++-- 2 files changed, 66 insertions(+), 21 deletions(-) diff --git a/beacon_node/beacon_chain/src/partial_data_column_assembler.rs b/beacon_node/beacon_chain/src/partial_data_column_assembler.rs index 0ce754c8a0..3cf9a320d7 100644 --- a/beacon_node/beacon_chain/src/partial_data_column_assembler.rs +++ b/beacon_node/beacon_chain/src/partial_data_column_assembler.rs @@ -204,14 +204,16 @@ impl PartialDataColumnAssembler { .cloned() } - /// Get all current partials for a block for publishing after fetching local blobs. - /// To unlock future publishing, mark blobs as fetched locally. - /// We do this within one write lock to avoid useless double publishes. - pub fn get_partials_and_mark_as_local_fetched( + /// Get all current columns for a block (complete *and* incomplete) for publishing after + /// fetching local blobs. + /// + /// To unlock future publishing, mark blobs as fetched locally. We do this within one write + /// lock to avoid useless double publishes. + pub fn get_columns_and_mark_as_local_fetched( &self, block_root: Hash256, header: &Arc>, - ) -> Vec> { + ) -> Vec> { let mut assemblies = self.assemblies.write(); let assembly = assemblies.get_or_insert_mut(block_root, || PartialAssembly { header: header.clone(), @@ -221,17 +223,7 @@ impl PartialDataColumnAssembler { assembly.has_local_blobs = true; - assembly - .columns - .values() - .filter_map(|value| { - if let AssemblyColumn::Incomplete(partial) = value { - Some(partial.clone()) - } else { - None - } - }) - .collect() + assembly.columns.values().cloned().collect() } /// Get header for a block if we have an active assembly @@ -473,6 +465,37 @@ mod tests { assert_eq!(result.updated_partials[0].index(), 0); } + #[test] + fn get_columns_returns_complete_and_incomplete() { + let assembler = make_assembler(); + let root = Hash256::repeat_byte(1); + let header = Arc::new(make_header(4)); + + // One complete column (all cells present) and one still-incomplete column. + let complete = make_partial(root, 0, 4, &[0, 1, 2, 3]); + let incomplete = make_partial(root, 1, 4, &[0, 1]); + assembler.merge_partials(root, vec![complete, incomplete], header.clone()); + + // Both must be returned for seeding. Previously the complete column was dropped, so it was + // published as an empty placeholder and never served to peers. + let columns = assembler.get_columns_and_mark_as_local_fetched(root, &header); + assert_eq!(columns.len(), 2); + assert_eq!( + columns + .iter() + .filter(|c| matches!(c, AssemblyColumn::Complete(_))) + .count(), + 1 + ); + assert_eq!( + columns + .iter() + .filter(|c| matches!(c, AssemblyColumn::Incomplete(_))) + .count(), + 1 + ); + } + // -- mark_as_complete tests -- #[test] diff --git a/beacon_node/network/src/network_beacon_processor/mod.rs b/beacon_node/network/src/network_beacon_processor/mod.rs index f3c773eb25..a9579caaeb 100644 --- a/beacon_node/network/src/network_beacon_processor/mod.rs +++ b/beacon_node/network/src/network_beacon_processor/mod.rs @@ -6,6 +6,7 @@ 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::partial_data_column_assembler::AssemblyColumn; use beacon_chain::test_utils::{BeaconChainHarness, EphemeralHarnessType}; use beacon_chain::{AvailabilityProcessingStatus, BeaconChain, BeaconChainTypes, BlockError}; use beacon_processor::{ @@ -972,14 +973,35 @@ impl NetworkBeaconProcessor { // Publish partial columns without eager send // TODO(gloas): implement publish partial columns without eager send if let Some(assembler) = self.chain.data_availability_checker.partial_assembler() { - let columns = assembler.get_partials_and_mark_as_local_fetched(block_root, &header); + let columns = assembler.get_columns_and_mark_as_local_fetched(block_root, &header); + // Republish both complete and incomplete columns as partials + let columns: Vec<_> = columns + .into_iter() + .filter_map(|column| match column { + AssemblyColumn::Incomplete(partial) => Some(partial.into_inner()), + AssemblyColumn::Complete(full) => { + let DataColumnSidecar::Fulu(fulu) = full.as_data_column() else { + return None; + }; + match fulu.to_partial() { + Ok(partial) => Some(Arc::new(partial)), + Err(err) => { + error!( + %block_root, + column_index = %full.index(), + ?err, + "Failed to convert complete column to partial for re-seeding" + ); + None + } + } + } + }) + .collect(); if !columns.is_empty() { debug!(block = %block_root, "Publishing all partials after getBlobs"); self.send_network_message(NetworkMessage::PublishPartialColumns { - columns: columns - .into_iter() - .map(|partial| partial.into_inner()) - .collect(), + columns, header, }); } else { From 47e0901965fd643b40882993477972fb6a2d9d15 Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Wed, 10 Jun 2026 03:41:26 +0200 Subject: [PATCH 23/26] Gloas lookup sync (#9155) Co-Authored-By: dapplion <35266934+dapplion@users.noreply.github.com> Co-Authored-By: Pawan Dhananjay --- Makefile | 9 +- beacon_node/beacon_chain/src/beacon_chain.rs | 6 +- .../beacon_chain/src/block_verification.rs | 6 + .../beacon_chain/tests/block_verification.rs | 2 +- .../lighthouse_network/src/rpc/protocol.rs | 2 +- .../gossip_methods.rs | 32 +- .../network_beacon_processor/sync_methods.rs | 11 +- .../src/network_beacon_processor/tests.rs | 26 +- .../network/src/sync/backfill_sync/mod.rs | 2 +- .../network/src/sync/block_lookups/mod.rs | 150 +++-- .../src/sync/block_lookups/parent_chain.rs | 2 +- .../sync/block_lookups/single_block_lookup.rs | 377 ++++++++++++- .../src/sync/block_sidecar_coupling.rs | 24 + beacon_node/network/src/sync/manager.rs | 24 +- .../network/src/sync/network_context.rs | 31 +- .../src/sync/network_context/custody.rs | 9 +- beacon_node/network/src/sync/tests/lookups.rs | 511 +++++++++++++++--- beacon_node/network/src/sync/tests/mod.rs | 6 +- beacon_node/network/src/sync/tests/range.rs | 38 +- 19 files changed, 1075 insertions(+), 193 deletions(-) diff --git a/Makefile b/Makefile index 3c00883ce9..94ad55bf6b 100644 --- a/Makefile +++ b/Makefile @@ -30,17 +30,15 @@ TEST_FEATURES ?= # Cargo profile for regular builds. 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 +# List of recent hard forks before Gloas. Used by tests that do not support Gloas yet. RECENT_FORKS_BEFORE_GLOAS=fulu -# List of all recent hard forks. This list is used to set env variables for http_api tests +# List of all recent hard forks. This list is used to set env variables for several tests. # Include phase0 to test the code paths in sync that are pre blobs 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) +TEST_NETWORK_FORKS=phase0 $(RECENT_FORKS) # Extra flags for Cargo CARGO_INSTALL_EXTRA_FLAGS?= @@ -228,7 +226,6 @@ test-op-pool-%: -p operation_pool # Run the tests in the `network` crate for all known forks. -# TODO(EIP-7732) Extend to support gloas by using RECENT_FORKS instead test-network: $(patsubst %,test-network-%,$(TEST_NETWORK_FORKS)) test-network-%: diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index dc786fb7fb..8b50f4b18f 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -3397,6 +3397,7 @@ impl BeaconChain { { return Err(BlockError::ParentUnknown { parent_root: blob.block_parent_root(), + parent_block_hash: None, }); } } @@ -3523,7 +3524,10 @@ impl BeaconChain { .fork_choice_read_lock() .contains_block(&parent_root) { - return Err(BlockError::ParentUnknown { parent_root }); + return Err(BlockError::ParentUnknown { + parent_root, + parent_block_hash: None, + }); } self.emit_sse_data_column_sidecar_events( diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 49ab4a06d2..7b9b7e8218 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -96,6 +96,7 @@ use store::{Error as DBError, KeyValueStore}; use strum::{AsRefStr, IntoStaticStr}; use task_executor::JoinHandle; use tracing::{Instrument, Span, debug, debug_span, error, info_span, instrument}; +use types::ExecutionBlockHash; use types::{ BeaconBlockRef, BeaconState, BeaconStateError, BlobsList, ChainSpec, DataColumnSidecarList, Epoch, EthSpec, FullPayload, Hash256, InconsistentFork, KzgProofs, RelativeEpoch, @@ -125,6 +126,7 @@ pub enum BlockError { /// its parent. ParentUnknown { parent_root: Hash256, + parent_block_hash: Option, }, /// The block slot is greater than the present slot. /// @@ -1446,6 +1448,7 @@ impl ExecutionPendingBlock { ParentImportStatus::UnknownBlock | ParentImportStatus::UnknownPayload => { return Err(BlockError::ParentUnknown { parent_root: block.parent_root(), + parent_block_hash: block.as_block().payload_bid_parent_block_hash().ok(), }); } } @@ -1821,6 +1824,7 @@ pub fn check_block_is_finalized_checkpoint_or_descendant< } else { Err(BlockError::ParentUnknown { parent_root: block.parent_root(), + parent_block_hash: block.as_block().payload_bid_parent_block_hash().ok(), }) } } @@ -1915,6 +1919,7 @@ fn verify_parent_block_and_envelope_are_known( ParentImportStatus::UnknownBlock | ParentImportStatus::UnknownPayload => { Err(BlockError::ParentUnknown { parent_root: block.parent_root(), + parent_block_hash: block.payload_bid_parent_block_hash().ok(), }) } } @@ -1947,6 +1952,7 @@ fn load_parent>( { return Err(BlockError::ParentUnknown { parent_root: block.parent_root(), + parent_block_hash: block.as_block().payload_bid_parent_block_hash().ok(), }); } diff --git a/beacon_node/beacon_chain/tests/block_verification.rs b/beacon_node/beacon_chain/tests/block_verification.rs index 74a521a79c..d7bba13eb0 100644 --- a/beacon_node/beacon_chain/tests/block_verification.rs +++ b/beacon_node/beacon_chain/tests/block_verification.rs @@ -1358,7 +1358,7 @@ async fn block_gossip_verification() { assert!( matches!( unwrap_err(harness.chain.verify_block_for_gossip(Arc::new(SignedBeaconBlock::from_block(block, signature))).await), - BlockError::ParentUnknown {parent_root: p} + BlockError::ParentUnknown {parent_root: p, ..} if p == parent_root ), "should not import a block for an unknown parent" diff --git a/beacon_node/lighthouse_network/src/rpc/protocol.rs b/beacon_node/lighthouse_network/src/rpc/protocol.rs index 056ffc03b8..ed9dc5666a 100644 --- a/beacon_node/lighthouse_network/src/rpc/protocol.rs +++ b/beacon_node/lighthouse_network/src/rpc/protocol.rs @@ -730,7 +730,7 @@ pub fn rpc_data_column_limits( if fork_name.gloas_enabled() { RpcLimits::new( DataColumnSidecarGloas::::min_size(), - DataColumnSidecarGloas::::max_size( + DataColumnSidecarFulu::::max_size( spec.max_blobs_per_block(current_digest_epoch) as usize ), ) diff --git a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs index 29e43b18c2..2668a14dc5 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -732,6 +732,13 @@ impl NetworkBeaconProcessor { %unknown_block_root, "Unknown block root for column" ); + // Data columns are only propagated once the block has been seen for both Fulu + // and Gloas. `UnknownBlockHashFromAttestation` declares that `peer_id` has + // imported `unknown_block_root`. + self.send_sync_message(SyncMessage::UnknownBlockHashFromAttestation( + peer_id, + unknown_block_root, + )); self.propagate_validation_result( message_id.clone(), peer_id, @@ -1076,10 +1083,9 @@ impl NetworkBeaconProcessor { %unknown_block_root, "Unknown block root for partial column" ); - // TODO(gloas): wire this into proper lookup sync. Sending - // `UnknownBlockHashFromAttestation` here is a Fulu-shaped fallback that - // mixes column processing with the attestation lookup path and is not - // the right primitive for Gloas column lookups. + // Data columns are only propagated once the block has been seen for both Fulu + // and Gloas. `UnknownBlockHashFromAttestation` declares that `peer_id` has + // imported `unknown_block_root`. self.send_sync_message(SyncMessage::UnknownBlockHashFromAttestation( peer_id, unknown_block_root, @@ -2714,14 +2720,10 @@ impl NetworkBeaconProcessor { if allow_reprocess { // We don't know the block, get the sync manager to handle the block lookup, and // send the attestation to be scheduled for re-processing. - self.sync_tx - .send(SyncMessage::UnknownBlockHashFromAttestation( - peer_id, - *beacon_block_root, - )) - .unwrap_or_else(|_| { - warn!(msg = "UnknownBlockHash", "Failed to send to sync service") - }); + self.send_sync_message(SyncMessage::UnknownBlockHashFromAttestation( + peer_id, + *beacon_block_root, + )); let msg = match failed_att { FailedAtt::Aggregate { attestation, @@ -3994,13 +3996,17 @@ impl NetworkBeaconProcessor { | PayloadAttestationError::PriorPayloadAttestationMessageKnown { .. } => { self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); } - PayloadAttestationError::UnknownHeadBlock { .. } => { + PayloadAttestationError::UnknownHeadBlock { beacon_block_root } => { debug!( %peer_id, %message_slot, "Payload attestation references unknown block" ); self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); + self.send_sync_message(SyncMessage::UnknownBlockHashFromAttestation( + peer_id, + *beacon_block_root, + )) } PayloadAttestationError::NotInPTC { .. } => { self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Reject); 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 8245b5dc0c..528a261bb8 100644 --- a/beacon_node/network/src/network_beacon_processor/sync_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/sync_methods.rs @@ -28,7 +28,7 @@ use logging::crit; use std::sync::Arc; use std::time::Duration; use tracing::{debug, debug_span, error, info, instrument, warn}; -use types::{BlockImportSource, DataColumnSidecarList, Epoch, Hash256}; +use types::{BlockImportSource, DataColumnSidecarList, Epoch, ExecutionBlockHash, Hash256}; /// Id associated to a batch processing request, either a sync batch or a parent lookup. #[derive(Clone, Debug, PartialEq)] @@ -962,13 +962,14 @@ impl NetworkBeaconProcessor { /// The classified outcome of submitting a block / blob / column for processing, ready for the /// lookup state machine to act on without re-inspecting `BlockError`. -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum BlockProcessingResult { /// `fully_imported` is true if the lookup is complete; false if `MissingComponents` (the /// lookup must keep fetching). `info` is a stable label for logs / metrics. Imported(bool, &'static str), ParentUnknown { parent_root: Hash256, + parent_block_hash: Option, }, /// Processing failed. `penalty` is `Some` when an attributable peer should be downscored; /// the third tuple element is the `report_peer` telemetry msg. `reason` is for logs only. @@ -1000,9 +1001,13 @@ impl From> for BlockProcessingR return Self::Imported(true, "duplicate"); } BlockError::GenesisBlock => return Self::Imported(true, "genesis"), - BlockError::ParentUnknown { parent_root, .. } => { + BlockError::ParentUnknown { + parent_root, + parent_block_hash, + } => { return Self::ParentUnknown { parent_root: *parent_root, + parent_block_hash: *parent_block_hash, }; } BlockError::BeaconChainError(_) | BlockError::InternalError(_) => None, diff --git a/beacon_node/network/src/network_beacon_processor/tests.rs b/beacon_node/network/src/network_beacon_processor/tests.rs index 8ccfe38fa3..6b7c623230 100644 --- a/beacon_node/network/src/network_beacon_processor/tests.rs +++ b/beacon_node/network/src/network_beacon_processor/tests.rs @@ -907,7 +907,10 @@ async fn data_column_reconstruction_at_slot_start() { // reconstruction deadline. #[tokio::test] async fn data_column_reconstruction_at_deadline() { - if test_spec::().fulu_fork_epoch.is_none() { + let spec = test_spec::(); + // Pre-Gloas data-column path: a Gloas block carries its columns in the payload envelope, so the + // harness produces no block-level data columns and this gossip/reconstruction flow doesn't apply. + if spec.fulu_fork_epoch.is_none() || spec.gloas_fork_epoch.is_some() { return; }; @@ -1094,7 +1097,11 @@ async fn import_gossip_block_unacceptably_early() { /// Data columns that have already been processed but unobserved should be propagated without re-importing. #[tokio::test] async fn accept_processed_gossip_data_columns_without_import() { - if test_spec::().fulu_fork_epoch.is_none() { + let spec = test_spec::(); + // Pre-Gloas data-column path: a Gloas block carries its columns in the payload envelope, so the + // harness produces no block-level data columns and this gossip flow doesn't apply. + // TODO(gloas): re-enable this test + if spec.fulu_fork_epoch.is_none() || spec.gloas_fork_epoch.is_some() { return; }; @@ -1983,6 +1990,11 @@ async fn test_payload_envelopes_by_range() { // Manually store payload envelopes for each block in the range let mut expected_roots = Vec::new(); for slot in start_slot..slot_count { + // Genesis (slot 0) has no canonical execution payload, so the by-range handler filters it + // out via `block_has_canonical_payload` even if an envelope is stored for it. + if slot == 0 { + continue; + } if let Some(root) = rig .chain .block_root_at_slot(Slot::new(slot), WhenSlotSkipped::None) @@ -2076,14 +2088,10 @@ async fn test_payload_envelopes_by_root_unknown_root_returns_empty() { let mut rig = TestRig::new(64).await; - // Request envelope for a root that has no stored envelope - let block_root = rig - .chain - .block_root_at_slot(Slot::new(1), WhenSlotSkipped::None) - .unwrap() - .unwrap(); + // Use a root with no block: the harness persists an envelope for every block it produces, so a + // real block root would already have one. An unknown root has no stored envelope. + let block_root = Hash256::repeat_byte(0xaa); - // Don't store any envelope — the handler should return 0 envelopes let roots = RuntimeVariableList::new(vec![block_root], 1).unwrap(); rig.enqueue_payload_envelopes_by_root_request(roots); diff --git a/beacon_node/network/src/sync/backfill_sync/mod.rs b/beacon_node/network/src/sync/backfill_sync/mod.rs index f3dab7f395..36816fb5d6 100644 --- a/beacon_node/network/src/sync/backfill_sync/mod.rs +++ b/beacon_node/network/src/sync/backfill_sync/mod.rs @@ -1229,7 +1229,7 @@ mod tests { fn request_batches_should_not_loop_infinitely() { let harness = BeaconChainHarness::builder(MinimalEthSpec) .default_spec() - .deterministic_keypairs(4) + .deterministic_keypairs(8) .fresh_ephemeral_store() .build(); diff --git a/beacon_node/network/src/sync/block_lookups/mod.rs b/beacon_node/network/src/sync/block_lookups/mod.rs index 0cbeb5ee4e..d403382e9e 100644 --- a/beacon_node/network/src/sync/block_lookups/mod.rs +++ b/beacon_node/network/src/sync/block_lookups/mod.rs @@ -22,13 +22,16 @@ use self::parent_chain::{NodeChain, compute_parent_chains}; pub use self::single_block_lookup::DownloadResult; -use self::single_block_lookup::{LookupRequestError, LookupResult, SingleBlockLookup}; +use self::single_block_lookup::{LookupRequestError, PeerType, SingleBlockLookup}; use super::manager::{BlockProcessType, SLOT_IMPORT_TOLERANCE}; use super::network_context::{RpcResponseError, SyncNetworkContext}; use crate::metrics; use crate::network_beacon_processor::BlockProcessingResult; use crate::sync::SyncMessage; use crate::sync::block_lookups::parent_chain::find_oldest_fork_ancestor; +use crate::sync::block_lookups::single_block_lookup::{ + AwaitingParent, ImportedParent, LookupResult, +}; use beacon_chain::BeaconChainTypes; use fnv::FnvHashMap; use lighthouse_network::PeerId; @@ -39,7 +42,10 @@ use std::sync::Arc; use std::time::Duration; use store::Hash256; use tracing::{debug, error, warn}; -use types::{DataColumnSidecarList, EthSpec, SignedBeaconBlock}; +use types::{ + DataColumnSidecarList, EthSpec, ExecutionBlockHash, SignedBeaconBlock, + SignedExecutionPayloadEnvelope, +}; pub mod parent_chain; mod single_block_lookup; @@ -73,6 +79,8 @@ const MAX_LOOKUPS: usize = 200; type BlockDownloadResponse = Result>>, RpcResponseError>; type CustodyDownloadResponse = Result>, RpcResponseError>; +type PayloadDownloadResponse = + Result>>, RpcResponseError>; pub enum BlockComponent { Block(DownloadResult>>), @@ -169,22 +177,29 @@ impl BlockLookups { block_root: Hash256, block_component: BlockComponent, parent_root: Hash256, + parent_block_hash: Option, peer_id: PeerId, cx: &mut SyncNetworkContext, ) -> bool { - let parent_lookup_exists = - self.search_parent_of_child(parent_root, block_root, &[peer_id], cx); + let parent_lookup_exists = self.search_parent_of_child( + parent_root, + &PeerType::new(parent_block_hash), + block_root, + &[peer_id], + cx, + ); // Only create the child lookup if the parent exists if parent_lookup_exists { // `search_parent_of_child` ensures that the parent lookup exists so we can safely wait for it self.new_current_lookup( block_root, Some(block_component), - Some(parent_root), + Some(AwaitingParent::new(parent_root, parent_block_hash)), // On a `UnknownParentBlock` or `UnknownParentSidecarHeader` 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. &[], + &PeerType::Block, cx, ) } else { @@ -202,7 +217,7 @@ impl BlockLookups { peer_source: &[PeerId], cx: &mut SyncNetworkContext, ) -> bool { - self.new_current_lookup(block_root, None, None, peer_source, cx) + self.new_current_lookup(block_root, None, None, peer_source, &PeerType::Block, cx) } /// A block or blob triggers the search of a parent. @@ -215,6 +230,7 @@ impl BlockLookups { pub fn search_parent_of_child( &mut self, block_root_to_search: Hash256, + peer_type: &PeerType, child_block_root_trigger: Hash256, peers: &[PeerId], cx: &mut SyncNetworkContext, @@ -307,7 +323,7 @@ impl BlockLookups { } // `block_root_to_search` is a failed chain check happens inside new_current_lookup - self.new_current_lookup(block_root_to_search, None, None, peers, cx) + self.new_current_lookup(block_root_to_search, None, None, peers, peer_type, cx) } /// Searches for a single block hash. If the blocks parent is unknown, a chain of blocks is @@ -318,8 +334,9 @@ impl BlockLookups { &mut self, block_root: Hash256, block_component: Option>, - awaiting_parent: Option, + awaiting_parent: Option, peers: &[PeerId], + peer_type: &PeerType, cx: &mut SyncNetworkContext, ) -> bool { // If this block or it's parent is part of a known ignored chain, ignore it. @@ -341,7 +358,8 @@ impl BlockLookups { } } - if let Err(e) = self.add_peers_to_lookup_and_ancestors(lookup_id, peers, cx) { + if let Err(e) = self.add_peers_to_lookup_and_ancestors(lookup_id, peers, peer_type, cx) + { warn!(error = ?e, "Error adding peers to ancestor lookup"); } @@ -353,7 +371,7 @@ impl BlockLookups { && !self .single_block_lookups .iter() - .any(|(_, lookup)| lookup.is_for_block(awaiting_parent)) + .any(|(_, lookup)| lookup.block_root() == awaiting_parent.parent_root()) { warn!(block_root = ?awaiting_parent, "Ignoring child lookup parent lookup not found"); return false; @@ -368,7 +386,8 @@ impl BlockLookups { // If we know that this lookup has unknown parent (is awaiting a parent lookup to resolve), // signal here to hold processing downloaded data. - let mut lookup = SingleBlockLookup::new(block_root, peers, cx.next_id(), awaiting_parent); + let mut lookup = + SingleBlockLookup::new(block_root, peers, peer_type, cx.next_id(), awaiting_parent); let _guard = lookup.span.clone().entered(); // Add block components to the new request @@ -389,9 +408,7 @@ impl BlockLookups { debug!( ?peers, ?block_root, - awaiting_parent = awaiting_parent - .map(|root| root.to_string()) - .unwrap_or("none".to_owned()), + ?awaiting_parent, id = lookup.id, "Created block lookup" ); @@ -438,6 +455,23 @@ impl BlockLookups { self.on_lookup_result(id.lookup_id, result, "custody_download_response", cx); } + pub fn on_payload_download_response( + &mut self, + id: SingleLookupReqId, + response: PayloadDownloadResponse, + cx: &mut SyncNetworkContext, + ) { + let Some(lookup) = self.single_block_lookups.get_mut(&id.lookup_id) else { + debug!( + ?id, + "Payload envelope returned for a lookup id that doesn't exist" + ); + return; + }; + let result = lookup.on_payload_download_response(id.req_id, response, cx); + self.on_lookup_result(id.lookup_id, result, "payload_download_response", cx); + } + /* Error responses */ pub fn peer_disconnected(&mut self, peer_id: &PeerId) { @@ -472,25 +506,62 @@ impl BlockLookups { ); let lookup_result = match process_type { - BlockProcessType::SingleBlock { .. } => lookup.on_block_processing_result(result, cx), + BlockProcessType::SingleBlock { .. } => { + // Update the result of the lookup first, here we may start the download of Gloas + // payload, which may error. + let lookup_result = lookup.on_block_processing_result(result.clone(), cx); + let lookup_is_awaiting_event = lookup.is_awaiting_event(); + let block_root = lookup.block_root(); + // Then, as a side-effect continue the EMPTY children of this lookup. Only if the + // block just imported which ensures we just do it once per lookup. + if let BlockProcessingResult::Imported(..) = result + && let Some(bid_block_hash) = lookup.peek_downloaded_bid_block_hash() + { + self.continue_child_lookups( + block_root, + ImportedParent::OnlyGloasBlock(bid_block_hash), + cx, + ); + } + // Then if this lookup happens to have only empty children we can remove it now. We + // must make sure that no other lookup is awaiting this one, and that no requests + // are on-going. + if !lookup_is_awaiting_event && !self.has_any_awaiting_children(block_root) { + Ok(LookupResult::Completed) + } else { + lookup_result + } + } BlockProcessType::SingleCustodyColumn(_) => { lookup.on_data_processing_result(result, cx) } - // TODO(gloas): route into the payload envelope lookup state machine. - BlockProcessType::SinglePayloadEnvelope(_) => Ok(LookupResult::Pending), + BlockProcessType::SinglePayloadEnvelope(_) => { + lookup.on_payload_processing_result(result, cx) + } }; self.on_lookup_result(lookup_id, lookup_result, "processing_result", cx); } + pub fn has_any_awaiting_children(&self, block_root: Hash256) -> bool { + self.single_block_lookups + .iter() + .any(|(_, lookup)| lookup.is_awaiting_block(block_root)) + } + /// Makes progress on the immediate children of `block_root` - pub fn continue_child_lookups(&mut self, block_root: Hash256, cx: &mut SyncNetworkContext) { + pub fn continue_child_lookups( + &mut self, + parent_root: Hash256, + imported_parent: ImportedParent, + cx: &mut SyncNetworkContext, + ) { let mut lookup_results = vec![]; // < need to buffer lookup results to not re-borrow &mut self for (id, lookup) in self.single_block_lookups.iter_mut() { - if lookup.awaiting_parent() == Some(block_root) { + if lookup.is_awaiting_parent(parent_root, imported_parent) { lookup.resolve_awaiting_parent(); debug!( - parent_root = ?block_root, + ?imported_parent, id, block_root = ?lookup.block_root(), "Continuing child lookup" @@ -523,7 +594,7 @@ impl BlockLookups { let child_lookups = self .single_block_lookups .iter() - .filter(|(_, lookup)| lookup.awaiting_parent() == Some(dropped_lookup.block_root())) + .filter(|(_, lookup)| lookup.is_awaiting_block(dropped_lookup.block_root())) .map(|(id, _)| *id) .collect::>(); @@ -546,10 +617,17 @@ impl BlockLookups { Ok(LookupResult::Pending) => true, Ok(LookupResult::ParentUnknown { parent_root, + parent_block_hash, block_root, peers, }) => { - if self.search_parent_of_child(parent_root, block_root, &peers, cx) { + if self.search_parent_of_child( + parent_root, + &PeerType::new(parent_block_hash), + block_root, + &peers, + cx, + ) { true } else { self.drop_lookup_and_children(id, "Failed"); @@ -567,16 +645,17 @@ impl BlockLookups { metrics::inc_counter(&metrics::SYNC_LOOKUP_COMPLETED); self.metrics.completed_lookups += 1; // Block imported, continue the requests of pending child blocks - self.continue_child_lookups(lookup.block_root(), cx); + self.continue_child_lookups( + lookup.block_root(), + ImportedParent::LookupComplete, + cx, + ); self.update_metrics(); } else { debug!(id, "Attempting to drop non-existent lookup"); } false } - // If UnknownLookup do not log the request error. No need to drop child lookups nor - // update metrics because the lookup does not exist. - Err(LookupRequestError::UnknownLookup) => false, Err(error) => { debug!(id, source, ?error, "Dropping lookup on request error"); self.drop_lookup_and_children(id, error.into()); @@ -708,7 +787,7 @@ impl BlockLookups { if let Some(lookup) = self .single_block_lookups .values() - .find(|l| l.block_root() == awaiting_parent) + .find(|l| l.block_root() == awaiting_parent.parent_root()) { self.find_oldest_ancestor_lookup(lookup) } else { @@ -729,6 +808,7 @@ impl BlockLookups { &mut self, lookup_id: SingleLookupId, peers: &[PeerId], + peer_type: &PeerType, cx: &mut SyncNetworkContext, ) -> Result<(), String> { let lookup = self @@ -738,7 +818,7 @@ impl BlockLookups { let mut added_some_peer = false; for peer in peers { - if lookup.add_peer(*peer) { + if lookup.add_peer(*peer, peer_type) { added_some_peer = true; debug!( block_root = ?lookup.block_root(), @@ -748,15 +828,21 @@ impl BlockLookups { } } - if let Some(parent_root) = lookup.awaiting_parent() { + if let Some(&awaiting_parent) = lookup.awaiting_parent() { + // Regardless of gloas full/empty the lookup to add peers to is keyed by block_root if let Some((&parent_id, _)) = self .single_block_lookups .iter() - .find(|(_, l)| l.block_root() == parent_root) + .find(|(_, l)| l.block_root() == awaiting_parent.parent_root()) { - self.add_peers_to_lookup_and_ancestors(parent_id, peers, cx) + self.add_peers_to_lookup_and_ancestors( + parent_id, + peers, + &awaiting_parent.into_peer_type(), + cx, + ) } else { - Err(format!("Lookup references unknown parent {parent_root:?}")) + Err(format!("Lookup references unknown {awaiting_parent:?}")) } } else if added_some_peer { // If this lookup is not awaiting a parent and we added at least one peer, attempt to diff --git a/beacon_node/network/src/sync/block_lookups/parent_chain.rs b/beacon_node/network/src/sync/block_lookups/parent_chain.rs index 5deea1dd94..120ce5b1cc 100644 --- a/beacon_node/network/src/sync/block_lookups/parent_chain.rs +++ b/beacon_node/network/src/sync/block_lookups/parent_chain.rs @@ -13,7 +13,7 @@ impl From<&SingleBlockLookup> for Node { fn from(value: &SingleBlockLookup) -> Self { Self { block_root: value.block_root(), - parent_root: value.awaiting_parent(), + parent_root: value.awaiting_parent().map(|a| a.parent_root()), } } } 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 157da5d806..f03eed1638 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 @@ -1,6 +1,8 @@ use super::{BlockComponent, PeerId, SINGLE_BLOCK_LOOKUP_MAX_ATTEMPTS}; use crate::network_beacon_processor::BlockProcessingResult; -use crate::sync::block_lookups::{BlockDownloadResponse, CustodyDownloadResponse}; +use crate::sync::block_lookups::{ + BlockDownloadResponse, CustodyDownloadResponse, PayloadDownloadResponse, +}; use crate::sync::manager::BlockProcessType; use crate::sync::network_context::{ LookupRequestResult, PeerGroup, ReqId, RpcRequestSendError, RpcResponseError, @@ -11,13 +13,16 @@ use beacon_chain::block_verification_types::AsBlock; use educe::Educe; use lighthouse_network::service::api_types::Id; use parking_lot::RwLock; -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use std::sync::Arc; use std::time::{Duration, Instant}; use store::Hash256; use strum::IntoStaticStr; use tracing::{Span, debug_span}; -use types::{DataColumnSidecarList, EthSpec, SignedBeaconBlock, Slot}; +use types::{ + DataColumnSidecarList, EthSpec, ExecutionBlockHash, SignedBeaconBlock, + SignedExecutionPayloadEnvelope, Slot, +}; // Dedicated enum for LookupResult to force its usage #[must_use = "LookupResult must be handled with on_lookup_result"] @@ -29,6 +34,7 @@ pub enum LookupResult { /// Block's parent is not known to fork-choice, a parent lookup is needed ParentUnknown { parent_root: Hash256, + parent_block_hash: Option, block_root: Hash256, peers: Vec, }, @@ -46,8 +52,6 @@ pub enum LookupRequestError { BadState(String), /// Lookup failed for some other reason and should be dropped Failed(/* reason: */ String), - /// Attempted to retrieve a not known lookup id - UnknownLookup, /// Received a download result for a different request id than the in-flight request. /// There should only exist a single request at a time. Having multiple requests is a bug and /// can result in undefined state, so it's treated as a hard error and the lookup is dropped. @@ -57,6 +61,31 @@ pub enum LookupRequestError { }, } +#[derive(Debug, Clone, Copy)] +pub struct AwaitingParent { + parent_root: Hash256, + parent_block_hash: Option, +} + +impl AwaitingParent { + pub fn new(parent_root: Hash256, parent_block_hash: Option) -> Self { + Self { + parent_root, + parent_block_hash, + } + } + + pub fn parent_root(&self) -> Hash256 { + self.parent_root + } + + pub fn into_peer_type(self) -> PeerType { + PeerType::new(self.parent_block_hash) + } +} + +type PeerSet = Arc>>; + #[derive(Debug)] struct BlockRequest { state: SingleLookupRequestState>>, @@ -79,6 +108,9 @@ enum DataRequest { WaitingForBlock, Request { slot: Slot, + /// Peers to fetch the data columns from. Pre-Gloas this is the lookup's `peers`; for FULL + /// Gloas blocks this is the `gloas_child_peers` set proven to hold the columns. + peers: PeerSet, state: SingleLookupRequestState>, }, NoData, @@ -94,7 +126,62 @@ impl DataRequest { } } -type PeerSet = Arc>>; +/// Tracks the download + processing of a Gloas execution payload envelope. For FULL Gloas blocks the +/// execution payload arrives as a separate `SignedExecutionPayloadEnvelope`, mirroring the way data +/// columns are fetched and processed by `DataRequest`. +#[derive(Debug)] +enum PayloadRequest { + /// Block not yet downloaded, can't tell if a payload is needed. + WaitingForBlock, + /// Post-Gloas block: an execution payload envelope must be fetched and processed *if* the block + /// is FULL. We can't tell FULL from EMPTY from the block alone: only a FULL child of this block + /// proves a payload was published, which is signalled by `peers` becoming non-empty. + Request { + peers: PeerSet, + state: SingleLookupRequestState>>, + }, + /// Pre-Gloas block: no payload envelope exists, nothing to fetch. + PreGloas, +} + +impl PayloadRequest { + fn is_complete(&self) -> bool { + match &self { + PayloadRequest::WaitingForBlock => false, + PayloadRequest::Request { state, .. } => state.is_processed(), + PayloadRequest::PreGloas => true, + } + } +} + +/// Classifies how a peer relates to a lookup, controlling which peer set it is added to. +pub enum PeerType { + /// The peer can serve the looked-up block and (pre-Gloas) its data columns. + Block, + /// The peer claims to have imported a FULL child of this block whose bid references + /// `ExecutionBlockHash` as its parent. Such peers can serve this block's payload envelope and + /// data columns. + PayloadEnvelope(ExecutionBlockHash), +} + +impl PeerType { + /// `PayloadEnvelope` when the block's bid `parent_block_hash` is known (post-Gloas), else `Block`. + pub fn new(parent_block_hash: Option) -> Self { + match parent_block_hash { + Some(execution_hash) => PeerType::PayloadEnvelope(execution_hash), + None => PeerType::Block, + } + } +} + +/// Used by `is_awaiting_parent` to decide if it can resolve its awaiting parent status +#[derive(Debug, Clone, Copy)] +pub enum ImportedParent { + /// All requests of a lookup are complete, both for pre and post Gloas + LookupComplete, + /// Only post-Gloas, the block request has just been completed. Includes the bid block hash + OnlyGloasBlock(ExecutionBlockHash), +} #[derive(Educe)] #[educe(Debug(bound(T: BeaconChainTypes)))] @@ -103,13 +190,19 @@ pub struct SingleBlockLookup { block_root: Hash256, block_request: BlockRequest, data_request: DataRequest, + payload_request: PayloadRequest, /// Peers that claim to have imported this set of block components. This state is shared with /// the custody request to have an updated view of the peers that claim to have imported the /// block associated with this lookup. The peer set of a lookup can change rapidly, and faster /// than the lifetime of a custody request. #[educe(Debug(method(fmt_peer_set_as_len)))] peers: PeerSet, - awaiting_parent: Option, + /// Post-Gloas only: peers that claim to have imported a FULL child of this block, keyed by the + /// child's bid `parent_block_hash`. These (not `peers`) are the peers proven to hold this + /// block's payload envelope and data columns. + #[educe(Debug(method(fmt_peer_map_as_len)))] + gloas_child_peers: HashMap, + awaiting_parent: Option, created: Instant, pub(crate) span: Span, } @@ -118,8 +211,9 @@ impl SingleBlockLookup { pub fn new( requested_block_root: Hash256, peers: &[PeerId], + peer_type: &PeerType, id: Id, - awaiting_parent: Option, + awaiting_parent: Option, ) -> Self { let lookup_span = debug_span!( "lh_single_block_lookup", @@ -127,12 +221,23 @@ impl SingleBlockLookup { id = id, ); + let block_peers: PeerSet = Arc::new(RwLock::new(peers.iter().copied().collect())); + let mut gloas_child_peers = HashMap::new(); + match peer_type { + PeerType::Block => {} + PeerType::PayloadEnvelope(execution_hash) => { + gloas_child_peers.insert(*execution_hash, block_peers.clone()); + } + } + Self { id, block_root: requested_block_root, block_request: BlockRequest::new(), data_request: DataRequest::WaitingForBlock, - peers: Arc::new(RwLock::new(peers.iter().copied().collect())), + payload_request: PayloadRequest::WaitingForBlock, + peers: block_peers, + gloas_child_peers, awaiting_parent, created: Instant::now(), span: lookup_span, @@ -147,19 +252,41 @@ impl SingleBlockLookup { .map(|block| block.slot()) } + pub fn peek_downloaded_bid_block_hash(&self) -> Option { + self.block_request + .state + .peek_downloaded_data() + .and_then(|block| { + block + .message() + .body() + .signed_execution_payload_bid() + .ok() + .map(|bid| bid.message.block_hash) + }) + } + /// Get the block root that is being requested. pub fn block_root(&self) -> Hash256 { self.block_root } - pub fn awaiting_parent(&self) -> Option { - self.awaiting_parent + pub fn awaiting_parent(&self) -> Option<&AwaitingParent> { + self.awaiting_parent.as_ref() + } + + pub fn is_awaiting_block(&self, block_root: Hash256) -> bool { + if let Some(awaiting_parent) = &self.awaiting_parent { + awaiting_parent.parent_root() == block_root + } else { + false + } } /// Mark this lookup as awaiting a parent lookup from being processed. Meanwhile don't send /// components for processing. - pub fn set_awaiting_parent(&mut self, parent_root: Hash256) { - self.awaiting_parent = Some(parent_root) + pub fn set_awaiting_parent(&mut self, parent: AwaitingParent) { + self.awaiting_parent = Some(parent); } /// Mark this lookup as no longer awaiting a parent lookup. Components can be sent for @@ -168,6 +295,37 @@ impl SingleBlockLookup { self.awaiting_parent = None; } + /// Check if this lookup awaiting_parent status can be resolved given that `parent_root` and + /// `imported_parent` have just been imported + pub fn is_awaiting_parent( + &mut self, + parent_root: Hash256, + imported_parent: ImportedParent, + ) -> bool { + let Some(awaiting_parent) = self.awaiting_parent else { + return false; + }; + if awaiting_parent.parent_root() != parent_root { + return false; + } + match imported_parent { + ImportedParent::LookupComplete => true, + ImportedParent::OnlyGloasBlock(bid_block_hash) => { + if let Some(parent_block_hash) = awaiting_parent.parent_block_hash { + // This lookup is the execution child of `parent_execution_hash`. If the + // parent hash the same `bid_block_hash` this is FULL child and we must wait + // for the entire parent lookup to be imported. Otherwise it's a EMPTY child + // and we can import now. + parent_block_hash != bid_block_hash + } else { + // A parent that's gloas imported and this lookup claims to be before gloas. + debug_assert!(false, "Received post-gloas action for pre-gloas lookup"); + false + } + } + } + } + /// Returns the time elapsed since this lookup was created pub fn elapsed_since_created(&self) -> Duration { self.created.elapsed() @@ -201,6 +359,11 @@ impl SingleBlockLookup { DataRequest::Request { state, .. } => state.is_awaiting_event(), DataRequest::NoData => false, } + || match &self.payload_request { + PayloadRequest::WaitingForBlock => true, + PayloadRequest::Request { state, .. } => state.is_awaiting_event(), + PayloadRequest::PreGloas => false, + } } /// Makes progress on all requests of this lookup. Any error is not recoverable and must result @@ -235,6 +398,7 @@ impl SingleBlockLookup { } else if cx.chain.should_fetch_custody_columns(block_epoch) { DataRequest::Request { slot: block.slot(), + peers: self.get_data_peers(block.payload_bid_block_hash().ok()), state: SingleLookupRequestState::new(), } } else { @@ -244,18 +408,14 @@ impl SingleBlockLookup { break; } } - DataRequest::Request { slot, state } => { + DataRequest::Request { slot, peers, state } => { state.maybe_start_downloading(|| { - cx.custody_lookup_request( - self.id, - self.block_root, - *slot, - self.peers.clone(), - ) + cx.custody_lookup_request(self.id, self.block_root, *slot, peers.clone()) })?; - // Wait for the parent to be imported, data column processing result handle does + // Wait for the current block and parent to be imported, data column processing result handle does // not support `ParentUnknown`. - if self.awaiting_parent.is_none() + if self.block_request.state.is_processed() + && self.awaiting_parent.is_none() && let Some(data) = state.maybe_start_processing() { cx.send_custody_columns_for_processing( @@ -273,16 +433,78 @@ impl SingleBlockLookup { } } + // === Payload request (Gloas only) === + loop { + match &mut self.payload_request { + PayloadRequest::WaitingForBlock => { + if let Some(block) = self.block_request.state.peek_downloaded_data() { + self.payload_request = if block.fork_name_unchecked().gloas_enabled() { + PayloadRequest::Request { + peers: self.get_data_peers(block.payload_bid_block_hash().ok()), + state: SingleLookupRequestState::new(), + } + } else { + PayloadRequest::PreGloas + }; + } else { + break; + } + } + PayloadRequest::Request { peers, state } => { + state.maybe_start_downloading(|| { + cx.payload_lookup_request(self.id, peers.clone(), self.block_root) + })?; + // The envelope can only be verified once the block itself is imported; + // otherwise processing returns `BlockRootUnknown` and the lookup burns retries + // until `TooManyAttempts` while the block is parked awaiting its parent. + if self.block_request.state.is_processed() + && let Some(data) = state.maybe_start_processing() + { + cx.send_payload_for_processing( + self.block_root, + data.value, + data.seen_timestamp, + BlockProcessType::SinglePayloadEnvelope(self.id), + ) + .map_err(LookupRequestError::SendFailedProcessor)?; + } + break; + } + PayloadRequest::PreGloas => break, + } + } + // If all components of this lookup are already processed, there will be no future events // that can make progress so it must be dropped. Consider the lookup completed. // This case can happen if we receive the components from gossip during a retry. - if self.block_request.is_complete() && self.data_request.is_complete() { + if self.block_request.is_complete() + && self.data_request.is_complete() + && self.payload_request.is_complete() + { return Ok(LookupResult::Completed); } Ok(LookupResult::Pending) } + /// Returns the peers that should serve this block's data columns and payload envelope. For FULL + /// Gloas blocks these are the peers that claimed to have imported a FULL child of this block + /// (keyed by this block's bid `block_hash`). Pre-Gloas blocks carry no bid, so this returns the + /// lookup's `peers` unchanged. + fn get_data_peers(&mut self, bid_block_hash: Option) -> PeerSet { + if let Some(bid_block_hash) = bid_block_hash { + // Gloas: the child-attested peer set for this bid is the canonical peer set. DO NOT + // default to `self.peers`: post-Gloas `self.peers` have not claimed to import this + // block's data nor its payload. This set may remain empty until a FULL child arrives. + self.gloas_child_peers + .entry(bid_block_hash) + .or_default() + .clone() + } else { + self.peers.clone() + } + } + /// Handle block processing result. Advances the lookup state machine. pub fn on_block_processing_result( &mut self, @@ -293,15 +515,22 @@ impl SingleBlockLookup { BlockProcessingResult::Imported(_fully_imported, _info) => { self.block_request.state.on_processing_success()?; } - BlockProcessingResult::ParentUnknown { parent_root } => { + BlockProcessingResult::ParentUnknown { + parent_root, + parent_block_hash, + } => { // `BlockError::ParentUnknown` is only returned when processing blocks. Revert the // block request to `Downloaded` and park this lookup until the parent resolves; a // future call to `continue_requests` will re-submit the block for processing once // the parent lookup completes. self.block_request.state.revert_to_awaiting_processing()?; - self.set_awaiting_parent(parent_root); + self.set_awaiting_parent(AwaitingParent { + parent_root, + parent_block_hash, + }); return Ok(LookupResult::ParentUnknown { parent_root, + parent_block_hash, block_root: self.block_root, peers: self.all_peers(), }); @@ -345,6 +574,37 @@ impl SingleBlockLookup { self.continue_requests(cx) } + /// Handle payload envelope processing result (Gloas only). + pub fn on_payload_processing_result( + &mut self, + result: BlockProcessingResult, + cx: &mut SyncNetworkContext, + ) -> Result { + let PayloadRequest::Request { state, .. } = &mut self.payload_request else { + return Err(LookupRequestError::BadState( + "no payload_request".to_owned(), + )); + }; + + match result { + BlockProcessingResult::Imported(_fully_imported, _info) => { + state.on_processing_success()?; + } + BlockProcessingResult::ParentUnknown { .. } => { + return Err(LookupRequestError::BadState( + "payload processing returned ParentUnknown".to_owned(), + )); + } + BlockProcessingResult::Error { penalty, .. } => { + let peers = state.on_processing_failure()?; + if let Some((action, whom, msg)) = penalty { + whom.apply(action, &peers, msg, cx); + } + } + } + self.continue_requests(cx) + } + /// Handle a block download response. Updates download state and advances the lookup. pub fn on_block_download_response( &mut self, @@ -373,6 +633,23 @@ impl SingleBlockLookup { self.continue_requests(cx) } + /// Handle a payload envelope download response. Updates download state and advances the lookup. + pub fn on_payload_download_response( + &mut self, + req_id: ReqId, + result: PayloadDownloadResponse, + cx: &mut SyncNetworkContext, + ) -> Result { + let PayloadRequest::Request { state, .. } = &mut self.payload_request else { + return Err(LookupRequestError::BadState( + "no payload_request".to_owned(), + )); + }; + + state.on_download_response(req_id, result)?; + self.continue_requests(cx) + } + /// Get all unique peers that claim to have imported this set of block components pub fn all_peers(&self) -> Vec { self.peers.read().iter().copied().collect() @@ -380,18 +657,54 @@ impl SingleBlockLookup { /// Add peer to all request states. The peer must be able to serve this request. /// Returns true if the peer was newly inserted into any peer set. - pub fn add_peer(&mut self, peer_id: PeerId) -> bool { - self.peers.write().insert(peer_id) + pub fn add_peer(&mut self, peer_id: PeerId, peer_type: &PeerType) -> bool { + let mut added = false; + match peer_type { + PeerType::PayloadEnvelope(execution_hash) => { + // This peer claims to have imported a FULL child of this block whose bid references + // `execution_hash` as its parent. It is therefore proven to hold this block's + // payload envelope and data columns. + added |= self + .gloas_child_peers + .entry(*execution_hash) + .or_default() + .write() + .insert(peer_id); + } + PeerType::Block => {} + } + // Always add to the main block peers, they can at least serve the block. + added |= self.peers.write().insert(peer_id); + added } /// Remove peer from available peers. pub fn remove_peer(&mut self, peer_id: &PeerId) { self.peers.write().remove(peer_id); + for set in self.gloas_child_peers.values() { + set.write().remove(peer_id); + } } /// Returns true if this lookup has zero peers pub fn has_no_peers(&self) -> bool { - self.peers.read().is_empty() + if self.block_request.is_complete() + && let Some(block) = self.block_request.state.peek_downloaded_data() + && let Ok(bid_block_hash) = block.payload_bid_block_hash() + { + // Gloas block request complete, the main peer set is irrelevant. Check only the gloas + // child peers + match self.gloas_child_peers.get(&bid_block_hash) { + Some(set) => set.read().is_empty(), + None => false, + } + } else { + self.peers.read().is_empty() + && self + .gloas_child_peers + .values() + .all(|set| set.read().is_empty()) + } } } @@ -702,3 +1015,11 @@ fn fmt_peer_set_as_len( ) -> Result<(), std::fmt::Error> { write!(f, "{}", peer_set.read().len()) } + +fn fmt_peer_map_as_len( + peer_map: &HashMap, + f: &mut std::fmt::Formatter, +) -> Result<(), std::fmt::Error> { + let total = peer_map.values().map(|set| set.read().len()).sum::(); + write!(f, "{}", total) +} diff --git a/beacon_node/network/src/sync/block_sidecar_coupling.rs b/beacon_node/network/src/sync/block_sidecar_coupling.rs index 5ec45c8fea..999b3dd30e 100644 --- a/beacon_node/network/src/sync/block_sidecar_coupling.rs +++ b/beacon_node/network/src/sync/block_sidecar_coupling.rs @@ -515,6 +515,15 @@ mod tests { } } + /// The custody-column coupling tests below build Fulu data-column sidecars directly, which is + /// incompatible with a Gloas genesis (Gloas columns have a different structure). Skip them when + /// `FORK_NAME` schedules Gloas at genesis. TODO(gloas): port the harness to build Gloas columns. + fn skip_under_gloas() -> bool { + test_spec::() + .fork_name_at_epoch(Epoch::new(0)) + .gloas_enabled() + } + fn blocks_id(parent_request_id: ComponentsByRangeRequestId) -> BlocksByRangeRequestId { BlocksByRangeRequestId { id: 1, @@ -619,6 +628,9 @@ mod tests { #[test] fn rpc_block_with_custody_columns() { + if skip_under_gloas() { + return; + } let mut spec = test_spec::(); spec.deneb_fork_epoch = Some(Epoch::new(0)); spec.fulu_fork_epoch = Some(Epoch::new(0)); @@ -697,6 +709,9 @@ mod tests { #[test] fn rpc_block_with_custody_columns_batched() { + if skip_under_gloas() { + return; + } let mut spec = test_spec::(); spec.deneb_fork_epoch = Some(Epoch::new(0)); spec.fulu_fork_epoch = Some(Epoch::new(0)); @@ -791,6 +806,9 @@ mod tests { #[test] fn missing_custody_columns_from_faulty_peers() { + if skip_under_gloas() { + return; + } // GIVEN: A request expecting sampling columns from multiple peers let spec = Arc::new(test_spec::()); let da_checker = Arc::new(test_da_checker(spec.clone(), NodeCustodyType::Fullnode)); @@ -886,6 +904,9 @@ mod tests { #[test] fn retry_logic_after_peer_failures() { + if skip_under_gloas() { + return; + } // GIVEN: A request expecting sampling columns where some peers initially fail let mut spec = test_spec::(); spec.deneb_fork_epoch = Some(Epoch::new(0)); @@ -1002,6 +1023,9 @@ mod tests { #[test] fn max_retries_exceeded_behavior() { + if skip_under_gloas() { + return; + } // GIVEN: A request where peers consistently fail to provide required columns let mut spec = test_spec::(); spec.deneb_fork_epoch = Some(Epoch::new(0)); diff --git a/beacon_node/network/src/sync/manager.rs b/beacon_node/network/src/sync/manager.rs index 66bb13ae98..7a90163852 100644 --- a/beacon_node/network/src/sync/manager.rs +++ b/beacon_node/network/src/sync/manager.rs @@ -71,8 +71,8 @@ use strum::IntoStaticStr; use tokio::sync::mpsc; use tracing::{debug, error, info, trace}; use types::{ - BlobSidecar, DataColumnSidecar, EthSpec, ForkContext, Hash256, SignedBeaconBlock, - SignedExecutionPayloadEnvelope, Slot, + BlobSidecar, DataColumnSidecar, EthSpec, ExecutionBlockHash, ForkContext, Hash256, + SignedBeaconBlock, SignedExecutionPayloadEnvelope, Slot, }; /// The number of slots ahead of us that is allowed before requesting a long-range (batch) Sync @@ -852,11 +852,13 @@ impl SyncManager { SyncMessage::UnknownParentBlock(peer_id, block, block_root) => { let block_slot = block.slot(); let parent_root = block.parent_root(); + let parent_block_hash = block.payload_bid_parent_block_hash().ok(); debug!(%block_root, %parent_root, "Received unknown parent block message"); self.handle_unknown_parent( peer_id, block_root, parent_root, + parent_block_hash, block_slot, BlockComponent::Block(DownloadResult { value: block.block_cloned(), @@ -876,6 +878,9 @@ impl SyncManager { peer_id, block_root, parent_root, + // The event `UnknownParentSidecarHeader` only fires for pre-Gloas data + // structues, so the bid parent hash is None. + None, slot, BlockComponent::Sidecar, ); @@ -951,6 +956,7 @@ impl SyncManager { peer_id: PeerId, block_root: Hash256, parent_root: Hash256, + parent_block_hash: Option, slot: Slot, block_component: BlockComponent, ) { @@ -960,6 +966,7 @@ impl SyncManager { block_root, block_component, parent_root, + parent_block_hash, peer_id, &mut self.network, ) { @@ -1139,7 +1146,6 @@ 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, @@ -1194,13 +1200,17 @@ impl SyncManager { peer_id: PeerId, envelope: RpcEvent>>, ) { - if let Some(_resp) = self + 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. + self.block_lookups.on_payload_download_response( + id, + resp.map(|(value, seen_timestamp)| { + DownloadResult::new(value, PeerGroup::from_single(peer_id), seen_timestamp) + }), + &mut self.network, + ) } } diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index dfeb8d8f12..c9a48c9d5e 100644 --- a/beacon_node/network/src/sync/network_context.rs +++ b/beacon_node/network/src/sync/network_context.rs @@ -53,8 +53,8 @@ use task_executor::TaskExecutor; use tokio::sync::mpsc; use tracing::{Span, debug, debug_span, error, warn}; use types::{ - BlobSidecar, ColumnIndex, DataColumnSidecar, DataColumnSidecarList, EthSpec, ForkContext, - Hash256, SignedBeaconBlock, SignedExecutionPayloadEnvelope, Slot, + BlobSidecar, ChainSpec, ColumnIndex, DataColumnSidecar, DataColumnSidecarList, EthSpec, + ForkContext, Hash256, SignedBeaconBlock, SignedExecutionPayloadEnvelope, Slot, }; pub mod custody; @@ -98,6 +98,7 @@ pub type CustodyByRootResult = Result>, RpcResponseError>; #[derive(Debug)] +#[allow(private_interfaces)] pub enum RpcResponseError { RpcError(#[allow(dead_code)] RPCError), VerifyError(LookupVerifyError), @@ -310,6 +311,10 @@ impl SyncNetworkContext { } } + pub fn spec(&self) -> &ChainSpec { + &self.chain.spec + } + pub fn send_sync_message(&mut self, sync_message: SyncMessage) { self.network_beacon_processor .send_sync_message(sync_message); @@ -921,19 +926,23 @@ impl SyncNetworkContext { } /// 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, RpcRequestSendError> { + ) -> Result< + LookupRequestResult>>, + RpcRequestSendError, + > { // 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) { + // before the lookup got here). Return the cached envelope so the request completes. + if self.chain.envelope_is_known_to_fork_choice(&block_root) + && let Ok(Some(envelope)) = self.chain.get_payload_envelope(&block_root) + { return Ok(LookupRequestResult::NoRequestNeeded( "envelope already known to fork-choice", - (), + Arc::new(envelope), )); } @@ -1052,6 +1061,13 @@ impl SyncNetworkContext { block_slot: Slot, lookup_peers: Arc>>, ) -> Result>, RpcRequestSendError> { + // Code below will issue column requests even if `lookup_peers` is empty. This is not okay, + // as we want to have at least one signal that some of our peers has already seen the + // block's data. + if lookup_peers.read().is_empty() { + return Ok(LookupRequestResult::Pending("no peers")); + } + let custody_indexes_imported = self .chain .cached_data_column_indexes(&block_root, block_slot) @@ -1567,7 +1583,6 @@ impl SyncNetworkContext { }) } - #[allow(dead_code)] pub fn send_payload_for_processing( &self, block_root: Hash256, diff --git a/beacon_node/network/src/sync/network_context/custody.rs b/beacon_node/network/src/sync/network_context/custody.rs index e74b74ec08..b1a4b52867 100644 --- a/beacon_node/network/src/sync/network_context/custody.rs +++ b/beacon_node/network/src/sync/network_context/custody.rs @@ -310,11 +310,10 @@ impl ActiveCustodyRequest { // 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. // - // 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(), + // Post-Gloas the lookup peer set is the `gloas_child_peers`: peers that imported + // a FULL child, which requires the parent's columns. They provably custody the + // columns, so withholding is penalizable just like pre-Gloas. + lookup_peers.contains(&peer_id), ) .map_err(Error::SendFailed)?; diff --git a/beacon_node/network/src/sync/tests/lookups.rs b/beacon_node/network/src/sync/tests/lookups.rs index 91227d77f8..fb0956cf50 100644 --- a/beacon_node/network/src/sync/tests/lookups.rs +++ b/beacon_node/network/src/sync/tests/lookups.rs @@ -38,11 +38,15 @@ use tokio::sync::mpsc; use tracing::info; use types::{ BlobSidecar, BlockImportSource, ColumnIndex, DataColumnSidecar, DataColumnSubnetId, - ForkContext, ForkName, Hash256, MinimalEthSpec as E, SignedBeaconBlock, Slot, + ForkContext, ForkName, Hash256, MinimalEthSpec as E, SignedBeaconBlock, + SignedExecutionPayloadEnvelope, Slot, }; const D: Duration = Duration::new(0, 0); +/// Gloas genesis needs enough validators to populate `proposer_lookahead`. +const TEST_RIG_VALIDATOR_COUNT: usize = 8; + /// Configuration for how the test rig should respond to sync requests. /// /// Controls simulated peer behavior during lookup tests, including RPC errors, @@ -59,6 +63,10 @@ pub struct SimulateConfig { return_too_few_data_n_times: usize, return_no_columns_on_indices_n_times: usize, return_no_columns_on_indices: Vec, + /// Only omit columns for this block root, if set. + return_no_columns_for_block: Option, + /// Leave matching envelope requests unanswered. + hold_envelope_for_block: Option, skip_by_range_routes: bool, // Use a callable fn because BlockProcessingResult does not implement Clone #[educe(Debug(ignore))] @@ -132,6 +140,16 @@ impl SimulateConfig { self } + fn return_no_columns_for_block(mut self, block_root: Hash256) -> Self { + self.return_no_columns_for_block = Some(block_root); + self + } + + fn hold_envelope_for_block(mut self, block_root: Hash256) -> Self { + self.hold_envelope_for_block = Some(block_root); + self + } + pub(super) fn return_rpc_error(mut self, error: RPCError) -> Self { self.return_rpc_error = Some(error); self @@ -211,6 +229,14 @@ pub(crate) struct TestRigConfig { node_custody_type_override: Option, } +struct FullEmptyFork { + a: Hash256, + b: Hash256, + c: Hash256, + b_block: Arc>, + c_block: Arc>, +} + impl TestRig { pub(crate) fn new(test_rig_config: TestRigConfig) -> Self { // Use `fork_from_env` logic to set correct fork epochs @@ -221,10 +247,10 @@ impl TestRig { Duration::from_secs(12), ); - // Initialise a new beacon chain + // Gloas genesis needs enough validators for proposer lookahead. let harness = BeaconChainHarness::>::builder(E) .spec(spec.clone()) - .deterministic_keypairs(1) + .deterministic_keypairs(TEST_RIG_VALIDATOR_COUNT) .fresh_ephemeral_store() .mock_execution_layer() .testing_slot_clock(clock.clone()) @@ -304,6 +330,7 @@ impl TestRig { fork_name, network_blocks_by_root: <_>::default(), network_blocks_by_slot: <_>::default(), + network_envelopes_by_root: <_>::default(), penalties: <_>::default(), seen_lookups: <_>::default(), requests: <_>::default(), @@ -428,9 +455,9 @@ impl TestRig { process_fn.await } } - Work::RpcBlobs { process_fn } | Work::RpcCustodyColumn(process_fn) => { - process_fn.await - } + Work::RpcBlobs { process_fn } + | Work::RpcCustodyColumn(process_fn) + | Work::RpcEnvelope(process_fn) => process_fn.await, Work::ChainSegment { process_fn, process_id: (chain_id, batch_epoch), @@ -557,11 +584,14 @@ impl TestRig { } let will_omit_columns = req.data_column_ids.iter().any(|id| { - id.columns.iter().any(|c| { - self.complete_strategy - .return_no_columns_on_indices - .contains(c) - }) + self.complete_strategy + .return_no_columns_for_block + .is_none_or(|root| id.block_root == root) + && id.columns.iter().any(|c| { + self.complete_strategy + .return_no_columns_on_indices + .contains(c) + }) }); let columns_to_omit = if will_omit_columns && self.complete_strategy.return_no_columns_on_indices_n_times > 0 @@ -615,15 +645,34 @@ impl TestRig { .return_wrong_sidecar_for_block_n_times -= 1; let first = columns.first_mut().expect("empty columns"); let column = Arc::make_mut(first); - column - .signed_block_header_mut() - .expect("not fulu") - .message - .body_root = Hash256::ZERO; + // Corrupt the claimed block root. + match column { + DataColumnSidecar::Fulu(col) => { + col.signed_block_header.message.body_root = Hash256::ZERO; + } + DataColumnSidecar::Gloas(col) => { + col.beacon_block_root = Hash256::ZERO; + } + } } self.send_rpc_columns_response(req_id, peer_id, &columns); } + (RequestType::PayloadEnvelopesByRoot(req), AppRequestId::Sync(req_id)) => { + // Lookup sync requests one envelope root at a time. + let block_root = req + .beacon_block_roots + .as_slice() + .first() + .copied() + .unwrap_or_else(|| panic!("empty envelope request: {req:?}")); + if self.complete_strategy.hold_envelope_for_block == Some(block_root) { + return; + } + let envelope = self.network_envelopes_by_root.get(&block_root).cloned(); + self.send_rpc_envelope_response(req_id, peer_id, envelope); + } + (RequestType::BlocksByRange(req), AppRequestId::Sync(req_id)) => { if self.complete_strategy.skip_by_range_routes { return; @@ -883,16 +932,44 @@ impl TestRig { }); } + fn send_rpc_envelope_response( + &mut self, + sync_request_id: SyncRequestId, + peer_id: PeerId, + envelope: Option>>, + ) { + self.log(&format!( + "Completing request {sync_request_id:?} to {peer_id} with envelope {:?}", + envelope.as_ref().map(|e| e.slot()) + )); + + self.push_sync_message(SyncMessage::RpcPayloadEnvelope { + sync_request_id, + peer_id, + envelope: envelope.clone(), + seen_timestamp: D, + }); + // Stream termination + self.push_sync_message(SyncMessage::RpcPayloadEnvelope { + sync_request_id, + peer_id, + envelope: None, + seen_timestamp: D, + }); + } + + #[allow(dead_code)] + fn is_after_gloas(&self) -> bool { + self.fork_name.gloas_enabled() + } + // Preparation steps - /// Returns the block root of the tip of the built chain - pub(super) async fn build_chain(&mut self, block_count: usize) -> Hash256 { - let mut blocks = vec![]; - + fn get_external_harness_with_genesis(&mut self) -> BeaconChainHarness> { // Initialise a new beacon chain let external_harness = BeaconChainHarness::>::builder(E) .spec(self.harness.spec.clone()) - .deterministic_keypairs(1) + .deterministic_keypairs(TEST_RIG_VALIDATOR_COUNT) .fresh_ephemeral_store() .mock_execution_layer() .testing_slot_clock(self.harness.chain.slot_clock.clone()) @@ -912,7 +989,17 @@ impl TestRig { self.network_blocks_by_slot .insert(genesis_block.slot(), genesis_block); - for i in 0..block_count { + external_harness + } + + /// Returns the block root of the tip of the built chain + pub(super) async fn build_chain(&mut self, block_count: usize) -> Hash256 { + let mut blocks = vec![]; + + // Initialise a new beacon chain + let external_harness = self.get_external_harness_with_genesis(); + + for _ in 0..block_count { external_harness.advance_slot(); let block_root = external_harness .extend_chain( @@ -922,23 +1009,17 @@ impl TestRig { ) .await; let block = external_harness.get_full_block(&block_root); - let block_root = block.canonical_root(); let block_slot = block.slot(); - self.network_blocks_by_root - .insert(block_root, block.clone()); - self.network_blocks_by_slot.insert(block_slot, block); - self.log(&format!( - "Produced block {} index {i} in external harness", - block_slot, - )); + self.insert_external_block( + block, + external_harness + .chain + .get_payload_envelope(&block_root) + .unwrap(), + ); blocks.push((block_slot, block_root)); } - // Re-log to have a nice list of block roots at the end - for block in &blocks { - self.log(&format!("Build chain {block:?}")); - } - // Auto-update the clock on the main harness to accept the blocks self.harness .set_current_slot(external_harness.get_current_slot()); @@ -946,6 +1027,152 @@ impl TestRig { blocks.last().expect("empty blocks").1 } + /// Builds: + /// + /// ```text + /// G (full) -> A (full) -> B (FULL: bid.parent_block_hash == A.block_hash) + /// A -> C (EMPTY: bid.parent_block_hash == G.block_hash) + /// ``` + pub(super) async fn build_full_empty_fork(&mut self) -> (Hash256, Hash256, Hash256) { + // Initialise a new beacon chain (mirrors `build_chain`). + let external_harness = self.get_external_harness_with_genesis(); + + // G: full canonical block on genesis. + external_harness.advance_slot(); + let g_root = external_harness + .extend_chain( + 1, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + let g_block_hash = external_harness + .get_full_block(&g_root) + .as_block() + .payload_bid_block_hash() + .unwrap(); + + // A: full block on G, imported with its envelope so the FULL child below sees A as full. + external_harness.advance_slot(); + let a_slot = external_harness.get_current_slot(); + let (a_contents, a_envelope, a_state) = external_harness + .make_block_with_envelope(external_harness.get_current_state(), a_slot) + .await; + let a_block = a_contents.0.clone(); + let a_root = a_block.canonical_root(); + let a_block_hash = a_block.as_block().payload_bid_block_hash().unwrap(); + external_harness + .process_block(a_slot, a_root, a_contents) + .await + .unwrap(); + + external_harness.advance_slot(); + let child_slot = external_harness.get_current_slot(); + + // C: EMPTY child of A. Built before A's envelope is imported, so its bid points at G. + let (c_contents, c_envelope, c_state) = external_harness + .make_block_with_envelope(a_state.clone(), child_slot) + .await; + let c_block = c_contents.0.clone(); + let c_root = c_block.canonical_root(); + + // Import A's envelope so the next child sees A as full. + let a_envelope = a_envelope.expect("A should have envelope"); + external_harness + .process_envelope(a_root, a_envelope, &a_state, a_block.state_root()) + .await; + + // B: FULL child of A. Built after A's envelope is imported, so its bid points at A. + let (b_contents, b_envelope, b_state) = external_harness + .make_block_with_envelope(a_state.clone(), child_slot) + .await; + let b_block = b_contents.0.clone(); + let b_root = b_block.canonical_root(); + + assert_eq!( + ( + b_block.parent_root(), + c_block.parent_root(), + b_block.is_parent_block_full(a_block_hash), + c_block.is_parent_block_full(a_block_hash), + c_block.is_parent_block_full(g_block_hash), + ), + (a_root, a_root, true, false, true) + ); + + // Import both children (and their envelopes) so every block is served through the same + // `get_full_block` path as the rest of the chain. + external_harness + .process_block(child_slot, c_root, c_contents) + .await + .unwrap(); + if let Some(c_envelope) = c_envelope { + external_harness + .process_envelope(c_root, c_envelope, &c_state, c_block.state_root()) + .await; + } + external_harness + .process_block(child_slot, b_root, b_contents) + .await + .unwrap(); + if let Some(b_envelope) = b_envelope { + external_harness + .process_envelope(b_root, b_envelope, &b_state, b_block.state_root()) + .await; + } + + // Cache every block through the single `get_full_block` + `insert_external_block2` path. + for root in [g_root, a_root, c_root, b_root] { + let block = external_harness.get_full_block(&root); + let envelope = external_harness.chain.get_payload_envelope(&root).unwrap(); + self.insert_external_block(block, envelope); + } + + self.harness.set_current_slot(child_slot); + + (a_root, b_root, c_root) + } + + async fn new_gloas_full_empty_fork() -> Option<(Self, FullEmptyFork)> { + let Some(mut r) = Self::new_fulu_peer_test(FuluTestType::WeSupernodeThemSupernode) else { + return None; + }; + if !r.is_after_gloas() { + return None; + } + + let (a, b, c) = r.build_full_empty_fork().await; + let fork = FullEmptyFork { + a, + b, + c, + b_block: r.network_blocks_by_root.get(&b).unwrap().block_cloned(), + c_block: r.network_blocks_by_root.get(&c).unwrap().block_cloned(), + }; + + Some((r, fork)) + } + + fn insert_external_block( + &mut self, + block: RangeSyncBlock, + envelope: Option>, + ) { + let block_root = block.canonical_root(); + let block_slot = block.slot(); + self.network_blocks_by_root + .insert(block_root, block.clone()); + self.network_blocks_by_slot.insert(block_slot, block); + // Cache Gloas envelopes for lookup RPCs. + if let Some(envelope) = envelope { + self.network_envelopes_by_root + .insert(block_root, envelope.into()); + } + self.log(&format!( + "Produced block {block_root:?} slot {block_slot} in external harness", + )); + } + fn corrupt_last_block_signature(&mut self) { let range_sync_block = self.get_last_block().clone(); let mut block = (*range_sync_block.block_cloned()).clone(); @@ -978,7 +1205,16 @@ impl TestRig { } fn corrupt_last_column_kzg_proof(&mut self) { - let range_sync_block = self.get_last_block().clone(); + let block_root = self.get_last_block().canonical_root(); + self.corrupt_column_kzg_proof(block_root); + } + + fn corrupt_column_kzg_proof(&mut self, block_root: Hash256) { + let range_sync_block = self + .network_blocks_by_root + .get(&block_root) + .unwrap_or_else(|| panic!("No block for root {block_root}")) + .clone(); let block = range_sync_block.block_cloned(); let blobs = range_sync_block.block_data().blobs(); let mut columns = range_sync_block @@ -989,7 +1225,7 @@ impl TestRig { let column = Arc::make_mut(first); let proof = column.kzg_proofs_mut().first_mut().expect("no kzg proofs"); *proof = kzg::KzgProof::empty(); - self.re_insert_block(block, blobs, Some(columns)); + self.upsert_block(block, blobs, Some(columns)); } fn get_last_block(&self) -> &RangeSyncBlock { @@ -1009,6 +1245,15 @@ impl TestRig { ) { self.network_blocks_by_slot.clear(); self.network_blocks_by_root.clear(); + self.upsert_block(block, blobs, columns); + } + + fn upsert_block( + &mut self, + block: Arc>, + blobs: Option>, + columns: Option>, + ) { let block_root = block.canonical_root(); let block_slot = block.slot(); let block_data = if let Some(columns) = columns { @@ -1135,6 +1380,10 @@ impl TestRig { self.harness.chain.head().head_slot() } + pub(super) fn head_root(&self) -> Hash256 { + self.harness.chain.head().head_block_root() + } + pub(super) fn assert_head_slot(&self, slot: u64) { assert_eq!(self.head_slot(), Slot::new(slot), "Unexpected head slot"); } @@ -1341,6 +1590,40 @@ impl TestRig { self.fork_name.fulu_enabled() } + fn trigger_unknown_parent_blocks_from_all_peers( + &mut self, + blocks: &[Arc>], + ) { + for peer in self.new_connected_peers_for_peerdas() { + for block in blocks { + self.trigger_unknown_parent_block(peer, block.clone()); + } + } + } + + fn trigger_full_empty_fork(&mut self, fork: &FullEmptyFork) { + self.trigger_unknown_parent_blocks_from_all_peers(&[ + fork.b_block.clone(), + fork.c_block.clone(), + ]); + } + + async fn trigger_custody_lookup_from_all_peers(&mut self) -> Option { + if self.is_after_gloas() { + self.build_chain(2).await; + let child = self.get_last_block().block_cloned(); + let parent_root = child.parent_root(); + self.trigger_unknown_parent_blocks_from_all_peers(&[child]); + Some(parent_root) + } else { + let block_root = self.build_chain(1).await; + for peer in self.new_connected_peers_for_peerdas() { + self.trigger_unknown_block_from_attestation(block_root, peer); + } + None + } + } + fn trigger_unknown_parent_block(&mut self, peer_id: PeerId, block: Arc>) { let block_root = block.canonical_root(); self.send_sync_message(SyncMessage::UnknownParentBlock(peer_id, block, block_root)) @@ -1351,17 +1634,17 @@ impl TestRig { peer_id: PeerId, data_column: Arc>, ) { - let block_root = data_column.block_root(); - let slot = data_column.slot(); - let parent_root = match data_column.as_ref() { - DataColumnSidecar::Fulu(column) => column.block_parent_root(), - DataColumnSidecar::Gloas(_) => panic!("Gloas data column not supported in this test"), + let DataColumnSidecar::Fulu(col) = data_column.as_ref() else { + self.log(&format!( + "trigger_unknown_parent_data_column noop for Gloas peer {peer_id:?}" + )); + return; }; self.send_sync_message(SyncMessage::UnknownParentSidecarHeader { peer_id, - block_root, - parent_root, - slot, + block_root: col.block_root(), + parent_root: col.block_parent_root(), + slot: col.slot(), }); } @@ -1395,6 +1678,13 @@ impl TestRig { self.sync_manager.block_lookups().active_single_lookups() } + fn active_lookup_roots(&self) -> Vec { + self.active_single_lookups() + .iter() + .map(|l| l.block_root) + .collect() + } + fn active_single_lookups_count(&self) -> usize { self.active_single_lookups().len() } @@ -1789,6 +2079,10 @@ async fn happy_path_unknown_data_parent(depth: usize) { let Some(mut r) = TestRig::new_after_fulu() else { return; }; + // No unknown-parent data-column trigger post-Gloas. + if r.is_after_gloas() { + return; + } r.build_chain(depth).await; r.trigger_with_last_unknown_data_column_parent(); r.simulate(SimulateConfig::happy_path()).await; @@ -1806,7 +2100,9 @@ async fn happy_path_multiple_triggers(depth: usize) { r.trigger_with_last_block(); r.trigger_with_last_unknown_block_parent(); r.trigger_with_last_unknown_block_parent(); - r.trigger_with_last_unknown_data_column_parent(); + if !r.is_after_gloas() { + 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(); @@ -1838,7 +2134,10 @@ async fn bad_peer_empty_data_response(depth: usize) { r.simulate(SimulateConfig::new().return_no_data_once()) .await; // We register a penalty, retry and complete sync successfully - r.assert_penalties(&["NotEnoughResponsesReturned"]); + if !r.is_after_gloas() { + // TODO(gloas): tip columns have no attributable FULL-child peer here. + r.assert_penalties(&["NotEnoughResponsesReturned"]); + } r.assert_successful_lookup_sync(); // TODO(tree-sync) Assert that a single lookup is created (no drops) } @@ -1853,7 +2152,10 @@ async fn bad_peer_too_few_data_response(depth: usize) { r.simulate(SimulateConfig::new().return_too_few_data_once()) .await; // We register a penalty, retry and complete sync successfully - r.assert_penalties(&["NotEnoughResponsesReturned"]); + if !r.is_after_gloas() { + // TODO(gloas): tip columns have no attributable FULL-child peer here. + r.assert_penalties(&["NotEnoughResponsesReturned"]); + } r.assert_successful_lookup_sync(); // TODO(tree-sync) Assert that a single lookup is created (no drops) } @@ -1878,8 +2180,13 @@ async fn bad_peer_wrong_data_response(depth: usize) { r.build_chain_and_trigger_last_block(depth).await; r.simulate(SimulateConfig::new().return_wrong_sidecar_for_block_once()) .await; - // We register a penalty, retry and complete sync successfully - r.assert_penalties(&["UnrequestedBlockRoot"]); + // We register a penalty, retry and complete sync successfully. Under Gloas the tip block + // (depth 1) has no attributable FULL-child peer so no custody request is made and no penalty + // is possible; at depth >= 2 the parent's columns are served by the tip (its FULL child), so + // the wrong-sidecar penalty is attributable. + if !r.is_after_gloas() || depth >= 2 { + r.assert_penalties(&["UnrequestedBlockRoot"]); + } r.assert_successful_lookup_sync(); // TODO(tree-sync) Assert that a single lookup is created (no drops) } @@ -1953,10 +2260,16 @@ async fn unknown_parent_does_not_add_peers_to_itself() { r.build_chain(2).await; r.trigger_with_last_unknown_block_parent(); r.trigger_with_last_unknown_block_parent(); - r.trigger_with_last_unknown_data_column_parent(); + // No data-column parent trigger post-Gloas. + let parent_lookup_peers = if r.is_after_gloas() { + 2 + } else { + r.trigger_with_last_unknown_data_column_parent(); + 3 + }; r.simulate(SimulateConfig::happy_path()).await; r.assert_peers_at_lookup_of_slot(2, 0); - r.assert_peers_at_lookup_of_slot(1, 3); + r.assert_peers_at_lookup_of_slot(1, parent_lookup_peers); assert_eq!(r.created_lookups(), 2, "Don't create extra lookups"); // All lookups should NOT complete on this test, however note the following for the tip lookup, // it's the lookup for the tip block which has 0 peers and a block cached: @@ -1996,6 +2309,10 @@ async fn test_single_block_lookup_ignored_response() { /// Assert that if the beacon processor returns DuplicateFullyImported, the lookup completes successfully async fn test_single_block_lookup_duplicate_response() { let mut r = TestRig::default(); + // The mock only covers block processing; Gloas also needs real envelope/column results. + if r.is_after_gloas() { + return; + } r.build_chain_and_trigger_last_block(1).await; // Send a DuplicateFullyImported response, the lookup should complete successfully r.simulate( @@ -2060,6 +2377,10 @@ async fn lookups_form_chain() { /// Assert that if a lookup chain (by appending ancestors) is too long we drop it async fn test_parent_lookup_too_deep_grow_ancestor_one() { let mut r = TestRig::default(); + // TODO(gloas): range sync does not fetch payload envelopes yet. + if r.is_after_gloas() { + return; + } r.build_chain(PARENT_DEPTH_TOLERANCE + 1).await; r.trigger_with_last_block(); r.simulate(SimulateConfig::happy_path()).await; @@ -2210,6 +2531,10 @@ async fn block_in_da_checker_skips_download() { let Some(mut r) = TestRig::new_after_fulu() else { return; }; + // TODO(gloas): the helper does not populate the envelope missing-component path yet. + if r.is_after_gloas() { + return; + } // Add block to da_checker // Complete test with happy path // Assert that there were no requests for blocks @@ -2279,14 +2604,13 @@ async fn custody_lookup_some_custody_failures(test_type: FuluTestType) { let Some(mut r) = TestRig::new_fulu_peer_test(test_type) else { return; }; - let block_root = r.build_chain(1).await; - // Send the same trigger from all peers, so that the lookup has all peers - for peer in r.new_connected_peers_for_peerdas() { - r.trigger_unknown_block_from_attestation(block_root, peer); - } + let block_under_test = r.trigger_custody_lookup_from_all_peers().await; let custody_columns = r.custody_columns(); - r.simulate(SimulateConfig::new().return_no_columns_on_indices(&custody_columns[..4], 3)) - .await; + let mut config = SimulateConfig::new().return_no_columns_on_indices(&custody_columns[..4], 3); + if let Some(block_root) = block_under_test { + config = config.return_no_columns_for_block(block_root); + } + r.simulate(config).await; r.assert_penalties_of_type("NotEnoughResponsesReturned"); r.assert_successful_lookup_sync(); } @@ -2295,20 +2619,15 @@ async fn custody_lookup_permanent_custody_failures(test_type: FuluTestType) { let Some(mut r) = TestRig::new_fulu_peer_test(test_type) else { return; }; - let block_root = r.build_chain(1).await; - - // Send the same trigger from all peers, so that the lookup has all peers - for peer in r.new_connected_peers_for_peerdas() { - r.trigger_unknown_block_from_attestation(block_root, peer); - } + let block_under_test = r.trigger_custody_lookup_from_all_peers().await; let custody_columns = r.custody_columns(); - r.simulate( - SimulateConfig::new().return_no_columns_on_indices(&custody_columns[..2], usize::MAX), - ) - .await; - // Every peer that does not return a column is part of the lookup because it claimed to have - // imported the lookup, so we will penalize. + let mut config = + SimulateConfig::new().return_no_columns_on_indices(&custody_columns[..2], usize::MAX); + if let Some(block_root) = block_under_test { + config = config.return_no_columns_for_block(block_root); + } + r.simulate(config).await; r.assert_penalties_of_type("NotEnoughResponsesReturned"); r.assert_failed_lookup_sync(); } @@ -2346,6 +2665,10 @@ async fn crypto_on_fail_with_bad_column_proposer_signature() { let Some(mut r) = TestRig::new_fulu_peer_test(FuluTestType::WeSupernodeThemSupernode) else { return; }; + // Gloas columns have no per-column proposer signature. + if r.is_after_gloas() { + return; + } r.build_chain(1).await; r.corrupt_last_column_proposer_signature(); r.trigger_with_last_block(); @@ -2364,9 +2687,16 @@ async fn crypto_on_fail_with_bad_column_kzg_proof() { let Some(mut r) = TestRig::new_fulu_peer_test(FuluTestType::WeSupernodeThemSupernode) else { return; }; - r.build_chain(1).await; - r.corrupt_last_column_kzg_proof(); - r.trigger_with_last_block(); + if r.is_after_gloas() { + r.build_chain(2).await; + let child = r.get_last_block().block_cloned(); + r.corrupt_column_kzg_proof(child.parent_root()); + r.trigger_unknown_parent_blocks_from_all_peers(&[child]); + } else { + r.build_chain(1).await; + r.corrupt_last_column_kzg_proof(); + r.trigger_with_last_block(); + } r.simulate(SimulateConfig::happy_path()).await; if cfg!(feature = "fake_crypto") { r.assert_successful_lookup_sync(); @@ -2376,3 +2706,36 @@ async fn crypto_on_fail_with_bad_column_kzg_proof() { r.assert_penalties_of_type("AvailabilityCheck"); } } + +#[tokio::test] +async fn gloas_full_empty_children_retain_parent_for_payload() { + let Some((mut r, fork)) = TestRig::new_gloas_full_empty_fork().await else { + return; + }; + + r.trigger_full_empty_fork(&fork); + + r.simulate(SimulateConfig::happy_path()).await; + r.assert_successful_lookup_sync(); +} + +#[tokio::test] +async fn gloas_empty_child_continues_while_parent_payload_withheld() { + let Some((mut r, fork)) = TestRig::new_gloas_full_empty_fork().await else { + return; + }; + + r.trigger_full_empty_fork(&fork); + + r.simulate(SimulateConfig::happy_path().hold_envelope_for_block(fork.a)) + .await; + + assert_eq!(r.head_root(), fork.c); + assert_eq!(r.created_lookups(), 4); + assert_eq!(r.completed_lookups(), 2); + assert_eq!(r.dropped_lookups(), 0); + assert_eq!(r.active_lookup_roots(), vec![fork.a, fork.b]); + r.assert_no_penalties(); + r.assert_empty_network(); + r.assert_empty_processor(); +} diff --git a/beacon_node/network/src/sync/tests/mod.rs b/beacon_node/network/src/sync/tests/mod.rs index 4e185cc081..2f318bfb9a 100644 --- a/beacon_node/network/src/sync/tests/mod.rs +++ b/beacon_node/network/src/sync/tests/mod.rs @@ -21,7 +21,7 @@ use tokio::sync::mpsc; use tracing_subscriber::fmt::MakeWriter; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; -use types::{ForkName, Hash256, MinimalEthSpec as E, Slot}; +use types::{ForkName, Hash256, MinimalEthSpec as E, SignedExecutionPayloadEnvelope, Slot}; mod lookups; mod range; @@ -77,6 +77,10 @@ struct TestRig { /// Blocks that will be used in the test but may not be known to `harness` yet. network_blocks_by_root: HashMap>, network_blocks_by_slot: HashMap>, + /// Gloas execution payload envelopes keyed by block root, populated during `build_chain` + /// from the external harness store. The rig serves these when a lookup issues a + /// `PayloadEnvelopesByRoot` request. + network_envelopes_by_root: HashMap>>, penalties: Vec, /// All seen lookups through the test run seen_lookups: HashMap, diff --git a/beacon_node/network/src/sync/tests/range.rs b/beacon_node/network/src/sync/tests/range.rs index 1499ae5016..e6890cf242 100644 --- a/beacon_node/network/src/sync/tests/range.rs +++ b/beacon_node/network/src/sync/tests/range.rs @@ -34,6 +34,13 @@ use types::{Epoch, EthSpec, Hash256, MinimalEthSpec as E, Slot}; const SLOTS_PER_EPOCH: usize = 8; impl TestRig { + /// Range sync doesn't yet ingest Gloas blocks in these tests: the range harness doesn't serve + /// payload envelopes, so a Gloas block never becomes fully available and sync can't complete. + /// Skip the affected completion tests under a Gloas genesis. TODO(gloas): support range sync. + fn skip_range_sync_under_gloas(&self) -> bool { + self.fork_name.gloas_enabled() + } + fn add_head_peer(&mut self) -> PeerId { let local_info = self.local_info(); self.add_supernode_peer(SyncInfo { @@ -260,6 +267,9 @@ impl TestRig { #[tokio::test] async fn head_sync_completes() { let mut r = TestRig::default(); + if r.skip_range_sync_under_gloas() { + return; + } r.setup_head_sync().await; r.simulate(SimulateConfig::happy_path()).await; r.assert_head_sync_completed(); @@ -271,6 +281,9 @@ async fn head_sync_completes() { #[tokio::test] async fn finalized_to_head_transition() { let mut r = TestRig::default(); + if r.skip_range_sync_under_gloas() { + return; + } r.setup_finalized_and_head_sync().await; r.simulate(SimulateConfig::happy_path()).await; r.assert_range_sync_completed(); @@ -282,6 +295,9 @@ async fn finalized_to_head_transition() { #[tokio::test] async fn finalized_sync_completes() { let mut r = TestRig::default(); + if r.skip_range_sync_under_gloas() { + return; + } r.setup_finalized_sync().await; r.simulate(SimulateConfig::happy_path()).await; r.assert_range_sync_completed(); @@ -293,6 +309,9 @@ async fn finalized_sync_completes() { #[tokio::test] async fn batch_rpc_error_retries() { let mut r = TestRig::default(); + if r.skip_range_sync_under_gloas() { + return; + } r.setup_finalized_sync().await; r.simulate(SimulateConfig::happy_path().return_rpc_error(RPCError::UnsupportedProtocol)) .await; @@ -361,6 +380,9 @@ async fn batch_peer_returns_partial_columns_then_succeeds() { #[tokio::test] async fn batch_non_faulty_failure_retries() { let mut r = TestRig::default(); + if r.skip_range_sync_under_gloas() { + return; + } r.setup_finalized_sync().await; r.simulate(SimulateConfig::happy_path().with_range_non_faulty_failures(1)) .await; @@ -372,6 +394,9 @@ async fn batch_non_faulty_failure_retries() { #[tokio::test] async fn batch_faulty_failure_redownloads() { let mut r = TestRig::default(); + if r.skip_range_sync_under_gloas() { + return; + } r.setup_finalized_sync().await; r.simulate(SimulateConfig::happy_path().with_range_faulty_failures(1)) .await; @@ -428,6 +453,9 @@ async fn late_response_for_removed_chain() { #[tokio::test] async fn ee_offline_then_online_resumes_sync() { let mut r = TestRig::default(); + if r.skip_range_sync_under_gloas() { + return; + } r.setup_finalized_sync().await; r.simulate(SimulateConfig::happy_path().with_ee_offline_for_n_range_responses(2)) .await; @@ -440,6 +468,9 @@ async fn ee_offline_then_online_resumes_sync() { #[tokio::test] async fn finalized_sync_with_local_head_partial() { let mut r = TestRig::default(); + if r.skip_range_sync_under_gloas() { + return; + } r.setup_finalized_sync_with_local_head(3).await; r.simulate(SimulateConfig::happy_path()).await; r.assert_range_sync_completed(); @@ -450,6 +481,9 @@ async fn finalized_sync_with_local_head_partial() { #[tokio::test] async fn finalized_sync_with_local_head_near_target() { let mut r = TestRig::default(); + if r.skip_range_sync_under_gloas() { + return; + } let target_epochs = 5; let local_slots = (target_epochs * SLOTS_PER_EPOCH) - 1; // all blocks except last r.build_chain(target_epochs * SLOTS_PER_EPOCH).await; @@ -468,7 +502,7 @@ async fn finalized_sync_with_local_head_near_target() { #[tokio::test] async fn not_enough_custody_peers_then_peers_arrive() { let mut r = TestRig::default(); - if !r.fork_name.fulu_enabled() { + if !r.fork_name.fulu_enabled() || r.skip_range_sync_under_gloas() { return; } let remote_info = r.setup_finalized_sync_insufficient_peers().await; @@ -495,7 +529,7 @@ async fn not_enough_custody_peers_then_peers_arrive() { #[tokio::test] async fn finalized_sync_not_enough_custody_peers_resume_after_peer_cgc_update() { let mut r = TestRig::default(); - if !r.fork_name.fulu_enabled() { + if !r.fork_name.fulu_enabled() || r.skip_range_sync_under_gloas() { return; } From 844c6dd4a0d11b5d8e40fb5b3c3409fff29436ff Mon Sep 17 00:00:00 2001 From: chonghe <44791194+chong-he@users.noreply.github.com> Date: Wed, 10 Jun 2026 15:12:57 +0800 Subject: [PATCH 24/26] Refactor `payload_attestation_service` and add payload attestation test to validator client (#9357) Co-Authored-By: Tan Chee Keong --- Cargo.lock | 9 + testing/validator_test_rig/Cargo.toml | 1 + .../src/mock_beacon_node.rs | 116 +++- .../validator_services/Cargo.toml | 17 + .../src/payload_attestation_service.rs | 574 ++++++++++++++++-- 5 files changed, 670 insertions(+), 47 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1bfc32a7a0..c0f337a6a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9876,24 +9876,32 @@ dependencies = [ name = "validator_services" version = "0.1.0" dependencies = [ + "account_utils", "beacon_node_fallback", "bls", "either", "eth2", + "eth2_keystore", "futures", "graffiti_file", + "initialized_validators", + "lighthouse_validator_store", "logging", + "mockito", "parking_lot", "reqwest", "safe_arith", + "slashing_protection", "slot_clock", "task_executor", + "tempfile", "tokio", "tracing", "tree_hash", "types", "validator_metrics", "validator_store", + "validator_test_rig", ] [[package]] @@ -9912,6 +9920,7 @@ name = "validator_test_rig" version = "0.1.0" dependencies = [ "eth2", + "ethereum_ssz", "mockito", "regex", "reqwest", diff --git a/testing/validator_test_rig/Cargo.toml b/testing/validator_test_rig/Cargo.toml index 2057a9fdc8..dc8b2babd7 100644 --- a/testing/validator_test_rig/Cargo.toml +++ b/testing/validator_test_rig/Cargo.toml @@ -5,6 +5,7 @@ edition = { workspace = true } [dependencies] eth2 = { workspace = true } +ethereum_ssz = { workspace = true } mockito = { workspace = true } regex = { workspace = true } reqwest = { workspace = true } diff --git a/testing/validator_test_rig/src/mock_beacon_node.rs b/testing/validator_test_rig/src/mock_beacon_node.rs index 1ecdd85f3b..4501379d25 100644 --- a/testing/validator_test_rig/src/mock_beacon_node.rs +++ b/testing/validator_test_rig/src/mock_beacon_node.rs @@ -4,18 +4,23 @@ use mockito::{Matcher, Mock, Server, ServerGuard}; use regex::Regex; use reqwest::StatusCode; use sensitive_url::SensitiveUrl; +use ssz::Decode; use std::marker::PhantomData; use std::str::FromStr; use std::sync::{Arc, Mutex}; use std::time::Duration; use tracing::info; -use types::{ChainSpec, ConfigAndPreset, EthSpec, SignedBlindedBeaconBlock}; +use types::{ + ChainSpec, ConfigAndPreset, EthSpec, ForkName, PayloadAttestationData, + PayloadAttestationMessage, SignedBlindedBeaconBlock, Slot, +}; pub struct MockBeaconNode { server: ServerGuard, pub beacon_api_client: BeaconNodeHttpClient, _phantom: PhantomData, pub received_blocks: Arc>>>, + pub payload_attestation_message: Arc>>, } impl MockBeaconNode { @@ -31,6 +36,7 @@ impl MockBeaconNode { beacon_api_client, _phantom: PhantomData, received_blocks: Arc::new(Mutex::new(Vec::new())), + payload_attestation_message: Arc::new(Mutex::new(Vec::new())), } } @@ -124,4 +130,112 @@ impl MockBeaconNode { ) .create() } + + /// Mocks `GET /eth/v1/validator/payload_attestations_data/{slot}` + pub fn mock_get_validator_payload_attestation_data( + &mut self, + data: &PayloadAttestationData, + fork_name: ForkName, + slot: Slot, + ) -> Mock { + let path_pattern = Regex::new(&format!( + r"^/eth/v1/validator/payload_attestation_data/{}$", + slot.as_u64() + )) + .unwrap(); + + let body = serde_json::json!({ + "version": fork_name.to_string(), + "data": data, + }); + + self.server + .mock("GET", Matcher::Regex(path_pattern.to_string())) + .with_status(200) + .with_header("content-type", "application/json") + .with_body(serde_json::to_string(&body).unwrap()) + .create() + } + + /// Mocks `GET /eth/v1/validator/payload_attestation_data/{slot}` returning error + pub fn mock_get_validator_payload_attestation_data_error(&mut self, slot: Slot) -> Mock { + let path_pattern = Regex::new(&format!( + r"^/eth/v1/validator/payload_attestation_data/{}$", + slot.as_u64() + )) + .unwrap(); + + self.server + .mock("GET", Matcher::Regex(path_pattern.to_string())) + .with_status(500) + .with_header("content-type", "application/json") + .with_body(r#"{"message":"Internal server error"}"#) + .create() + } + + /// Mocks `POST /eth/v1/beacon/pool/payload_attestations` + pub fn mock_post_beacon_pool_payload_attestations(&mut self) -> Mock { + let path_pattern = Regex::new(r"^/eth/v1/beacon/pool/payload_attestations$").unwrap(); + let payload_attestation_message = Arc::clone(&self.payload_attestation_message); + + self.server + .mock("POST", Matcher::Regex(path_pattern.to_string())) + .match_header("content-type", "application/json") + .with_status(200) + .with_body_from_request(move |request| { + let body = request.body().expect("Failed to get request body"); + let message: Vec = serde_json::from_slice(body) + .expect("Failed to deserialize payload attestations"); + payload_attestation_message.lock().unwrap().extend(message); + vec![] + }) + .create() + } + + /// Mocks `POST /eth/v1/beacon/pool/payload_attestations` (SSZ) with an optional `delay`. + pub fn mock_post_beacon_pool_payload_attestations_ssz(&mut self, delay: Duration) -> Mock { + let path_pattern = Regex::new(r"^/eth/v1/beacon/pool/payload_attestations$").unwrap(); + let url = self.server.url(); + + let payload_attestation_message = Arc::clone(&self.payload_attestation_message); + + self.server + .mock("POST", Matcher::Regex(path_pattern.to_string())) + .match_header("content-type", "application/octet-stream") + .with_status(200) + .with_body_from_request(move |request| { + info!( + "Received payload attestation SSZ on server {} with delay {} ms", + url, + delay.as_secs(), + ); + let body = request.body().expect("Failed to get request body"); + + let chunk_size = ::ssz_fixed_len(); + let messages: Vec = body + .chunks(chunk_size) + .map(|chunk| { + PayloadAttestationMessage::from_ssz_bytes(chunk) + .expect("Failed to deserialize PayloadAttestationMessage from SSZ") + }) + .collect(); + + payload_attestation_message.lock().unwrap().extend(messages); + std::thread::sleep(delay); + vec![] + }) + .create() + } + + /// Mocks `POST /eth/v1/beacon/pool/payload_attestations` (SSZ) returning error + pub fn mock_post_beacon_pool_payload_attestations_ssz_error(&mut self) -> Mock { + let path_pattern = Regex::new(r"^/eth/v1/beacon/pool/payload_attestations$").unwrap(); + + self.server + .mock("POST", Matcher::Regex(path_pattern.to_string())) + .match_header("content-type", "application/octet-stream") + .with_status(500) + .with_body(r#"{"message":"Internal server error"}"#) + .create() + } } diff --git a/validator_client/validator_services/Cargo.toml b/validator_client/validator_services/Cargo.toml index 2582968265..f2fbf193e1 100644 --- a/validator_client/validator_services/Cargo.toml +++ b/validator_client/validator_services/Cargo.toml @@ -23,3 +23,20 @@ tree_hash = { workspace = true } types = { workspace = true } validator_metrics = { workspace = true } validator_store = { workspace = true } + +[dev-dependencies] +account_utils = { workspace = true } +eth2_keystore = { workspace = true } +initialized_validators = { workspace = true } +lighthouse_validator_store = { workspace = true } +mockito = { workspace = true } +slashing_protection = { workspace = true } +tempfile = { workspace = true } +tokio = { workspace = true, features = [ + "rt-multi-thread", + "sync", + "signal", + "macros", + "test-util", +] } +validator_test_rig = { workspace = true } diff --git a/validator_client/validator_services/src/payload_attestation_service.rs b/validator_client/validator_services/src/payload_attestation_service.rs index f41893941f..f4cd26552a 100644 --- a/validator_client/validator_services/src/payload_attestation_service.rs +++ b/validator_client/validator_services/src/payload_attestation_service.rs @@ -7,7 +7,7 @@ use std::sync::Arc; use task_executor::TaskExecutor; use tokio::time::sleep; use tracing::{debug, error, info}; -use types::{ChainSpec, EthSpec}; +use types::{ChainSpec, EthSpec, Slot}; use validator_store::ValidatorStore; pub struct Inner { @@ -39,7 +39,11 @@ impl Deref for PayloadAttestationService { } } -impl PayloadAttestationService { +impl PayloadAttestationService +where + S: ValidatorStore + 'static, + T: SlotClock + 'static, +{ pub fn new( duties_service: Arc>, validator_store: Arc, @@ -61,11 +65,8 @@ impl PayloadAttestationServ } pub fn start_update_service(self) -> Result<(), String> { - let slot_duration = self.chain_spec.get_slot_duration(); - let payload_attestation_due = self.chain_spec.get_payload_attestation_due(); - info!( - payload_attestation_due_ms = payload_attestation_due.as_millis(), + payload_attestation_due_ms = self.chain_spec.get_payload_attestation_due().as_millis(), "Payload attestation service started" ); @@ -73,46 +74,7 @@ impl PayloadAttestationServ let interval_fut = async move { loop { - let Some(duration_to_next_slot) = self.slot_clock.duration_to_next_slot() else { - error!("Failed to read slot clock"); - sleep(slot_duration).await; - continue; - }; - - let Some(current_slot) = self.slot_clock.now() else { - error!("Failed to read slot clock after trigger"); - continue; - }; - - if !self - .chain_spec - .fork_name_at_slot::(current_slot) - .gloas_enabled() - { - let duration_to_next_epoch = self - .slot_clock - .duration_to_next_epoch(S::E::slots_per_epoch()) - .unwrap_or_else(|| { - self.chain_spec.get_slot_duration() * S::E::slots_per_epoch() as u32 - }); - sleep(duration_to_next_epoch).await; - continue; - } - - sleep(duration_to_next_slot + payload_attestation_due).await; - - let Some(attestation_slot) = self.slot_clock.now() else { - error!("Failed to read slot clock after sleep"); - continue; - }; - - let service = self.clone(); - self.executor.spawn( - async move { - service.produce_and_publish(attestation_slot).await; - }, - "payload_attestation_producer", - ); + self.run_update().await; } }; @@ -120,6 +82,60 @@ impl PayloadAttestationServ Ok(()) } + async fn run_update(&self) { + let Some(attestation_slot) = self.wait_for_attestation_slot().await else { + return; + }; + + let service = self.clone(); + self.executor.spawn( + async move { + service.produce_and_publish(attestation_slot).await; + }, + "payload_attestation_producer", + ); + } + + async fn wait_for_attestation_slot(&self) -> Option { + let slot_duration = self.chain_spec.get_slot_duration(); + let payload_attestation_due = self.chain_spec.get_payload_attestation_due(); + + let Some(duration_to_next_slot) = self.slot_clock.duration_to_next_slot() else { + error!("Failed to read slot clock"); + sleep(slot_duration).await; + return None; + }; + + let Some(current_slot) = self.slot_clock.now() else { + error!("Failed to read slot clock after trigger"); + return None; + }; + + if !self + .chain_spec + .fork_name_at_slot::(current_slot) + .gloas_enabled() + { + let duration_to_next_epoch = self + .slot_clock + .duration_to_next_epoch(S::E::slots_per_epoch()) + .unwrap_or_else(|| { + self.chain_spec.get_slot_duration() * S::E::slots_per_epoch() as u32 + }); + sleep(duration_to_next_epoch).await; + return None; + } + + sleep(duration_to_next_slot + payload_attestation_due).await; + + let Some(attestation_slot) = self.slot_clock.now() else { + error!("Failed to read slot clock after sleep"); + return None; + }; + + Some(attestation_slot) + } + async fn produce_and_publish(&self, slot: types::Slot) { let duties = self.duties_service.get_ptc_duties_for_slot(slot); @@ -249,3 +265,469 @@ impl PayloadAttestationServ } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::duties_service::DutiesServiceBuilder; + use account_utils::validator_definitions::{PasswordStorage, ValidatorDefinition}; + use beacon_node_fallback::{ + BeaconNodeFallback, CandidateBeaconNode, Config as BeaconNodeConfig, + }; + use bls::{Keypair, PublicKeyBytes}; + use eth2::types::PtcDuty; + use eth2_keystore::KeystoreBuilder; + use futures::FutureExt; + use initialized_validators::InitializedValidators; + use lighthouse_validator_store::LighthouseValidatorStore; + use slashing_protection::{SLASHING_PROTECTION_FILENAME, SlashingDatabase}; + use slot_clock::ManualSlotClock; + use std::sync::Arc; + use std::time::Duration; + use task_executor::test_utils::TestRuntime; + use tempfile::{TempDir, tempdir}; + use types::{Epoch, ForkName, Hash256, MainnetEthSpec, PayloadAttestationData, Slot}; + use validator_test_rig::mock_beacon_node::MockBeaconNode; + + type E = MainnetEthSpec; + type S = LighthouseValidatorStore; + + async fn create_validator_store( + slot_clock: ManualSlotClock, + spec: Arc, + executor: TaskExecutor, + num_validators: usize, + ) -> (Arc, Vec, TempDir) { + let validator_dir = tempdir().unwrap(); + let password = b"test"; + + let mut validator_definitions = Vec::with_capacity(num_validators); + let mut pubkeys = Vec::with_capacity(num_validators); + + for i in 0..num_validators { + let keypair = Keypair::random(); + let keystore = KeystoreBuilder::new(&keypair, password, String::new()) + .unwrap() + .build() + .unwrap(); + let keystore_path = validator_dir + .path() + .join(format!("voting-keystore-{i}.json")); + keystore + .to_json_writer(std::fs::File::create(&keystore_path).unwrap()) + .unwrap(); + + let validator_definition = ValidatorDefinition::new_keystore_with_password( + keystore_path, + PasswordStorage::ValidatorDefinitions( + String::from_utf8(password.to_vec()).unwrap().into(), + ), + None, + None, + None, + None, + None, + None, + ) + .unwrap(); + + pubkeys.push(keypair.pk.into()); + validator_definitions.push(validator_definition); + } + + let initialized_validators = InitializedValidators::from_definitions( + validator_definitions.into(), + validator_dir.path().into(), + Default::default(), + ) + .await + .unwrap(); + + let slashing_db_path = validator_dir.path().join(SLASHING_PROTECTION_FILENAME); + let slashing_protection = SlashingDatabase::open_or_create(&slashing_db_path).unwrap(); + + let validator_store = Arc::new(LighthouseValidatorStore::<_, E>::new( + initialized_validators, + slashing_protection, + Hash256::ZERO, + spec, + None, + slot_clock, + &Default::default(), + executor, + )); + + for (i, pubkey) in pubkeys.iter().enumerate() { + validator_store.set_validator_index(pubkey, i as u64); + } + + (validator_store, pubkeys, validator_dir) + } + + struct TestHarness { + mock_beacon_node_1: MockBeaconNode, + mock_beacon_node_2: MockBeaconNode, + service: PayloadAttestationService, + pubkeys: Vec, + _test_runtime: TestRuntime, + _validator_dir: TempDir, + } + + impl TestHarness { + async fn create_validators(num_validators: usize) -> Self { + let mut default_spec = MainnetEthSpec::default_spec(); + default_spec.gloas_fork_epoch = Some(Epoch::new(0)); + let spec = Arc::new(default_spec); + + let test_runtime = TestRuntime::default(); + let executor = test_runtime.task_executor.clone(); + let slot_duration = spec.get_slot_duration(); + let slot_clock = + ManualSlotClock::new(Slot::new(0), Duration::from_secs(0), slot_duration); + + let (validator_store, pubkeys, validator_dir) = create_validator_store( + slot_clock.clone(), + spec.clone(), + executor.clone(), + num_validators, + ) + .await; + + let mock_beacon_node_1 = MockBeaconNode::::new().await; + let mock_beacon_node_2 = MockBeaconNode::::new().await; + + let beacon_node_1 = + CandidateBeaconNode::new(mock_beacon_node_1.beacon_api_client.clone(), 0); + let beacon_node_2 = + CandidateBeaconNode::new(mock_beacon_node_2.beacon_api_client.clone(), 1); + + let beacon_node_fallback = Arc::new(BeaconNodeFallback::new( + vec![beacon_node_1, beacon_node_2], + BeaconNodeConfig::default(), + vec![], + spec.clone(), + )); + + let duties_service = Arc::new( + DutiesServiceBuilder::new() + .validator_store(validator_store.clone()) + .slot_clock(slot_clock.clone()) + .beacon_nodes(beacon_node_fallback.clone()) + .executor(executor.clone()) + .spec(spec.clone()) + .build() + .unwrap(), + ); + + let service = PayloadAttestationService::new( + duties_service, + validator_store, + slot_clock, + beacon_node_fallback, + executor, + spec, + ); + + Self { + mock_beacon_node_1, + mock_beacon_node_2, + service, + pubkeys, + _test_runtime: test_runtime, + _validator_dir: validator_dir, + } + } + + fn insert_ptc_duties(&self, slot: Slot) { + let duties = self + .pubkeys + .iter() + .enumerate() + .map(|(i, pubkey)| PtcDuty { + pubkey: *pubkey, + validator_index: i as u64, + slot, + }) + .collect(); + self.service + .duties_service + .ptc_duties + .write() + .insert(Epoch::new(0), (Hash256::ZERO, duties)); + } + } + + // advance_time so that we don't have to wait for real-time to elapse in the test + async fn advance_time(slot_clock: &ManualSlotClock, duration: Duration) { + slot_clock.advance_time(duration); + tokio::time::advance(duration).await; + } + + #[tokio::test] + async fn test_wait_for_attestation_slot() { + tokio::time::pause(); + + let harness = TestHarness::create_validators(1).await; + let service = &harness.service; + let service_wait = service.wait_for_attestation_slot(); + tokio::pin!(service_wait); + + // This first call of .now_or_never() starts the timer and registers the sleep timer with tokio + // It calls sleep(duration_to_next_slot + payload_attestation_due).await which registers a timer with a deadline of 21s + assert!(service_wait.as_mut().now_or_never().is_none()); + + let duration_to_next_slot = harness.service.slot_clock.duration_to_next_slot().unwrap(); + let payload_attestation_due = harness.service.chain_spec.get_payload_attestation_due(); + let duration_to_wait = duration_to_next_slot + payload_attestation_due; + // Advance both slot_clock and tokio::time to 21s (the sleep deadline) + // The timer hasn't fired yet because tokio requires time to be strictly past the deadline. + // so the following assert! should return None + // This verifies that the function wait_for_attestation_slot waits for the correct duration before returning a slot. + advance_time(&harness.service.slot_clock, duration_to_wait).await; + assert!( + service_wait.as_mut().now_or_never().is_none(), + "Function should return None before the sleep duration has elapsed" + ); + + // Advance time for 1 more second, the sleep should have completed and the function should return Some(attestation_slot) + // slot_clock is now at 22s, which is slot 1 + // Removing this advance_time should cause the following assert_eq! to fail + advance_time(&harness.service.slot_clock, Duration::from_secs(1)).await; + assert_eq!( + service_wait.as_mut().now_or_never().unwrap(), + Some(Slot::new(1)) + ); + } + + #[tokio::test] + async fn publish_payload_attestation_ssz() { + let mut harness = TestHarness::create_validators(1).await; + + let attestation_slot = Slot::new(1); + harness.insert_ptc_duties(attestation_slot); + + let expected_payload_attestation = PayloadAttestationData { + beacon_block_root: Hash256::ZERO, + slot: attestation_slot, + payload_present: true, + blob_data_available: true, + }; + + harness + .mock_beacon_node_1 + .mock_get_validator_payload_attestation_data( + &expected_payload_attestation, + ForkName::Gloas, + attestation_slot, + ); + + let mock_ssz = harness + .mock_beacon_node_1 + .mock_post_beacon_pool_payload_attestations_ssz(Duration::from_secs(0)); + let mock_json = harness + .mock_beacon_node_2 + .mock_post_beacon_pool_payload_attestations(); + + let service = harness.service; + service.produce_and_publish(attestation_slot).await; + + let messages = harness + .mock_beacon_node_1 + .payload_attestation_message + .lock() + .unwrap(); + + // We create one validator with one PTC duty, so the PayloadAttestationMessage length should be 1 + assert_eq!( + messages.len(), + 1, + "Expected one payload attestation message" + ); + + // First try on beacon_node_1 (mock_ssz) is successful + // therefore mock_json is not hit at all + mock_ssz.expect(1).assert(); + mock_json.expect(0).assert(); + + let result = &messages[0]; + assert_eq!(result.validator_index, 0); + assert_eq!( + result.data.beacon_block_root, + expected_payload_attestation.beacon_block_root + ); + assert_eq!(result.data.slot, attestation_slot); + assert!(result.data.payload_present); + assert!(result.data.blob_data_available); + } + + #[tokio::test] + async fn publish_payload_attestation_ssz_fails_fallback_to_json() { + let mut harness = TestHarness::create_validators(1).await; + + let attestation_slot = Slot::new(1); + harness.insert_ptc_duties(attestation_slot); + + let expected_payload_attestation = PayloadAttestationData { + beacon_block_root: Hash256::ZERO, + slot: attestation_slot, + payload_present: true, + blob_data_available: true, + }; + + harness + .mock_beacon_node_1 + .mock_get_validator_payload_attestation_data( + &expected_payload_attestation, + ForkName::Gloas, + Slot::new(1), + ); + + // mock_ssz returns 500 to simulate BN does not support SSZ, so that it fallbacks to mock_json + let mock_ssz = harness + .mock_beacon_node_1 + .mock_post_beacon_pool_payload_attestations_ssz_error(); + let mock_json = harness + .mock_beacon_node_2 + .mock_post_beacon_pool_payload_attestations(); + + let service = harness.service; + service.produce_and_publish(attestation_slot).await; + + // first_success function tries both beacon nodes for SSZ post payload attestation: + // first pass: both fail (mock_ssz returns 500, mock_json does not support SSZ) + // second pass: repeats the first pass + // Therefore mock_ssz is hit twice. + // When SSZ fails, it fallbacks to JSON and should succeed on first call on mock_json. + mock_ssz.expect(2).assert(); + mock_json.expect(1).assert(); + + let messages = harness + .mock_beacon_node_2 + .payload_attestation_message + .lock() + .unwrap(); + + assert_eq!( + messages.len(), + 1, + "Expected one payload attestation via JSON fallback" + ); + } + + #[tokio::test] + async fn no_duties_no_publish() { + let mut harness = TestHarness::create_validators(1).await; + + // we do not insert any duties in this test + let mock = harness + .mock_beacon_node_1 + .mock_post_beacon_pool_payload_attestations_ssz(Duration::from_secs(0)); + + let service = harness.service; + + // when there is no duty, produce_and_publish should return early + // therefore, the beacon node is not called, expected to hit 0 + service.produce_and_publish(Slot::new(1)).await; + mock.expect(0).assert(); + + assert!( + harness + .mock_beacon_node_1 + .payload_attestation_message + .lock() + .unwrap() + .is_empty(), + "No payload attestation should be published when there are no duties" + ); + } + + #[tokio::test] + async fn test_get_payload_attestation_data_error() { + let mut harness = TestHarness::create_validators(1).await; + + let attestation_slot = Slot::new(1); + // We have PTC duties + harness.insert_ptc_duties(attestation_slot); + + // However, we simulate that both BNs have error in get_validator_payload_attestation_data + harness + .mock_beacon_node_1 + .mock_get_validator_payload_attestation_data_error(attestation_slot); + harness + .mock_beacon_node_2 + .mock_get_validator_payload_attestation_data_error(attestation_slot); + + let mock_ssz = harness + .mock_beacon_node_1 + .mock_post_beacon_pool_payload_attestations_ssz(Duration::from_secs(0)); + let mock_json = harness + .mock_beacon_node_2 + .mock_post_beacon_pool_payload_attestations(); + + let service = harness.service; + // The produce_and_publish() should return early before reaching the POST endpoint + service.produce_and_publish(attestation_slot).await; + + // Both beacon nodes should not be called at all + mock_ssz.expect(0).assert(); + mock_json.expect(0).assert(); + + // No payload attestation message published + assert!( + harness + .mock_beacon_node_1 + .payload_attestation_message + .lock() + .unwrap() + .is_empty(), + "No payload attestation should be published when get data fails" + ); + } + + #[tokio::test] + async fn publish_multiple_payload_attestation_messages() { + // Create 3 validators with 1 PTC duty for each validator + let mut harness = TestHarness::create_validators(3).await; + + let attestation_slot = Slot::new(1); + harness.insert_ptc_duties(attestation_slot); + + let expected_payload_attestation = PayloadAttestationData { + beacon_block_root: Hash256::ZERO, + slot: attestation_slot, + payload_present: true, + blob_data_available: true, + }; + + harness + .mock_beacon_node_1 + .mock_get_validator_payload_attestation_data( + &expected_payload_attestation, + ForkName::Gloas, + attestation_slot, + ); + + let mock_ssz = harness + .mock_beacon_node_1 + .mock_post_beacon_pool_payload_attestations_ssz(Duration::from_secs(0)); + + let service = harness.service; + service.produce_and_publish(attestation_slot).await; + + let messages = harness + .mock_beacon_node_1 + .payload_attestation_message + .lock() + .unwrap(); + + // With 3 PTC duties in total, we should have 3 PayloadAttestationMessage + assert_eq!( + messages.len(), + 3, + "Expected three payload attestation messages" + ); + // mock_ssz is only hit once + // this is to verify that a single call to the POST endpoint can publish multiple messages in one go + mock_ssz.expect(1).assert(); + } +} From db3192e00144f338d833396cd2b98bb6a6d33d81 Mon Sep 17 00:00:00 2001 From: Pawan Dhananjay Date: Wed, 10 Jun 2026 16:00:57 +0530 Subject: [PATCH 25/26] Gloas range sync (#9362) N/A Implement range sync in gloas. Basically requests blocks and payloads post gloas from the same peer, couples them and sends it for processing. Does not change sync much at all other than adding the machinery for payloads by range requests. Main changes are: `RangeSyncBlock` which used to be a struct is an enum to account for the Gloas case. This allows a clear separation between gloas and pre-gloas code. `AvailableBlockData` now has a `BlockInEnvelope` variant. This is to clearly indicate the post gloas case. I feel this is simpler to follow compared to `NoData` variant. Tries to extract post gloas logic into its own functions so that there is minimal logic change in mainnet range sync behaviour. This is meant as a stable base on which we can iterate further to make range sync cleaner and for unblocking range sync support on devnet. Some ideas for later is removing the retry mechanism in favour of delegating column fetching to lookup sync which can be done post #9155 and batch signature verifying envelopes. Co-Authored-By: Pawan Dhananjay Co-Authored-By: dapplion <35266934+dapplion@users.noreply.github.com> Co-Authored-By: Eitan Seri-Levi --- beacon_node/beacon_chain/src/beacon_chain.rs | 36 +- .../beacon_chain/src/block_verification.rs | 21 +- .../src/block_verification_types.rs | 163 +++++-- .../src/data_availability_checker.rs | 24 +- .../payload_envelope_verification/import.rs | 58 ++- .../src/payload_envelope_verification/mod.rs | 28 +- beacon_node/beacon_chain/src/test_utils.rs | 65 ++- .../tests/attestation_production.rs | 7 +- .../beacon_chain/tests/block_verification.rs | 292 +++++++++---- beacon_node/beacon_chain/tests/store_tests.rs | 3 +- beacon_node/http_api/src/block_id.rs | 8 +- .../lighthouse_network/src/rpc/protocol.rs | 9 +- .../src/service/api_types.rs | 14 + .../network_beacon_processor/sync_methods.rs | 46 +- beacon_node/network/src/router.rs | 29 +- .../network/src/sync/backfill_sync/mod.rs | 3 + beacon_node/network/src/sync/batch.rs | 1 + .../src/sync/block_sidecar_coupling.rs | 397 +++++++++++++++++- beacon_node/network/src/sync/manager.rs | 30 +- .../network/src/sync/network_context.rs | 152 ++++++- .../src/sync/network_context/requests.rs | 2 + .../requests/payload_envelopes_by_range.rs | 48 +++ .../network/src/sync/range_sync/chain.rs | 3 + beacon_node/network/src/sync/tests/lookups.rs | 104 +++-- 24 files changed, 1311 insertions(+), 232 deletions(-) create mode 100644 beacon_node/network/src/sync/network_context/requests/payload_envelopes_by_range.rs diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 8b50f4b18f..569f0eea50 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -3108,6 +3108,15 @@ impl BeaconChain { let mut blocks = filtered_chain_segment.split_off(last_index); std::mem::swap(&mut blocks, &mut filtered_chain_segment); + // Extract envelopes before passing blocks to signature verification. + let envelopes: Vec<_> = blocks + .iter() + .map(|(_, block)| match block { + RangeSyncBlock::Gloas { envelope, .. } => envelope.clone(), + RangeSyncBlock::Base(_) => None, + }) + .collect(); + let chain = self.clone(); let signature_verification_future = self.spawn_blocking_handle( move || signature_verify_chain_segment(blocks, &chain), @@ -3132,11 +3141,15 @@ impl BeaconChain { }; // Import the blocks into the chain. - for signature_verified_block in signature_verified_blocks { + for (signature_verified_block, maybe_envelope) in + signature_verified_blocks.into_iter().zip(envelopes) + { let block_slot = signature_verified_block.slot(); + let block_root = signature_verified_block.block_root(); + let block = signature_verified_block.block_cloned(); match self .process_block( - signature_verified_block.block_root(), + block_root, signature_verified_block, notify_execution_layer, BlockImportSource::RangeSync, @@ -3166,11 +3179,8 @@ impl BeaconChain { } } Err(BlockError::DuplicateFullyImported(block_root)) => { - debug!( - ?block_root, - "Ignoring already known blocks while processing chain segment" - ); - continue; + // Block was already imported, envelope might need re-import + imported_blocks.push((block_root, block_slot)); } Err(error) => { return ChainSegmentResult::Failed { @@ -3179,6 +3189,18 @@ impl BeaconChain { }; } } + + // Process the envelope after the block has been imported. + if let Some(envelope) = maybe_envelope + && let Err(e) = self + .process_range_sync_envelope(envelope, block_root, block) + .await + { + return ChainSegmentResult::Failed { + imported_blocks, + error: BlockError::EnvelopeError(Box::new(e)), + }; + } } } diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 7b9b7e8218..6b1ac3b033 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -52,6 +52,7 @@ use crate::beacon_snapshot::PreProcessingSnapshot; use crate::block_verification_types::{AsBlock, BlockImportData, LookupBlock, RangeSyncBlock}; use crate::data_availability_checker::{ AvailabilityCheckError, AvailableBlock, AvailableBlockData, MaybeAvailableBlock, + verify_columns_against_block, }; use crate::data_column_verification::GossipDataColumnError; use crate::execution_payload::{ @@ -652,14 +653,16 @@ pub fn signature_verify_chain_segment( )?; let mut available_blocks = Vec::with_capacity(chain_segment.len()); + let mut envelopes = Vec::with_capacity(chain_segment.len()); let mut signature_verified_blocks = Vec::with_capacity(chain_segment.len()); for (block_root, block) in chain_segment { let consensus_context = ConsensusContext::new(block.slot()).set_current_block_root(block_root); - let available_block = block.into_available_block(); + let (available_block, envelope) = block.into_available_block()?; available_blocks.push(available_block.clone()); + envelopes.push(envelope); signature_verified_blocks.push(SignatureVerifiedBlock { block: MaybeAvailableBlock::Available(available_block), block_root, @@ -667,12 +670,17 @@ pub fn signature_verify_chain_segment( consensus_context, }); } - // TODO(gloas) When implementing range and backfill sync for gloas - // we need a batch verify kzg function in the new da checker as well. + chain .data_availability_checker .batch_verify_kzg_for_available_blocks(&available_blocks)?; + for (available_block, maybe_envelope) in available_blocks.iter().zip(envelopes.iter()) { + if let Some(envelope) = maybe_envelope { + verify_columns_against_block(&chain.kzg, available_block.block(), &envelope.columns)?; + } + } + // verify signatures let pubkey_cache = get_validator_pubkey_cache(chain)?; let mut signature_verifier = get_signature_verifier(&state, &pubkey_cache, &chain.spec); @@ -1341,10 +1349,13 @@ impl IntoExecutionPendingBlock for RangeSyncBlock Result, BlockSlashInfo> { // Perform an early check to prevent wasting time on irrelevant blocks. + let header = self.signed_block_header(); let block_root = check_block_relevancy(self.as_block(), block_root, chain) - .map_err(|e| BlockSlashInfo::SignatureNotChecked(self.signed_block_header(), e))?; + .map_err(|e| BlockSlashInfo::SignatureNotChecked(header.clone(), e))?; - let available_block = self.into_available_block(); + let (available_block, _envelope) = self.into_available_block().map_err(|e| { + BlockSlashInfo::SignatureNotChecked(header.clone(), BlockError::AvailabilityCheck(e)) + })?; chain .data_availability_checker .verify_kzg_for_available_block(&available_block) diff --git a/beacon_node/beacon_chain/src/block_verification_types.rs b/beacon_node/beacon_chain/src/block_verification_types.rs index be73ef15d7..18e95f58f3 100644 --- a/beacon_node/beacon_chain/src/block_verification_types.rs +++ b/beacon_node/beacon_chain/src/block_verification_types.rs @@ -2,10 +2,12 @@ use crate::data_availability_checker::{AvailabilityCheckError, DataAvailabilityC pub use crate::data_availability_checker::{ AvailableBlock, AvailableBlockData, MaybeAvailableBlock, }; +use crate::payload_envelope_verification::AvailableEnvelope; +use crate::payload_envelope_verification::gossip_verified_envelope::verify_envelope_consistency; use crate::{BeaconChainTypes, PayloadVerificationOutcome}; -use educe::Educe; use state_processing::ConsensusContext; use std::fmt::{Debug, Formatter}; +use std::hash::{Hash, Hasher}; use std::sync::Arc; use types::data::BlobIdentifier; use types::{ @@ -40,15 +42,27 @@ impl LookupBlock { } } -/// A fully available block that has been constructed by range sync. -/// The block contains all the data required to import into fork choice. -/// This includes any and all blobs/columns required, including zero if -/// none are required. This can happen if the block is pre-deneb or if -/// it's simply past the DA boundary. -#[derive(Clone, Educe)] -#[educe(Hash(bound(E: EthSpec)))] -pub struct RangeSyncBlock { - block: AvailableBlock, +/// A block that has been constructed by range sync, ready for import. +/// Pre-Gloas: wraps an `AvailableBlock` with all data. +/// Gloas: carries the block and an optional envelope which contains the sidecar data. +/// +/// Note: In the gloas case, we only ensure that the block is consistent with the envelope +/// if the envelope is `Some` when constructing a `RangeSyncBlock` type. +/// If `envelope` is None, then there is no guarantee that the canonical chain also contains +/// an empty payload. The only way to ensure that is to process the next block. +#[derive(Clone)] +pub enum RangeSyncBlock { + Base(AvailableBlock), + Gloas { + block: Arc>, + envelope: Option>, + }, +} + +impl Hash for RangeSyncBlock { + fn hash(&self, state: &mut H) { + self.block_root().hash(state); + } } impl Debug for RangeSyncBlock { @@ -59,31 +73,48 @@ impl Debug for RangeSyncBlock { impl RangeSyncBlock { pub fn block_root(&self) -> Hash256 { - self.block.block_root() + match self { + Self::Base(block) => block.block_root(), + Self::Gloas { block, .. } => block.canonical_root(), + } } pub fn as_block(&self) -> &SignedBeaconBlock { - self.block.block() + match self { + Self::Base(block) => block.block(), + Self::Gloas { block, .. } => block, + } } pub fn block_cloned(&self) -> Arc> { - self.block.block_cloned() + match self { + Self::Base(block) => block.block_cloned(), + Self::Gloas { block, .. } => block.clone(), + } } pub fn block_data(&self) -> &AvailableBlockData { - self.block.data() + match self { + Self::Base(block) => block.data(), + Self::Gloas { .. } => &AvailableBlockData::NoData, + } + } + + /// Returns the data columns associated with this block. For Gloas blocks the columns are + /// carried by the payload envelope rather than `block_data`, so this unwraps that case. + pub fn data_columns(&self) -> Option> { + match self { + Self::Base(block) => block.data().data_columns(), + Self::Gloas { envelope, .. } => envelope + .as_ref() + .map(|envelope| envelope.columns.clone()) + .filter(|columns| !columns.is_empty()), + } } } impl RangeSyncBlock { - /// Constructs an `RangeSyncBlock` from a block and availability data. - /// - /// # Errors - /// - /// Returns `AvailabilityCheckError` if: - /// - `InvalidAvailableBlockData`: Block data is provided but not required. - /// - `MissingBlobs`: Block requires blobs but they are missing or incomplete. - /// - `MissingCustodyColumns`: Block requires custody columns but they are incomplete. + /// Constructs a `RangeSyncBlock` from a block and availability data (pre-Gloas). pub fn new( block: Arc>, block_data: AvailableBlockData, @@ -93,33 +124,91 @@ impl RangeSyncBlock { where T: BeaconChainTypes, { + if block.fork_name_unchecked().gloas_enabled() { + return Err(AvailabilityCheckError::InvalidVariant); + } let available_block = AvailableBlock::new(block, block_data, da_checker, spec)?; - Ok(Self { - block: available_block, - }) + Ok(Self::Base(available_block)) + } + + /// Constructs a Gloas `RangeSyncBlock` with block and optional `AvailableEnvelope` + /// which wraps the payload envelope with its data columns. + /// + /// This function only checks for consistency between the block and the envelope + /// if envelope.is_some() == true . + /// In the `None` case, we cannot guarantee that the payload is empty until we + /// process the block that builds on top of this block. + /// + /// Expects `block.canonical_root() == envelope.beacon_block_root` as they are coupled. + pub fn new_gloas( + block: Arc>, + envelope: Option>, + ) -> Result { + if let Some(envelope) = envelope.as_ref() { + let execution_bid = &block + .message() + .body() + .signed_execution_payload_bid() + .map_err(|e| format!("missing signed_execution_payload_bid: {e:?}"))? + .message; + // Skip the finalized-slot check; range sync imports historical (finalized) blocks. + let latest_finalized_slot = Slot::new(0); + verify_envelope_consistency( + envelope.message(), + &block, + execution_bid, + latest_finalized_slot, + ) + .map_err(|e| format!("Inconsistent envelope: {e:?}"))?; + } + + Ok(Self::Gloas { block, envelope }) } #[allow(clippy::type_complexity)] pub fn deconstruct(self) -> (Hash256, Arc>, AvailableBlockData) { - self.block.deconstruct() + match self { + Self::Base(block) => block.deconstruct(), + Self::Gloas { block, .. } => { + (block.canonical_root(), block, AvailableBlockData::NoData) + } + } } pub fn n_blobs(&self) -> usize { - match self.block_data() { - AvailableBlockData::NoData | AvailableBlockData::DataColumns(_) => 0, - AvailableBlockData::Blobs(blobs) => blobs.len(), + match self { + Self::Base(block) => match block.data() { + AvailableBlockData::NoData | AvailableBlockData::DataColumns(_) => 0, + AvailableBlockData::Blobs(blobs) => blobs.len(), + }, + Self::Gloas { .. } => 0, } } pub fn n_data_columns(&self) -> usize { - match self.block_data() { - AvailableBlockData::NoData | AvailableBlockData::Blobs(_) => 0, - AvailableBlockData::DataColumns(columns) => columns.len(), + match self { + Self::Base(block) => match block.data() { + AvailableBlockData::NoData | AvailableBlockData::Blobs(_) => 0, + AvailableBlockData::DataColumns(columns) => columns.len(), + }, + Self::Gloas { .. } => 0, } } - pub fn into_available_block(self) -> AvailableBlock { - self.block + /// Converts into an `AvailableBlock` for import, returning any associated envelope + /// separately. Callers processing Gloas blocks must handle the envelope themselves. + #[allow(clippy::type_complexity)] + pub fn into_available_block( + self, + ) -> Result<(AvailableBlock, Option>), AvailabilityCheckError> { + match self { + Self::Base(block) => Ok((block, None)), + Self::Gloas { block, envelope } => { + let available = + AvailableBlock::new_gloas(block).map_err(AvailabilityCheckError::Unexpected)?; + Ok((available, envelope)) + } + } } } @@ -405,13 +494,13 @@ impl AsBlock for RangeSyncBlock { self.as_block().message() } fn as_block(&self) -> &SignedBeaconBlock { - self.block.as_block() + RangeSyncBlock::as_block(self) } fn block_cloned(&self) -> Arc> { - self.block.block_cloned() + RangeSyncBlock::block_cloned(self) } fn canonical_root(&self) -> Hash256 { - self.block.block_root() + self.block_root() } } diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index 9829db0f1d..a0b117f072 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -669,7 +669,7 @@ impl DataAvailabilityChecker { /// Verify a batch of data columns belonging to a single block, picking the right commitment /// source for the block's fork (Fulu: inline on column; Gloas: from the embedded payload bid). -fn verify_columns_against_block( +pub fn verify_columns_against_block( kzg: &Kzg, block: &SignedBeaconBlock, columns: &[Arc>], @@ -792,7 +792,12 @@ async fn availability_cache_maintenance_service( #[derive(Debug, Clone)] // TODO(#8633) move this to `block_verification_types.rs` pub enum AvailableBlockData { - /// Block is pre-Deneb or has zero blobs + /// Block has no inline DA object for block import. + /// + /// This covers: + /// - pre-Deneb blocks, + /// - blocks with zero blobs, and + /// - Gloas blocks, where DA is checked on the payload envelope instead. NoData, /// Block is post-Deneb, pre-PeerDAS and has more than zero blobs Blobs(BlobSidecarList), @@ -953,6 +958,19 @@ impl AvailableBlock { }) } + pub fn new_gloas(block: Arc>) -> Result { + if block.fork_name_unchecked().gloas_enabled() { + Ok(Self { + block_root: block.canonical_root(), + block, + blob_data: AvailableBlockData::NoData, + blobs_available_timestamp: None, + }) + } else { + Err("Block is not gloas".to_owned()) + } + } + pub fn block(&self) -> &SignedBeaconBlock { &self.block } @@ -1294,7 +1312,7 @@ mod test { let available_blocks = blocks_with_columns .into_iter() - .map(|block| block.into_available_block()) + .map(|block| block.into_available_block().unwrap().0) .collect::>(); // WHEN verifying all blocks together (totalling 256 data columns) diff --git a/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs b/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs index a10372b933..00806f0e17 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs @@ -4,9 +4,10 @@ use std::time::Duration; use eth2::types::{EventKind, SseExecutionPayload, SseExecutionPayloadAvailable}; use fork_choice::PayloadVerificationStatus; use slot_clock::SlotClock; +use state_processing::{VerifySignatures, envelope_processing::verify_execution_payload_envelope}; use store::StoreOp; use tracing::{debug, error, info, info_span, instrument, warn}; -use types::{BlockImportSource, Hash256, SignedExecutionPayloadEnvelope}; +use types::{BlockImportSource, Hash256, SignedBeaconBlock, SignedExecutionPayloadEnvelope}; use super::{ AvailableEnvelope, AvailableExecutedEnvelope, EnvelopeError, @@ -19,6 +20,7 @@ use crate::{ metrics, payload_envelope_verification::{ AvailabilityPendingExecutedEnvelope, ExecutionPendingEnvelope, + load_snapshot_from_state_root, payload_notifier::PayloadNotifier, }, validator_monitor::get_slot_delay_ms, }; @@ -372,4 +374,58 @@ impl BeaconChain { )); } } + + /// Process an envelope received during range sync. The associated block must already + /// be imported into fork choice. This performs signature verification, state processing, + /// EL verification and import. + #[instrument(skip_all, level = "debug")] + pub async fn process_range_sync_envelope( + self: &Arc, + available_envelope: AvailableEnvelope, + block_root: Hash256, + block: Arc>, + ) -> Result<(), EnvelopeError> { + let signed_envelope = available_envelope.envelope().clone(); + + // Load the state snapshot for envelope processing + let state_root = block.state_root(); + let snapshot = load_snapshot_from_state_root::(block_root, state_root, &self.store)?; + + // Verify envelope signature and state processing + verify_execution_payload_envelope( + &snapshot.pre_state, + &signed_envelope, + VerifySignatures::True, + snapshot.state_root, + &self.spec, + )?; + + // Send to EL for verification + let payload_notifier = PayloadNotifier::new( + self.clone(), + signed_envelope.clone(), + block, + NotifyExecutionLayer::Yes, + )?; + + let payload_verification_status = payload_notifier.notify_new_payload().await?; + + // Import directly — we already have all components (envelope + columns). + let chain = self.clone(); + let _ = self + .spawn_blocking_handle( + move || { + chain.import_execution_payload_envelope( + available_envelope, + block_root, + payload_verification_status, + ) + }, + "range_sync_envelope_import", + ) + .await + .map_err(|e| EnvelopeError::BeaconChainError(Box::new(e)))?; + + Ok(()) + } } diff --git a/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs b/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs index a1cbac35b3..a0d34949c6 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs @@ -40,7 +40,7 @@ mod payload_notifier; pub use execution_pending_envelope::ExecutionPendingEnvelope; -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct AvailableEnvelope { envelope: Arc>, pub columns: DataColumnSidecarList, @@ -54,6 +54,10 @@ impl AvailableEnvelope { Self { envelope, columns } } + pub fn envelope(&self) -> &Arc> { + &self.envelope + } + pub fn message(&self) -> &ExecutionPayloadEnvelope { &self.envelope.message } @@ -180,6 +184,28 @@ impl std::fmt::Display for EnvelopeError { } } +impl EnvelopeError { + pub fn penalize_peer(&self) -> bool { + match self { + EnvelopeError::BadSignature + | EnvelopeError::BuilderIndexMismatch { .. } + | EnvelopeError::SlotMismatch { .. } + | EnvelopeError::BlockHashMismatch { .. } + | EnvelopeError::UnknownValidator { .. } + | EnvelopeError::IncorrectBlockProposer { .. } + | EnvelopeError::EnvelopeProcessingError(_) => true, + EnvelopeError::ExecutionPayloadError(e) => e.penalize_peer(), + EnvelopeError::BlockRootUnknown { .. } + | EnvelopeError::PriorToFinalization { .. } + | EnvelopeError::BeaconChainError(_) + | EnvelopeError::BeaconStateError(_) + | EnvelopeError::OptimisticSyncNotSupported { .. } + | EnvelopeError::BlockRootNotInForkChoice(_) + | EnvelopeError::InternalError(_) => false, + } + } +} + impl From for EnvelopeError { fn from(e: BeaconChainError) -> Self { EnvelopeError::BeaconChainError(Box::new(e)) diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index db2a9a902d..62c7fb3a45 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -4,6 +4,7 @@ use crate::data_availability_checker::DataAvailabilityChecker; use crate::graffiti_calculator::GraffitiSettings; use crate::kzg_utils::{build_data_column_sidecars_fulu, build_data_column_sidecars_gloas}; use crate::observed_operations::ObservationOutcome; +use crate::payload_envelope_verification::AvailableEnvelope; pub use crate::persisted_beacon_chain::PersistedBeaconChain; use crate::{BeaconBlockResponseWrapper, CustodyContext, get_block_root}; use crate::{ @@ -2929,18 +2930,29 @@ where block: Arc>, ) -> RangeSyncBlock { let block_root = block_root.unwrap_or_else(|| get_block_root(&block)); + let is_gloas = block.fork_name_unchecked().gloas_enabled(); // For Gloas, kzg commitments live in the bid (`signed_execution_payload_bid`), so the // body's `blob_kzg_commitments()` accessor returns Err. `num_expected_blobs` already // handles both shapes. let has_blobs = block.num_expected_blobs() > 0; if !has_blobs { - return RangeSyncBlock::new( - block, - AvailableBlockData::NoData, - &self.chain.data_availability_checker, - self.chain.spec.clone(), - ) - .unwrap(); + return if is_gloas { + let envelope = self + .chain + .get_payload_envelope(&block_root) + .unwrap() + .map(Arc::new) + .map(|envelope| AvailableEnvelope::new(envelope, vec![])); + RangeSyncBlock::new_gloas(block, envelope).unwrap() + } else { + RangeSyncBlock::new( + block, + AvailableBlockData::NoData, + &self.chain.data_availability_checker, + self.chain.spec.clone(), + ) + .unwrap() + }; } // Blobs are stored as data columns from Fulu (PeerDAS) @@ -2952,14 +2964,24 @@ where .unwrap() .unwrap(); let custody_columns = columns.into_iter().collect::>(); - let block_data = AvailableBlockData::new_with_data_columns(custody_columns); - RangeSyncBlock::new( - block, - block_data, - &self.chain.data_availability_checker, - self.chain.spec.clone(), - ) - .unwrap() + if is_gloas { + let envelope = self + .chain + .get_payload_envelope(&block_root) + .unwrap() + .map(Arc::new) + .map(|envelope| AvailableEnvelope::new(envelope, custody_columns)); + RangeSyncBlock::new_gloas(block, envelope).unwrap() + } else { + let block_data = AvailableBlockData::new_with_data_columns(custody_columns); + RangeSyncBlock::new( + block, + block_data, + &self.chain.data_availability_checker, + self.chain.spec.clone(), + ) + .unwrap() + } } else { let blobs = self.chain.get_blobs(&block_root).unwrap().blobs(); let block_data = if let Some(blobs) = blobs { @@ -2984,6 +3006,19 @@ where block: Arc>>, blob_items: Option<(KzgProofs, BlobsList)>, ) -> Result, BlockError> { + if block.fork_name_unchecked().gloas_enabled() { + let columns = blob_items + .map(|_| generate_data_column_sidecars_from_block(&block, &self.spec)) + .unwrap_or_default(); + let envelope = self + .chain + .get_payload_envelope(&block.canonical_root()) + .map_err(|e| BlockError::BeaconChainError(Box::new(e)))? + .map(Arc::new) + .map(|envelope| AvailableEnvelope::new(envelope, columns)); + return RangeSyncBlock::new_gloas(block, envelope).map_err(BlockError::InternalError); + } + Ok(if self.spec.is_peer_das_enabled_for_epoch(block.epoch()) { let epoch = block.slot().epoch(E::slots_per_epoch()); let sampling_columns = self.chain.sampling_columns_for_epoch(epoch); diff --git a/beacon_node/beacon_chain/tests/attestation_production.rs b/beacon_node/beacon_chain/tests/attestation_production.rs index 862c2a9fe8..9d32b37134 100644 --- a/beacon_node/beacon_chain/tests/attestation_production.rs +++ b/beacon_node/beacon_chain/tests/attestation_production.rs @@ -235,7 +235,7 @@ async fn produces_attestations() { let range_sync_block = harness .build_range_sync_block_from_store_blobs(Some(block_root), Arc::new(block.clone())); - let available_block = range_sync_block.into_available_block(); + let (available_block, _envelope) = range_sync_block.into_available_block().unwrap(); // For Gloas non-same-slot attestations, the early attester cache returns None. let is_same_slot_attestation = slot == block_slot; @@ -300,12 +300,13 @@ async fn early_attester_cache_old_request() { .get_block(&head.beacon_block_root) .unwrap(); - let available_block = harness + let (available_block, _envelope) = harness .build_range_sync_block_from_store_blobs( Some(head.beacon_block_root), head.beacon_block.clone(), ) - .into_available_block(); + .into_available_block() + .unwrap(); harness .chain diff --git a/beacon_node/beacon_chain/tests/block_verification.rs b/beacon_node/beacon_chain/tests/block_verification.rs index d7bba13eb0..94d4b3b9da 100644 --- a/beacon_node/beacon_chain/tests/block_verification.rs +++ b/beacon_node/beacon_chain/tests/block_verification.rs @@ -3,13 +3,14 @@ use beacon_chain::block_verification_types::{AsBlock, ExecutedBlock, LookupBlock, RangeSyncBlock}; use beacon_chain::data_availability_checker::{AvailabilityCheckError, AvailableBlockData}; use beacon_chain::data_column_verification::CustodyDataColumn; +use beacon_chain::payload_envelope_verification::AvailableEnvelope; use beacon_chain::{ AvailabilityProcessingStatus, BeaconChain, BeaconChainTypes, ExecutionPendingBlock, WhenSlotSkipped, custody_context::NodeCustodyType, test_utils::{ AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType, - MakeAttestationOptions, fork_name_from_env, test_spec, + MakeAttestationOptions, test_spec, }, }; use beacon_chain::{ @@ -158,19 +159,37 @@ where .zip(chain_segment_sidecars.iter()) .map(|(snapshot, data_sidecars)| { let block = snapshot.beacon_block.clone(); - build_range_sync_block(block, data_sidecars, chain.clone()) + build_range_sync_block( + block, + snapshot.execution_envelope.clone(), + data_sidecars, + chain.clone(), + ) }) .collect() } fn build_range_sync_block( block: Arc>, + execution_envelope: Option>>, data_sidecars: &Option>, chain: Arc>, ) -> RangeSyncBlock where T: BeaconChainTypes, { + if block.fork_name_unchecked().gloas_enabled() { + let columns = match data_sidecars { + Some(DataSidecars::DataColumns(columns)) => columns + .iter() + .map(|c| c.as_data_column().clone()) + .collect::>(), + Some(DataSidecars::Blobs(_)) | None => vec![], + }; + let envelope = execution_envelope.map(|envelope| AvailableEnvelope::new(envelope, columns)); + return RangeSyncBlock::new_gloas(block, envelope).unwrap(); + } + match data_sidecars { Some(DataSidecars::Blobs(blobs)) => { let block_data = AvailableBlockData::new_with_blobs(blobs.clone()); @@ -286,6 +305,14 @@ fn update_proposal_signatures( } } +fn update_envelope_block_root(snapshot: &mut BeaconSnapshot) { + if let Some(envelope) = snapshot.execution_envelope.as_ref() { + let mut envelope = envelope.as_ref().clone(); + envelope.message.beacon_block_root = snapshot.beacon_block.canonical_root(); + snapshot.execution_envelope = Some(Arc::new(envelope)); + } +} + fn update_parent_roots(snapshots: &mut [BeaconSnapshot], blobs: &mut [Option>]) { for i in 0..snapshots.len() { let root = snapshots[i].beacon_block.canonical_root(); @@ -304,6 +331,7 @@ fn update_parent_roots(snapshots: &mut [BeaconSnapshot], blobs: &mut [Option< } } child.beacon_block = new_child; + update_envelope_block_root(child); } } } @@ -369,10 +397,6 @@ fn update_data_column_signed_header( #[tokio::test] async fn chain_segment_full_segment() { - // TODO(gloas): re-enable for Gloas once range sync imports payload envelopes. - if fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { - return; - } let harness = get_harness(VALIDATOR_COUNT, NodeCustodyType::Fullnode); let (chain_segment, chain_segment_blobs) = get_chain_segment().await; store_envelopes_for_chain_segment(chain_segment, &harness); @@ -413,10 +437,6 @@ async fn chain_segment_full_segment() { #[tokio::test] async fn chain_segment_varying_chunk_size() { - // TODO(gloas): re-enable for Gloas once range sync imports payload envelopes. - if fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { - return; - } let (chain_segment, chain_segment_blobs) = get_chain_segment().await; let harness = get_harness(VALIDATOR_COUNT, NodeCustodyType::Fullnode); let blocks: Vec> = @@ -495,13 +515,18 @@ async fn chain_segment_non_linear_parent_roots() { let (mut block, signature) = blocks[3].as_block().clone().deconstruct(); *block.parent_root_mut() = Hash256::zero(); - blocks[3] = RangeSyncBlock::new( - Arc::new(SignedBeaconBlock::from_block(block, signature)), - blocks[3].block_data().clone(), - &harness.chain.data_availability_checker, - harness.spec.clone(), - ) - .unwrap(); + let mutated_block = Arc::new(SignedBeaconBlock::from_block(block, signature)); + blocks[3] = if mutated_block.fork_name_unchecked().gloas_enabled() { + RangeSyncBlock::new_gloas(mutated_block, None).unwrap() + } else { + RangeSyncBlock::new( + mutated_block, + blocks[3].block_data().clone(), + &harness.chain.data_availability_checker, + harness.spec.clone(), + ) + .unwrap() + }; assert!( matches!( @@ -535,13 +560,18 @@ async fn chain_segment_non_linear_slots() { .collect(); let (mut block, signature) = blocks[3].as_block().clone().deconstruct(); *block.slot_mut() = Slot::new(0); - blocks[3] = RangeSyncBlock::new( - Arc::new(SignedBeaconBlock::from_block(block, signature)), - blocks[3].block_data().clone(), - &harness.chain.data_availability_checker, - harness.spec.clone(), - ) - .unwrap(); + let mutated_block = Arc::new(SignedBeaconBlock::from_block(block, signature)); + blocks[3] = if mutated_block.fork_name_unchecked().gloas_enabled() { + RangeSyncBlock::new_gloas(mutated_block, None).unwrap() + } else { + RangeSyncBlock::new( + mutated_block, + blocks[3].block_data().clone(), + &harness.chain.data_availability_checker, + harness.spec.clone(), + ) + .unwrap() + }; assert!( matches!( @@ -565,13 +595,18 @@ async fn chain_segment_non_linear_slots() { .collect(); let (mut block, signature) = blocks[3].as_block().clone().deconstruct(); *block.slot_mut() = blocks[2].slot(); - blocks[3] = RangeSyncBlock::new( - Arc::new(SignedBeaconBlock::from_block(block, signature)), - blocks[3].block_data().clone(), - &harness.chain.data_availability_checker, - harness.chain.spec.clone(), - ) - .unwrap(); + let mutated_block = Arc::new(SignedBeaconBlock::from_block(block, signature)); + blocks[3] = if mutated_block.fork_name_unchecked().gloas_enabled() { + RangeSyncBlock::new_gloas(mutated_block, None).unwrap() + } else { + RangeSyncBlock::new( + mutated_block, + blocks[3].block_data().clone(), + &harness.chain.data_availability_checker, + harness.chain.spec.clone(), + ) + .unwrap() + }; assert!( matches!( @@ -599,7 +634,12 @@ async fn assert_invalid_signature( .iter() .zip(chain_segment_blobs.iter()) .map(|(snapshot, blobs)| { - build_range_sync_block(snapshot.beacon_block.clone(), blobs, harness.chain.clone()) + build_range_sync_block( + snapshot.beacon_block.clone(), + snapshot.execution_envelope.clone(), + blobs, + harness.chain.clone(), + ) }) .collect(); @@ -638,7 +678,12 @@ async fn assert_invalid_signature( .zip(chain_segment_blobs.iter()) .filter(|(snapshot, _)| snapshot.beacon_block.slot() > finalized_slot) .map(|(snapshot, blobs)| { - build_range_sync_block(snapshot.beacon_block.clone(), blobs, harness.chain.clone()) + build_range_sync_block( + snapshot.beacon_block.clone(), + snapshot.execution_envelope.clone(), + blobs, + harness.chain.clone(), + ) }) .collect(); // We don't care if this fails, we just call this to ensure that all prior blocks have been @@ -656,6 +701,7 @@ async fn assert_invalid_signature( snapshots[block_index].beacon_block.canonical_root(), build_range_sync_block( snapshots[block_index].beacon_block.clone(), + snapshots[block_index].execution_envelope.clone(), &chain_segment_blobs[block_index], harness.chain.clone(), ), @@ -697,10 +743,6 @@ async fn get_invalid_sigs_harness( } #[tokio::test] async fn invalid_signature_gossip_block() { - // TODO(gloas): re-enable for Gloas once range sync imports payload envelopes. - if fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { - return; - } let (chain_segment, chain_segment_blobs) = get_chain_segment().await; for &block_index in BLOCK_INDICES { // Ensure the block will be rejected if imported on its own (without gossip checking). @@ -721,7 +763,12 @@ async fn invalid_signature_gossip_block() { .take(block_index) .zip(chain_segment_blobs.iter()) .map(|(snapshot, blobs)| { - build_range_sync_block(snapshot.beacon_block.clone(), blobs, harness.chain.clone()) + build_range_sync_block( + snapshot.beacon_block.clone(), + snapshot.execution_envelope.clone(), + blobs, + harness.chain.clone(), + ) }) .collect(); harness @@ -757,10 +804,6 @@ async fn invalid_signature_gossip_block() { #[tokio::test] async fn invalid_signature_block_proposal() { - // TODO(gloas): re-enable for Gloas once range sync imports payload envelopes. - if fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { - return; - } let (chain_segment, chain_segment_blobs) = get_chain_segment().await; for &block_index in BLOCK_INDICES { let harness = get_invalid_sigs_harness(chain_segment).await; @@ -778,7 +821,12 @@ async fn invalid_signature_block_proposal() { .iter() .zip(chain_segment_blobs.iter()) .map(|(snapshot, blobs)| { - build_range_sync_block(snapshot.beacon_block.clone(), blobs, harness.chain.clone()) + build_range_sync_block( + snapshot.beacon_block.clone(), + snapshot.execution_envelope.clone(), + blobs, + harness.chain.clone(), + ) }) .collect::>(); // Ensure the block will be rejected if imported in a chain segment. @@ -800,10 +848,6 @@ async fn invalid_signature_block_proposal() { #[tokio::test] async fn invalid_signature_randao_reveal() { - // TODO(gloas): re-enable for Gloas once range sync imports payload envelopes. - if fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { - return; - } let (chain_segment, ref_blobs) = get_chain_segment().await; let mut chain_segment_blobs = ref_blobs.clone(); for &block_index in BLOCK_INDICES { @@ -817,6 +861,7 @@ async fn invalid_signature_randao_reveal() { *block.body_mut().randao_reveal_mut() = junk_signature(); snapshots[block_index].beacon_block = Arc::new(SignedBeaconBlock::from_block(block, signature)); + update_envelope_block_root(&mut snapshots[block_index]); update_parent_roots(&mut snapshots, &mut chain_segment_blobs); update_proposal_signatures(&mut snapshots, &harness); assert_invalid_signature( @@ -833,10 +878,6 @@ async fn invalid_signature_randao_reveal() { #[tokio::test] async fn invalid_signature_proposer_slashing() { - // TODO(gloas): re-enable for Gloas once range sync imports payload envelopes. - if fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { - return; - } let (chain_segment, ref_blobs) = get_chain_segment().await; let mut chain_segment_blobs = ref_blobs.clone(); for &block_index in BLOCK_INDICES { @@ -864,6 +905,7 @@ async fn invalid_signature_proposer_slashing() { .expect("should update proposer slashing"); snapshots[block_index].beacon_block = Arc::new(SignedBeaconBlock::from_block(block, signature)); + update_envelope_block_root(&mut snapshots[block_index]); update_parent_roots(&mut snapshots, &mut chain_segment_blobs); update_proposal_signatures(&mut snapshots, &harness); assert_invalid_signature( @@ -880,10 +922,6 @@ async fn invalid_signature_proposer_slashing() { #[tokio::test] async fn invalid_signature_attester_slashing() { - // TODO(gloas): re-enable for Gloas once range sync imports payload envelopes. - if fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { - return; - } let (chain_segment, ref_blobs) = get_chain_segment().await; let mut chain_segment_blobs = ref_blobs.clone(); for &block_index in BLOCK_INDICES { @@ -990,6 +1028,7 @@ async fn invalid_signature_attester_slashing() { } snapshots[block_index].beacon_block = Arc::new(SignedBeaconBlock::from_block(block, signature)); + update_envelope_block_root(&mut snapshots[block_index]); update_parent_roots(&mut snapshots, &mut chain_segment_blobs); update_proposal_signatures(&mut snapshots, &harness); assert_invalid_signature( @@ -1006,10 +1045,6 @@ async fn invalid_signature_attester_slashing() { #[tokio::test] async fn invalid_signature_attestation() { - // TODO(gloas): re-enable for Gloas once range sync imports payload envelopes. - if fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { - return; - } let (chain_segment, ref_blobs) = get_chain_segment().await; let mut chain_segment_blobs = ref_blobs.clone(); let mut checked_attestation = false; @@ -1060,6 +1095,7 @@ async fn invalid_signature_attestation() { if block.body().attestations_len() > 0 { snapshots[block_index].beacon_block = Arc::new(SignedBeaconBlock::from_block(block, signature)); + update_envelope_block_root(&mut snapshots[block_index]); update_parent_roots(&mut snapshots, &mut chain_segment_blobs); update_proposal_signatures(&mut snapshots, &harness); assert_invalid_signature( @@ -1112,13 +1148,19 @@ async fn invalid_signature_deposit() { .expect("should update deposit"); snapshots[block_index].beacon_block = Arc::new(SignedBeaconBlock::from_block(block, signature)); + update_envelope_block_root(&mut snapshots[block_index]); update_parent_roots(&mut snapshots, &mut chain_segment_blobs); update_proposal_signatures(&mut snapshots, &harness); let blocks: Vec> = snapshots .iter() .zip(chain_segment_blobs.iter()) .map(|(snapshot, blobs)| { - build_range_sync_block(snapshot.beacon_block.clone(), blobs, harness.chain.clone()) + build_range_sync_block( + snapshot.beacon_block.clone(), + snapshot.execution_envelope.clone(), + blobs, + harness.chain.clone(), + ) }) .collect(); assert!( @@ -1137,10 +1179,6 @@ async fn invalid_signature_deposit() { #[tokio::test] async fn invalid_signature_exit() { - // TODO(gloas): re-enable for Gloas once range sync imports payload envelopes. - if fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { - return; - } let (chain_segment, ref_blobs) = get_chain_segment().await; let mut chain_segment_blobs = ref_blobs.clone(); for &block_index in BLOCK_INDICES { @@ -1165,6 +1203,7 @@ async fn invalid_signature_exit() { .expect("should update deposit"); snapshots[block_index].beacon_block = Arc::new(SignedBeaconBlock::from_block(block, signature)); + update_envelope_block_root(&mut snapshots[block_index]); update_parent_roots(&mut snapshots, &mut chain_segment_blobs); update_proposal_signatures(&mut snapshots, &harness); assert_invalid_signature( @@ -2158,13 +2197,19 @@ async fn import_duplicate_block_unrealized_justification() { // Create two verified variants of the block, representing the same block being processed in // parallel. let notify_execution_layer = NotifyExecutionLayer::Yes; - let range_sync_block = RangeSyncBlock::new( - block.clone(), - AvailableBlockData::NoData, - &harness.chain.data_availability_checker, - harness.spec.clone(), - ) - .unwrap(); + let range_sync_block = if block.fork_name_unchecked().gloas_enabled() { + // Fine to pass no envelope as we are testing duplicate block import + // which is not related. + RangeSyncBlock::new_gloas(block.clone(), None).unwrap() + } else { + RangeSyncBlock::new( + block.clone(), + AvailableBlockData::NoData, + &harness.chain.data_availability_checker, + harness.spec.clone(), + ) + .unwrap() + }; let verified_block1 = range_sync_block .clone() .into_execution_pending_block(block_root, chain, notify_execution_layer) @@ -2239,6 +2284,111 @@ async fn import_execution_pending_block( } } +async fn make_gloas_range_sync_block_inputs() +-> Option<(Arc>, SignedExecutionPayloadEnvelope)> { + let spec = test_spec::(); + if !spec.fork_name_at_slot::(Slot::new(1)).gloas_enabled() { + return None; + } + + let harness = BeaconChainHarness::builder(MainnetEthSpec) + .spec(spec.into()) + .keypairs(KEYPAIRS[0..VALIDATOR_COUNT].to_vec()) + .node_custody_type(NodeCustodyType::Supernode) + .fresh_ephemeral_store() + .mock_execution_layer() + .build(); + + harness.advance_slot(); + + let state = harness.get_current_state(); + let slot = harness.get_current_slot(); + let ((block, _), envelope, _) = harness.make_block_with_envelope(state, slot).await; + Some((block, envelope.expect("gloas block should have envelope"))) +} + +#[tokio::test] +async fn range_sync_block_new_gloas_accepts_matching_envelope() { + let Some((block, envelope)) = make_gloas_range_sync_block_inputs().await else { + return; + }; + + let available_envelope = AvailableEnvelope::new(Arc::new(envelope), vec![]); + let result = RangeSyncBlock::new_gloas(block, Some(available_envelope)); + + assert!( + result.is_ok(), + "new_gloas should accept matching block/envelope, got: {:?}", + result + ); +} + +#[tokio::test] +async fn range_sync_block_new_gloas_allows_missing_envelope() { + let Some((block, _)) = make_gloas_range_sync_block_inputs().await else { + return; + }; + + let result = RangeSyncBlock::new_gloas(block, None); + + assert!( + result.is_ok(), + "new_gloas should allow None envelope, got: {:?}", + result + ); +} + +#[tokio::test] +async fn range_sync_block_new_gloas_rejects_slot_mismatch() { + let Some((block, mut envelope)) = make_gloas_range_sync_block_inputs().await else { + return; + }; + + envelope.message.payload.slot_number += 1; + let available_envelope = AvailableEnvelope::new(Arc::new(envelope), vec![]); + let result = RangeSyncBlock::new_gloas(block, Some(available_envelope)); + + assert!( + matches!(result, Err(ref err) if err.contains("SlotMismatch")), + "new_gloas should reject mismatched slot, got: {:?}", + result + ); +} + +#[tokio::test] +async fn range_sync_block_new_gloas_rejects_builder_index_mismatch() { + let Some((block, mut envelope)) = make_gloas_range_sync_block_inputs().await else { + return; + }; + + envelope.message.builder_index += 1; + let available_envelope = AvailableEnvelope::new(Arc::new(envelope), vec![]); + let result = RangeSyncBlock::new_gloas(block, Some(available_envelope)); + + assert!( + matches!(result, Err(ref err) if err.contains("BuilderIndexMismatch")), + "new_gloas should reject mismatched builder index, got: {:?}", + result + ); +} + +#[tokio::test] +async fn range_sync_block_new_gloas_rejects_block_hash_mismatch() { + let Some((block, mut envelope)) = make_gloas_range_sync_block_inputs().await else { + return; + }; + + envelope.message.payload.block_hash = ExecutionBlockHash::repeat_byte(0x22); + let available_envelope = AvailableEnvelope::new(Arc::new(envelope), vec![]); + let result = RangeSyncBlock::new_gloas(block, Some(available_envelope)); + + assert!( + matches!(result, Err(ref err) if err.contains("BlockHashMismatch")), + "new_gloas should reject mismatched block hash, got: {:?}", + result + ); +} + // Test that RpcBlock::new() rejects blocks when blob count doesn't match expected. #[tokio::test] async fn range_sync_block_construction_fails_with_wrong_blob_count() { diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index b70961c499..4d392ef524 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -3304,7 +3304,8 @@ async fn weak_subjectivity_sync_test( let range_sync_block = harness .build_range_sync_block_from_store_blobs(Some(block_root), Arc::new(full_block)); - let fully_available_block = range_sync_block.into_available_block(); + let (fully_available_block, _envelope) = + range_sync_block.into_available_block().unwrap(); harness .chain .data_availability_checker diff --git a/beacon_node/http_api/src/block_id.rs b/beacon_node/http_api/src/block_id.rs index 8843541c11..dce4713245 100644 --- a/beacon_node/http_api/src/block_id.rs +++ b/beacon_node/http_api/src/block_id.rs @@ -533,7 +533,8 @@ mod tests { use super::*; use beacon_chain::{ PayloadVerificationStatus, - block_verification_types::{AvailableBlockData, RangeSyncBlock}, + block_verification_types::AvailableBlockData, + data_availability_checker::AvailableBlock, test_utils::{ BeaconChainHarness, EphemeralHarnessType, fork_name_from_env, generate_data_column_sidecars_from_block, @@ -611,14 +612,13 @@ mod tests { "precondition: {fork_name:?} test block must produce data columns" ); - let available_block = RangeSyncBlock::new( + let available_block = AvailableBlock::new( block.clone(), AvailableBlockData::new_with_data_columns(data_columns), &chain.data_availability_checker, chain.spec.clone(), ) - .unwrap() - .into_available_block(); + .unwrap(); let current_slot = harness.get_current_slot(); let cached_head = chain.canonical_head.cached_head(); diff --git a/beacon_node/lighthouse_network/src/rpc/protocol.rs b/beacon_node/lighthouse_network/src/rpc/protocol.rs index ed9dc5666a..b444608468 100644 --- a/beacon_node/lighthouse_network/src/rpc/protocol.rs +++ b/beacon_node/lighthouse_network/src/rpc/protocol.rs @@ -726,20 +726,17 @@ pub fn rpc_data_column_limits( spec: &ChainSpec, ) -> RpcLimits { let fork_name = spec.fork_name_at_epoch(current_digest_epoch); + let max_blobs = spec.max_blobs_per_block(current_digest_epoch) as usize; if fork_name.gloas_enabled() { RpcLimits::new( DataColumnSidecarGloas::::min_size(), - DataColumnSidecarFulu::::max_size( - spec.max_blobs_per_block(current_digest_epoch) as usize - ), + DataColumnSidecarFulu::::max_size(max_blobs), ) } else { RpcLimits::new( DataColumnSidecarFulu::::min_size(), - DataColumnSidecarFulu::::max_size( - spec.max_blobs_per_block(current_digest_epoch) as usize - ), + DataColumnSidecarFulu::::max_size(max_blobs), ) } } diff --git a/beacon_node/lighthouse_network/src/service/api_types.rs b/beacon_node/lighthouse_network/src/service/api_types.rs index 1d0d181cb3..4a8c6c55eb 100644 --- a/beacon_node/lighthouse_network/src/service/api_types.rs +++ b/beacon_node/lighthouse_network/src/service/api_types.rs @@ -31,6 +31,8 @@ pub enum SyncRequestId { BlobsByRange(BlobsByRangeRequestId), /// Data columns by range request DataColumnsByRange(DataColumnsByRangeRequestId), + /// Payload envelopes by range request + PayloadEnvelopesByRange(PayloadEnvelopesByRangeRequestId), } /// Request ID for data_columns_by_root requests. Block lookups do not issue this request directly. @@ -57,6 +59,12 @@ pub struct BlobsByRangeRequestId { pub parent_request_id: ComponentsByRangeRequestId, } +#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] +pub struct PayloadEnvelopesByRangeRequestId { + pub id: Id, + pub parent_request_id: ComponentsByRangeRequestId, +} + #[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] pub struct DataColumnsByRangeRequestId { /// Id to identify this attempt at a data_columns_by_range request for `parent_request_id` @@ -259,6 +267,12 @@ macro_rules! impl_display { impl_display!(BlocksByRangeRequestId, "{}/{}", id, parent_request_id); impl_display!(BlobsByRangeRequestId, "{}/{}", id, parent_request_id); impl_display!(DataColumnsByRangeRequestId, "{}/{}", id, parent_request_id); +impl_display!( + PayloadEnvelopesByRangeRequestId, + "{}/{}", + id, + parent_request_id +); impl_display!(ComponentsByRangeRequestId, "{}/{}", id, requester); impl_display!(DataColumnsByRootRequestId, "{}/{}", id, requester); impl_display!(SingleLookupReqId, "{}/Lookup/{}", req_id, lookup_id); 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 528a261bb8..e2226af094 100644 --- a/beacon_node/network/src/network_beacon_processor/sync_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/sync_methods.rs @@ -682,10 +682,26 @@ impl NetworkBeaconProcessor { downloaded_blocks: Vec>, ) -> (usize, Result<(), ChainSegmentFailed>) { let total_blocks = downloaded_blocks.len(); - let available_blocks = downloaded_blocks + let available_blocks = match downloaded_blocks .into_iter() - .map(|block| block.into_available_block()) - .collect::>(); + .map(|block| { + block + .into_available_block() + .map(|(available, _envelope)| available) + }) + .collect::, _>>() + { + Ok(blocks) => blocks, + Err(e) => { + return ( + 0, + Err(ChainSegmentFailed { + peer_action: Some(PeerAction::LowToleranceError), + message: format!("Block failed availability construction: {:?}", e), + }), + ); + } + }; // TODO(gloas) when implementing backfill sync for gloas // we need a batch verify kzg function in the new da checker @@ -893,6 +909,17 @@ impl NetworkBeaconProcessor { peer_action: None, }) } + ref err @ BlockError::EnvelopeError(ref envelope_error) => { + debug!(error = ?err, "Invalid execution payload envelope"); + Err(ChainSegmentFailed { + message: format!("Invalid execution payload envelope: {err:?}"), + peer_action: if envelope_error.penalize_peer() { + Some(PeerAction::LowToleranceError) + } else { + None + }, + }) + } ref err @ BlockError::ExecutionPayloadError(ref epe) => { if !epe.penalize_peer() { // These errors indicate an issue with the EL and not the `ChainSegment`. @@ -1030,9 +1057,16 @@ impl From> for BlockProcessingR None } } - BlockError::EnvelopeError(_) => { - // TODO(gloas): penalize correctly in range sync PR - None + BlockError::EnvelopeError(epe) => { + if epe.penalize_peer() { + Some(( + PeerAction::MidToleranceError, + WhichPeerToPenalize::BlockPeer, + (&e).into(), + )) + } else { + None + } } // Remaining invalid blocks: penalize the block peer. Listed explicitly so a // new `BlockError` variant forces a compile error here. diff --git a/beacon_node/network/src/router.rs b/beacon_node/network/src/router.rs index 277ece0aa8..315ec9387d 100644 --- a/beacon_node/network/src/router.rs +++ b/beacon_node/network/src/router.rs @@ -352,10 +352,8 @@ impl Router { 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"); + Response::PayloadEnvelopesByRange(envelope) => { + self.on_payload_envelopes_by_range_response(peer_id, app_request_id, envelope); } // Lighthouse currently only serves BlocksByHead and does not issue it as a client, // so receiving a response is unexpected. Drop it without crashing. @@ -802,6 +800,29 @@ impl Router { }); } + /// Handle a `PayloadEnvelopesByRange` response from the peer. + pub fn on_payload_envelopes_by_range_response( + &mut self, + peer_id: PeerId, + app_request_id: AppRequestId, + envelope: Option>>, + ) { + let sync_request_id = match app_request_id { + AppRequestId::Sync(id @ SyncRequestId::PayloadEnvelopesByRange { .. }) => id, + other => { + crit!(request = ?other, %peer_id, "PayloadEnvelopesByRange 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/backfill_sync/mod.rs b/beacon_node/network/src/sync/backfill_sync/mod.rs index 36816fb5d6..c8bb17243e 100644 --- a/beacon_node/network/src/sync/backfill_sync/mod.rs +++ b/beacon_node/network/src/sync/backfill_sync/mod.rs @@ -351,6 +351,9 @@ impl BackFillSync { CouplingError::BlobPeerFailure(msg) => { debug!(?batch_id, msg, "Blob peer failure"); } + CouplingError::EnvelopePeerFailure(msg) => { + debug!(?batch_id, msg, "Envelope peer failure"); + } CouplingError::InternalError(msg) => { error!(?batch_id, msg, "Block components coupling internal error"); } diff --git a/beacon_node/network/src/sync/batch.rs b/beacon_node/network/src/sync/batch.rs index 10af1bf503..8d40ec8b7f 100644 --- a/beacon_node/network/src/sync/batch.rs +++ b/beacon_node/network/src/sync/batch.rs @@ -34,6 +34,7 @@ pub type BatchId = Epoch; pub enum ByRangeRequestType { BlocksAndColumns, BlocksAndBlobs, + BlocksAndEnvelopesAndColumns, Blocks, Columns(HashSet), } diff --git a/beacon_node/network/src/sync/block_sidecar_coupling.rs b/beacon_node/network/src/sync/block_sidecar_coupling.rs index 999b3dd30e..b64ae4a4c5 100644 --- a/beacon_node/network/src/sync/block_sidecar_coupling.rs +++ b/beacon_node/network/src/sync/block_sidecar_coupling.rs @@ -1,3 +1,4 @@ +use beacon_chain::payload_envelope_verification::AvailableEnvelope; use beacon_chain::{ BeaconChainTypes, block_verification_types::{AvailableBlockData, RangeSyncBlock}, @@ -9,14 +10,15 @@ use lighthouse_network::{ PeerId, service::api_types::{ BlobsByRangeRequestId, BlocksByRangeRequestId, DataColumnsByRangeRequestId, + PayloadEnvelopesByRangeRequestId, }, }; use ssz_types::RuntimeVariableList; use std::{collections::HashMap, sync::Arc}; -use tracing::{Span, debug}; +use tracing::{Span, debug, warn}; use types::{ BlobSidecar, ChainSpec, ColumnIndex, DataColumnSidecar, DataColumnSidecarList, EthSpec, - Hash256, SignedBeaconBlock, + Hash256, SignedBeaconBlock, SignedExecutionPayloadEnvelope, }; use crate::sync::network_context::MAX_COLUMN_RETRIES; @@ -37,6 +39,13 @@ pub struct RangeBlockComponentsRequest { blocks_request: ByRangeRequest>>>, /// Sidecars we have received awaiting for their corresponding block. block_data_request: RangeBlockDataRequest, + /// Payload envelopes for Gloas blocks. + payloads_request: Option< + ByRangeRequest< + PayloadEnvelopesByRangeRequestId, + Vec>>, + >, + >, /// Span to track the range request and all children range requests. pub(crate) request_span: Span, } @@ -71,6 +80,7 @@ pub enum CouplingError { exceeded_retries: bool, }, BlobPeerFailure(String), + EnvelopePeerFailure(String), } impl RangeBlockComponentsRequest { @@ -88,6 +98,7 @@ impl RangeBlockComponentsRequest { Vec<(DataColumnsByRangeRequestId, Vec)>, Vec, )>, + payloads_req_id: Option, request_span: Span, ) -> Self { let block_data_request = if let Some(blobs_req_id) = blobs_req_id { @@ -110,6 +121,7 @@ impl RangeBlockComponentsRequest { Self { blocks_request: ByRangeRequest::Active(blocks_req_id), block_data_request, + payloads_request: payloads_req_id.map(ByRangeRequest::Active), request_span, } } @@ -191,6 +203,17 @@ impl RangeBlockComponentsRequest { } } + pub fn add_payload_envelopes( + &mut self, + req_id: PayloadEnvelopesByRangeRequestId, + envelopes: Vec>>, + ) -> Result<(), String> { + match &mut self.payloads_request { + Some(req) => req.finish(req_id, envelopes), + None => Err("received payload envelopes but none were expected".to_owned()), + } + } + /// Attempts to construct RPC blocks from all received components. /// /// Returns `None` if not all expected requests have completed. @@ -208,6 +231,11 @@ impl RangeBlockComponentsRequest { return None; }; + // Check if payload envelopes are still pending + if let Some(ByRangeRequest::Active(_)) = &self.payloads_request { + return None; + } + // Increment the attempt once this function returns the response or errors match &mut self.block_data_request { RangeBlockDataRequest::NoData => Some(Self::responses_with_blobs( @@ -254,6 +282,12 @@ impl RangeBlockComponentsRequest { } } + let payload_envelopes = self.payloads_request.as_ref().and_then(|request| { + request + .to_finished() + .map(|payload_envelopes| payload_envelopes.to_vec()) + }); + let resp = Self::responses_with_custody_columns( blocks.to_vec(), data_columns, @@ -262,6 +296,7 @@ impl RangeBlockComponentsRequest { *attempt, da_checker, spec, + payload_envelopes, ); if let Err(CouplingError::DataColumnPeerFailure { @@ -352,6 +387,7 @@ impl RangeBlockComponentsRequest { Ok(responses) } + #[allow(clippy::too_many_arguments)] fn responses_with_custody_columns( blocks: Vec>>, data_columns: DataColumnSidecarList, @@ -360,10 +396,19 @@ impl RangeBlockComponentsRequest { attempt: usize, da_checker: Arc>, spec: Arc, + payload_envelopes: Option>>>, ) -> Result>, CouplingError> where T: BeaconChainTypes, { + // Index envelopes by beacon_block_root for correct coupling. + let mut envelopes_by_block_root = payload_envelopes.map(|envelopes| { + envelopes + .into_iter() + .map(|e| (e.beacon_block_root(), e)) + .collect::>() + }); + // Group data columns by block_root and index let mut data_columns_by_block = HashMap::>>>::new(); @@ -393,7 +438,7 @@ impl RangeBlockComponentsRequest { let exceeded_retries = attempt >= MAX_COLUMN_RETRIES; for block in blocks { let block_root = get_block_root(&block); - range_sync_blocks.push(if block.num_expected_blobs() > 0 { + let custody_columns = if block.num_expected_blobs() > 0 { let Some(mut data_columns_by_index) = data_columns_by_block.remove(&block_root) else { let responsible_peers = column_to_peer.iter().map(|c| (*c.0, *c.1)).collect(); @@ -401,7 +446,6 @@ impl RangeBlockComponentsRequest { error: format!("No columns for block {block_root:?} with data"), faulty_peers: responsible_peers, exceeded_retries, - }); }; @@ -415,16 +459,21 @@ impl RangeBlockComponentsRequest { custody_columns.push(CustodyDataColumn::from_asserted_custody(data_column)); } else { let Some(responsible_peer) = column_to_peer.get(index) else { - return Err(CouplingError::InternalError(format!("Internal error, no request made for column {}", index))); + return Err(CouplingError::InternalError(format!( + "Internal error, no request made for column {}", + index + ))); }; naughty_peers.push((*index, *responsible_peer)); } } if !naughty_peers.is_empty() { return Err(CouplingError::DataColumnPeerFailure { - error: format!("Peers did not return column for block_root {block_root:?} {naughty_peers:?}"), + error: format!( + "Peers did not return column for block_root {block_root:?} {naughty_peers:?}" + ), faulty_peers: naughty_peers, - exceeded_retries + exceeded_retries, }); } @@ -439,15 +488,31 @@ impl RangeBlockComponentsRequest { ); } - let block_data = AvailableBlockData::new_with_data_columns(custody_columns.iter().map(|c| c.as_data_column().clone()).collect::>()); - - RangeSyncBlock::new(block, block_data, &da_checker, spec.clone()) - .map_err(|e| CouplingError::InternalError(format!("{:?}", e)))? + custody_columns + .iter() + .map(|c| c.as_data_column().clone()) + .collect::>() } else { - // Block has no data, expects zero columns + vec![] + }; + let range_sync_block = if let Some(envelopes_by_block_root) = + envelopes_by_block_root.as_mut() + { + let envelope = envelopes_by_block_root.remove(&block_root); + let available_envelope = + envelope.map(|env| AvailableEnvelope::new(env, custody_columns)); + + RangeSyncBlock::new_gloas(block, available_envelope) + .map_err(CouplingError::EnvelopePeerFailure)? + } else if custody_columns.is_empty() { RangeSyncBlock::new(block, AvailableBlockData::NoData, &da_checker, spec.clone()) .map_err(|e| CouplingError::InternalError(format!("{:?}", e)))? - }); + } else { + let block_data = AvailableBlockData::new_with_data_columns(custody_columns); + RangeSyncBlock::new(block, block_data, &da_checker, spec.clone()) + .map_err(|e| CouplingError::InternalError(format!("{:?}", e)))? + }; + range_sync_blocks.push(range_sync_block); } // Assert that there are no columns left for other blocks @@ -458,6 +523,13 @@ impl RangeBlockComponentsRequest { debug!(?remaining_roots, "Not all columns consumed for block"); } + // Recoverable error, log and continue + if let Some(envelopes_by_block_root) = envelopes_by_block_root + && !envelopes_by_block_root.is_empty() + { + warn!("Peer returned extra envelopes not matching any block"); + } + Ok(range_sync_blocks) } } @@ -489,21 +561,28 @@ mod tests { use crate::sync::network_context::MAX_COLUMN_RETRIES; use super::RangeBlockComponentsRequest; + use beacon_chain::block_verification_types::RangeSyncBlock; use beacon_chain::custody_context::NodeCustodyType; + use beacon_chain::data_availability_checker::DataAvailabilityChecker; use beacon_chain::test_utils::{ - NumBlobs, generate_rand_block_and_blobs, generate_rand_block_and_data_columns, - test_da_checker, test_spec, + EphemeralHarnessType, NumBlobs, generate_rand_block_and_blobs, + generate_rand_block_and_data_columns, test_da_checker, test_spec, }; + use bls::Signature; use lighthouse_network::{ PeerId, service::api_types::{ BlobsByRangeRequestId, BlocksByRangeRequestId, ComponentsByRangeRequestId, - DataColumnsByRangeRequestId, DataColumnsByRangeRequester, Id, RangeRequestId, + DataColumnsByRangeRequestId, DataColumnsByRangeRequester, Id, + PayloadEnvelopesByRangeRequestId, RangeRequestId, }, }; use std::{collections::HashMap, sync::Arc}; use tracing::Span; - use types::{Epoch, ForkName, MinimalEthSpec as E, SignedBeaconBlock}; + use types::{ + ChainSpec, DataColumnSidecarList, Epoch, ExecutionPayloadEnvelope, ForkName, + MinimalEthSpec as E, SignedBeaconBlock, SignedExecutionPayloadEnvelope, + }; fn components_id() -> ComponentsByRangeRequestId { ComponentsByRangeRequestId { @@ -538,6 +617,15 @@ mod tests { } } + fn payloads_id( + parent_request_id: ComponentsByRangeRequestId, + ) -> PayloadEnvelopesByRangeRequestId { + PayloadEnvelopesByRangeRequestId { + id: 1, + parent_request_id, + } + } + fn columns_id( id: Id, parent_request_id: DataColumnsByRangeRequester, @@ -555,8 +643,166 @@ mod tests { info.responses(da_checker, spec).is_some() } + fn gloas_spec() -> ChainSpec { + let mut spec = test_spec::(); + spec.deneb_fork_epoch = Some(Epoch::new(0)); + spec.fulu_fork_epoch = Some(Epoch::new(0)); + spec.gloas_fork_epoch = Some(Epoch::new(0)); + spec + } + + fn matching_envelope(block: &SignedBeaconBlock) -> Arc> { + let bid = &block + .message() + .body() + .signed_execution_payload_bid() + .expect("Gloas block should have payload bid") + .message; + let mut envelope = SignedExecutionPayloadEnvelope { + message: ExecutionPayloadEnvelope::empty(), + signature: Signature::empty(), + }; + envelope.message.beacon_block_root = block.canonical_root(); + envelope.message.parent_beacon_block_root = block.parent_root(); + envelope.message.builder_index = bid.builder_index; + envelope.message.payload.slot_number = block.slot(); + envelope.message.payload.parent_hash = bid.parent_block_hash; + envelope.message.payload.block_hash = bid.block_hash; + Arc::new(envelope) + } + + #[allow(clippy::type_complexity)] + fn make_gloas_blocks_and_columns( + count: usize, + spec: &ChainSpec, + ) -> Vec<( + Arc>, + DataColumnSidecarList, + Arc>, + )> { + let mut u = types::test_utils::test_unstructured(); + (0..count) + .map(|_| { + let (block, data_columns) = generate_rand_block_and_data_columns::( + ForkName::Gloas, + NumBlobs::Number(1), + &mut u, + spec, + ) + .unwrap(); + let envelope = matching_envelope(&block); + (Arc::new(block), data_columns, envelope) + }) + .collect() + } + + #[allow(clippy::type_complexity)] + fn add_all_columns( + info: &mut RangeBlockComponentsRequest, + blocks: &[( + Arc>, + DataColumnSidecarList, + Arc>, + )], + columns_req_id: &[(DataColumnsByRangeRequestId, Vec)], + expected_custody_columns: &[u64], + ) { + for (i, &column_index) in expected_custody_columns.iter().enumerate() { + let (req, _columns) = columns_req_id.get(i).unwrap(); + info.add_custody_columns( + *req, + blocks + .iter() + .flat_map(|(_, columns, _)| { + columns + .iter() + .filter(|column| *column.index() == column_index) + .cloned() + }) + .collect(), + ) + .unwrap(); + } + } + + #[allow(clippy::type_complexity)] + struct GloasSetup { + info: RangeBlockComponentsRequest, + da_checker: Arc>>, + spec: Arc, + blocks: Vec<( + Arc>, + DataColumnSidecarList, + Arc>, + )>, + payloads_req_id: PayloadEnvelopesByRangeRequestId, + expected_custody_columns: Vec, + } + + /// Builds a Gloas coupling request with `count` blocks and all custody columns added, + /// ready for the per-test payload-envelope step. + fn setup_gloas_coupling(count: usize) -> GloasSetup { + let spec = Arc::new(gloas_spec()); + let da_checker = Arc::new(test_da_checker(spec.clone(), NodeCustodyType::Fullnode)); + let expected_custody_columns = da_checker + .custody_context() + .sampling_columns_for_epoch(Epoch::new(0), &spec) + .to_vec(); + let blocks = make_gloas_blocks_and_columns(count, &spec); + + let components_id = components_id(); + let blocks_req_id = blocks_id(components_id); + let payloads_req_id = payloads_id(components_id); + let columns_req_id = expected_custody_columns + .iter() + .enumerate() + .map(|(i, column)| { + ( + columns_id( + i as Id, + DataColumnsByRangeRequester::ComponentsByRange(components_id), + ), + vec![*column], + ) + }) + .collect::>(); + let mut info = RangeBlockComponentsRequest::::new( + blocks_req_id, + None, + Some((columns_req_id.clone(), expected_custody_columns.clone())), + Some(payloads_req_id), + Span::none(), + ); + + info.add_blocks( + blocks_req_id, + blocks.iter().map(|(block, _, _)| block.clone()).collect(), + ) + .unwrap(); + add_all_columns( + &mut info, + &blocks, + &columns_req_id, + &expected_custody_columns, + ); + + GloasSetup { + info, + da_checker, + spec, + blocks, + payloads_req_id, + expected_custody_columns, + } + } + #[test] fn no_blobs_into_responses() { + // This exercises the pre-Gloas blobs/no-data coupling path. Gloas coupling is covered + // by the dedicated `setup_gloas_coupling` tests below. + if skip_under_gloas() { + return; + } let spec = Arc::new(test_spec::()); let mut u = types::test_utils::test_unstructured(); @@ -575,7 +821,7 @@ mod tests { let blocks_req_id = blocks_id(components_id()); let mut info = - RangeBlockComponentsRequest::::new(blocks_req_id, None, None, Span::none()); + RangeBlockComponentsRequest::::new(blocks_req_id, None, None, None, Span::none()); // Send blocks and complete terminate response info.add_blocks(blocks_req_id, blocks).unwrap(); @@ -606,6 +852,7 @@ mod tests { blocks_req_id, Some(blobs_req_id), None, + None, Span::none(), ); @@ -672,6 +919,7 @@ mod tests { blocks_req_id, None, Some((columns_req_id.clone(), expects_custody_columns.clone())), + None, Span::none(), ); // Send blocks and complete terminate response @@ -751,6 +999,7 @@ mod tests { blocks_req_id, None, Some((columns_req_id.clone(), expected_sampling_columns.clone())), + None, Span::none(), ); @@ -804,6 +1053,115 @@ mod tests { info.responses(da_checker, spec).unwrap().unwrap(); } + #[test] + fn gloas_payload_envelopes_must_complete_before_responses() { + let GloasSetup { + mut info, + da_checker, + spec, + .. + } = setup_gloas_coupling(2); + + // No payload envelopes added yet, so the request must not be complete. + assert!(info.responses(da_checker, spec).is_none()); + } + + #[test] + fn gloas_payload_envelopes_are_coupled_by_block_root() { + let GloasSetup { + mut info, + da_checker, + spec, + blocks, + payloads_req_id, + expected_custody_columns, + } = setup_gloas_coupling(2); + + // Supply envelopes in reverse order to prove coupling is by block root, not position. + info.add_payload_envelopes( + payloads_req_id, + blocks + .iter() + .rev() + .map(|(_, _, envelope)| envelope.clone()) + .collect(), + ) + .unwrap(); + + let responses = info.responses(da_checker, spec).unwrap().unwrap(); + assert_eq!(responses.len(), blocks.len()); + for response in responses { + match response { + RangeSyncBlock::Gloas { + block, + envelope: Some(envelope), + } => { + assert_eq!( + envelope.envelope().beacon_block_root(), + block.canonical_root() + ); + assert_eq!(envelope.columns.len(), expected_custody_columns.len()); + } + other => panic!("expected Gloas block with envelope, got {other:?}"), + } + } + } + + #[test] + fn gloas_payload_envelopes_allow_missing_envelopes() { + let GloasSetup { + mut info, + da_checker, + spec, + blocks, + payloads_req_id, + .. + } = setup_gloas_coupling(2); + + // Supply an envelope for only one of the two blocks. + info.add_payload_envelopes(payloads_req_id, vec![blocks[0].2.clone()]) + .unwrap(); + + let responses = info.responses(da_checker, spec).unwrap().unwrap(); + let count_with = |with_envelope: bool| { + responses + .iter() + .filter(|response| { + matches!(response, RangeSyncBlock::Gloas { envelope, .. } if envelope.is_some() == with_envelope) + }) + .count() + }; + assert_eq!(count_with(true), 1); + assert_eq!(count_with(false), 1); + } + + #[test] + fn gloas_payload_envelope_mismatch_fails_coupling() { + let GloasSetup { + mut info, + da_checker, + spec, + blocks, + payloads_req_id, + .. + } = setup_gloas_coupling(1); + + let mut bad_envelope = (*blocks[0].2).clone(); + bad_envelope.message.payload.slot_number += 1; + info.add_payload_envelopes(payloads_req_id, vec![Arc::new(bad_envelope)]) + .unwrap(); + + let result = info.responses(da_checker, spec).unwrap(); + assert!( + matches!( + result, + Err(super::CouplingError::EnvelopePeerFailure(ref error)) + if error.contains("SlotMismatch") + ), + "expected envelope slot mismatch, got {result:?}" + ); + } + #[test] fn missing_custody_columns_from_faulty_peers() { if skip_under_gloas() { @@ -848,6 +1206,7 @@ mod tests { blocks_req_id, None, Some((columns_req_id.clone(), expected_sampling_columns.clone())), + None, Span::none(), ); @@ -949,6 +1308,7 @@ mod tests { blocks_req_id, None, Some((columns_req_id.clone(), expected_sampling_columns.clone())), + None, Span::none(), ); @@ -1068,6 +1428,7 @@ mod tests { blocks_req_id, None, Some((columns_req_id.clone(), expected_sampling_columns.clone())), + None, Span::none(), ); diff --git a/beacon_node/network/src/sync/manager.rs b/beacon_node/network/src/sync/manager.rs index 7a90163852..8e7b8cd05a 100644 --- a/beacon_node/network/src/sync/manager.rs +++ b/beacon_node/network/src/sync/manager.rs @@ -57,7 +57,8 @@ use lighthouse_network::service::api_types::{ BlobsByRangeRequestId, BlocksByRangeRequestId, ComponentsByRangeRequestId, CustodyBackFillBatchRequestId, CustodyBackfillBatchId, CustodyRequester, DataColumnsByRangeRequestId, DataColumnsByRangeRequester, DataColumnsByRootRequestId, - DataColumnsByRootRequester, Id, SingleLookupReqId, SyncRequestId, + DataColumnsByRootRequester, Id, PayloadEnvelopesByRangeRequestId, SingleLookupReqId, + SyncRequestId, }; use lighthouse_network::types::{NetworkGlobals, SyncState}; use lighthouse_network::{PeerAction, PeerId}; @@ -504,6 +505,8 @@ impl SyncManager { SyncRequestId::DataColumnsByRange(req_id) => { self.on_data_columns_by_range_response(req_id, peer_id, RpcEvent::RPCError(error)) } + SyncRequestId::PayloadEnvelopesByRange(req_id) => self + .on_payload_envelopes_by_range_response(req_id, peer_id, RpcEvent::RPCError(error)), } } @@ -1160,6 +1163,13 @@ impl SyncManager { peer_id, RpcEvent::from_chunk(envelope, seen_timestamp), ), + SyncRequestId::PayloadEnvelopesByRange(req_id) => { + self.on_payload_envelopes_by_range_response( + req_id, + peer_id, + RpcEvent::from_chunk(envelope, seen_timestamp), + ); + } _ => { crit!(%peer_id, "bad request id for payload envelope"); } @@ -1214,6 +1224,24 @@ impl SyncManager { } } + fn on_payload_envelopes_by_range_response( + &mut self, + id: PayloadEnvelopesByRangeRequestId, + peer_id: PeerId, + envelope: RpcEvent>>, + ) { + if let Some(resp) = self + .network + .on_payload_envelopes_by_range_response(id, peer_id, envelope) + { + self.on_range_components_response( + id.parent_request_id, + peer_id, + RangeBlockComponent::PayloadEnvelope(id, resp), + ); + } + } + fn on_data_columns_by_root_response( &mut self, req_id: DataColumnsByRootRequestId, diff --git a/beacon_node/network/src/sync/network_context.rs b/beacon_node/network/src/sync/network_context.rs index c9a48c9d5e..8e8abd4fa6 100644 --- a/beacon_node/network/src/sync/network_context.rs +++ b/beacon_node/network/src/sync/network_context.rs @@ -24,14 +24,17 @@ use beacon_chain::block_verification_types::{AsBlock, RangeSyncBlock}; use beacon_chain::{BeaconChain, BeaconChainTypes, BlockProcessStatus, EngineState}; use custody::CustodyRequestResult; use fnv::FnvHashMap; -use lighthouse_network::rpc::methods::{BlobsByRangeRequest, DataColumnsByRangeRequest}; +use lighthouse_network::rpc::methods::{ + BlobsByRangeRequest, DataColumnsByRangeRequest, PayloadEnvelopesByRangeRequest, +}; use lighthouse_network::rpc::{BlocksByRangeRequest, GoodbyeReason, RPCError, RequestType}; pub use lighthouse_network::service::api_types::RangeRequestId; use lighthouse_network::service::api_types::{ AppRequestId, BlobsByRangeRequestId, BlocksByRangeRequestId, ComponentsByRangeRequestId, CustodyBackFillBatchRequestId, CustodyBackfillBatchId, CustodyId, CustodyRequester, DataColumnsByRangeRequestId, DataColumnsByRangeRequester, DataColumnsByRootRequestId, - DataColumnsByRootRequester, Id, SingleLookupReqId, SyncRequestId, + DataColumnsByRootRequester, Id, PayloadEnvelopesByRangeRequestId, SingleLookupReqId, + SyncRequestId, }; use lighthouse_network::{Client, NetworkGlobals, PeerAction, PeerId, ReportSource}; use parking_lot::RwLock; @@ -39,7 +42,7 @@ pub use requests::LookupVerifyError; use requests::{ ActiveRequests, BlobsByRangeRequestItems, BlocksByRangeRequestItems, BlocksByRootRequestItems, DataColumnsByRangeRequestItems, DataColumnsByRootRequestItems, - PayloadEnvelopesByRootRequestItems, + PayloadEnvelopesByRangeRequestItems, PayloadEnvelopesByRootRequestItems, }; #[cfg(test)] use slot_clock::SlotClock; @@ -217,6 +220,11 @@ pub struct SyncNetworkContext { /// A mapping of active DataColumnsByRange requests data_columns_by_range_requests: ActiveRequests>, + /// A mapping of active PayloadEnvelopesByRange requests + payload_envelopes_by_range_requests: ActiveRequests< + PayloadEnvelopesByRangeRequestId, + PayloadEnvelopesByRangeRequestItems, + >, /// Mapping of active custody column requests for a block root custody_by_root_requests: FnvHashMap>, @@ -254,6 +262,10 @@ pub enum RangeBlockComponent { DataColumnsByRangeRequestId, RpcResponseResult>>>, ), + PayloadEnvelope( + PayloadEnvelopesByRangeRequestId, + RpcResponseResult>>>, + ), } #[cfg(test)] @@ -302,6 +314,7 @@ impl SyncNetworkContext { blocks_by_range_requests: ActiveRequests::new("blocks_by_range"), blobs_by_range_requests: ActiveRequests::new("blobs_by_range"), data_columns_by_range_requests: ActiveRequests::new("data_columns_by_range"), + payload_envelopes_by_range_requests: ActiveRequests::new("payload_envelopes_by_range"), custody_by_root_requests: <_>::default(), components_by_range_requests: FnvHashMap::default(), custody_backfill_data_column_batch_requests: FnvHashMap::default(), @@ -334,6 +347,7 @@ impl SyncNetworkContext { blocks_by_range_requests, blobs_by_range_requests, data_columns_by_range_requests, + payload_envelopes_by_range_requests, // custody_by_root_requests is a meta request of data_columns_by_root_requests custody_by_root_requests: _, // components_by_range_requests is a meta request of various _by_range requests @@ -369,12 +383,17 @@ impl SyncNetworkContext { .active_requests_of_peer(peer_id) .into_iter() .map(|req_id| SyncRequestId::DataColumnsByRange(*req_id)); + let payload_envelope_by_range_ids = payload_envelopes_by_range_requests + .active_requests_of_peer(peer_id) + .into_iter() + .map(|req_id| SyncRequestId::PayloadEnvelopesByRange(*req_id)); blocks_by_root_ids .chain(payload_envelopes_by_root_ids) .chain(data_column_by_root_ids) .chain(blocks_by_range_ids) .chain(blobs_by_range_ids) .chain(data_column_by_range_ids) + .chain(payload_envelope_by_range_ids) .collect() } @@ -431,6 +450,7 @@ impl SyncNetworkContext { blocks_by_range_requests, blobs_by_range_requests, data_columns_by_range_requests, + payload_envelopes_by_range_requests, // custody_by_root_requests is a meta request of data_columns_by_root_requests custody_by_root_requests: _, // components_by_range_requests is a meta request of various _by_range requests @@ -453,6 +473,7 @@ impl SyncNetworkContext { .chain(blocks_by_range_requests.iter_request_peers()) .chain(blobs_by_range_requests.iter_request_peers()) .chain(data_columns_by_range_requests.iter_request_peers()) + .chain(payload_envelopes_by_range_requests.iter_request_peers()) { *active_request_count_by_peer.entry(peer_id).or_default() += 1; } @@ -585,24 +606,26 @@ impl SyncNetworkContext { }; // Attempt to find all required custody peers before sending any request or creating an ID - let columns_by_range_peers_to_request = - if matches!(batch_type, ByRangeRequestType::BlocksAndColumns) { - let epoch = Slot::new(*request.start_slot()).epoch(T::EthSpec::slots_per_epoch()); - let column_indexes = self - .chain - .sampling_columns_for_epoch(epoch) - .iter() - .cloned() - .collect(); - Some(self.select_columns_by_range_peers_to_request( - &column_indexes, - column_peers, - active_request_count_by_peer, - peers_to_deprioritize, - )?) - } else { - None - }; + let columns_by_range_peers_to_request = if matches!( + batch_type, + ByRangeRequestType::BlocksAndColumns | ByRangeRequestType::BlocksAndEnvelopesAndColumns + ) { + let epoch = Slot::new(*request.start_slot()).epoch(T::EthSpec::slots_per_epoch()); + let column_indexes = self + .chain + .sampling_columns_for_epoch(epoch) + .iter() + .cloned() + .collect(); + Some(self.select_columns_by_range_peers_to_request( + &column_indexes, + column_peers, + active_request_count_by_peer, + peers_to_deprioritize, + )?) + } else { + None + }; // Create the overall components_by_range request ID before its individual components let id = ComponentsByRangeRequestId { @@ -666,6 +689,26 @@ impl SyncNetworkContext { }) .transpose()?; + let payloads_req_id = + if matches!(batch_type, ByRangeRequestType::BlocksAndEnvelopesAndColumns) { + Some(self.send_payload_envelopes_by_range_request( + block_peer, + PayloadEnvelopesByRangeRequest { + start_slot: *request.start_slot(), + count: *request.count(), + }, + id, + new_range_request_span!( + self, + "outgoing_envelopes_by_range", + range_request_span.clone(), + block_peer + ), + )?) + } else { + None + }; + let epoch = Slot::new(*request.start_slot()).epoch(T::EthSpec::slots_per_epoch()); let info = RangeBlockComponentsRequest::new( blocks_req_id, @@ -676,6 +719,7 @@ impl SyncNetworkContext { self.chain.sampling_columns_for_epoch(epoch).to_vec(), ) }), + payloads_req_id, range_request_span, ); self.components_by_range_requests.insert(id, info); @@ -778,6 +822,17 @@ impl SyncNetworkContext { }) }) } + RangeBlockComponent::PayloadEnvelope(req_id, resp) => { + resp.and_then(|(envelopes, _)| { + request + .add_payload_envelopes(req_id, envelopes) + .map_err(|e| { + RpcResponseError::BlockComponentCouplingError( + CouplingError::InternalError(e), + ) + }) + }) + } } } { entry.remove(); @@ -1269,6 +1324,43 @@ impl SyncNetworkContext { Ok((id, requested_columns)) } + fn send_payload_envelopes_by_range_request( + &mut self, + peer_id: PeerId, + request: PayloadEnvelopesByRangeRequest, + parent_request_id: ComponentsByRangeRequestId, + request_span: Span, + ) -> Result { + let id = PayloadEnvelopesByRangeRequestId { + id: self.next_id(), + parent_request_id, + }; + + self.send_network_msg(NetworkMessage::SendRequest { + peer_id, + request: RequestType::PayloadEnvelopesByRange(request.clone()), + app_request_id: AppRequestId::Sync(SyncRequestId::PayloadEnvelopesByRange(id)), + }) + .map_err(|_| RpcRequestSendError::InternalError("network send error".to_owned()))?; + + debug!( + method = "PayloadEnvelopesByRange", + slots = request.count, + peer = %peer_id, + %id, + "Sync RPC request sent" + ); + + self.payload_envelopes_by_range_requests.insert( + id, + peer_id, + false, + PayloadEnvelopesByRangeRequestItems::new(request), + request_span, + ); + Ok(id) + } + pub fn is_execution_engine_online(&self) -> bool { self.execution_engine_state == EngineState::Online } @@ -1349,7 +1441,10 @@ impl SyncNetworkContext { "To deal with alignment with deneb boundaries, batches need to be of just one epoch" ); - if self + if self.chain.spec.fork_name_at_epoch(epoch).gloas_enabled() { + // TODO(gloas): Not precise and we can be post-gloas and not require columns + ByRangeRequestType::BlocksAndEnvelopesAndColumns + } else if self .chain .data_availability_checker .data_columns_required_for_epoch(epoch) @@ -1485,6 +1580,19 @@ impl SyncNetworkContext { self.on_rpc_response_result(resp, peer_id) } + #[allow(clippy::type_complexity)] + pub(crate) fn on_payload_envelopes_by_range_response( + &mut self, + id: PayloadEnvelopesByRangeRequestId, + peer_id: PeerId, + rpc_event: RpcEvent>>, + ) -> Option>>>> { + let resp = self + .payload_envelopes_by_range_requests + .on_response(id, rpc_event); + self.on_rpc_response_result(resp, peer_id) + } + /// Common handler for consistent scoring of RpcResponseError fn on_rpc_response_result( &mut self, diff --git a/beacon_node/network/src/sync/network_context/requests.rs b/beacon_node/network/src/sync/network_context/requests.rs index 72dd2c22d0..cc74785098 100644 --- a/beacon_node/network/src/sync/network_context/requests.rs +++ b/beacon_node/network/src/sync/network_context/requests.rs @@ -15,6 +15,7 @@ pub use data_columns_by_range::DataColumnsByRangeRequestItems; pub use data_columns_by_root::{ DataColumnsByRootRequestItems, DataColumnsByRootSingleBlockRequest, }; +pub use payload_envelopes_by_range::PayloadEnvelopesByRangeRequestItems; pub use payload_envelopes_by_root::{ PayloadEnvelopesByRootRequestItems, PayloadEnvelopesByRootSingleRequest, }; @@ -28,6 +29,7 @@ mod blocks_by_range; mod blocks_by_root; mod data_columns_by_range; mod data_columns_by_root; +mod payload_envelopes_by_range; mod payload_envelopes_by_root; #[derive(Debug, PartialEq, Eq, IntoStaticStr)] diff --git a/beacon_node/network/src/sync/network_context/requests/payload_envelopes_by_range.rs b/beacon_node/network/src/sync/network_context/requests/payload_envelopes_by_range.rs new file mode 100644 index 0000000000..13e6454a23 --- /dev/null +++ b/beacon_node/network/src/sync/network_context/requests/payload_envelopes_by_range.rs @@ -0,0 +1,48 @@ +use super::{ActiveRequestItems, LookupVerifyError}; +use lighthouse_network::rpc::methods::PayloadEnvelopesByRangeRequest; +use std::sync::Arc; +use types::{EthSpec, SignedExecutionPayloadEnvelope}; + +/// Accumulates results of a payload_envelopes_by_range request. Only returns items after +/// receiving the stream termination. +pub struct PayloadEnvelopesByRangeRequestItems { + request: PayloadEnvelopesByRangeRequest, + items: Vec>>, +} + +impl PayloadEnvelopesByRangeRequestItems { + pub fn new(request: PayloadEnvelopesByRangeRequest) -> Self { + Self { + request, + items: vec![], + } + } +} + +impl ActiveRequestItems for PayloadEnvelopesByRangeRequestItems { + type Item = Arc>; + + fn add(&mut self, envelope: Self::Item) -> Result { + if envelope.slot().as_u64() < self.request.start_slot + || envelope.slot().as_u64() >= self.request.start_slot + self.request.count + { + return Err(LookupVerifyError::UnrequestedSlot(envelope.slot())); + } + + if self + .items + .iter() + .any(|existing| existing.slot() == envelope.slot()) + { + return Err(LookupVerifyError::DuplicatedData(envelope.slot(), 0)); + } + + self.items.push(envelope); + + Ok(self.items.len() >= self.request.count as usize) + } + + fn consume(&mut self) -> Vec { + std::mem::take(&mut self.items) + } +} diff --git a/beacon_node/network/src/sync/range_sync/chain.rs b/beacon_node/network/src/sync/range_sync/chain.rs index d533d8ed0d..6292388339 100644 --- a/beacon_node/network/src/sync/range_sync/chain.rs +++ b/beacon_node/network/src/sync/range_sync/chain.rs @@ -952,6 +952,9 @@ impl SyncingChain { CouplingError::BlobPeerFailure(msg) => { debug!(?batch_id, msg, "Blob peer failure"); } + CouplingError::EnvelopePeerFailure(msg) => { + debug!(?batch_id, msg, "Envelope peer failure"); + } CouplingError::InternalError(msg) => { error!(?batch_id, msg, "Block components coupling internal error"); } diff --git a/beacon_node/network/src/sync/tests/lookups.rs b/beacon_node/network/src/sync/tests/lookups.rs index fb0956cf50..13eeaee9aa 100644 --- a/beacon_node/network/src/sync/tests/lookups.rs +++ b/beacon_node/network/src/sync/tests/lookups.rs @@ -12,6 +12,7 @@ use crate::sync::{ }; use beacon_chain::block_verification_types::LookupBlock; use beacon_chain::custody_context::NodeCustodyType; +use beacon_chain::payload_envelope_verification::AvailableEnvelope; use beacon_chain::{ AvailabilityProcessingStatus, EngineState, NotifyExecutionLayer, block_verification_types::{AsBlock, AvailableBlockData}, @@ -613,7 +614,6 @@ impl TestRig { .unwrap_or_else(|| { panic!("Test consumer requested unknown block: {id:?}") }) - .block_data() .data_columns() .unwrap_or_else(|| panic!("Block id {id:?} has no columns")); id.columns @@ -763,7 +763,7 @@ impl TestRig { .return_wrong_range_column_indices_n_times -= 1; let wrong_columns = (req.start_slot..req.start_slot + req.count) .filter_map(|slot| self.network_blocks_by_slot.get(&Slot::new(slot))) - .filter_map(|block| block.block_data().data_columns()) + .filter_map(|block| block.data_columns()) .flat_map(|columns| { columns .into_iter() @@ -787,7 +787,7 @@ impl TestRig { let wrong_columns = self .network_blocks_by_slot .get(&Slot::new(wrong_slot)) - .and_then(|block| block.block_data().data_columns()) + .and_then(|block| block.data_columns()) .into_iter() .flat_map(|columns| { columns @@ -804,7 +804,7 @@ impl TestRig { self.complete_strategy.return_partial_range_columns_n_times -= 1; let columns = (req.start_slot..req.start_slot + req.count) .filter_map(|slot| self.network_blocks_by_slot.get(&Slot::new(slot))) - .filter_map(|block| block.block_data().data_columns()) + .filter_map(|block| block.data_columns()) .flat_map(|columns| { columns .into_iter() @@ -820,7 +820,7 @@ impl TestRig { let columns = (req.start_slot..req.start_slot + req.count) .filter_map(|slot| self.network_blocks_by_slot.get(&Slot::new(slot))) - .filter_map(|block| block.block_data().data_columns()) + .filter_map(|block| block.data_columns()) .flat_map(|columns| { columns .into_iter() @@ -830,6 +830,25 @@ impl TestRig { self.send_rpc_columns_response(req_id, peer_id, &columns); } + (RequestType::PayloadEnvelopesByRange(req), AppRequestId::Sync(req_id)) => { + if self.complete_strategy.skip_by_range_routes { + return; + } + + let envelopes = (req.start_slot..req.start_slot + req.count) + .filter_map(|slot| self.network_blocks_by_slot.get(&Slot::new(slot))) + .filter_map(|block| { + let block_root = block.canonical_root(); + // Respect a withheld payload envelope. + if self.complete_strategy.hold_envelope_for_block == Some(block_root) { + return None; + } + self.network_envelopes_by_root.get(&block_root).cloned() + }) + .collect::>(); + self.send_rpc_envelopes_response(req_id, peer_id, &envelopes); + } + (RequestType::Status(_req), AppRequestId::Router) => { // Ignore Status requests for now } @@ -958,6 +977,34 @@ impl TestRig { }); } + fn send_rpc_envelopes_response( + &mut self, + sync_request_id: SyncRequestId, + peer_id: PeerId, + envelopes: &[Arc>], + ) { + let slots = envelopes.iter().map(|e| e.slot()).collect::>(); + self.log(&format!( + "Completing request {sync_request_id:?} to {peer_id} with envelopes {slots:?}" + )); + + for envelope in envelopes { + self.push_sync_message(SyncMessage::RpcPayloadEnvelope { + sync_request_id, + peer_id, + envelope: Some(envelope.clone()), + seen_timestamp: D, + }); + } + // Stream termination + self.push_sync_message(SyncMessage::RpcPayloadEnvelope { + sync_request_id, + peer_id, + envelope: None, + seen_timestamp: D, + }); + } + #[allow(dead_code)] fn is_after_gloas(&self) -> bool { self.fork_name.gloas_enabled() @@ -1177,7 +1224,7 @@ impl TestRig { let range_sync_block = self.get_last_block().clone(); let mut block = (*range_sync_block.block_cloned()).clone(); let blobs = range_sync_block.block_data().blobs(); - let columns = range_sync_block.block_data().data_columns(); + let columns = range_sync_block.data_columns(); *block.signature_mut() = self.valid_signature(); self.re_insert_block(Arc::new(block), blobs, columns); } @@ -1192,10 +1239,7 @@ impl TestRig { let range_sync_block = self.get_last_block().clone(); let block = range_sync_block.block_cloned(); let blobs = range_sync_block.block_data().blobs(); - let mut columns = range_sync_block - .block_data() - .data_columns() - .expect("no columns"); + let mut columns = range_sync_block.data_columns().expect("no columns"); let first = columns.first_mut().expect("empty columns"); Arc::make_mut(first) .signed_block_header_mut() @@ -1217,10 +1261,7 @@ impl TestRig { .clone(); let block = range_sync_block.block_cloned(); let blobs = range_sync_block.block_data().blobs(); - let mut columns = range_sync_block - .block_data() - .data_columns() - .expect("no columns"); + let mut columns = range_sync_block.data_columns().expect("no columns"); let first = columns.first_mut().expect("empty columns"); let column = Arc::make_mut(first); let proof = column.kzg_proofs_mut().first_mut().expect("no kzg proofs"); @@ -1256,20 +1297,30 @@ impl TestRig { ) { let block_root = block.canonical_root(); let block_slot = block.slot(); - let block_data = if let Some(columns) = columns { - AvailableBlockData::new_with_data_columns(columns) - } else if let Some(blobs) = blobs { - AvailableBlockData::new_with_blobs(blobs) + let range_sync_block = if block.fork_name_unchecked().gloas_enabled() { + // Gloas carries data columns in the payload envelope, not in `block_data`. + let envelope = self + .network_envelopes_by_root + .get(&block_root) + .cloned() + .map(|envelope| AvailableEnvelope::new(envelope, columns.unwrap_or_default())); + RangeSyncBlock::new_gloas(block, envelope).unwrap() } else { - AvailableBlockData::NoData + let block_data = if let Some(columns) = columns { + AvailableBlockData::new_with_data_columns(columns) + } else if let Some(blobs) = blobs { + AvailableBlockData::new_with_blobs(blobs) + } else { + AvailableBlockData::NoData + }; + RangeSyncBlock::new( + block, + block_data, + &self.harness.chain.data_availability_checker, + self.harness.chain.spec.clone(), + ) + .unwrap() }; - let range_sync_block = RangeSyncBlock::new( - block, - block_data, - &self.harness.chain.data_availability_checker, - self.harness.chain.spec.clone(), - ) - .unwrap(); self.network_blocks_by_slot .insert(block_slot, range_sync_block.clone()); self.network_blocks_by_root @@ -1367,7 +1418,6 @@ impl TestRig { let peer_id = self.new_connected_supernode_peer(); let columns = self .get_last_block() - .block_data() .data_columns() .expect("No data columns"); let column = columns.first().expect("empty columns"); From 43c2e78139b38aafe0ea42f221420b68d211d35d Mon Sep 17 00:00:00 2001 From: Daniel Knopik <107140945+dknopik@users.noreply.github.com> Date: Wed, 10 Jun 2026 21:39:07 +0200 Subject: [PATCH 26/26] Better partial diagnosics (#9436) Small collection of improved diagnostics for partials. - In the peer info struct (exposing peer data via `/lighthouse/peers`), add a new field to indicate on which subnets the peer supports partials. - Fix the description of several metrics. - Downgrade some noisy logging when sending partials to trace, and downgrade one warning that should not worry the user. Co-Authored-By: Daniel Knopik --- beacon_node/beacon_chain/src/metrics.rs | 6 +- .../src/partial_data_column_assembler.rs | 2 +- .../src/peer_manager/mod.rs | 64 +++++++++---------- .../src/peer_manager/peerdb.rs | 9 ++- .../src/peer_manager/peerdb/peer_info.rs | 12 +++- .../lighthouse_network/src/service/mod.rs | 16 +++-- .../lighthouse_network/src/types/partial.rs | 8 +-- .../gossip_methods.rs | 2 +- 8 files changed, 69 insertions(+), 50 deletions(-) diff --git a/beacon_node/beacon_chain/src/metrics.rs b/beacon_node/beacon_chain/src/metrics.rs index df1b005820..4f7bdbbb52 100644 --- a/beacon_node/beacon_chain/src/metrics.rs +++ b/beacon_node/beacon_chain/src/metrics.rs @@ -1711,21 +1711,21 @@ pub static PARTIAL_DATA_COLUMN_SIDECAR_HEADER_PROCESSING_REQUESTS: LazyLock> = LazyLock::new(|| { try_create_int_counter( "beacon_partial_data_column_sidecar_header_processing_dupes_total", - "Number of partial data column sidecars verified for gossip (excluding dupes)", + "Number of partial data column sidecar headers received that matched a cached header", ) }); pub static PARTIAL_DATA_COLUMN_SIDECAR_HEADER_PROCESSING_SUCCESSES: LazyLock> = LazyLock::new(|| { try_create_int_counter( "beacon_partial_data_column_sidecar_header_processing_successes_total", - "Number of partial data column sidecar headers verified for gossip (excluding dupes)", + "Number of partial data column sidecar headers verified for gossip", ) }); pub static PARTIAL_DATA_COLUMN_SIDECAR_HEADER_GOSSIP_VERIFICATION_TIMES: LazyLock< diff --git a/beacon_node/beacon_chain/src/partial_data_column_assembler.rs b/beacon_node/beacon_chain/src/partial_data_column_assembler.rs index 3cf9a320d7..ee59102cfd 100644 --- a/beacon_node/beacon_chain/src/partial_data_column_assembler.rs +++ b/beacon_node/beacon_chain/src/partial_data_column_assembler.rs @@ -106,7 +106,7 @@ impl PartialDataColumnAssembler { let merged = match existing.merge(&partial) { Ok(merged) => merged, Err(err) => { - error!("Unexpected error merging partial data column: {:?}", err); + error!(error = ?err, "Unexpected error merging partial data column"); continue; } }; diff --git a/beacon_node/lighthouse_network/src/peer_manager/mod.rs b/beacon_node/lighthouse_network/src/peer_manager/mod.rs index 6b5144fa6f..898b97a85f 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/mod.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/mod.rs @@ -2040,11 +2040,11 @@ mod tests { .peer_info_mut(&peer0) .unwrap() .set_meta_data(MetaData::V3(metadata)); - peer_manager - .network_globals - .peers - .write() - .add_subscription(&peer0, Subnet::Attestation(1.into())); + peer_manager.network_globals.peers.write().add_subscription( + &peer0, + Subnet::Attestation(1.into()), + false, + ); let mut attnets = crate::types::EnrAttestationBitfield::::new(); attnets.set(10, true).unwrap(); @@ -2061,11 +2061,11 @@ mod tests { .peer_info_mut(&peer2) .unwrap() .set_meta_data(MetaData::V3(metadata)); - peer_manager - .network_globals - .peers - .write() - .add_subscription(&peer2, Subnet::Attestation(10.into())); + peer_manager.network_globals.peers.write().add_subscription( + &peer2, + Subnet::Attestation(10.into()), + false, + ); let mut syncnets = crate::types::EnrSyncCommitteeBitfield::::new(); syncnets.set(3, true).unwrap(); @@ -2082,11 +2082,11 @@ mod tests { .peer_info_mut(&peer4) .unwrap() .set_meta_data(MetaData::V3(metadata)); - peer_manager - .network_globals - .peers - .write() - .add_subscription(&peer4, Subnet::SyncCommittee(3.into())); + peer_manager.network_globals.peers.write().add_subscription( + &peer4, + Subnet::SyncCommittee(3.into()), + false, + ); // Perform the heartbeat. peer_manager.heartbeat(); @@ -2183,11 +2183,11 @@ mod tests { peer_info.update_sync_status(empty_synced_status()); } - peer_manager - .network_globals - .peers - .write() - .add_subscription(&peer, Subnet::DataColumn(subnet.into())); + peer_manager.network_globals.peers.write().add_subscription( + &peer, + Subnet::DataColumn(subnet.into()), + false, + ); println!("{},{},{}", x, subnet, peer); peers.push(peer); } @@ -2304,7 +2304,7 @@ mod tests { .network_globals .peers .write() - .add_subscription(&peer, subnet); + .add_subscription(&peer, subnet, false); } println!("{},{}", x, peer); peers.push(peer); @@ -2408,7 +2408,7 @@ mod tests { .network_globals .peers .write() - .add_subscription(&peer, subnet); + .add_subscription(&peer, subnet, false); } peers.push(peer); } @@ -2507,7 +2507,7 @@ mod tests { .network_globals .peers .write() - .add_subscription(&peer, subnet); + .add_subscription(&peer, subnet, false); } println!("{},{}", peer_idx, peer); peers.push(peer); @@ -2679,7 +2679,7 @@ mod tests { .network_globals .peers .write() - .add_subscription(&peer, subnet); + .add_subscription(&peer, subnet, false); } peers.push(peer); } @@ -2746,11 +2746,11 @@ mod tests { .unwrap() .set_meta_data(MetaData::V3(metadata)); - peer_manager - .network_globals - .peers - .write() - .add_subscription(&peer, Subnet::Attestation((subnet as u64).into())); + peer_manager.network_globals.peers.write().add_subscription( + &peer, + Subnet::Attestation((subnet as u64).into()), + false, + ); peers.push(peer); } @@ -2851,7 +2851,7 @@ mod tests { .network_globals .peers .write() - .add_subscription(&peer, subnet); + .add_subscription(&peer, subnet, false); } peers.push(peer); @@ -2937,7 +2937,7 @@ mod tests { } for subnet in peer_info.long_lived_subnets() { - peers_db.add_subscription(&peer, subnet); + peers_db.add_subscription(&peer, subnet, false); } peers.push(peer); @@ -3158,7 +3158,7 @@ mod tests { peer_info.set_custody_subnets(condition.custody_subnets.clone()); for subnet in peer_info.long_lived_subnets() { - peer_db.add_subscription(&condition.peer_id, subnet); + peer_db.add_subscription(&condition.peer_id, subnet, false); } } diff --git a/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs b/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs index 23f47c67a7..0a338bb011 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs @@ -714,9 +714,14 @@ impl PeerDB { /// Adds a gossipsub subscription to a peer in the peerdb. // VISIBILITY: The behaviour is able to adjust subscriptions. - pub(crate) fn add_subscription(&mut self, peer_id: &PeerId, subnet: Subnet) { + pub(crate) fn add_subscription( + &mut self, + peer_id: &PeerId, + subnet: Subnet, + supports_partials: bool, + ) { if let Some(info) = self.peers.get_mut(peer_id) { - info.insert_subnet(subnet); + info.insert_subnet(subnet, supports_partials); } } diff --git a/beacon_node/lighthouse_network/src/peer_manager/peerdb/peer_info.rs b/beacon_node/lighthouse_network/src/peer_manager/peerdb/peer_info.rs index c289cb9a69..8ad7d10a88 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/peerdb/peer_info.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/peerdb/peer_info.rs @@ -41,6 +41,8 @@ pub struct PeerInfo { meta_data: Option>, /// Subnets the peer is connected to. subnets: HashSet, + /// Subnets the peer is connected to, requesting partial messages. + partial_message_subnets: HashSet, /// This is computed from either metadata or the ENR, and contains the subnets that the peer /// is *assigned* to custody, rather than *connected* to (different to `self.subnets`). /// Note: Another reason to keep this separate to `self.subnets` is an upcoming change to @@ -68,6 +70,7 @@ impl Default for PeerInfo { listening_addresses: Vec::new(), seen_multiaddrs: HashSet::new(), subnets: HashSet::new(), + partial_message_subnets: HashSet::new(), custody_subnets: HashSet::new(), sync_status: SyncStatus::Unknown, meta_data: None, @@ -428,18 +431,23 @@ impl PeerInfo { } /// Adds a known subnet for the peer. - pub(super) fn insert_subnet(&mut self, subnet: Subnet) { + pub(super) fn insert_subnet(&mut self, subnet: Subnet, supports_partials: bool) { self.subnets.insert(subnet); + if supports_partials { + self.partial_message_subnets.insert(subnet); + } } /// Removes a subnet from the peer. pub(super) fn remove_subnet(&mut self, subnet: &Subnet) { self.subnets.remove(subnet); + self.partial_message_subnets.remove(subnet); } /// Removes all subnets from the peer. pub(super) fn clear_subnets(&mut self) { - self.subnets.clear() + self.subnets.clear(); + self.partial_message_subnets.clear() } /// Applies decay rates to a non-trusted peer's score. diff --git a/beacon_node/lighthouse_network/src/service/mod.rs b/beacon_node/lighthouse_network/src/service/mod.rs index 93c8410490..862281c910 100644 --- a/beacon_node/lighthouse_network/src/service/mod.rs +++ b/beacon_node/lighthouse_network/src/service/mod.rs @@ -1456,13 +1456,19 @@ impl Network { } } } - Event::Subscribed { peer_id, topic, .. } => { + Event::Subscribed { + peer_id, + topic, + supports_partial, + .. + } => { if let Ok(topic) = GossipTopic::decode(topic.as_str()) { if let Some(subnet_id) = topic.subnet_id() { - self.network_globals - .peers - .write() - .add_subscription(&peer_id, subnet_id); + self.network_globals.peers.write().add_subscription( + &peer_id, + subnet_id, + supports_partial, + ); } // Try to send the cached messages for this topic if let Some(msgs) = self.gossip_cache.retrieve(&topic) { diff --git a/beacon_node/lighthouse_network/src/types/partial.rs b/beacon_node/lighthouse_network/src/types/partial.rs index 26705b7106..4b5dcd8ad6 100644 --- a/beacon_node/lighthouse_network/src/types/partial.rs +++ b/beacon_node/lighthouse_network/src/types/partial.rs @@ -6,7 +6,7 @@ use ssz::{Decode, Encode}; use std::collections::HashSet; use std::fmt::Debug; use std::sync::Arc; -use tracing::{debug, error}; +use tracing::{error, trace}; use types::core::{EthSpec, Hash256}; use types::data::{ PartialDataColumn, PartialDataColumnHeader, PartialDataColumnPartsMetadata, @@ -168,7 +168,7 @@ impl Partial for OutgoingPartialColumn { Box::new(MaybeKnownMetadata::::Unknown) as Box, ) }); - debug!( + trace!( peer=%peer_id, group_id=%self.partial_column.block_root, column_index=self.partial_column.index, @@ -209,7 +209,7 @@ impl Partial for OutgoingPartialColumn { PartialError::InvalidFormat })? .map(|sidecar| { - debug!( + trace!( peer=%peer_id, group_id=%self.partial_column.block_root, column_index=self.partial_column.index, @@ -233,7 +233,7 @@ impl Partial for OutgoingPartialColumn { }); if send.is_none() { - debug!( + trace!( peer=%peer_id, group_id=%self.partial_column.block_root, column_index=self.partial_column.index, 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 2668a14dc5..98c143eaeb 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -1161,7 +1161,7 @@ impl NetworkBeaconProcessor { metrics::inc_counter( &metrics::BEACON_PROCESSOR_GOSSIP_PARTIAL_DATA_COLUMN_SIDECAR_MISSING_HEADER_TOTAL, ); - warn!( + debug!( error = ?err, %block_root, %index,